/* 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 . */ 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('#bios'), anim.selector('#grub'), anim.selector('#grub-term'), anim.selector('#openrc'), ...ttyLines.flatMap((v) => v.kind === 'clear' ? [anim.selector('#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('#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(`#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('#bios .bdsdxe-load'); const s2 = anim.selector('#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('#grub .grub-2s'); const g1s = anim.selector('#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('#grub-term .load-kernel'); s.style(hiddenStyles); anim._internal_timeline.now += 1; s.style(visibleStyles); anim.in(200, next); }, (next) => { const s = anim.selector('#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('#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('#openrc .openrc-username-anim'), time); } anim.in(time, next); }), // Typing Username ...(() => { const typingBar = anim.selector('#openrc .openrc-username-anim'); return login.username.split('').map((_, i) => { const character = anim.selector( `#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( '#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('#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('#openrc .openrc-password-anim'); const characterTime = getDelay(login); typeCharacter(s, null, next, characterTime); }) .map((v, i) => v(i)), (next) => { const s = anim.selector('#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(`#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( `${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( `${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('#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-3.0-OR-LATER * @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.text-\\[\\${v.colour}\\]{color:${v.colour};}` : '', v.bg ? `.ttytext-block.bg-\\[\\${v.bg}\\]{background:${v.bg};}` : '', ] : []), ]) .filter((v, i, a) => v.length !== 0 && a.indexOf(v) === i) .join('\n')} `, ) .join('')} ${[...anim.exportToObject().values()] .map((v) => `#app.skip-animation ${v.selector}`) .join(',\n')} { animation-name: none; animation-duration: 0.01ms; } `; 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}`, );