Website: chapter 8 labels on the layers' corners, one layer at a time; phone centred
Labels: each layer's pin now sits on its outer rounded corner, the layer's rightmost point, so a tag straight across from it is joined by a short line that crosses nothing; the stack moves left by just enough for the tags to fit, and the open stack is a little smaller so it keeps clear of the chapter's heading. While the stack is open, scrolling walks through the layers from the glass to the wallpaper: the one looked at stays bright and its tag lights up cyan, the others dim (where the tags are shown). Chapter 8 is three screens long for it. Centring: the labels' list sat in the stage's grid flow, so the grid split its height into two rows and the story phone was centred in the top one (about 100 px high on a 1146 px window). It is out of the flow now; measured equal gaps above and below at 2000 x 1146, 1440 x 900 and 1280 x 720. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
058fecd27f
commit
0fbefd2ca6
2 files changed
+85
-46
No files matched your search
+79
-43
@@ -123,8 +123,9 @@
|
||||
const MENU_X = 120;
|
||||
const CLOCK_W = 4 * CW - 36;
|
||||
const DEFAULT_WEIGHT = 300;
|
||||
/** The story runs from 0 to 9: chapters 1 to 7 take one unit each, chapter 8 two. */
|
||||
const STORY_END = 9;
|
||||
/** The story runs from 0 to 10: chapters 1 to 7 take one unit each, chapter 8 three. */
|
||||
const STORY_END = 10;
|
||||
const LONG = 3;
|
||||
/** Icon shapes, as border radii for icons and for the picture widget. */
|
||||
const SHAPES = {
|
||||
squircle: { icon: '30%', widget: '30px' },
|
||||
@@ -529,13 +530,13 @@
|
||||
const s = blank();
|
||||
P = clamp(P, 0, STORY_END);
|
||||
if (P >= 7) {
|
||||
// Chapter 8, two units long: the list closes and the phone is home again. From there on the
|
||||
// Chapter 8, three units long: the list closes and the phone is home again. From there on the
|
||||
// layered phone (setupAnatomy) takes its place and comes apart; this one is hidden.
|
||||
const t = (P - 7) / 2;
|
||||
s.drawer = 1 - E(t, 0, 0.1);
|
||||
const t = (P - 7) / LONG;
|
||||
s.drawer = 1 - E(t, 0, 0.06);
|
||||
s.list = ph.scrollFor(L('P'));
|
||||
s.hidden = { Podcasts: 1 };
|
||||
s.snack = 1 - E(t, 0, 0.08);
|
||||
s.snack = 1 - E(t, 0, 0.05);
|
||||
return s;
|
||||
}
|
||||
const ch = Math.min(6, Math.floor(P));
|
||||
@@ -648,9 +649,16 @@
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Chapter 8, from its start (0) to its end (1): when the phones swap, and how far apart the layers are. */
|
||||
const SWAP_AT = 0.12;
|
||||
const layersApart = (t) => smooth(seg(t, 0.14, 0.42)) * (1 - smooth(seg(t, 0.74, 0.94)));
|
||||
/**
|
||||
* Chapter 8, from its start (0) to its end (1): when the phones swap, how far apart the layers
|
||||
* are, and which layer is looked at (0 the glass to 4 the wallpaper) while the stack is open.
|
||||
*/
|
||||
const SWAP_AT = 0.08;
|
||||
const layersApart = (t) => smooth(seg(t, 0.09, 0.26)) * (1 - smooth(seg(t, 0.85, 0.97)));
|
||||
const lookingAt = (t) => ({
|
||||
amount: smooth(seg(t, 0.27, 0.32)) * (1 - smooth(seg(t, 0.78, 0.83))),
|
||||
at: seg(t, 0.33, 0.77) * 4,
|
||||
});
|
||||
|
||||
function setupStory(ph, layered) {
|
||||
const box = document.querySelector('.story__steps');
|
||||
@@ -667,14 +675,13 @@
|
||||
const P = clamp((-box.getBoundingClientRect().top + vh / 2) / unit, 0, STORY_END);
|
||||
const ch = P >= 7 ? 7 : Math.floor(P);
|
||||
// Without motion, each chapter shows its finished picture (chapter 8: pulled apart).
|
||||
const shown = reduced ? (ch === 7 ? 7.98 : ch + 0.985) : P;
|
||||
const shown = reduced ? (ch === 7 ? 7 + LONG * 0.2 : ch + 0.985) : P;
|
||||
if (force || Math.abs(shown - lastP) > 0.0002) {
|
||||
lastP = shown;
|
||||
ph.render(storyState(ph, shown));
|
||||
if (layered) {
|
||||
const t = shown >= 7 ? (shown - 7) / 2 : 0;
|
||||
const swapped = shown >= 7 && t >= SWAP_AT;
|
||||
layered.set(swapped ? layersApart(t) : 0, swapped, force);
|
||||
const t = shown >= 7 ? (shown - 7) / LONG : 0;
|
||||
layered.set(t, shown >= 7 && t >= SWAP_AT, force);
|
||||
}
|
||||
}
|
||||
if (ch !== lastChapter) {
|
||||
@@ -1610,21 +1617,19 @@
|
||||
svg.append(path, dot);
|
||||
return { path, dot };
|
||||
});
|
||||
const LABEL_W = 214;
|
||||
const GAP = 165; // between layers, in the phone's pixels
|
||||
let scale = 1;
|
||||
let lastE = -1;
|
||||
let lastT = -1;
|
||||
let swapped = null;
|
||||
/** Same picture as the story phone shows, so the swap is not seen. */
|
||||
function syncPictures() {
|
||||
pics.forEach((p, i) => p.classList.toggle('on', i === storyPhone.picIndex));
|
||||
}
|
||||
/** Puts the layers where e (0 closed, 1 fully apart) wants them. */
|
||||
function arrange(e) {
|
||||
const GAP = 150; // between layers, in the phone's pixels
|
||||
const wide = () => window.innerWidth > 900;
|
||||
function place(e, shift) {
|
||||
const tilt = 56 * e;
|
||||
const sc = scale * (1 - 0.36 * e);
|
||||
const wide = window.innerWidth > 900;
|
||||
// Opening, the stack moves left to leave room for its labels.
|
||||
const shift = wide ? -e * Math.min(140, stage.clientWidth * 0.17) : 0;
|
||||
const sc = scale * (1 - 0.44 * e);
|
||||
const ox = 207 * (scale - sc) + shift;
|
||||
// The layers rise as they separate; lower the whole stack by half of that, to keep it centred.
|
||||
const oy = 434 * (scale - sc) + e * 2.5 * GAP * Math.sin((tilt * Math.PI) / 180) * sc;
|
||||
@@ -1633,66 +1638,97 @@
|
||||
layers.forEach((l, i) => {
|
||||
l.style.transform = i ? `translateZ(${(i * GAP * e).toFixed(1)}px)` : '';
|
||||
});
|
||||
// Flat, it is just a home screen: the app list layer only shows once the stack opens.
|
||||
layers[4].style.opacity = smooth(seg(e, 0.15, 0.6)).toFixed(3);
|
||||
}
|
||||
/** The labels: one column beside the stack, evenly spaced, each joined to its layer's edge. */
|
||||
function label(e) {
|
||||
/**
|
||||
* Puts the layers where e (0 closed, 1 fully apart) wants them. The pins sit on each layer's
|
||||
* outer corner, its rightmost point; if the labels beside them would not fit, the stack moves
|
||||
* left by just enough.
|
||||
*/
|
||||
function arrange(e) {
|
||||
place(e, 0);
|
||||
if (!wide() || e <= 0) return;
|
||||
const room = stage.getBoundingClientRect().right - 12 - LABEL_W - 34;
|
||||
let corner = -Infinity;
|
||||
for (const pin of pins) corner = Math.max(corner, pin.getBoundingClientRect().left);
|
||||
if (corner > room) place(e, room - corner);
|
||||
}
|
||||
/** Each label straight across from its layer's corner, on a short line that crosses nothing. */
|
||||
function label(e, focus, weights) {
|
||||
const shown = seg(e, 0.55, 0.95);
|
||||
svg.style.opacity = shown.toFixed(3);
|
||||
labels.forEach((li) => { li.style.opacity = shown.toFixed(3); });
|
||||
if (window.innerWidth <= 900 || shown <= 0) return;
|
||||
svg.style.opacity = shown > 0 ? '1' : '0';
|
||||
if (!wide() || shown <= 0) {
|
||||
labels.forEach((li) => { li.style.opacity = '0'; });
|
||||
return;
|
||||
}
|
||||
const base = stage.getBoundingClientRect();
|
||||
let right = -Infinity;
|
||||
for (const l of layers) right = Math.max(right, l.getBoundingClientRect().right);
|
||||
const LABEL_W = 214;
|
||||
const colX = clamp(right - base.left + 34, 0, base.width - LABEL_W - 8);
|
||||
const pts = pins.map((pin) => {
|
||||
const r = pin.getBoundingClientRect();
|
||||
return { x: r.left - base.left, y: r.top - base.top };
|
||||
});
|
||||
const colX = Math.min(Math.max(...pts.map((pt) => pt.x)) + 34, base.width - LABEL_W - 12);
|
||||
const ys = [];
|
||||
let prev = -Infinity;
|
||||
for (const pt of pts) {
|
||||
const y = Math.max(pt.y, prev + 76); // never closer than one tag's height
|
||||
const y = Math.max(pt.y, prev + 74); // never closer than one tag's height
|
||||
ys.push(y);
|
||||
prev = y;
|
||||
}
|
||||
const spill = ys[ys.length - 1] + 44 - base.height;
|
||||
const spill = ys[ys.length - 1] + 40 - base.height;
|
||||
if (spill > 0) for (let k = 0; k < ys.length; k++) ys[k] -= spill;
|
||||
labels.forEach((li, k) => {
|
||||
const lit = 1 - focus * (1 - Math.max(0.35, weights[k]));
|
||||
li.style.opacity = (shown * lit).toFixed(3);
|
||||
li.classList.toggle('is-on', focus > 0.5 && weights[k] > 0.5);
|
||||
li.style.transform = `translate(${colX.toFixed(1)}px, ${ys[k].toFixed(1)}px)`;
|
||||
const { x, y } = pts[k];
|
||||
const bend = Math.max(x + 16, colX - 26);
|
||||
lines[k].path.setAttribute('d', `M${x.toFixed(1)} ${y.toFixed(1)} L${bend.toFixed(1)} ${ys[k].toFixed(1)} L${colX.toFixed(1)} ${ys[k].toFixed(1)}`);
|
||||
const d = Math.abs(ys[k] - y) < 0.5
|
||||
? `M${x.toFixed(1)} ${y.toFixed(1)} H${colX.toFixed(1)}`
|
||||
: `M${x.toFixed(1)} ${y.toFixed(1)} H${(x + 12).toFixed(1)} L${(colX - 12).toFixed(1)} ${ys[k].toFixed(1)} H${colX.toFixed(1)}`;
|
||||
lines[k].path.setAttribute('d', d);
|
||||
lines[k].path.style.opacity = (shown * lit).toFixed(3);
|
||||
lines[k].dot.setAttribute('cx', x.toFixed(1));
|
||||
lines[k].dot.setAttribute('cy', y.toFixed(1));
|
||||
lines[k].dot.style.opacity = (shown * lit).toFixed(3);
|
||||
});
|
||||
}
|
||||
const fit = () => {
|
||||
scale = host.clientWidth / 414;
|
||||
arrange(Math.max(0, lastE)); // scaled from the start: at full size it would widen the page
|
||||
place(0, 0); // scaled from the start: at full size it would widen the page
|
||||
lastT = -1;
|
||||
};
|
||||
fit();
|
||||
if ('ResizeObserver' in window) new ResizeObserver(fit).observe(host);
|
||||
function set(e, on, force) {
|
||||
/** t: where chapter 8 is, from 0 to 1. on: whether this phone is the one shown. */
|
||||
function set(t, on, force) {
|
||||
if (on !== swapped) {
|
||||
swapped = on;
|
||||
host.style.visibility = on ? 'visible' : 'hidden';
|
||||
storyPhone.host.style.visibility = on ? 'hidden' : '';
|
||||
if (on) syncPictures();
|
||||
lastE = -1;
|
||||
lastT = -1;
|
||||
}
|
||||
if (!on) {
|
||||
label(0);
|
||||
label(0, 0, []);
|
||||
rail.style.opacity = '';
|
||||
return;
|
||||
}
|
||||
if (!force && Math.abs(e - lastE) < 0.0005) return;
|
||||
lastE = e;
|
||||
if (!force && Math.abs(t - lastT) < 0.0002) return;
|
||||
lastT = t;
|
||||
const e = layersApart(t);
|
||||
arrange(e);
|
||||
rail.style.opacity = (1 - e).toFixed(3);
|
||||
label(e);
|
||||
// One layer at a time: the one looked at stays bright and its label lights up, the others
|
||||
// dim. Only where the labels are shown.
|
||||
const look = lookingAt(t);
|
||||
const focus = wide() ? look.amount : 0;
|
||||
const weights = labels.map((_, k) => clamp(1 - Math.abs(k - look.at)));
|
||||
layers.forEach((l, i) => {
|
||||
const k = 5 - i; // the labels run from the glass (layer 5) down
|
||||
const w = i === 0 ? 0 : weights[k];
|
||||
const base = i === 4 ? smooth(seg(e, 0.15, 0.6)) : 1; // flat, the list layer is not there
|
||||
l.style.opacity = (base * (1 - focus * (1 - Math.max(0.16, w)))).toFixed(3);
|
||||
});
|
||||
label(e, focus, weights);
|
||||
}
|
||||
return { set, syncPictures, element: xp };
|
||||
}
|
||||
|
||||
+6
-3
@@ -293,7 +293,7 @@ section { position: relative; }
|
||||
.story__phones .phone-host, .story__phones .xhost { position: absolute; inset: 0; height: auto; }
|
||||
.story__phones .xhost { visibility: hidden; }
|
||||
/* Chapter 8 is two screens long; its text stays put while the phone comes apart. */
|
||||
.step.step--long { min-height: 200vh; min-height: 200svh; align-items: flex-start; }
|
||||
.step.step--long { min-height: 300vh; min-height: 300svh; align-items: flex-start; }
|
||||
.step.step--long .step__card { position: sticky; top: max(96px, calc(50vh - 170px)); }
|
||||
.story__rail { position: absolute; right: 0; top: 50%; translate: 0 -50%; display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; }
|
||||
.story__rail li { width: 12px; height: 34px; border: var(--b) solid var(--fg); transition: background .3s, height .3s; }
|
||||
@@ -786,17 +786,20 @@ section { position: relative; }
|
||||
.xl--list .list { top: 46px; }
|
||||
.xl--list .alpha { bottom: 112px; }
|
||||
.xl--glass::before { content: ""; position: absolute; inset: 0; opacity: var(--e); background: linear-gradient(125deg, rgba(255, 255, 255, .2), rgba(255, 255, 255, 0) 38%, rgba(255, 255, 255, .07) 68%, rgba(255, 255, 255, 0)); }
|
||||
.xl__pin { position: absolute; right: 0; top: 30%; width: 1px; height: 1px; }
|
||||
.xl__pin { position: absolute; right: 14px; bottom: 14px; width: 1px; height: 1px; }
|
||||
.anatomy__lines { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; z-index: 2; opacity: 0; }
|
||||
.anatomy__lines path { fill: none; stroke: var(--cyan); stroke-width: 2; }
|
||||
.anatomy__lines circle { fill: var(--bg); stroke: var(--cyan); stroke-width: 2.5; }
|
||||
.anatomy__labels { list-style: none; margin: 0; padding: 0; }
|
||||
.anatomy__labels { position: absolute; inset: 0; list-style: none; margin: 0; padding: 0; pointer-events: none; } /* out of the flow, or the stage's grid would split its height and the phone sit high */
|
||||
.anatomy__labels li {
|
||||
position: absolute; left: 0; top: 0; z-index: 3; width: 214px; translate: 0 -50%;
|
||||
padding: 9px 12px 10px; background: rgba(7, 7, 13, .94);
|
||||
border: var(--b) solid var(--fg); box-shadow: 4px 4px 0 var(--violet);
|
||||
opacity: 0; pointer-events: none; will-change: transform;
|
||||
}
|
||||
.anatomy__labels li { transition: border-color .25s, box-shadow .25s; }
|
||||
.anatomy__labels li.is-on { border-color: var(--cyan); box-shadow: 4px 4px 0 var(--cyan); }
|
||||
.anatomy__labels li.is-on b { color: var(--cyan); }
|
||||
.anatomy__labels i { margin-right: 8px; font: 700 11px/1 var(--mono); font-style: normal; color: var(--cyan); }
|
||||
.anatomy__labels b { font: 900 13px/1.15 var(--display); text-transform: uppercase; letter-spacing: -.01em; }
|
||||
.anatomy__labels span { display: block; margin-top: 6px; font: 500 12px/1.3 var(--mono); color: var(--dim); white-space: nowrap; }
|
||||
|
||||
Reference in new issue
Block a user