The homepage and README host now holds the prop that matches the television: a gamepad for the RPG, a paintbrush and palette for drawing, a camera for photos, and a remote for the film. Small gestures and prop visibility use the existing SVG activity timeline; the still image retains the gamepad. Both README languages reuse the same asset. This phase changes only the standalone website, its generator/check, README descriptions and the completed TODO entry. Validation: website build and deterministic SVG check; 14 cut/loop boundary samples, all four shared poses, still/reduced-motion and mobile layout; TypeScript, 59 test files / 776 tests, production Web build and repository hygiene.
58 lines
3.6 KiB
JavaScript
58 lines
3.6 KiB
JavaScript
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { HERO_KINDS, HERO_CUT, heroFrame, heroMarkup } from '../site/assets/activities.js';
|
|
|
|
// The image stays self-contained for README/img consumers. Bake the shared
|
|
// activity poses with interpolation between samples.
|
|
const LOOP = HERO_CUT * HERO_KINDS.length, SAMPLES = HERO_CUT * 12;
|
|
const span = 100 / HERO_KINDS.length;
|
|
const round = n => Math.round(n * 1000) / 1000;
|
|
const declaration = style => Object.entries(style).map(([key,value])=>`${key}:${value}`).join(';');
|
|
const tracks = new Map();
|
|
for(const [index,kind] of HERO_KINDS.entries()) {
|
|
for(let i=0;i<=SAMPLES;i++) {
|
|
const progress=i/SAMPLES;
|
|
const frame=heroFrame(kind,progress);
|
|
const percent=round((index+progress)/HERO_KINDS.length*100);
|
|
for(const [id,style] of Object.entries(frame)) {
|
|
if(!tracks.has(id)) tracks.set(id,new Map());
|
|
tracks.get(id).set(percent,declaration(style));
|
|
}
|
|
}
|
|
}
|
|
const initial = Object.assign({},...HERO_KINDS.map(kind=>heroFrame(kind,0)));
|
|
const base = Object.entries(initial).map(([id,style])=>`#${id}{${declaration(style)}}`).join('')+
|
|
HERO_KINDS.map((kind,i)=>`#activity-${kind},#hero-prop-${kind}{opacity:${i===0?1:0}}`).join('');
|
|
const keyframes = [...tracks].map(([id,track])=>{
|
|
const entries=[...track];
|
|
// Keep both ends of a hold, but remove its repeated samples. Reset at the
|
|
// loop boundary, after the last visible frame, instead of fading into it.
|
|
if(!track.has(0)) entries.unshift([0,entries[0][1]]);
|
|
if(!track.has(100)) entries.push([100,entries.at(-1)[1]]);
|
|
else entries.splice(-1,1,[99.999,entries.at(-1)[1]],[100,declaration(initial[id])]);
|
|
const compact=entries.filter((entry,i)=>i===0||i===entries.length-1||entry[1]!==entries[i-1][1]||entry[1]!==entries[i+1][1]);
|
|
return `@keyframes ${id}{${compact.map(([p,value])=>`${p}%{${value}}`).join('')}}`;
|
|
}).join('');
|
|
const cuts = HERO_KINDS.map((kind,i)=>{
|
|
const points=new Map([[0,0],[i*span,1],[(i+1)*span,0],[100,0]]);
|
|
return `@keyframes activity-${kind}{${[...points].sort((a,b)=>a[0]-b[0]).map(([p,value])=>`${p}%{opacity:${value}}`).join('')}}`;
|
|
}).join('');
|
|
// Screen and held prop use the same cut, including the still-image pose.
|
|
const animated = [...tracks.keys(),...HERO_KINDS.flatMap(kind=>['activity-'+kind,'hero-prop-'+kind])];
|
|
const animation = [...tracks.keys()].map(id=>`#${id}{animation:${id} ${LOOP}s linear infinite}`).join('')+
|
|
HERO_KINDS.map(kind=>`#activity-${kind},#hero-prop-${kind}{animation:activity-${kind} ${LOOP}s steps(1,end) infinite}`).join('');
|
|
const loopMarkup = `${heroMarkup()}<style>${base}
|
|
@media(prefers-reduced-motion:no-preference){${animation}}
|
|
${animated.map(id=>`:root:target #${id}`).join(',')}{animation:none}
|
|
${keyframes}${cuts}</style>`;
|
|
|
|
const path = new URL('../site/assets/living-room.svg', import.meta.url);
|
|
const before = await readFile(path, 'utf8');
|
|
const start = '<!-- Shared activities: generated by scripts/update-website-hero.mjs. -->';
|
|
const end = '<!-- /Shared activities -->';
|
|
const content = `${start}\n <svg x="284" y="111" width="370" height="190" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice" stroke="none" font-family="ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">${loopMarkup}</svg>\n ${end}`;
|
|
const after = before.replace(new RegExp(`${start}[\\s\\S]*?${end}`), content);
|
|
if (!before.includes(start) || !before.includes(end)) throw new Error('Missing hero activity markers');
|
|
if (process.argv.includes('--check')) {
|
|
if (before !== after) throw new Error('Run node scripts/update-website-hero.mjs to refresh the hero');
|
|
} else if (before !== after) await writeFile(path, after);
|