Files
lemonade/src/components/BootScreen.tsx
T
jcoffey-dev 7fcb863771 Remaster: cel-shaded skin, a crowd that tracks trade, and a funk band
Two skins over one game. 1979 is the machine as it was; the remaster is
the same rules in cel-shaded vector art with a modern palette. The
simulation is untouched - only the paint differs - and the toggle sits
under the set.

The street is now driven by the game. Before prices are in, the number
of people walking on comes from the forecast; afterwards it comes from
the glasses that actually sold, so the report shows the crowd you
earned. A heat wave has them fanning themselves; a downpour leaves one
figure hurrying past under an umbrella.

Selling no longer snaps straight to the books. The stand trades for a
few seconds first, tally climbing and till filling, which is where that
crowd animation pays off. Skippable.

The synth grew a kit (pitched-sine kick, snare with body, hats), a
sweeping resonant lowpass for the bass, chords and a shuffle - enough
for a funk band. The chiptunes stay for the 1979 skin.

Boot is now a cassette load, which doubles as the gesture browsers
require before audio will play.

Fixes found on the way: the picture scaled about its centre while flex
auto-margins collapsed to zero on overflow, which pushed the report's
buttons off the bottom; and the validation line moved the button when
it appeared, so it is now reserved and faded. Sibling tabs in one
browser also keep their score tables in step.
2026-09-08 15:59:52 -07:00

103 lines
2.8 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
import { synth } from '../audio/synth'
import { Btn } from './Crt'
const LOAD_SECONDS = 4.2
/**
* Loading from cassette, the way the listing in the magazine arrived. It is
* also where the audio gets unlocked: browsers want a gesture before they
* will make a sound, and pressing PLAY is exactly that gesture.
*/
export function BootScreen({ onLoaded }: { onLoaded: () => void }) {
const [stage, setStage] = useState<'ready' | 'loading' | 'done'>('ready')
const [progress, setProgress] = useState(0)
const stopTape = useRef<(() => void) | null>(null)
useEffect(() => () => stopTape.current?.(), [])
useEffect(() => {
if (stage !== 'loading') return
const startedAt = performance.now()
let raf = 0
const frame = (now: number) => {
const p = Math.min(1, (now - startedAt) / (LOAD_SECONDS * 1000))
setProgress(p)
if (p < 1) raf = requestAnimationFrame(frame)
else {
stopTape.current?.()
setStage('done')
synth.beep()
window.setTimeout(onLoaded, 650)
}
}
raf = requestAnimationFrame(frame)
return () => cancelAnimationFrame(raf)
}, [stage, onLoaded])
const play = () => {
synth.ensure()
synth.beep()
stopTape.current = synth.tape(LOAD_SECONDS)
setStage('loading')
}
const skip = () => {
stopTape.current?.()
onLoaded()
}
return (
<div className="boot">
{/* Loader stripes bleed off both edges, the way tape loaders did. */}
{stage === 'loading' && <div className="boot-stripes" aria-hidden />}
<div className="boot-text">
<div>ATARI 8K BASIC&nbsp;&nbsp;REV. B</div>
<div>&nbsp;</div>
<div>READY</div>
<div>CLOAD</div>
{stage === 'ready' && (
<>
<div>&nbsp;</div>
<div>PRESS PLAY ON RECORDER, THEN RETURN.</div>
<div className="boot-cursor">&#9608;</div>
</>
)}
{stage !== 'ready' && (
<>
<div>&nbsp;</div>
<div>
LOADING &quot;LEMONADE&quot;
{stage === 'loading' ? '.'.repeat(1 + Math.floor(progress * 12)) : ''}
</div>
{stage === 'done' && (
<>
<div>&nbsp;</div>
<div>READY</div>
<div className="boot-cursor">&#9608;</div>
</>
)}
</>
)}
</div>
<div className="boot-controls">
{stage === 'ready' ? (
<Btn kind="primary" onClick={play}>
&#9658; PRESS PLAY
</Btn>
) : (
stage === 'loading' && (
<Btn kind="ghost" onClick={skip}>
SKIP THE LOAD
</Btn>
)
)}
</div>
</div>
)
}