/* 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'; 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 output = `/** * @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 . */ ${anim.exportToCSS()} ${ 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} { animation-duration: 0.01ms; }`).join('\n')} `; console.log(output);