Repository navigation
Expand file tree
/
Copy pathdsky.ts
More file actions
111 lines (100 loc) · 3.26 KB
/
Copy pathdsky.ts
File metadata and controls
111 lines (100 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { $, $$ } from './dom';
import { reducedMotion } from './scroll';
type Field = 'prog' | 'verb' | 'noun' | 'r1' | 'r2' | 'r3';
export interface DskyState {
prog?: string;
verb?: string;
noun?: string;
r1?: string;
r2?: string;
r3?: string;
lamps?: string[];
}
/** Drives a DSKY rendered by src/components/Dsky.astro */
export function createDsky(root: HTMLElement) {
// The registers are rewritten on every scroll frame: the cells are looked up once,
// and only the digits that change touch the DOM
const fields = new Map<Field, { sign: HTMLElement | null; cells: HTMLElement[] }>();
$$('[data-field]', root).forEach((el) =>
fields.set(el.dataset.field as Field, { sign: $('.sign', el), cells: $$('.d:not(.sign)', el) }),
);
const lamps = $$('[data-lamp]', root);
const keys = new Map($$('[data-key]', root).map((el) => [el.dataset.key!, el]));
const acty = $('[data-acty]', root);
let actyTimer = 0;
let pressId = 0;
let pressedKey: HTMLElement | undefined;
const clearPressed = () => {
pressId++;
if (pressedKey) {
pressedKey.classList.remove('is-pressed');
pressedKey = undefined;
}
};
const write = (el: HTMLElement, key: 'd' | 's', value: string) => {
if (el.dataset[key] !== value) el.dataset[key] = value;
};
function set(field: Field, value: string) {
const target = fields.get(field);
if (!target) return;
const { sign, cells } = target;
let digits = value;
if (sign) {
write(sign, 's', /^[+-]/.test(value) ? value[0] : '');
digits = value.replace(/^[+-]/, '');
}
const padded = digits.padStart(cells.length, ' ').slice(-cells.length);
cells.forEach((cell, i) => write(cell, 'd', padded[i]));
}
function show(state: DskyState) {
clearPressed();
for (const field of ['prog', 'verb', 'noun', 'r1', 'r2', 'r3'] as const) {
if (state[field] !== undefined) set(field, state[field]);
}
if (state.lamps) {
lamps.forEach((lamp) => lamp.classList.toggle('is-lit', state.lamps!.includes(lamp.dataset.lamp!)));
}
}
// COMP ACTY only blinks while the DSKY is on screen: the scene leaves it "computing"
// when the reader scrolls past it
let working = false;
let onScreen = false;
const blink = () => {
window.clearInterval(actyTimer);
acty?.classList.remove('is-lit');
if (!working || !onScreen || reducedMotion) return;
actyTimer = window.setInterval(() => acty?.classList.toggle('is-lit', Math.random() > 0.45), 90);
};
new IntersectionObserver(([entry]) => {
onScreen = entry.isIntersecting;
if (!onScreen) clearPressed();
blink();
}).observe(root);
/** COMP ACTY blinks while the computer is working */
function computing(on: boolean) {
working = on;
blink();
}
/** Presses a sequence of keys, as the astronaut would */
async function press(sequence: string[], delay = 170) {
clearPressed();
if (reducedMotion || !onScreen) return;
const id = pressId;
for (const k of sequence) {
const key = keys.get(k);
if (key) {
pressedKey = key;
key.classList.add('is-pressed');
}
await new Promise((r) => setTimeout(r, delay * 0.6));
if (id !== pressId) return;
if (key) {
key.classList.remove('is-pressed');
pressedKey = undefined;
}
await new Promise((r) => setTimeout(r, delay * 0.4));
if (id !== pressId) return;
}
}
return { set, show, computing, press };
}