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()}`; const path = new URL('../site/assets/living-room.svg', import.meta.url); const before = await readFile(path, 'utf8'); const start = ''; const end = ''; const content = `${start}\n ${loopMarkup}\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);