Skip to content

Commit 6e9816f

Browse files
committed
Layout: 404 sheds Lobby, rotate-hint fade + seamless loop, essay line tuning
1 parent fc8565c commit 6e9816f

3 files changed

Lines changed: 62 additions & 14 deletions

File tree

src/layouts/BaseLayout.astro

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ interface Props {
88
/** Absolute path under /public, e.g. "/og/hello-world.png". */
99
image?: string;
1010
mode?: 'ink' | 'paper';
11+
/** Override portrait-mobile stage height. Any valid CSS length; default
12+
30svh. Pages whose stage wants more breathing room (e.g. 404) pass a
13+
larger value. Feeds a CSS variable consumed by the mobile media
14+
query below — desktop layout isn't affected. */
15+
stageHeightMobile?: string;
1116
}
1217
const SITE_NAME = '眠海';
1318
const DEFAULT_DESCRIPTION = '眠海 / SUNKEN KEEP — 生成式与交互作品的陈列。';
@@ -16,14 +21,15 @@ const {
1621
description = DEFAULT_DESCRIPTION,
1722
image,
1823
mode = 'ink',
24+
stageHeightMobile = '30svh',
1925
} = Astro.props;
2026
const fullTitle = title === SITE_NAME ? SITE_NAME : `${title} · ${SITE_NAME}`;
2127
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
2228
const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
2329
---
2430

2531
<!DOCTYPE html>
26-
<html lang="zh-CN" class:list={[mode === 'paper' && 'paper']}>
32+
<html lang="zh-CN" class:list={[mode === 'paper' && 'paper']} style={`--stage-h-mobile: ${stageHeightMobile}`}>
2733
<head>
2834
<meta charset="utf-8" />
2935
<meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -127,7 +133,16 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
127133

128134
const dismiss = () => {
129135
try { localStorage.setItem(KEY, '1'); } catch {}
130-
document.body.classList.remove('show-rotate-hint');
136+
const body = document.body;
137+
if (!body.classList.contains('show-rotate-hint')) return;
138+
// Run the leave animation, then drop both classes. display:flex
139+
// stays active (via .show-rotate-hint) throughout the fade, so
140+
// the overlay remains visible while opacity animates to 0.
141+
body.classList.add('rotate-hint-leaving');
142+
window.setTimeout(() => {
143+
body.classList.remove('show-rotate-hint');
144+
body.classList.remove('rotate-hint-leaving');
145+
}, 460);
131146
};
132147

133148
const wire = () => {
@@ -365,8 +380,10 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
365380
position: sticky;
366381
top: 0;
367382
/* svh = small viewport height, ignores dynamic URL bar on mobile.
368-
Without it, heights jump when Safari collapses its chrome. */
369-
height: 30svh;
383+
Without it, heights jump when Safari collapses its chrome.
384+
Defaults to 30svh; any page passing stageHeightMobile to
385+
BaseLayout overrides via the --stage-h-mobile root var. */
386+
height: var(--stage-h-mobile, 30svh);
370387
z-index: 1;
371388
border-right: none;
372389
/* Hard 1px rule replaced by the body::before fade strip below,
@@ -401,7 +418,7 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
401418
z-index: 2;
402419
}
403420
body::before {
404-
top: calc(30svh - 0.8em);
421+
top: calc(var(--stage-h-mobile, 30svh) - 0.8em);
405422
height: 3.3em;
406423
background: linear-gradient(
407424
to bottom,
@@ -448,6 +465,16 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
448465
@media (max-width: 860px) and (orientation: portrait) {
449466
body.show-rotate-hint .rotate-hint { display: flex; }
450467
}
468+
/* Fade-out on dismiss. JS adds .rotate-hint-leaving before removing
469+
.show-rotate-hint after the animation finishes, so display:flex
470+
stays active while opacity animates to 0. */
471+
@keyframes rotate-hint-leave {
472+
from { opacity: 1; }
473+
to { opacity: 0; }
474+
}
475+
body.rotate-hint-leaving .rotate-hint {
476+
animation: rotate-hint-leave 460ms ease-out both;
477+
}
451478
.rotate-hint-card {
452479
text-align: center;
453480
color: var(--fg);
@@ -459,10 +486,13 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
459486
color: var(--accent);
460487
animation: rotate-hint-turn 3.6s cubic-bezier(0.65, 0, 0.35, 1) infinite;
461488
}
489+
/* Loop: hold portrait, rotate to landscape, hold, rotate back,
490+
hold briefly, repeat. The end state matches the start so the
491+
infinite cycle is seamless instead of snapping back. */
462492
@keyframes rotate-hint-turn {
463-
0%, 20% { transform: rotate(0deg); }
464-
45%, 70% { transform: rotate(-90deg); }
465-
95%, 100% { transform: rotate(-90deg); }
493+
0%, 15% { transform: rotate(0deg); }
494+
40%, 65% { transform: rotate(-90deg); }
495+
90%, 100% { transform: rotate(0deg); }
466496
}
467497
.rotate-hint-text {
468498
font-family: var(--font-body);
@@ -483,6 +513,7 @@ const ogImage = image ? new URL(image, Astro.site).toString() : undefined;
483513
@media (prefers-reduced-motion: reduce) {
484514
.rotate-hint { animation: none; }
485515
.rotate-hint-icon { animation: none; transform: rotate(-90deg); }
516+
body.rotate-hint-leaving .rotate-hint { animation: none; opacity: 0; }
486517
}
487518
</style>
488519
</body>

src/layouts/EssayLayout.astro

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,20 @@ const tintClass = tint ? `tint-${tint}` : '';
8282
// Pick the section with the largest top ≤ line — i.e., the last
8383
// one whose heading has crossed the reading line. Before any
8484
// section crosses (top of page), fall back to the first.
85-
const line = window.innerHeight * 0.35;
85+
//
86+
// Reading-line position differs by layout. On desktop each
87+
// section is min-height 72vh + flex-centered, so a section's
88+
// bounding-top sits well above its visible content — a line at
89+
// 35% fires when content is in the lower third (entering the
90+
// reader's attention). On portrait mobile the section hugs its
91+
// content (just 3em padding), so 35% would fire only after the
92+
// content was already in the user's focus zone — feels late.
93+
// Drop the line to 65% there so the cue fires as the section
94+
// enters, matching the desktop "about to read" feel.
95+
const portraitMobile =
96+
window.innerWidth <= 860 &&
97+
window.matchMedia('(orientation: portrait)').matches;
98+
const line = window.innerHeight * (portraitMobile ? 0.65 : 0.35);
8699
best = null;
87100
let bestTop = -Infinity;
88101
for (const n of nodes) {
@@ -229,6 +242,10 @@ const tintClass = tint ? `tint-${tint}` : '';
229242
canvas + tall 72vh sections would force too many scrolls per piece.
230243
Landscape phones fall through to the desktop pacing.
231244

245+
Padding stays generous (≈3em) so the border-top between sections
246+
feels like a breath, not a seam — desktop gets this for free via
247+
flex-centered 72vh sections, mobile needs it spelled out.
248+
232249
Last-child also drops its min-height here — otherwise its desktop
233250
40vh + flex-center would push the final section's content to the
234251
middle of its box, creating a bigger lead-in gap than preceding
@@ -237,7 +254,7 @@ const tintClass = tint ? `tint-${tint}` : '';
237254
@media (max-width: 860px) and (orientation: portrait) {
238255
.body :global(section) {
239256
min-height: 0;
240-
padding: 1.6em 0;
257+
padding: 3em 0;
241258
}
242259
.body :global(section:last-child) {
243260
min-height: 0;

src/pages/404.astro

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
import BaseLayout from '../layouts/BaseLayout.astro';
3-
import LobbySketch from '../components/LobbySketch.astro';
43
---
54

6-
<BaseLayout title="沉得太深" description="找不到这一页。它或许在海的另一侧。">
7-
<LobbySketch slot="stage" />
8-
5+
<BaseLayout
6+
title="沉得太深"
7+
description="找不到这一页。它或许在海的另一侧。"
8+
>
99
<article class="tint-mist">
1010
<p class="code enter" style="--i: 0;">404 / NOT FOUND</p>
1111
<h1 class="enter" style="--i: 1;">沉&nbsp;&nbsp;&nbsp;深</h1>

0 commit comments

Comments
 (0)