game room title: GAME ROOM ⇄ GAME SCROLL reel on the scroll aperture — TDD
The h2's second slot becomes a two-word vertical reel: GAME stays put, ROOM rests in view, SCROLL is parked one notch below in the slot's bottom fade. room-scroll.js toggles `.is-scroll` on the h2 from the SAME IntersectionObserver that already watches the table-hex aperture's scroll pane — ROOM slides up & out under the navbar line while SCROLL rises out of the page-aperture gradient (reverses on scroll-up). Table-phase only; the gate phase stays a plain GAME ROOM. One translateY drives both orientations. Portrait: the word is a short horizontal row in a short slot. Landscape: writing-mode: vertical-rl (inherited from the rotated gutter wordmark) makes the word a tall letter-column, so the same translateY slides it ALONG the wordmark — the user-chosen landscape behaviour for free. Landscape uses a shallower --gr-fade + a letter inset so the space-between end-letters parked at the slot edges aren't dimmed by the dissolve. Motion is deliberately old & rusty: a single cubic-bezier can overshoot at most once and can't oscillate, so the easing is a CSS linear() curve — stall against the grime, jerk free, clunk PAST the mark, then a damped end-wobble into place. Exposed as --gr-ease / --gr-dur / --gr-fade knobs on .gr-swap. base.html's letter-splitter now also splits the two .gr-word words; the .gr-swap window ships data-letters-split="1" so the splitter skips it (no 'roomscroll' run). Reel SCSS is scoped to .gr-swap/.gr-word; `> span.gr-swap` ties `> span:last-child` at (0,4,3) and wins on later source order [[feedback-scss-import-order-specificity]]. TRAP: libsass does NOT strip `//` comments INSIDE a CSS custom-property value — they leak into the compiled output, making the linear() (hence the whole `transition` shorthand) invalid-at-computed-value-time, which silently resets to 0s/ease (no animation). Keep every annotation OUTSIDE the linear(). [[feedback-libsass-comment-in-custom-property]] Reusable .gr-swap seam: my_sea gets GAME SEA → GAME SCROLL via a one-line header swap once its sea-scroll pane is built (deferred — the sea scroll doesn't exist yet). Tests: 2 ITs (RoomScrollOfEventsTest) — reel markup renders in the table phase, stays plain in the gate phase; 1 FT (test_scroll_swaps_room_title_to_scroll) — scrolling the aperture toggles GAME ROOM ⇄ GAME SCROLL both ways. collectstatic'd room-scroll.js for the FT [[feedback-collectstatic-before-ft]]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -277,6 +277,89 @@ body {
|
||||
> span[data-letters="3"] {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
// ── GAME ROOM ⇄ GAME SCROLL title reel ─────────────────
|
||||
// The second word becomes a two-word vertical reel inside
|
||||
// the 55% title slot. The base word (ROOM) rests in view;
|
||||
// SCROLL is parked one notch below, in the slot's bottom
|
||||
// fade. room-scroll.js toggles `.is-scroll` on this <h2>
|
||||
// when the table-hex aperture snaps to the provenance feed:
|
||||
// ROOM slides up & out under the navbar line while SCROLL
|
||||
// rises out of the bottom gradient — one reel, both words
|
||||
// travelling up. Reverses on scroll-up.
|
||||
//
|
||||
// The slide axis is vertical in BOTH orientations: portrait
|
||||
// the word is a short horizontal row; landscape (writing-
|
||||
// mode: vertical-rl, inherited from the rotated wordmark)
|
||||
// the word is a tall letter-column, so the same translateY
|
||||
// slides it ALONG the wordmark — the user-chosen landscape
|
||||
// behaviour falls out for free.
|
||||
> span.gr-swap { // (0,4,3) ties > span:last-child; later source order wins
|
||||
// Motion knobs — TWEAK here. The reel doesn't run on a
|
||||
// single cubic-bezier any more: a bezier is one curve
|
||||
// with two control points, so it can overshoot at most
|
||||
// ONCE and can't oscillate. To make the word feel old &
|
||||
// rusty — stall against the grime, jerk free, then clunk
|
||||
// past its mark and wobble into place — `--gr-ease` is a
|
||||
// CSS linear() easing: a piecewise list of `<output>
|
||||
// <progress%>` stops, where output > 1 overshoots.
|
||||
// The phases of the curve below, by stop:
|
||||
// 0 → 0.02 15% .......... rusty grip, barely budges
|
||||
// 0.16 25% → 0.8 56% .... breaks free, jerks across
|
||||
// 1.05 65% → 1.12 71% ... clunks PAST the mark
|
||||
// 0.965 78% → 1.05 84%
|
||||
// → 0.99 89% → 1.015 95% → 1 .. damped end-wobble
|
||||
// CRITICAL: keep NO inline `//` comments INSIDE the
|
||||
// linear() value — libsass leaves them in the custom-
|
||||
// property output, which invalidates the whole
|
||||
// `transition` (it falls back to 0s/ease). Annotate
|
||||
// ABOVE the declaration only. (Firefox 112+ / 151 here.)
|
||||
--gr-ease: linear(
|
||||
0, 0.015 7%, 0.02 25%,
|
||||
0.16 25%, 0.46 41%, 0.8 56%,
|
||||
1.15 65%, 1.12 71%,
|
||||
0.935 78%, 1.05 84%, 0.99 89%, 1.015 95%,
|
||||
1
|
||||
);
|
||||
--gr-dur: 0.95s; // heavier/slower so the stutter + wobble read
|
||||
--gr-fade: 16%; // top/bottom dissolve depth
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0; // reset :last-child's padding-inline-start (re-applied on .gr-word)
|
||||
// Symmetric top/bottom fade so the words dissolve into
|
||||
// the navbar line (top) + the page aperture (bottom).
|
||||
// Symmetric → rotation-invariant under landscape's
|
||||
// rotate(180deg).
|
||||
mask-image:
|
||||
linear-gradient(to bottom,
|
||||
transparent 0, #000 var(--gr-fade),
|
||||
#000 calc(100% - var(--gr-fade)), transparent 100%);
|
||||
-webkit-mask-image:
|
||||
linear-gradient(to bottom,
|
||||
transparent 0, #000 var(--gr-fade),
|
||||
#000 calc(100% - var(--gr-fade)), transparent 100%);
|
||||
}
|
||||
.gr-word {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-inline-start: 0.4em; // match > span:last-child word gap
|
||||
transition: transform var(--gr-dur, 0.55s) var(--gr-ease, ease);
|
||||
will-change: transform;
|
||||
|
||||
// 3-letter base word (e.g. my_sea's SEA) clusters like
|
||||
// the top-level 3-letter suffixes instead of parking at
|
||||
// the slot edges.
|
||||
&[data-letters="3"] { justify-content: space-around; }
|
||||
}
|
||||
.gr-word--base { transform: translateY(0); } // resting in view
|
||||
.gr-word--scroll { transform: translateY(100%); } // parked below the slot
|
||||
&.is-scroll {
|
||||
.gr-word--base { transform: translateY(-100%); } // up & out the top
|
||||
.gr-word--scroll { transform: translateY(0); } // up into view
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -433,6 +516,19 @@ body {
|
||||
// bottom edge of the first span — natural break between words.
|
||||
}
|
||||
|
||||
// Title reel in landscape — the rotated wordmark is a tall letter-column
|
||||
// that fills the gutter slot, so `space-between` parks the first/last
|
||||
// letter at the very slot edges. Inset the letters (padding-inline = the
|
||||
// vertical axis in writing-mode: vertical-rl) and use a shallower fade so
|
||||
// the dissolve lives in those margins instead of dimming resting letters.
|
||||
// translateY still slides the full slot length ALONG the wordmark.
|
||||
body .container .row .col-lg-6 h2 > span.gr-swap {
|
||||
--gr-fade: 7%;
|
||||
}
|
||||
body .container .row .col-lg-6 h2 .gr-word {
|
||||
padding-inline: 7%; // top+bottom inset (vertical-rl) — supersedes the 0.4em start gap
|
||||
}
|
||||
|
||||
// Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary)
|
||||
// Use body #id_footer (specificity 0,1,0,1) to beat base #id_footer (0,1,0,0)
|
||||
// which compiles later in the output and would otherwise override height: 100vh.
|
||||
|
||||
Reference in New Issue
Block a user