/* Copyright (C) 2024-2026 memdmp This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by Affero, Inc., at version 1. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Affero General Public License for more details. You should have received a copy of the Affero General Public License along with this program. If not, see . */ import { Animation, MultiObjectKeyframe } from '@memdmp/keyframegen'; import { biosStepInterval, biosSteps, getDelay, login, ttyLines, } from './shared.ts'; import fs from 'node:fs'; import esbuild from 'esbuild'; const anim = new Animation(); let ttyCtr = 0; const stages = [ anim.selector('.anmroot #bios'), anim.selector('.anmroot #grub'), anim.selector('.anmroot #grub-term'), anim.selector('.anmroot #openrc'), ...ttyLines.flatMap((v) => v.kind === 'clear' ? [anim.selector('.anmroot #tty-' + ttyCtr++)] : [], ), ]; const visibleStageStyles = `margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0; height: 100vh; width: 100vw; opacity: 1;`; const hiddenStageStyles = `margin-top: -99999vh; margin-bottom: 99999vh; margin-left: -99999vw; margin-right: 99999vw; height: 0px; width: 0px; opacity: 0;`; const visibleStyles = `margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0; transform: none; opacity: 1;`; const hiddenStyles = `margin-top: -99999vh; margin-bottom: 99999vh; margin-left: -99999vw; margin-right: 99999vw; transform: scaleX(0); opacity: 0;`; const altHiddenStyles = `opacity:0; transform:scaleX(0); width:0; height:0; margin-left:-100vw; margin-right:100vw;`; const altVisibleStyles = `opacity:1; transform:none; width:max-content; height:max-content; margin-left:0; margin-right:0;`; stages.forEach((v, i) => v.style(i === 0 ? visibleStageStyles : hiddenStageStyles), ); let currentStage: (typeof stages)[number]; const toStage = (stage: number) => { const oldStage = currentStage; currentStage = stages[stage]; if (typeof oldStage !== 'undefined') { oldStage.style(visibleStageStyles); currentStage.style(hiddenStageStyles); anim._internal_timeline.now += 1; oldStage.style(hiddenStageStyles); } currentStage.style(visibleStageStyles); }; type Step = (next: () => void) => void; const biosStepHandlers: Step[] = [ // Show BIOS (next) => { toStage(0); anim.in((1000 * 3) / 10, next); }, // Show Start boot option (next) => { const s = anim.selector('.anmroot #bios .start-text'); s.style(hiddenStyles); anim._internal_timeline.now += 1; s.style(visibleStyles); anim.in(1000, next); }, // Show bar parts ...new Array(biosSteps) .fill( (quantity: number, lastQuantity: number, index: number) => (next: () => void) => { const s = anim.selector(`.anmroot #bios .bar`); s.style(`${index === 0 ? hiddenStyles : visibleStyles} width: ${lastQuantity * 100}vw;`); anim._internal_timeline.now += 1; s.style(`${visibleStyles} width: ${quantity * 100}vw;`); anim.in(quantity === 1 ? 50 : biosStepInterval, next); }, ) .map((v, i, a) => v((i + 1) / a.length, i / a.length, i)), // Show bdsdxe (next) => { const s1 = anim.selector('.anmroot #bios .bdsdxe-load'); const s2 = anim.selector('.anmroot #bios .bdsdxe-start'); s1.style(hiddenStyles); s2.style(hiddenStyles); anim._internal_timeline.now += 1; s1.style(visibleStyles); anim.in(75, () => { s2.style(visibleStyles); anim.in(50, next); }); }, ]; const grubStepHandlers: Step[] = [ // Show Grub (next) => { toStage(1); anim.in(1000, next); }, // Hide 2s, show 1s (next) => { const g2s = anim.selector('.anmroot #grub .grub-2s'); const g1s = anim.selector('.anmroot #grub .grub-1s'); g2s.style('width:max-content;opacity:1;'); g1s.style('width:0;opacity:0;'); anim._internal_timeline.now += 1; g2s.style('width:0;opacity:0;'); g1s.style('width:max-content;opacity:1;'); anim.in(1000, next); }, // Show Booting Alpine Grub (next) => { toStage(2); anim.in(100, next); }, (next) => { const s = anim.selector('.anmroot #grub-term .load-kernel'); s.style(hiddenStyles); anim._internal_timeline.now += 1; s.style(visibleStyles); anim.in(200, next); }, (next) => { const s = anim.selector('.anmroot #grub-term .load-ramdisk'); s.style(hiddenStyles); anim._internal_timeline.now += 1; s.style(visibleStyles); anim.in(950, next); }, ]; const typingIndicatorFlashTime = 200; const typingIndicatorHidden = ` opacity: 0; transform: scaleX(0); `; const typingIndicatorVisible = ` opacity: 1; transform: none; `; /** Returns the last state it was in */ const idleTypingBar = ( typingBarSelector: MultiObjectKeyframe, idleFor: number, ) => { let flashingTimeCtr = 0; let isHiddenInCycle = false; typingBarSelector.style(typingIndicatorVisible); const v = () => isHiddenInCycle ? typingIndicatorHidden : typingIndicatorVisible; while ((flashingTimeCtr += typingIndicatorFlashTime) < idleFor) { if (flashingTimeCtr === typingIndicatorFlashTime) flashingTimeCtr /= 2; anim.in(flashingTimeCtr, () => { isHiddenInCycle = !isHiddenInCycle; typingBarSelector.style(v()); }); anim.in(flashingTimeCtr - 1, () => { typingBarSelector.style(v()); }); } return v(); }; const typeCharacter = ( typingBarSelector: MultiObjectKeyframe, characterSelector: MultiObjectKeyframe | undefined | null, next: () => void, characterTime: number, ) => { const characterHidden = ` transform:scaleX(0); width: 0; opacity: 0; `; const characterVisible = ` transform:none; width: max-content; opacity: 1; `; typingBarSelector.style(typingIndicatorVisible); if (characterSelector) { characterSelector.style(characterHidden); anim._internal_timeline.now += 1; characterSelector.style(characterVisible); } if (characterTime > typingIndicatorFlashTime) { const res = idleTypingBar(typingBarSelector, characterTime); anim.in(characterTime - 1, () => { typingBarSelector.style(res); }); anim.in(characterTime, () => { typingBarSelector.style(typingIndicatorHidden); }); anim.in(characterTime + 1000 / 60, next); } else { anim.in(characterTime, () => { typingBarSelector.style(typingIndicatorHidden); }); anim.in(characterTime + 1000 / 60, next); } }; const openrcStepHandlers = (multi: number): Step[] => [ // Show OpenRC (next) => { toStage(3); next(); }, ...[ // Steps 1-3 1000 / 60, 200, 1000 / 30, // Steps 4-9 1000 / 60, 1000 / 60, 4000 / 60, 1000 / 40, 400, 75, // Steps 10-15 150, 100, 85, 100, 3500 / 60, 125, // Steps 16-20 3500 / 60, 100, 150, 1000 / 60, 1000 / 60, 1000 / 60, // Steps 21-25 1000 / 85, 1000 / 45, 1000 / 60, 1000 / 60, 500, // Steps 26-30 750, 400, 100, 100, 400, // Time till typing: 1000, ] .map((v) => v / multi) .map((time, idx, arr) => (next: () => void) => { const s = anim.selector('.anmroot #openrc .openrc-boot-step-' + idx); s.style(`${hiddenStyles} max-height: 0;`); if (idx === arr.length - 1) { anim .selector('#openrc .openrc-hide-at-last-boot-step') .style('opacity:1;max-height:90vh;max-width:100vw;'); } anim._internal_timeline.now += 1; if (idx === arr.length - 1) { anim .selector('#openrc .openrc-hide-at-last-boot-step') .style('opacity:0;max-height:0;max-width:0;'); } s.style(`${visibleStyles} max-height: max-content;`); if (idx === arr.length - 1) { idleTypingBar(anim.selector('.anmroot #openrc .openrc-username-anim'), time); } anim.in(time, next); }), // Typing Username ...(() => { const typingBar = anim.selector('.anmroot #openrc .openrc-username-anim'); return login.username.split('').map((_, i) => { const character = anim.selector( `.anmroot #openrc .openrc-username .openrc-username-char.openrc-username-char-${i}`, ); return (next: () => void) => typeCharacter(typingBar, character, next, getDelay(login)); }); })(), // Hide Username Cursor (next) => { const s = anim.selector( '.anmroot #openrc .openrc-hide-at-login-prompt-username-done', ); s.style(`transform:none;width:max-content;opacity:1;`); anim._internal_timeline.now += 1; s.style(`transform:scaleX(0);width:0;opacity:0;`); next(); }, // Show Password (next) => { const s = anim.selector('.anmroot #openrc .openrc-pw-line'); s.style(`transform:scaleX(0);width:0;opacity:0;`); anim._internal_timeline.now += 1; s.style(`transform:none;width:max-content;opacity:1;`); next(); }, // Password Typing ...new Array(login.passwordLength) .fill((_idx: number) => (next: () => void) => { const s = anim.selector('.anmroot #openrc .openrc-password-anim'); const characterTime = getDelay(login); typeCharacter(s, null, next, characterTime); }) .map((v, i) => v(i)), (next) => { const s = anim.selector('.anmroot #openrc .openrc-password-anim'); s.style('opacity:0;transform:scaleX(0);'); anim.in(1, next); }, ]; const ttyStepHandlers: Step[] = [ (next) => anim.in(100, next), (next) => { const s = anim.selector(`.anmroot #openrc .ttylines-openrc`); s.style(altHiddenStyles); anim.in(1, () => { s.style(altVisibleStyles); next(); }); }, ...(() => { let isBeforeFirstClear = true; let ttyIdx = 0; return ttyLines.map((step) => (next: () => void) => { switch (step.kind) { case 'text': { const s = anim.selector( `.anmroot ${isBeforeFirstClear ? '#openrc' : '#tty-' + (ttyIdx - 1)} ${step.classes .map((v) => `.${v}`) .join('')}`, ); s.style(altHiddenStyles); anim.in(1, () => { s.style(altVisibleStyles); next(); }); break; } case 'removeNode': { const s = anim.selector( `.anmroot ${isBeforeFirstClear ? '#openrc' : '#tty-' + (ttyIdx - 1)} ${step.removedItemClassList .map((v) => `.${v}`) .join('')}`, ); s.style(altVisibleStyles); anim.in(1, () => { s.style(altHiddenStyles); next(); }); break; } case 'time': { anim.in(step.delay, next); break; } case 'cursorVisibility': { next(); break; } case 'clear': { if (isBeforeFirstClear) { isBeforeFirstClear = false; } toStage(3 + ++ttyIdx); next(); break; } } }); })(), ]; const handleSteps: Step[] = [ // (n) => { // toStage(0); // anim.in(500, n); // }, ...biosStepHandlers, ...grubStepHandlers, ...openrcStepHandlers(1), ...ttyStepHandlers, (n) => { const s = anim.selector('.anmroot #app .hidden-after-anim'); s.style(visibleStyles); anim._internal_timeline.now += 1; s.style(hiddenStyles); anim.in(1000, n); }, ]; const nextStep = () => handleSteps.shift()!(nextStep); handleSteps.push(() => { anim.style('#nonexistentelement', ''); // we done }); nextStep(); const comment = `/** * @generated * @license AGPL-1.0-ONLY * @copyright 2024 memdmp * * Copyright (C) 2024 memdmp * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */`; const exported = anim.exportToCSS(); const tail = `${stages .filter((_, i, a) => i !== a.length - 1) .map( (v) => `${v.selector} { ${hiddenStageStyles} } .ttytext-removed-after-done { display: inline-block; opacity: 0; width: 0; height: 0; margin-left: -100vw; margin-right: 100vw; } ${ttyLines .flatMap((v) => v.kind === 'text' ? [ ...v.value.map((v) => ({ kind: 'text' as const, ...v, })), ] : [], ) .flatMap((v) => [ ...(v.kind === 'text' ? [ v.colour ? `.ttytext-block.t\\${v.colour}{color:${v.colour};}` : '', v.bg ? `.ttytext-block.b\\${v.bg}{background:${v.bg};}` : '', ] : []), ]) .filter((v, i, a) => v.length !== 0 && a.indexOf(v) === i) .join('\n')} `, ) .join('')} `; fs.writeFileSync( 'src/routes/anim.css', `${comment} ${esbuild.buildSync({ stdin: { contents: `${exported} ${tail}`, loader: 'css', }, write: false, minify: false, }).outputFiles![0].text }`, ); fs.writeFileSync( 'src/routes/no-anim.css', `${comment} ${tail}`, );