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.
- package/README.md +3 -2
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/installation.md +20 -41
- package/docs/workflows.md +12 -9
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +3 -3
- package/profiles/presets/codex-only.json +3 -3
- package/profiles/presets/mixed.json +5 -4
- package/skills/axstack/references/role-roster.md +5 -3
- package/skills/axstack/references/ui-verification.md +6 -10
- package/skills/axstack-anatomy/ANATOMY.md +172 -0
- package/skills/axstack-anatomy/CHANGELOG.md +70 -0
- package/skills/axstack-anatomy/LICENSE +21 -0
- package/skills/axstack-anatomy/PROVENANCE.md +21 -0
- package/skills/axstack-anatomy/README.md +316 -0
- package/skills/axstack-anatomy/SKILL.md +74 -0
- package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
- package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
- package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
- package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
- package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
- package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
- package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
- package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
- package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
- package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
- package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
- package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
- package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
- package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
- package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
- package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
- package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
- package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
- package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
- package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
- package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
- package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
- package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
- package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
- package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
- package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
- package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
- package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
- package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
- package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
- package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
- package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
- package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
- package/skills/axstack-anatomy/kit/gl.mjs +184 -0
- package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
- package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
- package/skills/axstack-anatomy/kit/iso.css +106 -0
- package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
- package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
- package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
- package/skills/axstack-anatomy/kit/tube.mjs +306 -0
- package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
- package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
- package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
- package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
- package/skills/axstack-anatomy/references/3d.md +352 -0
- package/skills/axstack-anatomy/references/craft.md +325 -0
- package/skills/axstack-anatomy/references/kit.md +419 -0
- package/skills/axstack-anatomy/references/motion.md +165 -0
- package/skills/axstack-anatomy/references/react.md +42 -0
- package/skills/axstack-anatomy/references/verify.md +351 -0
- package/skills/axstack-anatomy/references/walkthrough.md +75 -0
- package/skills/axstack-anatomy/references/webgl.md +473 -0
- package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
- package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
- package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
- package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
- package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
- package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
- package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
- package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
- package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
- package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
- package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
- package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
- package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
- package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
- package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
- package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
- package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
- package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
- package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
- package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
- package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
- package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
- package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
- package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
- package/skills/axstack-anatomy/theme.js +56 -0
- package/skills/axstack-explain/SKILL.md +28 -15
- package/skills/axstack-explain/references/delivery.md +22 -0
- package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
- package/skills/axstack-explain/references/inline-pages.md +8 -8
- package/skills/axstack-explain/references/visual-qa.md +3 -5
- package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
- package/src/archify-legacy.js +70 -0
- package/src/capabilities.js +1 -29
- package/src/installer.js +11 -31
- package/skills/axstack-diagram/SKILL.md +0 -28
- package/skills/axstack-diagram/references/archify.md +0 -76
- package/src/archify-pin.js +0 -5
- 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
|
+
}
|