axstack 0.36.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +3 -2
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/installation.md +20 -41
  5. package/docs/workflows.md +12 -9
  6. package/package.json +1 -1
  7. package/profiles/presets/claude-only.json +3 -3
  8. package/profiles/presets/codex-only.json +3 -3
  9. package/profiles/presets/mixed.json +5 -4
  10. package/skills/axstack/references/role-roster.md +5 -3
  11. package/skills/axstack/references/ui-verification.md +6 -10
  12. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  13. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  14. package/skills/axstack-anatomy/LICENSE +21 -0
  15. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  16. package/skills/axstack-anatomy/README.md +316 -0
  17. package/skills/axstack-anatomy/SKILL.md +74 -0
  18. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  19. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  20. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  21. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  22. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  23. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  24. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  25. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  26. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  27. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  28. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  29. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  30. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  31. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  32. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  33. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  34. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  35. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  36. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  37. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  38. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  41. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  42. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  43. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  44. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  45. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  46. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  47. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  48. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  49. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  50. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  51. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  52. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  53. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  54. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  55. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  56. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  57. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  58. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  59. package/skills/axstack-anatomy/kit/iso.css +106 -0
  60. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  61. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  62. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  63. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  64. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  65. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  66. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  67. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  68. package/skills/axstack-anatomy/references/3d.md +352 -0
  69. package/skills/axstack-anatomy/references/craft.md +325 -0
  70. package/skills/axstack-anatomy/references/kit.md +419 -0
  71. package/skills/axstack-anatomy/references/motion.md +165 -0
  72. package/skills/axstack-anatomy/references/react.md +42 -0
  73. package/skills/axstack-anatomy/references/verify.md +351 -0
  74. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  75. package/skills/axstack-anatomy/references/webgl.md +473 -0
  76. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  77. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  78. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  79. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  80. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  81. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  82. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  83. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  84. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  85. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  86. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  87. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  88. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  89. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  90. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  91. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  92. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  93. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  94. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  95. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  96. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  97. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  98. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  99. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  100. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  101. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  102. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  103. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  104. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  105. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  106. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  107. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  108. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  109. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  110. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  111. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  113. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  114. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  115. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  116. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  117. package/skills/axstack-anatomy/theme.js +56 -0
  118. package/skills/axstack-explain/SKILL.md +28 -15
  119. package/skills/axstack-explain/references/delivery.md +22 -0
  120. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  121. package/skills/axstack-explain/references/inline-pages.md +8 -8
  122. package/skills/axstack-explain/references/visual-qa.md +3 -5
  123. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  124. package/src/archify-legacy.js +70 -0
  125. package/src/capabilities.js +1 -29
  126. package/src/installer.js +11 -31
  127. package/skills/axstack-diagram/SKILL.md +0 -28
  128. package/skills/axstack-diagram/references/archify.md +0 -76
  129. package/src/archify-pin.js +0 -5
  130. package/src/archify.js +0 -151
@@ -0,0 +1,597 @@
1
+
2
+ const D = __DATA__;
3
+ const P = D.P;
4
+ const K = { iso, pathOf, solidSvg, lineSvg, dotsSvg };
5
+ const PARTS = makeParts(K, G, P, { stick: D.stick });
6
+ const stage = document.querySelector(".iso-stage");
7
+ const svg = stage.querySelector("svg");
8
+ const $ = (sel) => svg.querySelector(sel);
9
+ const $$ = (sel) => [...svg.querySelectorAll(sel)];
10
+ const at = (p) => iso(p, P);
11
+ const readout = document.querySelector("[data-readout]");
12
+ const status = document.querySelector("[data-status]");
13
+ const still = matchMedia("(prefers-reduced-motion: reduce)");
14
+
15
+ const el = {
16
+ upper: $('[data-part="stick-upper"]'),
17
+ lower: $('[data-part="stick-lower"]'),
18
+ wire: $('[data-part="wire"]'),
19
+ fall: $('[data-part="coin-fall"]'),
20
+ chute: $('[data-part="coin-chute"]'),
21
+ fly: $('[data-part="coin-fly"]'),
22
+ pile: $('[data-part="pile"]'),
23
+ screen: $('[data-part="screen"]'),
24
+ raster: $('[data-part="raster"]'),
25
+ dot: $('[data-part="dot"]'),
26
+ test: $('[data-mode="test"]'),
27
+ attract: $('[data-mode="attract"]'),
28
+ steps: $$("[data-step]"),
29
+ formation: $('[data-part="formation"]'),
30
+ aliens: $$("[data-alien]"),
31
+ ship: $('[data-part="ship"]'),
32
+ shot: $('[data-part="shot"]'),
33
+ boom: $('[data-part="boom"]'),
34
+ score: $('[data-part="score"]'),
35
+ insert: $('[data-part="insert"]'),
36
+ start: $('[data-part="start"]'),
37
+ credit: $('[data-part="credit"]'),
38
+ meter: $('[data-part="meter"]'),
39
+ glass: $('[data-part="glass"]'),
40
+ stack: $$("[data-stack]"),
41
+ fire: $('[data-button="fire"]'),
42
+ p1: $('[data-button="p1"]'),
43
+ lamps: $$(".ac-lamp"),
44
+ leds: $$(".ac-led"),
45
+ marquee: $$('[data-name="marquee.tube"], [data-name="marquee.plexi"], .ac-marquee'),
46
+ };
47
+ const routes = {};
48
+ for (const g of $$("[data-route]")) (routes[g.dataset.route] ??= []).push(g);
49
+ for (const list of Object.values(routes)) list.sort((a, b) => a.dataset.chunk - b.dataset.chunk);
50
+
51
+ const smooth = (a, b, x) => {
52
+ const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
53
+ return t * t * (3 - 2 * t);
54
+ };
55
+ const lerp3 = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
56
+ const norm = (v) => {
57
+ const l = Math.hypot(v[0], v[1], v[2]) || 1;
58
+ return [v[0] / l, v[1] / l, v[2] / l];
59
+ };
60
+
61
+ const GRAV = 1962;
62
+ const FLY = 0.9;
63
+ const INSERT = 0.34;
64
+ const MECH = 0.42;
65
+ const DROP_FROM = D.mech.z0 + PARTS.COIN_R;
66
+ const FLOOR = D.box.z0 + 0.7 + PARTS.COIN_T / 2;
67
+ const HEATER = 2.2;
68
+ const TEST = [1.0, 1.7, 2.3, 2.8];
69
+ const ATTRACT_AT = 3.6;
70
+ const HKHZ = 15.734;
71
+
72
+ const state = {
73
+ power: 0,
74
+ powerAt: -99,
75
+ offAt: -99,
76
+ credits: 0,
77
+ meter: 4127,
78
+ coins: [],
79
+ pile: 0,
80
+ stack: D.tray.count,
81
+ stick: { x: 0, target: 0 },
82
+ fire: 0,
83
+ fireHeld: 0,
84
+ pulses: [],
85
+ wire: { x: 0, v: 0, push: 0 },
86
+ game: null,
87
+ clock: 0,
88
+ };
89
+
90
+ function newGame() {
91
+ return { step: 0, dir: 1, offset: 0, frame: 0, alive: D_FORMATION.map(() => true), shipX: 112, shot: null, boom: null, score: 0, cool: 1.2, userAt: -99, respawn: 0 };
92
+ }
93
+ const D_FORMATION = SCREEN.FORMATION;
94
+
95
+ const STACK_AT = (n) => [D.tray.x, D.tray.y, 1.6 + (n - 1) * D.tray.step + 0.2];
96
+ const SLOT_OUT = [D.slot.x, D.slot.y + 9, D.slot.z];
97
+ const SLOT_IN = [D.slot.x, D.slot.y - 12, D.slot.z];
98
+ const CH = D.chute;
99
+ const SN = 0.5;
100
+ const CS = Math.sqrt(3) / 2;
101
+ const chuteZ = (y) => CH.z0 - CH.slope * (CH.y0 - y);
102
+ const ROLL_FROM = D.mech.y0;
103
+ const ROLL_A = (2 / 3) * GRAV * SN;
104
+ const ROLL_LEN = (ROLL_FROM - CH.y1) / CS;
105
+ const T_ROLL = Math.sqrt((2 * ROLL_LEN) / ROLL_A);
106
+ const V_END = ROLL_A * T_ROLL;
107
+ const END = [D.slot.x, CH.y1 - PARTS.COIN_R * SN, chuteZ(CH.y1) + PARTS.COIN_R * CS];
108
+ const EDGE_Z = FLOOR + PARTS.COIN_R;
109
+ const T_DROP = (-V_END * SN + Math.sqrt((V_END * SN) ** 2 + 2 * GRAV * (END[2] - EDGE_Z))) / GRAV;
110
+ const LAND_Y = END[1] - V_END * CS * T_DROP;
111
+ function insertCoin() {
112
+ if (state.stack <= 0) state.stack = D.tray.count;
113
+ const from = STACK_AT(state.stack);
114
+ state.stack--;
115
+ showStack();
116
+ state.coins.push({ t: 0, from, landAt: pileSpot(state.pile + state.coins.length), switched: false, landed: false });
117
+ run();
118
+ }
119
+ function pileSpot(i) {
120
+ const a = i * 2.39996;
121
+ const r = Math.min(5, 1.1 * Math.sqrt(i));
122
+ const lo = D.box.y0 + 0.7 + PARTS.COIN_R + 0.3;
123
+ const hi = D.box.y1 - 0.7 - PARTS.COIN_R - 0.3;
124
+ return [D.slot.x + Math.cos(a) * r * 1.2, Math.max(lo, Math.min(hi, LAND_Y + Math.sin(a) * r)), FLOOR + Math.floor(i / 12) * PARTS.COIN_T];
125
+ }
126
+ function coinPose(c) {
127
+ let t = c.t;
128
+ if (t < FLY) {
129
+ const s = smooth(0, 1, t / FLY);
130
+ const mid = [(c.from[0] + SLOT_OUT[0]) / 2, (c.from[1] + SLOT_OUT[1]) / 2, Math.max(c.from[2], SLOT_OUT[2]) + 26];
131
+ const a = lerp3(c.from, mid, s);
132
+ const b = lerp3(mid, SLOT_OUT, s);
133
+ const p = lerp3(a, b, s);
134
+ const turn = smooth(0.15, 0.85, t / FLY);
135
+ return { layer: "fly", p, axis: norm(lerp3([0, 0, 1], [1, 0, 0], turn)), spin: s * 7 };
136
+ }
137
+ t -= FLY;
138
+ if (t < INSERT) return { layer: "fly", clip: "door", p: lerp3(SLOT_OUT, SLOT_IN, smooth(0, 1, t / INSERT)), axis: [1, 0, 0], spin: 7 };
139
+ t -= INSERT;
140
+ if (t < MECH) return { layer: "none", stage: "mech" };
141
+ t -= MECH;
142
+ if (t < T_ROLL) {
143
+ const s = 0.5 * ROLL_A * t * t;
144
+ const y = ROLL_FROM - s * CS;
145
+ const p = [D.slot.x, y - PARTS.COIN_R * SN, chuteZ(y) + PARTS.COIN_R * CS];
146
+ return { layer: "chute", clip: p[1] + PARTS.COIN_R > D.mech.y0 ? "mech" : null, p, axis: [1, 0, 0], spin: 7 + s / PARTS.COIN_R, stage: "roll" };
147
+ }
148
+ t -= T_ROLL;
149
+ const land = c.landAt;
150
+ if (t < T_DROP) {
151
+ const share = t / T_DROP;
152
+ const p = [END[0] + (land[0] - END[0]) * share, END[1] - V_END * CS * t, END[2] - V_END * SN * t - 0.5 * GRAV * t * t];
153
+ p[1] = END[1] + (land[1] - END[1]) * share;
154
+ return { layer: "fall", p, axis: [1, 0, 0], spin: 7 + ROLL_LEN / PARTS.COIN_R + t * 20, stage: "drop" };
155
+ }
156
+ const tl = t - T_DROP;
157
+ const tip = smooth(0, 0.16, tl);
158
+ const lie = [land[0], land[1], land[2] + PARTS.COIN_R * (1 - tip)];
159
+ return { layer: "fall", p: lie, axis: norm(lerp3([1, 0, 0], [0, 0, 1], tip)), spin: 7, settled: tl > 0.22, stage: "drop" };
160
+ }
161
+
162
+ const CLIP_DOOR = (() => {
163
+ const a = at([D.slot.x, D.slot.y, -100]);
164
+ const b = at([D.slot.x, D.slot.y, 300]);
165
+ return `M${a[0]} ${a[1]}L${b[0]} ${b[1]}L${b[0] + 400} ${b[1]}L${a[0] + 400} ${a[1]}Z`;
166
+ })();
167
+ const CLIP_MECH = (() => {
168
+ const a = at([D.slot.x, D.mech.y0, -100]);
169
+ const b = at([D.slot.x, D.mech.y0, 300]);
170
+ return `M${a[0]} ${a[1]}L${b[0]} ${b[1]}L${b[0] - 400} ${b[1]}L${a[0] - 400} ${a[1]}Z`;
171
+ })();
172
+ svg.querySelector("defs") || svg.insertAdjacentHTML("afterbegin", "<defs></defs>");
173
+ svg.querySelector("defs").insertAdjacentHTML("beforeend", `<clipPath id="ac-clip-door"><path d="${CLIP_DOOR}"/></clipPath><clipPath id="ac-clip-mech"><path d="${CLIP_MECH}"/></clipPath>`);
174
+
175
+ function drawCoins() {
176
+ const out = { fly: "", chute: "", fall: "" };
177
+ for (const c of state.coins) {
178
+ const pose = coinPose(c);
179
+ if (pose.layer === "none") continue;
180
+ const coin = PARTS.coin(pose.p, pose.axis, pose.spin);
181
+ out[pose.layer] += pose.clip ? `<g clip-path="url(#ac-clip-${pose.clip})">${coin}</g>` : coin;
182
+ }
183
+ for (const key of ["fly", "chute", "fall"]) if (el[key].innerHTML !== out[key]) el[key].innerHTML = out[key];
184
+ }
185
+ function showStack() {
186
+ el.stack.forEach((g, i) => (g.style.display = i < state.stack ? "" : "none"));
187
+ }
188
+ function drawPile() {
189
+ let out = "";
190
+ for (let i = 0; i < state.pile; i++) out += PARTS.coin(pileSpot(i), [0, 0, 1], i * 1.7, { lit: false });
191
+ el.pile.innerHTML = out;
192
+ }
193
+
194
+ function wireStep(dt, push) {
195
+ const w = state.wire;
196
+ if (push > w.x) {
197
+ w.x = push;
198
+ w.v = 0;
199
+ } else {
200
+ const k = 900;
201
+ const c = 9;
202
+ for (let i = 0; i < 4; i++) {
203
+ const h = dt / 4;
204
+ w.v += (-k * w.x - c * w.v) * h;
205
+ w.x += w.v * h;
206
+ }
207
+ if (Math.abs(w.x) < 1e-3 && Math.abs(w.v) < 1e-2) (w.x = 0), (w.v = 0);
208
+ }
209
+ const from = D.wire.from;
210
+ const angle = Math.atan2(w.x, D.wire.length);
211
+ const tip = [from[0] + D.wire.length * Math.cos(angle), from[1], from[2] - D.wire.length * Math.sin(angle)];
212
+ el.wire.innerHTML = lineSvg(pathOf([at(from), at(tip)]), { tone: w.x > 0.3 ? "lit" : "hi", free: "end" });
213
+ return w.x !== 0;
214
+ }
215
+ function wirePush() {
216
+ let push = 0;
217
+ const wz = D.wire.from[2];
218
+ const wy = D.wire.from[1];
219
+ for (const c of state.coins) {
220
+ const pose = coinPose(c);
221
+ if (pose.stage !== "roll") continue;
222
+ const dy = wy - pose.p[1];
223
+ if (Math.abs(dy) >= PARTS.COIN_R) continue;
224
+ const low = pose.p[2] - Math.sqrt(PARTS.COIN_R ** 2 - dy * dy);
225
+ push = Math.max(push, Math.min(1.2, wz - low));
226
+ }
227
+ return Math.max(0, push) * (D.wire.length / (D.slot.x - D.wire.from[0]));
228
+ }
229
+ function pulse(route, delay = 0, seconds = 0.5) {
230
+ if (!routes[route]) return;
231
+ state.pulses.push({ route, t: -delay, seconds });
232
+ }
233
+ function drawPulses(dt) {
234
+ const hot = new Set();
235
+ state.pulses = state.pulses.filter((p) => {
236
+ p.t += dt;
237
+ if (p.t < 0) return true;
238
+ const list = routes[p.route];
239
+ const at = (p.t / p.seconds) * (list.length + 1);
240
+ list.forEach((g, i) => Math.abs(i - at + 0.5) < 1.1 && hot.add(g));
241
+ return p.t < p.seconds * 1.25;
242
+ });
243
+ for (const list of Object.values(routes)) for (const g of list) g.toggleAttribute("data-hot", hot.has(g));
244
+ return state.pulses.length > 0;
245
+ }
246
+
247
+ function setPower(on) {
248
+ if (!!on === !!state.power) return;
249
+ state.power = on ? 1 : 0;
250
+ if (on) {
251
+ state.powerAt = state.clock;
252
+ state.game = newGame();
253
+ } else {
254
+ state.offAt = state.clock;
255
+ state.credits = 0;
256
+ }
257
+ el.leds.forEach((g) => g.toggleAttribute("data-on", !!on));
258
+ el.lamps.forEach((g) => g.toggleAttribute("data-on", !!on));
259
+ status.textContent = on ? "Coin accepted. The cabinet wakes and the CRT warms up." : "Switched off.";
260
+ run();
261
+ }
262
+ function marqueeLevel(t) {
263
+ if (!state.power) return 0;
264
+ if (t < 0.06) return 0;
265
+ if (t < 0.12) return 1;
266
+ if (t < 0.24) return 0;
267
+ if (t < 0.3) return 1;
268
+ if (t < 0.42) return 0.2;
269
+ return 1;
270
+ }
271
+ function picture() {
272
+ const t = state.clock - state.powerAt;
273
+ const off = state.clock - state.offAt;
274
+ let bright = 0;
275
+ let sx = 1;
276
+ let sy = 1;
277
+ let dot = 0;
278
+ let mode = "none";
279
+ if (state.power) {
280
+ bright = smooth(0.9, HEATER + 0.4, t);
281
+ mode = t < ATTRACT_AT ? (t > 0.9 ? "test" : "none") : t < ATTRACT_AT + 0.08 ? "none" : "attract";
282
+ } else if (off < 1.2 && state.offAt > 0) {
283
+ bright = 1;
284
+ mode = state.lastMode;
285
+ if (off < 0.09) sx = 1 - 0.99 * smooth(0, 0.09, off);
286
+ else if (off < 0.26) (sx = 0.01), (sy = 1 - smooth(0.09, 0.26, off));
287
+ else (sx = 0), (sy = 0);
288
+ dot = off < 0.2 ? 0 : 1 - smooth(0.26, 1.1, off);
289
+ }
290
+ if (state.power) state.lastMode = mode;
291
+ el.raster.setAttribute("transform", `scale(${Math.max(sx, 0.001).toFixed(3)} ${Math.max(sy, 0.004).toFixed(3)})`);
292
+ el.raster.style.opacity = (sx === 0 ? 0 : bright).toFixed(3);
293
+ el.dot.style.opacity = dot.toFixed(3);
294
+ el.test.style.display = mode === "test" ? "" : "none";
295
+ el.attract.style.display = mode === "attract" ? "" : "none";
296
+ el.steps.forEach((s, i) => (s.style.display = t > TEST[i] ? "" : "none"));
297
+ el.glass.toggleAttribute("data-on", bright > 0.05 && sx > 0);
298
+ const m = marqueeLevel(t);
299
+ el.marquee.forEach((g) => g.toggleAttribute("data-on", m > 0.5));
300
+ return { mode, t, busy: state.power ? t < ATTRACT_AT + 0.2 : off < 1.2 };
301
+ }
302
+
303
+ const SHOT_SPEED = 220;
304
+ function gameStep(dt, mode) {
305
+ const g = state.game;
306
+ if (!g || mode !== "attract") return;
307
+ g.step += dt;
308
+ if (g.step > 0.32) {
309
+ g.step = 0;
310
+ g.offset += g.dir * 2;
311
+ if (Math.abs(g.offset) >= 16) g.dir = -g.dir;
312
+ g.frame = 1 - g.frame;
313
+ el.formation.setAttribute("transform", `translate(${g.offset} 0)`);
314
+ for (const a of el.aliens) {
315
+ a.children[0].style.display = g.frame ? "none" : "";
316
+ a.children[1].style.display = g.frame ? "" : "none";
317
+ }
318
+ }
319
+ const user = state.clock - g.userAt < 3;
320
+ if (user) g.shipX += state.stick.x * 420 * dt;
321
+ else {
322
+ const live = D_FORMATION.map((a, i) => (g.alive[i] ? a.x + 5 + g.offset : null)).filter((x) => x !== null);
323
+ const target = live.length ? live[Math.floor((state.clock * 0.37) % live.length)] : 112;
324
+ g.shipX += Math.max(-50 * dt, Math.min(50 * dt, target - g.shipX));
325
+ }
326
+ g.shipX = Math.max(14, Math.min(210, g.shipX));
327
+ el.ship.setAttribute("transform", `translate(${g.shipX.toFixed(1)} ${SCREEN.SHIP_Y})`);
328
+ g.cool -= dt;
329
+ if (!g.shot && ((state.fire > 0 && user) || (!user && g.cool < 0))) {
330
+ g.shot = { x: Math.round(g.shipX), y: SCREEN.SHIP_Y - 6 };
331
+ g.cool = 0.9 + Math.random() * 0.8;
332
+ }
333
+ if (g.shot) {
334
+ g.shot.y -= SHOT_SPEED * dt;
335
+ let hit = -1;
336
+ D_FORMATION.forEach((a, i) => {
337
+ if (!g.alive[i] || hit >= 0) return;
338
+ const x = a.x + g.offset;
339
+ if (g.shot.x >= x && g.shot.x <= x + 10 && g.shot.y <= a.y + 8 && g.shot.y + 5 >= a.y) hit = i;
340
+ });
341
+ if (hit >= 0) {
342
+ g.alive[hit] = false;
343
+ el.aliens[hit].style.display = "none";
344
+ const a = D_FORMATION[hit];
345
+ g.boom = { x: a.x + g.offset, y: a.y, t: 0.22 };
346
+ g.score += a.kind === "eye" ? 30 : 10;
347
+ el.score.setAttribute("d", SCREEN.text(String(g.score).padStart(5, "0"), 16, 20));
348
+ g.shot = null;
349
+ } else if (g.shot.y < 30) g.shot = null;
350
+ }
351
+ el.shot.style.display = g.shot ? "" : "none";
352
+ if (g.shot) el.shot.setAttribute("transform", `translate(${g.shot.x} ${g.shot.y.toFixed(1)})`);
353
+ if (g.boom) {
354
+ g.boom.t -= dt;
355
+ el.boom.style.display = g.boom.t > 0 ? "" : "none";
356
+ el.boom.setAttribute("transform", `translate(${g.boom.x} ${g.boom.y})`);
357
+ if (g.boom.t <= 0) g.boom = null;
358
+ }
359
+ if (g.alive.every((a) => !a)) {
360
+ g.respawn += dt;
361
+ if (g.respawn > 1.2) {
362
+ g.respawn = 0;
363
+ g.alive = g.alive.map(() => true);
364
+ el.aliens.forEach((a) => (a.style.display = ""));
365
+ }
366
+ }
367
+ const blink = Math.floor(state.clock / 0.5) % 2 === 0;
368
+ el.insert.style.display = !state.credits && blink ? "" : "none";
369
+ el.start.style.display = state.credits && blink ? "" : "none";
370
+ el.p1?.toggleAttribute("data-blink", state.credits > 0 && blink);
371
+ }
372
+ let creditShown = -1;
373
+ function showCredit() {
374
+ if (creditShown === state.credits) return;
375
+ creditShown = state.credits;
376
+ el.credit.setAttribute("d", SCREEN.text(`CREDIT ${Math.min(9, state.credits)}`, 120, 274));
377
+ }
378
+
379
+ const TILT = 0.21;
380
+ let stickShown = null;
381
+ function stickStep(dt, calm) {
382
+ const s = state.stick;
383
+ const before = s.x;
384
+ s.x = calm ? s.target : s.x + (s.target - s.x) * (1 - Math.exp(-dt / 0.05));
385
+ if (Math.abs(s.x - s.target) < 1e-3) s.x = s.target;
386
+ const key = s.x.toFixed(3);
387
+ if (key !== stickShown) {
388
+ stickShown = key;
389
+ el.upper.innerHTML = PARTS.stickUpper(s.x * TILT, 0);
390
+ el.lower.innerHTML = PARTS.stickLower(s.x * TILT, 0);
391
+ }
392
+ return s.x !== s.target || before !== s.x;
393
+ }
394
+ const firePush = axisVector(P, D.stick.nc.map((v) => -v * 1.1));
395
+ function fireStep(dt) {
396
+ state.fireHeld = Math.max(0, state.fireHeld - dt);
397
+ const want = state.fireHeld > 0 ? 1 : 0;
398
+ state.fire += (want - state.fire) * (1 - Math.exp(-dt / 0.03));
399
+ if (Math.abs(want - state.fire) < 0.01) state.fire = want;
400
+ el.fire.setAttribute("transform", state.fire ? `translate(${(firePush[0] * state.fire).toFixed(2)} ${(firePush[1] * state.fire).toFixed(2)})` : "");
401
+ el.fire.toggleAttribute("data-down", state.fire > 0.5);
402
+ return state.fire !== want || state.fireHeld > 0;
403
+ }
404
+
405
+ let phase = "";
406
+ function say(mode, t) {
407
+ let text;
408
+ const coin = state.coins.find((c) => !c.landed);
409
+ if (coin) {
410
+ const tt = coin.t;
411
+ text = tt < FLY ? "coin · 24.26 mm quarter" : tt < FLY + INSERT ? "coin · into the slot" : tt < FLY + INSERT + MECH ? "mech · checking the coin" : coin.switched ? `coin switch · meter ${String(state.meter).padStart(6, "0")}` : "coin · rolling down the chute";
412
+ } else if (!state.power) text = state.clock - state.offAt < 1.2 && state.offAt > 0 ? "off · picture folds to a line" : `off · ${state.pile ? "insert another coin" : "insert coin"}`;
413
+ else if (mode === "test") text = t < TEST[1] ? "self-test" : t < TEST[2] ? "self-test · RAM OK" : "self-test · RAM OK · ROM OK";
414
+ else if (mode !== "attract") text = `warming · heater ${Math.min(HEATER, t).toFixed(1)} s`;
415
+ else {
416
+ const dir = state.stick.target < 0 ? " · stick ←" : state.stick.target > 0 ? " · stick →" : state.fireHeld > 0 ? " · fire" : "";
417
+ text = `credit ${state.credits} · ${HKHZ} kHz${dir}`;
418
+ }
419
+ if (readout.textContent !== text) readout.textContent = text;
420
+ const now = !state.power ? "off" : mode;
421
+ if (now !== phase) {
422
+ phase = now;
423
+ if (now === "attract") status.textContent = `Attract mode, ${state.credits} credit${state.credits === 1 ? "" : "s"}.`;
424
+ if (now === "test") status.textContent = "Self-test.";
425
+ }
426
+ }
427
+
428
+ let frame = 0;
429
+ let last = 0;
430
+ let visible = false;
431
+ let touring = !still.matches;
432
+ let tourClock = 0;
433
+ let idle = 0;
434
+ const TOUR = [
435
+ [1.4, () => insertCoin()],
436
+ [9.5, () => press("right", 0.5)],
437
+ [10.4, () => press("left", 0.6)],
438
+ [11.4, () => fireButton()],
439
+ [12.6, () => press("right", 0.35)],
440
+ [13.3, () => fireButton()],
441
+ [15.2, () => insertCoin()],
442
+ [24, () => setPower(0)],
443
+ [27.5, null],
444
+ ];
445
+ let tourIndex = 0;
446
+ function tourStep(dt) {
447
+ tourClock += dt;
448
+ while (tourIndex < TOUR.length && tourClock >= TOUR[tourIndex][0]) {
449
+ const action = TOUR[tourIndex][1];
450
+ tourIndex++;
451
+ if (action) action();
452
+ else {
453
+ tourClock = 0;
454
+ tourIndex = 0;
455
+ state.pile = 0;
456
+ state.stack = D.tray.count;
457
+ showStack();
458
+ drawPile();
459
+ }
460
+ }
461
+ }
462
+ let releaseTimer = 0;
463
+ function press(dir, seconds) {
464
+ state.stick.target = dir === "left" ? -1 : 1;
465
+ if (state.game) state.game.userAt = state.clock;
466
+ clearTimeout(releaseTimer);
467
+ releaseTimer = setTimeout(() => ((state.stick.target = 0), run()), seconds * 1000);
468
+ run();
469
+ }
470
+ function fireButton() {
471
+ state.fireHeld = 0.16;
472
+ if (state.game) state.game.userAt = state.clock;
473
+ run();
474
+ }
475
+
476
+ function tick(now) {
477
+ const dt = last ? Math.min((now - last) / 1000, 1 / 30) : 1 / 60;
478
+ last = now;
479
+ const calm = still.matches;
480
+ state.clock += dt;
481
+ if (touring) tourStep(dt);
482
+ for (const c of state.coins) {
483
+ c.t += calm ? 10 : dt;
484
+ const pose = coinPose(c);
485
+ if (!c.switched && (calm || (pose.stage === "roll" && pose.p[1] < D.wire.from[1]))) {
486
+ c.switched = true;
487
+ state.meter++;
488
+ el.meter.textContent = String(state.meter).padStart(6, "0");
489
+ pulse("wire.coin", 0, 0.45);
490
+ pulse("wire.meter", 0, 0.25);
491
+ if (!state.power) {
492
+ setTimeout(() => {
493
+ setPower(1);
494
+ pulse("cable.power", 0, 0.5);
495
+ pulse("cable.video", 0.5, 0.5);
496
+ }, calm ? 0 : 420);
497
+ }
498
+ setTimeout(() => {
499
+ state.credits++;
500
+ showCredit();
501
+ run();
502
+ }, calm ? 0 : 450);
503
+ }
504
+ if (!c.landed && (pose.settled || calm)) {
505
+ c.landed = true;
506
+ state.pile++;
507
+ drawPile();
508
+ }
509
+ }
510
+ state.coins = state.coins.filter((c) => !c.landed);
511
+ drawCoins();
512
+ const wireBusy = wireStep(dt, wirePush());
513
+ const pulsing = drawPulses(dt);
514
+ const { mode, t, busy } = picture();
515
+ gameStep(dt, mode);
516
+ showCredit();
517
+ const sticking = stickStep(dt, calm);
518
+ const firing = fireStep(dt);
519
+ say(mode, t);
520
+ const alive = touring || state.coins.length || wireBusy || pulsing || busy || sticking || firing || (state.power && !calm);
521
+ frame = visible && alive ? requestAnimationFrame(tick) : 0;
522
+ }
523
+ function run() {
524
+ if (!frame && visible) {
525
+ last = 0;
526
+ frame = requestAnimationFrame(tick);
527
+ }
528
+ }
529
+ function takeOver() {
530
+ if (touring) {
531
+ touring = false;
532
+ }
533
+ clearTimeout(idle);
534
+ idle = setTimeout(() => {
535
+ if (still.matches) return;
536
+ touring = true;
537
+ tourClock = state.power ? 4 : 0;
538
+ tourIndex = TOUR.findIndex(([t]) => t > tourClock);
539
+ if (state.power) tourIndex = TOUR.findIndex(([t]) => t > 9);
540
+ tourClock = state.power ? 9 : 0;
541
+ run();
542
+ }, 12000);
543
+ }
544
+ new IntersectionObserver((entries) => {
545
+ visible = entries[entries.length - 1].isIntersecting;
546
+ if (visible) run();
547
+ }, { rootMargin: "120px 0px" }).observe(stage);
548
+
549
+ stage.addEventListener("keydown", (event) => {
550
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
551
+ const key = event.key;
552
+ if (key === "c" || key === "C" || key === "5") {
553
+ event.preventDefault();
554
+ takeOver();
555
+ insertCoin();
556
+ } else if (key === "ArrowLeft" || key === "ArrowRight") {
557
+ event.preventDefault();
558
+ takeOver();
559
+ state.stick.target = key === "ArrowLeft" ? -1 : 1;
560
+ if (state.game) state.game.userAt = state.clock;
561
+ clearTimeout(releaseTimer);
562
+ run();
563
+ } else if (key === " " || key === "Enter") {
564
+ event.preventDefault();
565
+ takeOver();
566
+ state.fireHeld = 0.5;
567
+ if (state.game) state.game.userAt = state.clock;
568
+ run();
569
+ } else if (key === "Escape") {
570
+ event.preventDefault();
571
+ takeOver();
572
+ setPower(0);
573
+ }
574
+ });
575
+ stage.addEventListener("keyup", (event) => {
576
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
577
+ state.stick.target = 0;
578
+ run();
579
+ }
580
+ if (event.key === " " || event.key === "Enter") state.fireHeld = Math.min(state.fireHeld, 0.06);
581
+ });
582
+ svg.addEventListener("click", (event) => {
583
+ const hit = event.target.closest('[data-part="tray"], [data-name^="door"]');
584
+ if (hit) {
585
+ takeOver();
586
+ insertCoin();
587
+ stage.focus({ preventScroll: true });
588
+ }
589
+ });
590
+
591
+ showStack();
592
+ drawPile();
593
+ showCredit();
594
+ wireStep(0, 0);
595
+ picture();
596
+ say("none", 0);
597
+ if (still.matches) touring = false;
@@ -0,0 +1,89 @@
1
+
2
+ export function makeParts(k, G, P, D) {
3
+ const V = G.viewOf(P);
4
+ const at = (p) => k.iso(p, P);
5
+ const S = (paths, style) => k.solidSvg(paths, style);
6
+ const Ln = (d, style) => k.lineSvg(d, style);
7
+ const rot = (v, axis, angle) => {
8
+ const c = Math.cos(angle);
9
+ const s = Math.sin(angle);
10
+ const d = G.dot3(axis, v);
11
+ const x = G.cross3(axis, v);
12
+ return [v[0] * c + x[0] * s + axis[0] * d * (1 - c), v[1] * c + x[1] * s + axis[1] * d * (1 - c), v[2] * c + x[2] * s + axis[2] * d * (1 - c)];
13
+ };
14
+ const J = D.stick;
15
+ const UP = J.nc;
16
+ const ALONG = J.dc;
17
+ const X = [1, 0, 0];
18
+ const axisOf = (tx, tv) => G.unit3(rot(rot(UP, ALONG, tx), X, -tv));
19
+ const pivot = G.add3(J.o, G.mul3(UP, J.pivot));
20
+ const depth = (p) => G.dot3(p, V);
21
+
22
+ function stickUpper(tx = 0, tv = 0) {
23
+ const a = axisOf(tx, tv);
24
+ const F = G.frameAlong(pivot, a);
25
+ const t = -J.pivot / G.dot3(a, UP);
26
+ const cross = G.add3(pivot, G.mul3(a, t));
27
+ const W = G.frameOf(cross, UP, X, ALONG);
28
+ const washer = S(G.disc(0, 0.4, 4.4, W, P, { steps: 40 }), { tone: "mid" }) + Ln(G.circleOf(W, 0.4, 1.3, P, 20), { tone: "faint" });
29
+ const shaft = S(G.disc(t + 0.4, t + 8.95, 0.85, F, P, { steps: 16 }), { tone: "mid" });
30
+ const ballAt = G.add3(pivot, G.mul3(a, t + 12.4));
31
+ const ball = S(G.sphereOf(ballAt, 3.5, P, { steps: 40 }), { tone: "hi", lit: true });
32
+ return washer + shaft + ball;
33
+ }
34
+
35
+ function stickLower(tx = 0, tv = 0) {
36
+ const a = axisOf(tx, tv);
37
+ const F = G.frameAlong(pivot, a);
38
+ const parts = [];
39
+ parts.push({ d: depth(G.add3(pivot, G.mul3(a, -5))), svg: S(G.disc(-9.4, -0.6, 0.85, F, P, { steps: 16 }), { tone: "mid" }) });
40
+ const act = S(G.disc(-8.6, -4.6, 2.4, F, P, { steps: 32, bevel: 0.3 }), { tone: "hi" }) + Ln(G.arcOf(F, -6.6, 2.4, P), { tone: "faint" });
41
+ parts.push({ d: depth(G.add3(pivot, G.mul3(a, -6.6))) + 0.01, svg: act });
42
+ parts.push({ d: depth(G.add3(pivot, G.mul3(a, -9.6))), svg: S(G.disc(-10, -9.4, 1.5, F, P, { steps: 20 }), { tone: "lo" }) });
43
+ const closed = switchesClosed(tx, tv);
44
+ for (const sw of J.switches) {
45
+ const on = closed.includes(sw.id);
46
+ const body = G.frameOf(sw.c, UP, sw.out, G.cross3(UP, sw.out));
47
+ const poly = [[-0.65, -2], [0.65, -2], [0.65, 2], [-0.65, 2]];
48
+ const box = S(G.prismOf(body, poly, -1, 1, P), { tone: "mid" });
49
+ const lever0 = G.add3(sw.c, G.add3(G.mul3(sw.out, -0.65), G.mul3(G.cross3(UP, sw.out), 1.6)));
50
+ const tip = G.add3(sw.c, G.add3(G.mul3(sw.out, on ? -0.85 : -1.45), G.mul3(G.cross3(UP, sw.out), -1.2)));
51
+ const lever = Ln(k.pathOf([at(lever0), at(tip)]), { tone: on ? "lit" : "hi" });
52
+ const tabs = [-1.1, 0, 1.1].map((o) => G.add3(sw.c, G.add3(G.mul3(UP, -1), G.mul3(G.cross3(UP, sw.out), o))));
53
+ const tabSvg = tabs.map((p) => S(G.prismOf(G.frameOf(p, UP, sw.out, G.cross3(UP, sw.out)), [[-0.3, -0.35], [0.3, -0.35], [0.3, 0.35], [-0.3, 0.35]], -1, 0, P), { tone: "lo" })).join("");
54
+ parts.push({ d: depth(sw.c), svg: `<g class="ac-switch"${on ? " data-on" : ""}>${tabSvg}${box}${lever}</g>` });
55
+ }
56
+ parts.sort((p, q) => p.d - q.d);
57
+ return parts.map((p) => p.svg).join("");
58
+ }
59
+
60
+ function switchesClosed(tx, tv) {
61
+ const out = [];
62
+ const limit = 0.12;
63
+ if (tx > limit) out.push("left");
64
+ if (tx < -limit) out.push("right");
65
+ if (tv > limit) out.push("up");
66
+ if (tv < -limit) out.push("down");
67
+ return out;
68
+ }
69
+
70
+ const COIN_R = 2.426;
71
+ const COIN_T = 0.35;
72
+ function coin(centre, axis, spin = 0, { lit = true } = {}) {
73
+ const F0 = G.frameAlong(centre, axis);
74
+ const F = { o: F0.o, a: F0.a, u: rot(F0.u, F0.a, spin), v: rot(F0.v, F0.a, spin) };
75
+ const face = G.dot3(F.a, V) >= 0 ? COIN_T / 2 : -COIN_T / 2;
76
+ const body = S(G.disc(-COIN_T / 2, COIN_T / 2, COIN_R, F, P, { steps: 36 }), { tone: "hi", lit });
77
+ const facing = Math.abs(G.dot3(F.a, V));
78
+ let marks = "";
79
+ if (facing > 0.2) {
80
+ marks += Ln(G.circleOf(F, face, COIN_R - 0.35, P, 28), { tone: "lo" });
81
+ const p0 = G.pointOf(F, face, 0.3, 0);
82
+ const p1 = G.pointOf(F, face, 1.5, 0);
83
+ marks += Ln(k.pathOf([at(p0), at(p1)]), { tone: "lo", free: true });
84
+ }
85
+ return body + marks;
86
+ }
87
+
88
+ return { stickUpper, stickLower, switchesClosed, coin, axisOf, COIN_R, COIN_T };
89
+ }