layout: true class: middle --- class: center middle no-title bg-black p-0 background-image: url(/images/cover.svg) #
Using a modern web to recreate
1980s horribly slow and loud loading screens ??? Notes to self: - Run spectrum on http://localhost:5000 & cache - Check `fetch` for correct url - Mobile device - airplane mode, volume up --- class: list-reset list-col text-2xl p-0 - PWAs - WASMs - Machine Learnings - Much Perf - Virtual DOM - Accessibility - Usability - Service Workers - Very fancy - .plain[`¯\\\_(ツ)\_/¯`] ??? - Performance - Usability - User experience - Accessibility - Bytes over the wire (╯°□°)╯︵ ┻━┻ --- class: stretch ![./images/jake.jpg](./images/jake.jpg) ??? Big thanks to Jake --- background-image: url(./images/80s.jpg) class: text-white text-center text-cyan text-stroke # Let's go old skool ??? Just like the old days! --- class: center retro # Content Warning --- class: content-warning middle cw cw-eye # Content Warning - Flashing imagery .hashtag[\#SZR] --- class: content-warning middle cw-ear cw - Disorienting sounds .hashtag[\#DZY] --- class: content-warning middle cw-js cw - Questionable JavaScript .hashtag[\#WAT] --- background-image: url(./images/me.jpg) class: bg-cover top text-white slide-hi # Hi. ??? I was a kid of the 80s. Obviously this is me as Mr T. In early 1980s --- class: bg-black bg-cover slide-argos background-image: url(./images/argos.jpg) ??? ZX80 came out in 1980, ZX81 in '81, for £69.95 (about £180 in today's money) --- background-image: url(./images/manic-minor.jpg) class: no-title # How programs loaded --- class: bg-white background-image: url(./images/cassette_tape_art1.png) .hidden[ # When tapes fail ] ??? Sometimes they'll unspool and make cool art like this. But most of the time you're stuck sticking a pencil in the tape to rewind it all back up. --- class: content-warning cw cw-eye cw-ear
??? They would fail **How did they go wrong?** --- class: stage ## Stage 1 # Recreate the sound --- .wave1[ ![](./images/wave-pilot.gif) ] .wave2[ ![](./images/wave-syn.gif) ] .wave3[ ![](./images/wave-binary.gif) ] ??? The audio blocks are broken up into 3 distinct sounds: 1. Pilot - ~5 seconds 2. Sync - 2 pulses 3. Data --- class: cw cw-ear .wave1[ ![](./images/wave-pilot.gif) ] .run[```js const ctx = new window.AudioContext(); const src = ctx.createOscillator(); // ~pilot tone @ 830Hz src.frequency.setTargetAtTime(830, ctx.currentTime, 0); src.connect(ctx.destination); src.start(); ```] --- # programatic .wave2[ ![](./images/wave-syn.gif) ] .wave3[ ![](./images/wave-binary.gif) ] ??? These waves represent data Data is represented in 0s and 1s. --- ![](./images/wave-explained-height.png) # Irrelevant --- ![](./images/wave-explained-zero.png) # 1 pulse --- ![](./images/wave-explained-zero.png) # 855 T ??? 855 T states A T-state is an operation. ZX Spectrum runs at 3.5Mhz - so performs 3.5million ops per second (and we'll use this later) --- ![](./images/wave-explained-zero.png) # 0.000244286 ??? 0.244ms - ~ 1/4ms --- ![](./images/wave-explained-pair.png) # Binary 0 --- ![](./images/wave-explained-one.png) # Binary 1 --- class: binary-as-audio # Binary to samples ```js const SAMPLE_RATE = 44100; // 44.1Mhz const T = 1 / 3500000; // pulse width (half a wave) in ms @ 3.5Mhz ``` --- class: binary-as-audio # Binary to samples ```js const SAMPLE_RATE = 44100; // 44.1Mhz const T = 1 / 3500000; // pulse width (half a wave) in ms @ 3.5Mhz function asSamples(length) { // round up to a whole value return (length * T * SAMPLE_RATE + 0.5) | 0; } ``` --- class: binary-as-audio # Binary to samples ```js const SAMPLE_RATE = 44100; // 44.1Mhz const T = 1 / 3500000; // pulse width (half a wave) in ms @ 3.5Mhz function asSamples(length) { // round up to a whole value return (length * T * SAMPLE_RATE + 0.5) | 0; } asSamples(855); // binary 0 pulse = 11 samples asSamples(1710); // binary 1 pulse = 22 ``` --- class: binary-as-audio # Pulse as audio ```js const ZERO = 855; // binary 0 const samples = asSample(ZERO); // 11 const zeroBit = new Float32Array(samples * 2); // two pulses populateBit(zeroBit); // result: ˉˉ\__ (2 short pulses) // [ +1, +1, +1, … -1, -1, -1 ] ``` --- ```js const res = await fetch('https://smashingmagazine.com'); const bytes = new Uint8Array(await res.arrayBuffer()); const bits = bytes.length * 8; // 8 bits in a byte const bufferSize = asSamples(ONE * 2 * bits); // 2 pulses per bit ``` --- ```js const res = await fetch('https://smashingmagazine.com'); const bytes = new Uint8Array(await res.arrayBuffer()); const bits = bytes.length * 8; // 8 bits in a byte const bufferSize = asSamples(ONE * 2 * bits); // 2 pulses per bit // create mono audioBuffer @ 44.1Hz const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const output = buffer.getChannelData(0); // modifies output buffer generateAudio(bytes, output); ``` --- ```js const res = await fetch('https://smashingmagazine.com'); const bytes = new Uint8Array(await res.arrayBuffer()); const bits = bytes.length * 8; // 8 bits in a byte const bufferSize = asSamples(ONE * 2 * bits); // 2 pulses per bit // create mono audioBuffer @ 44.1Hz const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const output = buffer.getChannelData(0); // modifies output buffer generateAudio(bytes, output); const src = ctx.createBufferSource(); src.buffer = buffer; src.start(); src.connect(ctx.destination); // connect to speaker & partAAAY ``` --- class: cw cw-ear # Your site as audio bytes --- class: cw cw-ear iframe: true
??? Binary --- # Sound. # Check. --- class: stage ## Stage 2 # Loading bars --- background-image: url(./images/z80-bars.png) class: bg-cover ??? This effect originates from early z80 machines where the display and audio uses the same pin on the chip, so the audio would cause interference, but the width of the bands gave visual reassurance that everything worked. Loading bands then became common place (and coded into the software intentionally) as a visual cue that the program was loading. --- # .text-red[×] Bytes > Canvas .text-red[×] --- # .text-green[\\*] Bytes > 👂 Processor > Canvas .text-green[\\*] --- .text-center[ # Options ] .text-2xl[ 1. AnalyserNode 2. ScriptProcessorNode 3. AudioWorklet ] --- .text-center[ # Options ] .text-2xl[ 1. AnalyserNode 2. .text-green[\\*] **ScriptProcessorNode** .text-green[\\*] 3. AudioWorklet ] ??? ScriptProcessor node is marked as depreciated as of August 2014 in favour of AudioWorklets, but AudioWorklets don't have much support at the moment. Up side though is that the audio worklet and the script processor code is nearly the same - except the AudioWorklet needs to use postMessage to communicate status. --- .m-0[ ![](/images/wave-explained-1.png) ] ```js // ... method from my class read: function (samples) { let pulse; // sample count while ((pulse = this.readPulse(samples))) { // adds to this.bitPair, and if I have 2, // add the bit to the current byte this.readBit(pulse); } render(); // send changes to render } ``` --- class: cw cw-js ![](/images/slower.png) --- class: cw cw-js ![](/images/faster.png) --- class: cw cw-js ![](/images/fast-buffer.png) --- class: cw cw-ear cw-eye # Bars from audio --- iframe: true class: cw cw-ear cw-eye
--- class: stage ## Stage 3 # Rendering screens ??? Because webp, png, jpeg? Pah. We want to render obsolete formats like the ZX Spectrum SCREEN$ memory --- class: stretch autoplay: true
--- class: bg-white ![](./images/screen.png) --- class: bg-magenta center ![](./images/attribs.gif) --- ```js // index: 0..2047, 2048..4095, 4096..6143 // slice: 0 1 2 3 (attribs) const slice = index >> 11; ``` ![](./images/shift-11.png) ??? Shift right 11 bits --- ```js const imageData = new Uint8ClampedArray(4 * 8); // 1x8 pixel array // build the pixel line based on the 8bit byte for (let i = 7; i >= 0; i--) { // determines bit for i, based on MSb const bit = (byte >> i) & 1 ? 255 : 0; imageData.set([bit, bit, bit, 255], i * 4); } const x = index % 32; // 32 = 256/8 const y = ((index >> 5) * 8) % 64 + slice * 56; // maths ¯\_(ツ)_/¯ const offset = index >> 8; // adjust y position ctx.putImageData(new ImageData(imageData, 8, 1), x * 8, y + offset); // forced sleep slows the drawing so we can see it await sleep(0); ``` --- iframe: true class: bg-white
--- class: stage ## Stage 4 # Oldifying images --- class: center ![](/images/attrib.png) # Attribute clash --- class: text-2xl list-reset top - Paint the source image into a canvas (easy) -- - Atkinson dither the image with the 15 ZX Spectrum colour map (easy…ish) -- - **Determine 8x8 paper/ink split
(…ummm)** --- iframe: true
??? Count all the colours used in a block, order by popularity and limit to two. If ink or paper is white, swap the values…apparently this helps my result .plain[`¯\\\_(ツ)\_/¯`] If the popular colour is bright, set the bright bit. But if the popular colour is black, take the bright bit from the 2nd colour. Result: **nightmare face** --- class: stage ## Stage 5 # Wiring it all up --- class: font-speccy stage # Oldify + phone camera + audio lead + listen + process audio + bars + render… ??? Rather than demo, how about we use the camera to generate this image. --- class: cw cw-ear top-code autoplay: true ``` navigator.getUserMedia() .. ```
--- class: cw cw-ear top-code slide-echo-cancellation autoplay: true ``` echoCancellation: false . ```
--- class: cw cw-ear cw-eye iframe: true
--- class: why center # Why? ??? The ZX spectrum was this magic box that I could control as a box, and that's extremely empowering. The web has given me access to amazing people willing to share their knowledge and that allowed me to revisit my past. The question is: **why not**. --- background-image: url(/images/early/v0.png) --- background-image: url(/images/early/cpu.png) --- class: bg-cover background-image: url(/images/early/flame.png) --- class: bg-white ![](/images/early/comments.png) --- ![](/images/early/little-endian.jpg) --- class: bg-contain background-image: url(/images/early/hex.png) --- class: bg-cover bg-left background-image: url(/images/early/parse-0.png) --- class: bg-cover bg-left background-image: url(/images/early/parse-1.png) --- class: bg-cover bg-left background-image: url(/images/early/parse-2.png) --- class: bg-cover background-image: url(/images/early/attrib0.png) --- class: bg-cover background-image: url(/images/early/attrib1.png) --- class: bg-cover background-image: url(/images/early/attrib2.png) --- class: bg-cover background-image: url(/images/bandersnatch.png) ??? lol - real use --- class: bg-cover background-image: url(/images/bandersnatch-code.png) ??? lol - real use --- class: bg-contain background-image: url(/images/bigger-digger.png) ??? lol - real use The inception easter egg by Matt Westcott. More info: https://mobile.twitter.com/gasmanic/status/1079164419488268288 --- background-image: url(./images/thanks.png)