Wearable 一幕:巨型标题、景深换焦与照片滑轨
oryzo.ai 里节奏最漂亮的一幕:"it's wearable" 视口高的巨型标题跟着滚动从右横穿到左, 中段白热发光像打过曝的棚灯;随后换焦 —— 字化进虚焦里,取景框中的画面从模糊里显影, 杯垫已经别在了外套胸口。字与景互为景深,一次滚动完成一次"叙事接力"。 demo 用纯 CSS 占位画复现(滚轮 / 拖拽推进),不借任何照片。
要点
- 巨型标题横穿的速度与滚动 1:1(
translateX直接吃滚动值折算),是"页面在动"最诚实的信号 —— 比任何入场动画都跟手 - 辉光不是
filter: drop-shadow是三层text-shadow叠的 bloom(近层白、中层暖白、远层琥珀),强度吃进度的正弦信封:入场渐亮、中段过曝、出场熄灭 - 换焦(rack focus)就是两层
filter: blur互换:文字层 0→16px、场景层 18px→0,透明度同步交叉 —— 电影语言里最便宜的注意力转移,浏览器里两行 CSS - 字穿过取景框时在产品"层前层后"穿行的错觉:取景框和内容分层,标题 z 序夹在中间,加上模糊层次,纵深就有了
- 取景框四角刻度(细线框 + 4 个角标 + 轨道圆点)是整站的"摄影棚取景器"版式母题,一套 border 伪元素就够
- 场景层的"人像"是纯 CSS 画的占位(渐变外套 + 胸口徽章杯垫):拆解复刻不搬原站摄影素材,构图气质到位即可
照片滑轨
换焦之后这一幕的后半段:编辑部风格的玩梗大片(别帽上、当眼罩、咬嘴里、揣口袋……) 排成横向滑轨,纵向滚动被折算成横向走片,依次经过视口中央钉死的取景框。
- 纵向滚动折算
translateX:用户只会一种滚动,但画面多了一个行进方向 —— 单调的下滑变成了"走片" - 每张停一拍(手写版 scroll-snap):把滚动里程按卡切段(每卡固定 640 虚拟像素),段首 30% 钉在当前卡、后 70%
smoothstep滑向下一张;再加空闲吸附(松手 250ms 后 target 缓动到最近整卡位)—— 任何时刻框里都有一张端正的"正片",最后一张也能精确进框(里程 = (卡数-1)×段长,与轨道长度、视口宽度彻底解耦) - 过框感不用 IntersectionObserver:每帧算卡片中心到取景框中心的距离,距离 → scale / 亮度 / 饱和 / 模糊的连续函数 —— 无级过渡,倒着滚也天然对称
- 取景框钉死不动、卡片从底下流过:动的是内容不是相机,和转台一幕"文案钉住、世界在动"同一个版式哲学
- 玩梗贴纸是叙事的一部分:WARNING 缎带("专业人士操作,请勿模仿")、糖纸红底、斜排水印 —— 假产品的真广告腔全靠这些道具撑
- 卡片全是 CSS 画的占位大片:渐变布光 + 软木圆盘道具,一张图片都不用
源码(折叠)
wearable-typography-demo.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>巨型标题横穿:辉光与景深换焦 · ORYZO wearable 一幕复现</title>
<style>
:root { --ink: #f2e8d5; --accent: #f08c2e; }
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; background: #1b120a; }
body { font-family: "DM Mono", ui-monospace, monospace; color: var(--ink); }
/* 背景暖场 */
.amb { position: fixed; inset: 0; pointer-events: none;
background: radial-gradient(80% 70% at 50% 100%, #7a3c14aa 0%, transparent 65%); }
/* ---- 巨型标题:横穿 + 辉光 + 可整体虚焦,三个都由 JS 按进度驱动 ---- */
#mega {
position: fixed; top: 50%; left: 0; translate: 0 -50%;
font-family: system-ui, sans-serif; font-weight: 700; white-space: nowrap;
font-size: clamp(64px, 13vw, 170px); letter-spacing: .01em;
will-change: transform, filter; pointer-events: none;
}
/* ---- 产品检视框:细线框 + 四角刻度 + 轨道圆点,摄影棚取景器的味道 ---- */
.viewer {
position: fixed; left: 50%; top: 50%; translate: -50% -50%;
width: min(46vmin, 380px); aspect-ratio: 3/4;
border: 1px solid #f2e8d544; pointer-events: none;
}
.viewer i { position: absolute; width: 9px; height: 9px; border: 1px solid #f2e8d5aa; }
.viewer i:nth-child(1) { left: -1px; top: -1px; border-right: 0; border-bottom: 0; }
.viewer i:nth-child(2) { right: -1px; top: -1px; border-left: 0; border-bottom: 0; }
.viewer i:nth-child(3) { left: -1px; bottom: -1px; border-right: 0; border-top: 0; }
.viewer i:nth-child(4) { right: -1px; bottom: -1px; border-left: 0; border-top: 0; }
.viewer .dot { position: absolute; width: 5px; height: 5px; border-radius: 50%;
background: #fff; right: 8%; top: 6%; }
/* 框内两层内容:杯垫(前段焦点)与"人像"(后段焦点),换焦=两层 blur 互换 */
.layer { position: absolute; inset: 0; display: grid; place-items: center;
will-change: filter, opacity; }
.coaster {
width: 62%; aspect-ratio: 1; border-radius: 50%;
background: radial-gradient(circle at 38% 32%, #d9a266, #c98d4e 45%, #8a5a2b 80%);
box-shadow: inset 0 -12px 26px #00000055, 0 26px 60px #000a;
position: relative;
}
.coaster::after { content: ''; position: absolute; inset: 14%; border-radius: 50%;
background: radial-gradient(circle at 42% 36%, #b57f42, #8f5f2e 72%);
box-shadow: inset 0 8px 20px #0006; }
/* "人像"占位画:纯 CSS 的外套 + 别在胸口的杯垫(不借任何真实照片) */
.figure { width: 100%; height: 100%; position: relative; overflow: hidden;
background: linear-gradient(180deg, #241409 0%, #3a2010 55%, #4a2a12 100%); }
.figure .coat { position: absolute; inset: 18% -6% -4%;
background: radial-gradient(120% 90% at 50% 12%, #5a3316 0%, #402209 46%, #2c1505 100%);
border-radius: 44% 44% 0 0 / 30% 30% 0 0;
box-shadow: inset 0 18px 40px #00000088; }
.figure .zip { position: absolute; left: 50%; top: 22%; bottom: 0; width: 2px;
background: #00000055; }
.figure .badge { position: absolute; left: 34%; top: 38%; width: 26%; aspect-ratio: 1;
border-radius: 50%;
background: radial-gradient(circle at 40% 32%, #e2a760, #c98d4e 50%, #8a5a2b 85%);
box-shadow: 0 8px 22px #000c, inset 0 -6px 14px #0006; }
.figure .badge::after { content: ''; position: absolute; inset: 16%; border-radius: 50%;
background: radial-gradient(circle at 42% 36%, #b57f42, #8f5f2e 75%);
box-shadow: inset 0 5px 12px #0007; }
.kicker { position: fixed; left: 6%; top: 40%; font-size: 12px; letter-spacing: .2em; }
.kicker b { display: block; font-size: 15px; margin-top: 6px; letter-spacing: .1em; }
.hud { position: fixed; left: 16px; top: 14px; font-size: 12px; line-height: 1.7; letter-spacing: .06em; }
.hud b { font-size: 14px; letter-spacing: .12em; }
.hud em { font-style: normal; color: var(--accent); }
.hint { position: fixed; left: 50%; bottom: 14px; translate: -50% 0; font-size: 11px;
letter-spacing: .26em; opacity: .75; transition: opacity .4s; }
.hint.gone { opacity: 0; }
</style>
</head>
<body>
<div class="amb"></div>
<div id="mega">it's wearable</div>
<div class="viewer">
<i></i><i></i><i></i><i></i><span class="dot"></span>
<div class="layer" id="layerA"><div class="coaster"></div></div>
<div class="layer" id="layerB">
<div class="figure"><div class="coat"></div><div class="zip"></div><div class="badge"></div></div>
</div>
</div>
<div class="kicker">SO PORTABLE,<b id="kb">…</b></div>
<div class="hud"><b>RACK FOCUS</b><br><span id="stat">–</span></div>
<div class="hint" id="hint">↓ 滚动:标题横穿 → 辉光 → 焦点换到人像</div>
<script>
/* ============================================================================
* 巨型标题横穿 + 辉光 + 景深换焦 —— oryzo.ai "it's wearable" 一幕的复现
*
* 原站行为(滚动侦察逐帧观察):
* ① 视口高的巨型标题从右横穿到左,速度跟滚动 1:1,穿过产品检视框的层前层后;
* ② 中段标题白热发光(bloom),像打过曝的摄影棚灯;
* ③ 尾段"换焦":标题失焦化开,检视框里的内容从虚焦里显影 ——
* 焦点从"字"移到"人",杯垫已经别在外套上。文字层和场景层互为景深。
*
* 复现:一个虚拟滚动进度 p 同时驱动三件事 ——
* 标题 translateX(横穿)、text-shadow 强度(辉光)、两层 filter:blur 互换(换焦)。
* "人像"是纯 CSS 画的占位(外套渐变 + 胸口杯垫徽章),不借任何照片。
* ========================================================================== */
const MAXPX = 4200;
const sm = { target: 0, current: 0 };
addEventListener('wheel', e => { e.preventDefault();
sm.target = Math.max(0, Math.min(MAXPX, sm.target + e.deltaY * (e.deltaMode === 1 ? 40 : 1)));
}, { passive: false });
addEventListener('keydown', e => {
const map = { ArrowUp: -100, ArrowDown: 100, PageUp: -900, PageDown: 900 };
if (e.key in map) { e.preventDefault(); sm.target = Math.max(0, Math.min(MAXPX, sm.target + map[e.key])); }
});
let dragY = null;
addEventListener('pointerdown', e => dragY = e.clientY);
addEventListener('pointermove', e => {
if (dragY === null) return;
sm.target = Math.max(0, Math.min(MAXPX, sm.target + (dragY - e.clientY) * 3));
dragY = e.clientY;
});
addEventListener('pointerup', () => dragY = null);
const mega = document.getElementById('mega');
const layerA = document.getElementById('layerA'), layerB = document.getElementById('layerB');
const stat = document.getElementById('stat'), hint = document.getElementById('hint');
const kb = document.getElementById('kb');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const clamp01 = t => Math.max(0, Math.min(1, t));
const seg = (p, a, b) => clamp01((p - a) / (b - a));
function frame() {
sm.current += (sm.target - sm.current) * (reduceMotion ? 1 : 0.1);
const p = sm.current / MAXPX;
/* ① 横穿:从视口右外 → 左外,跟滚动 1:1 */
const x = (0.9 - p * 1.9) * innerWidth;
/* ② 辉光信封:中段最亮。三层 text-shadow 叠出 bloom,色温偏暖白 */
const glow = Math.sin(clamp01(seg(p, 0.15, 0.75)) * Math.PI);
/* ③ 换焦:前段字实景虚,尾段字虚景实 */
const focusScene = seg(p, 0.55, 0.85); // 0=聚焦文字 1=聚焦场景
const textBlur = focusScene * 16;
const megaColor = `rgb(${242+13*glow|0},${232+23*glow|0},${213+42*glow|0})`;
mega.style.transform = `translateX(${x}px)`;
mega.style.color = megaColor;
mega.style.filter = `blur(${textBlur.toFixed(1)}px)`;
mega.style.textShadow = glow > 0.02
? `0 0 ${18*glow}px #fff8, 0 0 ${55*glow}px #ffe9c4aa, 0 0 ${130*glow}px #ffdf9e66`
: 'none';
/* 场景两层:杯垫单品 →(虚焦交叉)→ 穿在身上 */
layerA.style.opacity = 1 - focusScene;
layerA.style.filter = `blur(${(focusScene * 14).toFixed(1)}px)`;
layerB.style.opacity = focusScene;
layerB.style.filter = `blur(${((1 - focusScene) * 18).toFixed(1)}px)`;
kb.textContent = focusScene > 0.5 ? "it's wearable" : '…';
hint.classList.toggle('gone', sm.current > 60);
stat.innerHTML = `进度 <em>${(p*100).toFixed(0)}%</em> · 辉光 <em>${(glow*100).toFixed(0)}%</em> · 焦点 <em>${focusScene < 0.5 ? '文字' : '场景'}</em>`;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>
photo-rail-demo.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>横向照片滑轨 · ORYZO 编辑部画廊复现</title>
<style>
:root { --ink: #f2e8d5; --accent: #f08c2e; }
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; background: #1b120a; }
body { font-family: "DM Mono", ui-monospace, monospace; color: var(--ink); }
.amb { position: fixed; inset: 0; pointer-events: none;
background: radial-gradient(80% 70% at 50% 100%, #6e351288 0%, transparent 65%); }
/* ---- 滑轨:纵向滚动 1:1 折算成横向位移 ---- */
#rail { position: fixed; top: 50%; left: 0; translate: 0 -50%;
display: flex; gap: 4vmin; align-items: center; will-change: transform; }
.card { flex: 0 0 auto; width: min(34vmin, 300px); aspect-ratio: 3/4;
position: relative; overflow: hidden; border-radius: 2px;
box-shadow: 0 24px 70px #000b;
will-change: scale, filter; } /* scale/filter 由 JS 每帧驱动,别加 CSS transition 打架 */
.card .cap { position: absolute; left: 10px; bottom: 8px; font-size: 10px;
letter-spacing: .18em; color: #fff9; text-transform: uppercase; }
/* —— 每张"大片"都是 CSS 画的占位:渐变布光 + 软木圆盘道具 —— */
.disc { position: absolute; border-radius: 50%;
background: radial-gradient(circle at 38% 32%, #d9a266, #c98d4e 48%, #8a5a2b 85%);
box-shadow: 0 10px 30px #000a, inset 0 -8px 18px #0006; }
.disc::after { content: ''; position: absolute; inset: 15%; border-radius: 50%;
background: radial-gradient(circle at 42% 36%, #b57f42, #8f5f2e 75%);
box-shadow: inset 0 6px 14px #0007; }
.p1 { background: linear-gradient(160deg, #3d2410, #241204 70%); }
.p1 .disc { width: 46%; aspect-ratio: 1; left: 28%; top: 30%; }
.p1::after { content: ''; position: absolute; inset: 55% -10% -10% 40%;
background: radial-gradient(closest-side, #67401d, transparent); }
.p2 { background: #b3271e; } /* 糖纸红 */
.p2 .disc { width: 58%; aspect-ratio: 1; left: 21%; top: 24%; }
.p2 .wm { position: absolute; inset: 0; font-size: 22px; font-weight: 700;
color: #ffffff2e; display: grid; place-items: center;
transform: rotate(-28deg) scale(2.4); letter-spacing: .4em; }
.p3 { background: linear-gradient(200deg, #d8cfc2, #a89a86 60%, #6b5c49); }
.p3 .hat { position: absolute; left: 18%; top: 16%; width: 64%; height: 34%;
border-radius: 50% 50% 8% 8% / 90% 90% 10% 10%;
background: radial-gradient(circle at 45% 30%, #c1622c, #96431c 70%); }
.p3 .disc { width: 30%; aspect-ratio: 1; left: 35%; top: 52%; }
.p4 { background: linear-gradient(180deg, #0d0a08, #241812 65%, #38251a); }
.p4 .face { position: absolute; right: -18%; top: -6%; width: 90%; height: 112%;
border-radius: 48% 52% 46% 54% / 58% 56% 44% 42%;
background: linear-gradient(200deg, #c98f68, #8f5c3c 65%, #5c3722); }
.p4 .disc { width: 34%; aspect-ratio: 1; right: 14%; top: 26%; }
.p5 { background: linear-gradient(170deg, #8f1f14, #6e150d 70%); } /* 红毛衣口袋 */
.p5::before { content: ''; position: absolute; inset: 30% 12% 20%;
background: repeating-linear-gradient(90deg, #a5271a 0 9px, #7d1a10 9px 18px);
border-radius: 10px; box-shadow: inset 0 14px 26px #0008; }
.p5 .disc { width: 40%; aspect-ratio: 1; left: 30%; top: 38%; }
.p6 { background: linear-gradient(180deg, #cfd6ce, #9aa694 60%, #5d685a); }
.p6 .cup { position: absolute; left: 30%; top: 22%; width: 40%; height: 42%;
background: linear-gradient(180deg, #f4efe6, #d9d2c4); border-radius: 6px 6px 10px 10px; }
.p6 .disc { width: 44%; aspect-ratio: 1; left: 28%; top: 62%; }
/* WARNING 缎带:玩梗照片上的"专业人士操作"贴条 */
.warn { position: absolute; left: 0; right: 0; bottom: 14%; padding: 5px 8px;
background: #0d0d0d; color: #ffd83d; font-size: 9px; letter-spacing: .12em;
display: flex; gap: 6px; align-items: center; }
.warn::before { content: '⚠'; }
/* ---- 取景框:钉死在视口中央,卡片从它"底下"流过 ---- */
.viewfinder { position: fixed; left: 50%; top: 50%; translate: -50% -50%;
width: min(38vmin, 330px); aspect-ratio: 3/4; pointer-events: none;
border: 1px solid #f2e8d555; }
.viewfinder i { position: absolute; width: 10px; height: 10px; border: 1px solid #f2e8d5cc; }
.viewfinder i:nth-child(1) { left: -1px; top: -1px; border-right: 0; border-bottom: 0; }
.viewfinder i:nth-child(2) { right: -1px; top: -1px; border-left: 0; border-bottom: 0; }
.viewfinder i:nth-child(3) { left: -1px; bottom: -1px; border-right: 0; border-top: 0; }
.viewfinder i:nth-child(4) { right: -1px; bottom: -1px; border-left: 0; border-top: 0; }
.kicker { position: fixed; left: 6%; top: 42%; font-size: 12px; letter-spacing: .2em; }
.kicker b { display: block; font-size: 15px; margin-top: 6px; }
.hud { position: fixed; left: 16px; top: 14px; font-size: 12px; line-height: 1.7; letter-spacing: .06em; }
.hud b { font-size: 14px; letter-spacing: .12em; }
.hud em { font-style: normal; color: var(--accent); }
.hint { position: fixed; left: 50%; bottom: 14px; translate: -50% 0; font-size: 11px;
letter-spacing: .26em; opacity: .75; transition: opacity .4s; }
.hint.gone { opacity: 0; }
</style>
</head>
<body>
<div class="amb"></div>
<div id="rail">
<div class="card p1"><span class="disc"></span><span class="cap">Studio 001</span></div>
<div class="card p2"><span class="wm">ORYZO</span><span class="disc"></span><span class="cap">Wrapped</span></div>
<div class="card p3"><span class="hat"></span><span class="disc"></span><span class="cap">Cap fit</span>
<div class="warn">PERFORMED BY PROFESSIONALS. DO NOT ATTEMPT.</div></div>
<div class="card p4"><span class="face"></span><span class="disc"></span><span class="cap">Eye wear</span></div>
<div class="card p5"><span class="disc"></span><span class="cap">Pocket</span></div>
<div class="card p6"><span class="cup"></span><span class="disc"></span><span class="cap">Rise & grind</span></div>
</div>
<div class="viewfinder"><i></i><i></i><i></i><i></i></div>
<div class="kicker">SO PORTABLE,<b>it's wearable</b></div>
<div class="hud"><b>PHOTO RAIL</b><br><span id="stat">–</span></div>
<div class="hint" id="hint">↓ 纵向滚动 = 横向走片(滚轮 / 拖拽 / 方向键)</div>
<script>
/* ============================================================================
* 横向照片滑轨 —— oryzo.ai wearable 一幕后半段的复现
*
* 原站行为(滚动侦察逐帧观察):纵向滚动被折算成横向走片,
* 编辑部风格的玩梗照片(别帽子上、当眼罩、咬嘴里、揣口袋…)依次经过
* 视口中央那只钉死的"取景框";过框的卡片放大、点亮,离框的压暗虚化。
*
* 复现要点:
* - 滑轨 translateX = -滚动值,纵向输入 1:1 变横向位移;
* - "激活"不用 IntersectionObserver —— 每帧算卡片中心到取景框中心的
* 距离,距离→scale/亮度/饱和的连续函数,无级过渡(原站的过框感同源);
* - 所有"照片"都是 CSS 画的占位大片(渐变布光 + 软木圆盘道具),
* WARNING 缎带、糖纸红、水印这些叙事贴纸也一并致敬。
* ========================================================================== */
const rail = document.getElementById('rail');
const cards = [...rail.children];
const stat = document.getElementById('stat'), hint = document.getElementById('hint');
const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
const sm = { target: 0, current: 0 };
// 手写版 scroll-snap:每张卡固定分到 VIRT px 虚拟里程,总里程 = (卡数-1)×VIRT。
// 里程和"轨道有多长/视口有多宽"彻底解耦 —— 最后一张永远能精确走到框中心
const VIRT = 640;
let MAXPX = (cards.length - 1) * VIRT;
let lastInput = -1e9; // 空闲吸附的计时起点
const bump = px => {
sm.target = Math.max(0, Math.min(MAXPX, sm.target + px));
lastInput = performance.now();
};
addEventListener('wheel', e => { e.preventDefault(); bump(e.deltaY * (e.deltaMode === 1 ? 40 : 1)); }, { passive: false });
addEventListener('keydown', e => { // 方向键 = 正好走一张
const map = { ArrowUp: -VIRT, ArrowDown: VIRT, ArrowLeft: -VIRT, ArrowRight: VIRT, PageUp: -VIRT, PageDown: VIRT };
if (e.key in map) { e.preventDefault(); bump(map[e.key]); }
});
let dragP = null;
addEventListener('pointerdown', e => dragP = { x: e.clientX, y: e.clientY });
addEventListener('pointermove', e => {
if (!dragP) return; // 横竖拖都认,取绝对位移更大的轴
const dx = dragP.x - e.clientX, dy = dragP.y - e.clientY;
bump((Math.abs(dx) > Math.abs(dy) ? dx : dy) * 2.4);
dragP = { x: e.clientX, y: e.clientY };
});
addEventListener('pointerup', () => dragP = null);
function frame() {
// 空闲吸附:松手 250ms 后把 target 缓动到最近的整卡位 —— 静止状态永远有一张卡端正在框里
if (!dragP && performance.now() - lastInput > 250) {
const snap = Math.round(sm.target / VIRT) * VIRT;
sm.target += (snap - sm.target) * 0.12;
}
sm.current += (sm.target - sm.current) * (reduceMotion ? 1 : 0.1);
// 里程 → 卡位:段首 30% 钉在当前卡(停留一拍),后 70% smoothstep 滑向下一张。
// 任何时刻框里都有一张"正片",而不是两张卡在半路
const t = Math.min(sm.current / VIRT, cards.length - 1);
const i = Math.min(cards.length - 2, Math.floor(t));
const f = Math.min(1, t - i);
const DWELL = 0.3;
let g = f <= DWELL ? 0 : (f - DWELL) / (1 - DWELL);
g = g * g * (3 - 2 * g); // smoothstep:起停都带缓动
const railX = cards[i].offsetLeft + (cards[i + 1].offsetLeft - cards[i].offsetLeft) * g;
// 卡片同宽:让"当前插值卡位"的中心对准视口中心(也就是取景框中心)
const offset = innerWidth / 2 - cards[0].offsetWidth / 2;
rail.style.transform = `translateX(${offset - railX}px)`;
// 过框感:到取景框中心的距离 → scale / 亮度 / 饱和(连续函数,无级)
const cx = innerWidth / 2;
let active = 0, best = 1e9;
cards.forEach((c, i) => {
const r = c.getBoundingClientRect();
const d = Math.abs(r.left + r.width / 2 - cx) / innerWidth; // 0=正中
const k = Math.max(0, 1 - d * 2.4); // 靠近才起效
c.style.scale = (0.86 + k * 0.2).toFixed(3);
c.style.filter = `brightness(${(0.55 + k * 0.55).toFixed(2)}) saturate(${(0.6 + k * 0.5).toFixed(2)}) blur(${((1 - k) * 1.6).toFixed(1)}px)`;
c.style.zIndex = (k * 100) | 0;
if (d < best) { best = d; active = i; }
});
hint.classList.toggle('gone', sm.current > 60);
stat.innerHTML = `里程 <em>${sm.current.toFixed(0)}px</em> · 过框 <em>${active + 1}/${cards.length}</em> · ${cards[active].querySelector('.cap').textContent}`;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>
</body>
</html>