/*
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('')}
`;
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}`,
);