docs: add design tokens (draft) with generated reference and preview
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sst Ontwerp-tokens</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; }
|
||||
html { -webkit-font-smoothing: antialiased; }
|
||||
body { background: var(--c-surface-chassis); color: var(--c-ink-primary); font: 13px/18px var(--f-text); min-height: 100vh; }
|
||||
|
||||
.wrap { max-width: 1180px; margin: 0 auto; padding: var(--s-7) var(--s-5) 96px; }
|
||||
.brushed { background-image: var(--tex-brushed), linear-gradient(180deg, var(--c-surface-metalHigh), var(--c-surface-metalLow)); }
|
||||
|
||||
/* silkscreen */
|
||||
.silk { font: 500 10px/12px var(--f-chrome); letter-spacing: .09em; text-transform: uppercase; color: var(--c-ink-tertiary); }
|
||||
.silk-s { font: 500 9px/11px var(--f-chrome); letter-spacing: .10em; text-transform: uppercase; color: var(--c-ink-tertiary); }
|
||||
.head { font: 500 12px/16px var(--f-chrome); letter-spacing: .12em; text-transform: uppercase; color: var(--c-ink-primary); }
|
||||
.mono { font: 500 12px/16px var(--f-mono); font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* masthead */
|
||||
.mast { display: flex; align-items: stretch; margin-bottom: var(--s-6); background: var(--c-surface-panel); box-shadow: var(--sh-panel); border-top: 1px solid var(--c-line-seamDark); }
|
||||
.mast .id { padding: var(--s-5); flex: 1; }
|
||||
.mast h1 { font: 500 22px/26px var(--f-chrome); letter-spacing: .12em; text-transform: uppercase; margin-top: var(--s-2); }
|
||||
.mast p { color: var(--c-ink-secondary); max-width: 62ch; margin-top: var(--s-3); }
|
||||
.mast .stamp { padding: var(--s-5); border-left: 2px solid var(--c-line-seamDark); box-shadow: -1px 0 0 var(--c-line-seamLight); display: flex; flex-direction: column; justify-content: center; gap: var(--s-3); min-width: 220px; }
|
||||
.stamp .row { display: flex; align-items: center; gap: var(--s-3); }
|
||||
|
||||
section { margin-top: var(--s-7); }
|
||||
section > .sec-head { display: flex; align-items: baseline; gap: var(--s-4); margin-bottom: var(--s-4); padding-bottom: var(--s-3); border-bottom: 1px solid var(--c-line-seamDark); box-shadow: 0 1px 0 var(--c-line-seamLight); }
|
||||
.sec-head .no { font: 500 12px var(--f-mono); color: var(--c-ink-tertiary); }
|
||||
.sec-head .tk { margin-left: auto; font: 11px var(--f-mono); color: var(--c-ink-tertiary); }
|
||||
|
||||
.two { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-5); }
|
||||
@media (max-width: 860px) { .two { grid-template-columns: 1fr; } .mast { flex-direction: column; } .mast .stamp { border-left: 0; border-top: 2px solid var(--c-line-seamDark); } }
|
||||
|
||||
/* a finish is a complete, scoped set of variables */
|
||||
.finish { background: var(--c-surface-chassis); color: var(--c-ink-primary); padding: var(--s-5); border: 1px solid var(--c-line-seamDark); }
|
||||
.finish.brushed { background-image: var(--tex-brushed), linear-gradient(180deg, var(--c-surface-metalHigh), var(--c-surface-metalLow)); }
|
||||
.finish > .silk.top { display: flex; justify-content: space-between; margin-bottom: var(--s-4); }
|
||||
|
||||
/* swatches */
|
||||
.sw-group { margin-bottom: var(--s-4); }
|
||||
.sw-group .silk { margin-bottom: var(--s-2); display: block; }
|
||||
.sw { display: grid; grid-template-columns: 28px 1fr auto; align-items: center; gap: var(--s-3); padding: 3px 0; border-bottom: 1px solid var(--c-line-hairline); }
|
||||
.sw i { width: 28px; height: 18px; border: 1px solid var(--c-line-seamDark); display: block; }
|
||||
.sw b { font: 500 11px var(--f-mono); }
|
||||
.sw span { font: 11px var(--f-mono); color: var(--c-ink-tertiary); }
|
||||
|
||||
/* type */
|
||||
.ty { display: grid; grid-template-columns: 150px 1fr; gap: var(--s-4); padding: var(--s-3) 0; border-bottom: 1px solid var(--c-line-hairline); align-items: baseline; }
|
||||
.ty .spec { font: 11px/15px var(--f-mono); color: var(--c-ink-tertiary); }
|
||||
.ty .spec b { color: var(--c-ink-primary); display: block; font-weight: 500; }
|
||||
|
||||
/* spacing / radius / borders */
|
||||
.spc { display: flex; align-items: flex-end; gap: var(--s-5); flex-wrap: wrap; }
|
||||
.spc div { text-align: center; } .spc i { display: block; background: var(--c-ink-primary); height: 14px; margin-bottom: var(--s-2); }
|
||||
.rad { display: flex; gap: var(--s-5); flex-wrap: wrap; }
|
||||
.rad div { text-align: center; } .rad i { display: block; width: 56px; height: 56px; background: var(--c-surface-panel); border: 1px solid var(--c-line-seamDark); margin-bottom: var(--s-2); }
|
||||
|
||||
/* parts */
|
||||
.panel { background: var(--c-surface-panel); box-shadow: var(--sh-panel); border-top: 1px solid var(--c-line-seamDark); padding: var(--s-5); }
|
||||
.well { background: var(--c-surface-well); box-shadow: var(--sh-well); padding: var(--s-4); }
|
||||
.seam { border-top: 1px solid var(--c-line-seamDark); border-bottom: 1px solid var(--c-line-seamLight); height: 2px; }
|
||||
.key { display: inline-flex; align-items: center; justify-content: center; gap: var(--s-3); height: var(--z-control-height); min-width: var(--z-key-minWidth); padding: 0 var(--s-4);
|
||||
font: 500 11px/14px var(--f-chrome); letter-spacing: .06em; text-transform: uppercase; color: var(--c-ink-primary); cursor: pointer; user-select: none;
|
||||
background: linear-gradient(180deg, var(--c-surface-keyHigh), var(--c-surface-keyLow)); border: 1px solid var(--c-line-seamDark); border-radius: var(--r-sm);
|
||||
box-shadow: var(--sh-key-raised); transition: transform var(--m-key-release) var(--e-mechanical), box-shadow var(--m-key-release) var(--e-mechanical), background var(--m-key-release) var(--e-mechanical); }
|
||||
.key:active, .key.down { background: var(--c-surface-keyPressed); box-shadow: var(--sh-key-pressed); transform: translateY(1px); transition-duration: var(--m-key-press); }
|
||||
.key.rec { min-width: var(--z-key-recWidth); height: var(--z-key-recHeight); background: linear-gradient(180deg, var(--c-accent-record), var(--c-accent-record)); color: var(--c-ink-inverse); }
|
||||
.key.rec.down { filter: brightness(.88); }
|
||||
.key:focus-visible { outline: var(--b-focus) solid var(--c-line-focus); outline-offset: 2px; }
|
||||
.lamp { width: var(--z-lamp-diameter); height: var(--z-lamp-diameter); border-radius: var(--r-lamp); background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.55), rgba(255,255,255,0) 38%), var(--c-accent-recordUnlit);
|
||||
border: 1px solid var(--c-line-seamDark); transition: background var(--m-lamp-off), box-shadow var(--m-lamp-off); flex: none; }
|
||||
.lamp.on { background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.6), rgba(255,255,255,0) 38%), var(--c-accent-record); box-shadow: var(--sh-lamp-on); transition-duration: var(--m-lamp-on); }
|
||||
.lamp.big { width: var(--z-lamp-large); height: var(--z-lamp-large); }
|
||||
.lamp.amber.on { background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.6), rgba(255,255,255,0) 38%), var(--c-level-amber); box-shadow: none; }
|
||||
.lamp.amber { background: radial-gradient(circle at 35% 30%, rgba(255,255,255,.3), rgba(255,255,255,0) 38%), var(--c-level-amberUnlit); }
|
||||
.stack { display: flex; gap: var(--s-4); flex-wrap: wrap; align-items: center; }
|
||||
.stack.col { flex-direction: column; align-items: stretch; }
|
||||
.cap { margin-top: var(--s-2); }
|
||||
|
||||
/* paper list */
|
||||
.paper { background: var(--c-surface-paper); box-shadow: var(--sh-well); }
|
||||
.paper .r { display: grid; grid-template-columns: 76px 1fr; gap: var(--s-4); padding: var(--s-3) var(--s-4); min-height: var(--z-row-height); border-bottom: 1px solid var(--c-line-hairline); align-items: baseline; }
|
||||
.paper .r.sel { background: var(--c-surface-selection); }
|
||||
.paper .t { font: 500 12px/16px var(--f-mono); color: var(--c-ink-secondary); font-variant-numeric: tabular-nums; }
|
||||
.paper .x { font: 15px/22px var(--f-text); }
|
||||
mark { background: color-mix(in srgb, var(--c-level-amber) calc(var(--o-mark) * 100%), transparent); color: inherit; padding: 0 1px; }
|
||||
mark s { color: var(--c-ink-tertiary); text-decoration-thickness: 1px; margin-right: 4px; }
|
||||
|
||||
/* instruments */
|
||||
.rig { display: grid; grid-template-columns: auto 1fr; gap: var(--s-5); align-items: start; }
|
||||
@media (max-width: 640px) { .rig { grid-template-columns: 1fr; } }
|
||||
.vu { width: var(--m-vu-width); max-width: 100%; display: block; border-radius: var(--r-bezel); border: var(--b-bezel) solid var(--c-line-seamDark); box-shadow: var(--sh-well), 0 1px 0 var(--c-line-seamLight); background: var(--c-surface-meterFace); }
|
||||
.display { background: var(--c-surface-display); box-shadow: var(--sh-well); padding: var(--s-3) var(--s-4); display: inline-block; }
|
||||
.leds { display: flex; gap: var(--m-led-gap); }
|
||||
.leds i { width: 6px; height: 14px; background: var(--c-level-greenUnlit); border-radius: var(--r-sharp); display: block; }
|
||||
.leds i.g.on { background: var(--c-level-green); } .leds i.a { background: var(--c-level-amberUnlit); } .leds i.a.on { background: var(--c-level-amber); }
|
||||
.leds i.r { background: var(--c-accent-recordUnlit); } .leds i.r.on { background: var(--c-accent-record); }
|
||||
|
||||
ul.rules { list-style: none; padding: 0; display: grid; gap: var(--s-3); max-width: 70ch; }
|
||||
ul.rules li { padding-left: var(--s-5); position: relative; color: var(--c-ink-secondary); }
|
||||
ul.rules li::before { content: ""; position: absolute; left: 0; top: 8px; width: 8px; height: 1px; background: var(--c-ink-primary); }
|
||||
@media (prefers-reduced-motion: reduce) { * { transition-duration: 0ms !important; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script id="tokens" type="application/json">/*TOKENS_JSON*/</script>
|
||||
|
||||
<div class="wrap" id="root">
|
||||
|
||||
<header class="mast">
|
||||
<div class="id">
|
||||
<div class="silk">Ontwerpsysteem · Sst</div>
|
||||
<h1>Ontwerp-tokens</h1>
|
||||
<p id="concept"></p>
|
||||
</div>
|
||||
<div class="stamp">
|
||||
<div class="row"><span class="lamp amber on"></span><span class="silk">Ter goedkeuring</span></div>
|
||||
<div class="mono" id="ver"></div>
|
||||
<div class="silk-s">Bron · Design/tokens.json</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">01</span><span class="head">Instrumenten in gebruik</span><span class="tk">meter.* · motion.needle.* · segment.*</span></div>
|
||||
<div class="two">
|
||||
<div class="finish" data-finish="light"><div class="silk top"><span>Afwerking · Aluminium</span><span>licht</span></div><div class="rig-host"></div></div>
|
||||
<div class="finish" data-finish="dark"><div class="silk top"><span>Afwerking · Graphite</span><span>donker</span></div><div class="rig-host"></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">02</span><span class="head">Kleur</span><span class="tk">color.*</span></div>
|
||||
<div class="two" id="colors"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">03</span><span class="head">Diepte en materiaal</span><span class="tk">shadow.* · border.* · texture.*</span></div>
|
||||
<div class="two">
|
||||
<div class="finish brushed" data-finish="light" data-depth></div>
|
||||
<div class="finish brushed" data-finish="dark" data-depth></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">04</span><span class="head">Typografie</span><span class="tk">font.* · type.*</span></div>
|
||||
<div id="type" class="panel"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">05</span><span class="head">Ruimte, hoeken, randen</span><span class="tk">space.* · radius.* · border.*</span></div>
|
||||
<div class="panel stack col" style="gap:24px">
|
||||
<div><div class="silk cap">Ruimte (pt)</div><div class="spc" id="spc"></div></div>
|
||||
<div class="seam"></div>
|
||||
<div><div class="silk cap">Hoekradius</div><div class="rad" id="rad"></div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">06</span><span class="head">Beweging</span><span class="tk">motion.*</span></div>
|
||||
<div class="two">
|
||||
<div class="panel">
|
||||
<div class="silk cap">Toets en lampje</div>
|
||||
<div class="stack" style="margin-top:12px"><button class="key" type="button">Stop</button><button class="key" type="button" id="lampbtn">Lampje</button><span class="lamp big" id="lampdemo"></span></div>
|
||||
<p style="margin-top:12px;color:var(--c-ink-secondary)">Toets: 50 ms in, 90 ms uit, zonder veer. Lampje: 30 ms aan, 200 ms uit — dooft als een gloeidraad.</p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="silk cap">Wijzer-ballistiek</div>
|
||||
<div class="stack" style="margin-top:12px"><button class="key" type="button" id="step">Stap naar 0 VU</button><button class="key" type="button" id="stepHigh">Stap naar +3 VU</button></div>
|
||||
<p style="margin-top:12px;color:var(--c-ink-secondary)">Veer: 300 ms tot 99 %, demping 0,80 → ongeveer 1,5 % overschot. De meters bij 01 gebruiken dezelfde waarden.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="sec-head"><span class="no">07</span><span class="head">Wat het niet is</span></div>
|
||||
<ul class="rules">
|
||||
<li>Geen neon, vaporwave, synthwave, paars-roze verlopen, gloeiende tekst, chromen letters of rasterhorizonten.</li>
|
||||
<li>Eén accent: het rode opnamelampje. Amber en groen alleen als niveau-indicator of markering, nooit als versiering.</li>
|
||||
<li>Lampjes mogen een kleine gloed hebben. Tekst nooit.</li>
|
||||
<li>Geen eenmalige waarden in componenten: elke afmeting, kleur en tijd komt uit deze tokens.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const T = JSON.parse(document.getElementById('tokens').textContent);
|
||||
const $ = (s, r = document) => r.querySelector(s);
|
||||
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
|
||||
const kebab = n => n.replace(/\./g, '-');
|
||||
const rgba = (hex, a) => { const h = hex.slice(1); return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`; };
|
||||
const shadow = (tok, th) => tok.layers.map(l => `${l.inset ? 'inset ' : ''}${l.x}px ${l.y}px ${l.blur}px ${l.spread}px ${rgba(l.color, l.alpha[th])}`).join(', ');
|
||||
const grain = th => {
|
||||
const g = T.texture.brushed.grain[th];
|
||||
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='320' height='320'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.004 0.85' numOctaves='2' seed='7'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1.4 -0.2'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='${g * 9}'/></svg>`;
|
||||
return `url("data:image/svg+xml;utf8,${svg}")`;
|
||||
};
|
||||
|
||||
function vars(th) {
|
||||
const v = {};
|
||||
for (const [n, c] of Object.entries(T.color)) v['--c-' + kebab(n)] = c[th];
|
||||
for (const [n, s] of Object.entries(T.shadow)) v['--sh-' + kebab(n)] = shadow(s, th);
|
||||
for (const [n, o] of Object.entries(T.opacity)) v['--o-' + n] = o.value;
|
||||
for (const [n, s] of Object.entries(T.space)) v['--s-' + n] = s + 'px';
|
||||
for (const [n, r] of Object.entries(T.radius)) v['--r-' + n] = r.value + 'px';
|
||||
for (const [n, b] of Object.entries(T.border)) v['--b-' + n] = b.value + 'px';
|
||||
for (const [n, s] of Object.entries(T.size)) v['--z-' + kebab(n)] = s.value + 'px';
|
||||
for (const [n, f] of Object.entries(T.font)) v['--f-' + n.replace('family.', '')] = `"${f.value}", ${f.fallback}`;
|
||||
for (const [n, m] of Object.entries(T.motion)) {
|
||||
if (m.unit === 'ms') v['--m-' + kebab(n)] = m.value + 'ms'; else if (typeof m.value === 'string' && m.value.startsWith('cubic')) v['--e-' + n.replace('easing.', '')] = m.value;
|
||||
}
|
||||
v['--m-vu-width'] = T.meter['vu.width'].value + 'px';
|
||||
v['--m-led-gap'] = T.meter['led.gap'].value + 'px';
|
||||
v['--tex-brushed'] = grain(th);
|
||||
return v;
|
||||
}
|
||||
function apply(el, th) { for (const [k, val] of Object.entries(vars(th))) el.style.setProperty(k, val); el.dataset.finish = th; }
|
||||
apply(document.body, 'light');
|
||||
$$('.finish').forEach(el => apply(el, el.dataset.finish));
|
||||
|
||||
$('#concept').textContent = T.meta.concept;
|
||||
$('#ver').textContent = 'v' + T.meta.version;
|
||||
|
||||
/* ---- 02 colour swatches ---- */
|
||||
const groups = {};
|
||||
for (const [n, c] of Object.entries(T.color)) (groups[n.split('.')[0]] ??= []).push([n, c]);
|
||||
const groupLabel = { surface: 'Oppervlak', ink: 'Inkt', accent: 'Accent', level: 'Niveau', line: 'Lijnen', display: 'Display' };
|
||||
for (const th of ['light', 'dark']) {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'finish'; apply(box, th);
|
||||
box.innerHTML = `<div class="silk top"><span>Afwerking · ${th === 'light' ? 'Aluminium' : 'Graphite'}</span><span>${th === 'light' ? 'licht' : 'donker'}</span></div>` +
|
||||
Object.entries(groups).map(([g, items]) => `<div class="sw-group"><span class="silk">${groupLabel[g] || g}</span>` +
|
||||
items.map(([n, c]) => `<div class="sw" title="${c.use}"><i style="background:${c[th]}"></i><b>${n}</b><span>${c[th]}</span></div>`).join('') + '</div>').join('');
|
||||
$('#colors').appendChild(box);
|
||||
}
|
||||
|
||||
/* ---- 03 depth ---- */
|
||||
$$('[data-depth]').forEach(el => {
|
||||
el.innerHTML = `
|
||||
<div class="silk top"><span>Afwerking · ${el.dataset.finish === 'light' ? 'Aluminium' : 'Graphite'}</span><span>geborsteld metaal</span></div>
|
||||
<div class="panel stack col" style="gap:20px">
|
||||
<div class="stack"><button class="key" type="button">Toets</button><button class="key down" type="button">Ingedrukt</button><button class="key rec" type="button"><span class="lamp on"></span>Rec</button><button class="key rec down" type="button">Rec</button></div>
|
||||
<div class="seam"></div>
|
||||
<div class="stack" style="align-items:stretch"><div class="well" style="flex:1"><div class="silk">Verzonken vlak</div><div class="mono" style="margin-top:6px">well</div></div><div class="stack col" style="gap:8px;justify-content:center"><div class="stack"><span class="lamp big on"></span><span class="silk-s">Lamp aan</span></div><div class="stack"><span class="lamp big"></span><span class="silk-s">Lamp uit</span></div></div></div>
|
||||
<div class="paper"><div class="r"><span class="t">14:02:11</span><span class="x">Ik wil de meeting verplaatsen naar tomorrow morning.</span></div>
|
||||
<div class="r sel"><span class="t">14:03:40</span><span class="x">Hij stuurt het door naar <mark><s>cloud code</s>Claude Code</mark> vandaag.</span></div></div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
/* ---- 04 type ---- */
|
||||
const fam = f => `'${T.font['family.' + f].value}', ${T.font['family.' + f].fallback}`;
|
||||
const samples = { label: 'Opnameniveau', labelSmall: 'VU · dB', control: 'Start opname', heading: 'Transcripties', body: 'Doorzoek je eerdere transcripties en woordenboek.', reading: 'Ik wil de meeting verplaatsen naar tomorrow morning, want de deadline komt eraan.', meta: 'Gisteren, 14:02 · 8,4 s', counter: '00:12.4', counterSmall: '14:02:11', code: 'cloud code => Claude Code' };
|
||||
$('#type').innerHTML = Object.entries(T.type).map(([n, t]) => `
|
||||
<div class="ty"><div class="spec"><b>type.${n}</b>${t.family} ${t.size}/${t.line} · ${t.weight}${t.caps ? ' · CAPS' : ''}${t.tracking ? ' · +' + t.tracking + 'em' : ''}</div>
|
||||
<div style="font:${t.weight} ${t.size}px/${t.line}px ${fam(t.family)};letter-spacing:${t.tracking}em;${t.caps ? 'text-transform:uppercase;' : ''}${t.tabular ? 'font-variant-numeric:tabular-nums;' : ''}">${samples[n] || n}</div></div>`).join('');
|
||||
|
||||
/* ---- 05 spacing / radius ---- */
|
||||
$('#spc').innerHTML = Object.entries(T.space).map(([n, v]) => `<div><i style="width:${v || 1}px"></i><span class="mono">${n} · ${v}</span></div>`).join('');
|
||||
$('#rad').innerHTML = Object.entries(T.radius).map(([n, r]) => `<div><i style="border-radius:${Math.min(r.value, 28)}px"></i><span class="mono">${n} · ${r.value >= 999 ? 'rond' : r.value}</span></div>`).join('');
|
||||
|
||||
/* ---- instruments ---- */
|
||||
const VU = T.meter;
|
||||
const vuMin = VU['vu.min'].value, vuMax = VU['vu.max'].value, arc = VU['vu.arcDegrees'].value;
|
||||
const W = VU['vu.width'].value, H = VU['vu.height'].value;
|
||||
const pivot = { x: W / 2, y: H + 22 };
|
||||
const frac = v => Math.max(0, Math.min(1.03, (v - vuMin) / (vuMax - vuMin)));
|
||||
const ang = f => (-arc / 2 + arc * f) * Math.PI / 180;
|
||||
const pt = (r, a) => [pivot.x + r * Math.sin(a), pivot.y - r * Math.cos(a)];
|
||||
const f2 = n => n.toFixed(2);
|
||||
|
||||
function meterSvg() {
|
||||
const R = 142;
|
||||
let s = `<svg class="vu" viewBox="0 0 ${W} ${H}" role="img" aria-label="VU-meter" style="aspect-ratio:${W}/${H}">`;
|
||||
s += `<rect width="${W}" height="${H}" fill="var(--c-surface-meterFace)"/>`;
|
||||
const redFrom = frac(0);
|
||||
const arcPath = (r0, f0, f1) => { const [x0, y0] = pt(r0, ang(f0)), [x1, y1] = pt(r0, ang(f1)); return `M${f2(x0)} ${f2(y0)} A${r0} ${r0} 0 0 1 ${f2(x1)} ${f2(y1)}`; };
|
||||
s += `<path d="${arcPath(R, 0, redFrom)}" stroke="var(--c-ink-onMeter)" stroke-width="1" fill="none"/>`;
|
||||
s += `<path d="${arcPath(R - 2, redFrom, 1)}" stroke="var(--c-accent-record)" stroke-width="5" fill="none"/>`;
|
||||
for (let v = vuMin; v <= vuMax; v++) {
|
||||
const major = [-20, -10, -7, -5, -3, 0, 3].includes(v), a = ang(frac(v));
|
||||
const [x0, y0] = pt(R, a), [x1, y1] = pt(R - (major ? 11 : 6), a);
|
||||
s += `<line x1="${f2(x0)}" y1="${f2(y0)}" x2="${f2(x1)}" y2="${f2(y1)}" stroke="${v > 0 ? 'var(--c-accent-record)' : 'var(--c-ink-onMeter)'}" stroke-width="${major ? 1.4 : 1}"/>`;
|
||||
if (major) { const [tx, ty] = pt(R - 24, a); s += `<text x="${f2(tx)}" y="${f2(ty + 3)}" text-anchor="middle" font-family="${fam('chrome')}" font-weight="500" font-size="9" letter-spacing=".5" fill="${v > 0 ? 'var(--c-accent-record)' : 'var(--c-ink-onMeter)'}">${v > 0 ? '+' + v : v}</text>`; }
|
||||
}
|
||||
s += `<text x="${W / 2}" y="${H - 30}" text-anchor="middle" font-family="${fam('chrome')}" font-weight="500" font-size="15" letter-spacing="3" fill="var(--c-ink-onMeter)">VU</text>`;
|
||||
s += `<text x="14" y="${H - 10}" font-family="${fam('chrome')}" font-weight="500" font-size="8" letter-spacing="1.2" fill="var(--c-ink-onMeter)" opacity=".75">SST · FIELD RECORDER</text>`;
|
||||
s += `<line class="needle" x1="${pivot.x}" y1="${pivot.y}" x2="${pivot.x}" y2="${pivot.y - 146}" stroke="var(--c-ink-onMeter)" stroke-width="${VU['vu.needleWidth'].value}" stroke-linecap="butt"/>`;
|
||||
s += `<circle cx="${W - 18}" cy="18" r="5" class="peak" fill="var(--c-accent-recordUnlit)" stroke="var(--c-ink-onMeter)" stroke-width=".8"/><text x="${W - 18}" y="34" text-anchor="middle" font-family="${fam('chrome')}" font-size="7" font-weight="500" letter-spacing=".8" fill="var(--c-ink-onMeter)">PEAK</text>`;
|
||||
s += `</svg>`;
|
||||
return s;
|
||||
}
|
||||
|
||||
const SEG = T.segment, DW = SEG.digitWidth.value, DH = SEG.digitHeight.value, TH = SEG.thickness.value, GP = SEG.gap.value;
|
||||
const MAP = { 0: 'abcdef', 1: 'bc', 2: 'abdeg', 3: 'abcdg', 4: 'bcfg', 5: 'acdfg', 6: 'acdefg', 7: 'abc', 8: 'abcdefg', 9: 'abcdfg' };
|
||||
function hbar(x0, x1, cy) { const h = TH / 2; return `${x0},${cy} ${x0 + h},${cy - h} ${x1 - h},${cy - h} ${x1},${cy} ${x1 - h},${cy + h} ${x0 + h},${cy + h}`; }
|
||||
function vbar(cx, y0, y1) { const h = TH / 2; return `${cx},${y0} ${cx + h},${y0 + h} ${cx + h},${y1 - h} ${cx},${y1} ${cx - h},${y1 - h} ${cx - h},${y0 + h}`; }
|
||||
function digit(ch, ox, lit) {
|
||||
const h = TH / 2, L = h, Rr = DW - h, mid = DH / 2, g = GP;
|
||||
const P = { a: hbar(L + h + g, Rr - h - g, h), g: hbar(L + h + g, Rr - h - g, mid), d: hbar(L + h + g, Rr - h - g, DH - h),
|
||||
f: vbar(L, h + h + g, mid - h - g), b: vbar(Rr, h + h + g, mid - h - g), e: vbar(L, mid + h + g, DH - h - h - g), c: vbar(Rr, mid + h + g, DH - h - h - g) };
|
||||
const on = MAP[ch] || '';
|
||||
return `<g transform="translate(${ox},0)">` + Object.entries(P).map(([k, pts]) => `<polygon points="${pts}" fill="${lit && on.includes(k) ? 'var(--c-level-amber)' : 'var(--c-display-segmentUnlit)'}"/>`).join('') + `</g>`;
|
||||
}
|
||||
function counterSvg(text) {
|
||||
const adv = DW + 6; let x = 0, out = '';
|
||||
for (const ch of text) {
|
||||
if (ch === ':' || ch === '.') { out += `<rect x="${x + 2}" y="${ch === ':' ? DH * .28 : DH - 5}" width="4" height="4" fill="var(--c-level-amber)"/>` + (ch === ':' ? `<rect x="${x + 2}" y="${DH * .64}" width="4" height="4" fill="var(--c-level-amber)"/>` : ''); x += 10; }
|
||||
else { out += digit(ch, x, true); x += adv; }
|
||||
}
|
||||
return `<svg width="${x}" height="${DH}" viewBox="0 0 ${x} ${DH}" role="img" aria-label="Teller ${text}">${out}</svg>`;
|
||||
}
|
||||
const ghost8 = () => counterSvg('88:88.8').replace(/var\(--c-level-amber\)/g, 'var(--c-display-segmentUnlit)');
|
||||
|
||||
const rigs = [];
|
||||
$$('.rig-host').forEach(host => {
|
||||
host.innerHTML = `
|
||||
<div class="rig">
|
||||
<div>${meterSvg()}<div class="silk-s" style="margin-top:6px">Niveau · 0 VU = ${VU['vu.referenceDbfs'].value} dBFS</div></div>
|
||||
<div class="stack col" style="gap:14px">
|
||||
<div><div class="silk-s" style="margin-bottom:6px">Tijd</div>
|
||||
<div class="display"><div style="position:relative"><div class="ghost" style="position:absolute;inset:0">${ghost8()}</div><div class="live" style="position:relative"></div></div></div></div>
|
||||
<div><div class="silk-s" style="margin-bottom:6px">Niveau</div><div class="leds">${Array.from({length: 12}, (_, i) => `<i class="${i < 8 ? 'g' : i < 11 ? 'a' : 'r'}"></i>`).join('')}</div></div>
|
||||
<div class="stack"><button class="key rec down" type="button" data-rec><span class="lamp on"></span>Rec</button><button class="key" type="button" data-stop>Stop</button></div>
|
||||
</div>
|
||||
</div>`;
|
||||
rigs.push({ host, needle: $('.needle', host), peak: $('.peak', host), live: $('.live', host), leds: $$('.leds i', host), rec: $('[data-rec]', host), stop: $('[data-stop]', host), lamp: $('[data-rec] .lamp', host), x: 0, v: 0, target: -40, peakUntil: 0, running: true, t0: performance.now(), mode: 'speech' });
|
||||
});
|
||||
|
||||
/* spring: critically-parametrised second-order system, 99% settling = needle.response */
|
||||
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const zeta = reduce ? 1 : T.motion['needle.damping'].value, resp = T.motion['needle.response'].value;
|
||||
const wn = 4.6 / (zeta * resp);
|
||||
function envelope(t) {
|
||||
const cyc = t % 9;
|
||||
if (cyc > 6.2) return -40;
|
||||
const syl = Math.floor(t * 4.2);
|
||||
const r = Math.sin(syl * 12.9898) * 43758.5453; const rnd = r - Math.floor(r);
|
||||
const gap = Math.sin(t * 4.2 * Math.PI) > -0.55 ? 0 : -25;
|
||||
return rnd > .12 ? (-15 + rnd * 16.5 + gap) : -40;
|
||||
}
|
||||
let last = performance.now();
|
||||
function frame(now) {
|
||||
const dt = Math.min(.05, (now - last) / 1000); last = now;
|
||||
for (const r of rigs) {
|
||||
const t = (now - r.t0) / 1000;
|
||||
if (r.mode === 'speech') r.target = r.running ? envelope(t) : -40;
|
||||
const tgt = frac(r.target);
|
||||
const a = wn * wn * (tgt - r.x) - 2 * zeta * wn * r.v;
|
||||
r.v += a * dt; r.x += r.v * dt;
|
||||
const ang_ = ang(Math.max(-0.02, Math.min(1.05, r.x)));
|
||||
const [nx, ny] = pt(146, ang_);
|
||||
r.needle.setAttribute('x2', f2(nx)); r.needle.setAttribute('y2', f2(ny));
|
||||
const vuNow = vuMin + (vuMax - vuMin) * r.x;
|
||||
if (vuNow > 0 && r.running) r.peakUntil = now + VU['peak.holdMs'].value;
|
||||
r.peak.setAttribute('fill', now < r.peakUntil ? 'var(--c-accent-record)' : 'var(--c-accent-recordUnlit)');
|
||||
const lit = Math.round(Math.max(0, Math.min(1, (vuNow - vuMin) / (vuMax - vuMin))) * 12);
|
||||
r.leds.forEach((l, i) => l.classList.toggle('on', i < lit));
|
||||
const secs = r.running ? t : (r.frozen ?? 0); if (r.running) r.frozen = t;
|
||||
const mm = String(Math.floor(secs / 60) % 100).padStart(2, '0'), ss = String(Math.floor(secs % 60)).padStart(2, '0'), d = Math.floor((secs * 10) % 10);
|
||||
const txt = `${mm}:${ss}.${d}`;
|
||||
if (r.txt !== txt) { r.live.innerHTML = counterSvg(txt); r.txt = txt; }
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
rigs.forEach(r => {
|
||||
r.stop.onclick = () => { r.running = false; r.rec.classList.remove('down'); r.lamp.classList.remove('on'); };
|
||||
r.rec.onclick = () => { if (!r.running) { r.running = true; r.t0 = performance.now(); r.rec.classList.add('down'); r.lamp.classList.add('on'); } };
|
||||
});
|
||||
|
||||
/* ---- 06 motion demos ---- */
|
||||
$('#lampbtn').onclick = () => $('#lampdemo').classList.toggle('on');
|
||||
const stepTo = v => rigs.forEach(r => { r.mode = 'manual'; r.running = false; r.target = v; setTimeout(() => { r.target = -40; setTimeout(() => { r.mode = 'speech'; r.running = true; r.t0 = performance.now(); r.rec.classList.add('down'); r.lamp.classList.add('on'); }, 1800); }, 1800); r.rec.classList.remove('down'); r.lamp.classList.remove('on'); });
|
||||
$('#step').onclick = () => stepTo(0);
|
||||
$('#stepHigh').onclick = () => stepTo(3);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user