SVG path interpolation between topology-matched glyph shapes.
<!doctype html>
<!-- Example: masked-word-swap — vertical reel · clip-path block reveal · stage counter -->
<html data-duration="8" data-aspect="16:9"><head><style>
:root {
--cream:#f6f5f1; --cream-2:#efece4; --ink:#0a0a0a; --mute:#6b6862;
--line:#e3dfd3; --signal:#ff3b1f; --signal-2:#ff6a4a; --frame:#ffb800;
--green:#1f8a5b; --blue:#2b66ff;
}
* { box-sizing: border-box; }
body { margin:0; }
body { background: var(--ink); color: var(--cream); height: 100vh; overflow: hidden;
font-family: ui-serif, Georgia, serif; position: relative;
display: grid; grid-template-rows: auto 1fr auto; padding: 56px 80px; }
.ghost { position: absolute; left: -3vw; top: 10vh; font-size: 42vw; font-weight: 800;
letter-spacing: -0.06em; color: rgba(255,59,31,.045); pointer-events: none;
line-height: 0.85; font-family: ui-serif, Georgia, serif; will-change: transform; }
.hd { display: flex; justify-content: space-between; align-items: baseline;
font-family: ui-monospace, monospace; font-size: 11px; letter-spacing: .26em;
text-transform: uppercase; color: rgba(246,245,241,.5); position: relative; z-index: 2; }
.hd .live { color: var(--signal); display: inline-flex; align-items: center; gap: 7px; }
.hd .live::before { content:""; width: 7px; height: 7px; border-radius:50%; background: var(--signal); }
.stage { display: grid; align-content: center; gap: 14px; position: relative; z-index: 2; }
.lead { font-family: ui-monospace, monospace; font-size: 13px; letter-spacing: .22em;
text-transform: uppercase; color: rgba(246,245,241,.55); display: flex; align-items: center; gap: 14px; }
.lead .pip { width: 28px; height: 1px; background: rgba(246,245,241,.4); display: inline-block; }
.reel-wrap { overflow: hidden; height: 1.05em; position: relative; }
.reel { font-size: 200px; line-height: 1.05; letter-spacing: -0.05em; font-weight: 600;
color: var(--cream); will-change: transform; }
.reel .row { display: block; height: 1.05em; line-height: 1.05; position: relative;
color: rgba(246,245,241,.18); }
.reel .row.cur { color: var(--cream); }
.reel .row .mask { position: absolute; inset: 0; color: var(--cream);
clip-path: inset(0 100% 0 0); will-change: clip-path; }
.reel .row .mask.accent { color: var(--signal); font-style: italic; }
.foot { display: flex; justify-content: space-between; align-items: center; position: relative; z-index: 2;
font-family: ui-monospace, monospace; font-size: 11px; letter-spacing: .26em;
text-transform: uppercase; color: rgba(246,245,241,.5); }
.foot .ticks { display: flex; gap: 5px; }
.foot .ticks i { display: block; width: 5px; height: 14px; background: rgba(246,245,241,.2); }
.foot .ticks i.on { background: var(--signal); }
</style></head><body>
<div class="ghost" id="gh">HF</div>
<div class="hd">
<span>Stage <span id="stg">01</span> / 04 — role · system · output · proof</span>
<span class="live">live · t<span id="tc">0.00</span>s</span>
</div>
<div class="stage">
<div class="lead"><span class="pip"></span> Today's HyperFrame</div>
<div class="reel-wrap">
<div class="reel" id="reel">
<span class="row" data-word="Director"><span class="mask">Director</span></span>
<span class="row" data-word="Compiler"><span class="mask accent">Compiler</span></span>
<span class="row" data-word="Camera"><span class="mask">Camera</span></span>
<span class="row" data-word="Witness"><span class="mask">Witness</span></span>
</div>
</div>
<div class="lead"><span class="pip"></span> Role · system · output · proof</div>
</div>
<div class="foot">
<span>// rev. <span id="rv">000</span></span>
<span class="ticks" id="tk"></span>
<span>1920 × 1080</span>
</div>
<script>
var reelEl = document.getElementById('reel');
var rows = reelEl.children;
var N = rows.length;
var stg = document.getElementById('stg');
var tc = document.getElementById('tc');
var rv = document.getElementById('rv');
var gh = document.getElementById('gh');
var tk = document.getElementById('tk');
for (var k = 0; k < 12; k++) { var b = document.createElement('i'); tk.appendChild(b); }
var ticks = tk.children;
var t = 0;
var reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
function easeIO(x){ return x < .5 ? 4*x*x*x : 1 - Math.pow(-2*x+2,3)/2; }
function easeOut(x){ return 1 - Math.pow(1 - x, 3); }
function render() {
var tt = reduced ? 2.0 : t;
// 8s clip / 4 stages = 2s per stage. Each stage: 0.0–0.7 slide in + clip reveal, 0.7–2.0 hold.
var phase = tt / 2; // 0..4
var idx = Math.min(N - 1, Math.floor(phase));
var sub = phase - idx; // 0..1 within stage
var slide = easeIO(Math.min(1, sub / 0.42)); // slide finishes by 0.42s into stage
// Reel translates so that the active row sits on the baseline.
var y = -(idx + slide - (idx === 0 ? 0 : 0)) * 100;
// Above expression simplifies: we want the *previous* row to recede and the *current* row to enter.
// Just lerp the offset between idx-1 and idx during the slide window.
var fromIdx = Math.max(0, idx - 1);
var lerpIdx = fromIdx + (idx - fromIdx) * slide;
// translateY uses em (one row = 1.05em) — percentage would scale to the
// reel's full 4-row height and overshoot.
reelEl.style.transform = 'translateY(' + (-lerpIdx * 1.05).toFixed(3) + 'em)';
// Per-row mask reveal — the entering row's mask sweeps from 100% to 0% inset right.
for (var i = 0; i < N; i++) {
var row = rows[i];
var mask = row.querySelector('.mask');
var cur = (i === idx);
row.classList.toggle('cur', cur);
if (i < idx) {
mask.style.clipPath = 'inset(0 0 0 0)';
} else if (i === idx) {
var rev = easeOut(Math.min(1, Math.max(0, (sub - 0.18) / 0.5)));
mask.style.clipPath = 'inset(0 ' + ((1 - rev) * 100).toFixed(2) + '% 0 0)';
} else {
mask.style.clipPath = 'inset(0 100% 0 0)';
}
}
stg.textContent = String(idx + 1).padStart(2, '0');
tc.textContent = tt.toFixed(2);
rv.textContent = String(Math.floor(tt * 60)).padStart(3, '0');
// Ghost breathes — slow ambient pulse.
var br = 0.88 + 0.12 * Math.sin(tt * 0.55);
gh.style.transform = 'scale(' + br.toFixed(3) + ')';
// Footer progress ticks.
var nTicks = Math.min(ticks.length, Math.floor((tt / 8) * ticks.length + 0.5));
for (var j = 0; j < ticks.length; j++) ticks[j].classList.toggle('on', j < nTicks);
}
addEventListener('hf-seek', function(e) { t = e.detail.time; render(); });
render();
</script>
</body></html>Editorial line-by-line clip reveal with monospace ticker and accent flash.
Constrained damped-spring char stagger with decaying chromatic split.
Ink-bleed displacement with hand-drawn underline path-draw.