axstack 0.36.1 → 0.38.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 +5 -4
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/host-operations.md +9 -5
- 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/automations.md +7 -2
- package/skills/axstack/references/lifecycle.md +2 -1
- package/skills/axstack/references/review-manager-prompt.md +44 -3
- package/skills/axstack/references/role-roster.md +13 -6
- package/skills/axstack/references/routing.md +2 -1
- package/skills/axstack/references/run-record.md +2 -0
- 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/skills/axstack-research/SKILL.md +18 -6
- 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,114 @@
|
|
|
1
|
+
export const SCREEN = (() => {
|
|
2
|
+
const W = 224;
|
|
3
|
+
const H = 288;
|
|
4
|
+
const FONT = {
|
|
5
|
+
A: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"],
|
|
6
|
+
C: ["01110", "10001", "10000", "10000", "10000", "10001", "01110"],
|
|
7
|
+
D: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"],
|
|
8
|
+
E: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"],
|
|
9
|
+
F: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"],
|
|
10
|
+
H: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"],
|
|
11
|
+
I: ["01110", "00100", "00100", "00100", "00100", "00100", "01110"],
|
|
12
|
+
K: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"],
|
|
13
|
+
L: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"],
|
|
14
|
+
M: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"],
|
|
15
|
+
N: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"],
|
|
16
|
+
O: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
|
|
17
|
+
P: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"],
|
|
18
|
+
R: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"],
|
|
19
|
+
S: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"],
|
|
20
|
+
T: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"],
|
|
21
|
+
U: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"],
|
|
22
|
+
Y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"],
|
|
23
|
+
Z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"],
|
|
24
|
+
0: ["01110", "10011", "10101", "10101", "10101", "11001", "01110"],
|
|
25
|
+
1: ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
|
|
26
|
+
2: ["01110", "10001", "00001", "00110", "01000", "10000", "11111"],
|
|
27
|
+
3: ["11111", "00010", "00100", "00010", "00001", "10001", "01110"],
|
|
28
|
+
4: ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
|
|
29
|
+
5: ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
|
|
30
|
+
6: ["00110", "01000", "10000", "11110", "10001", "10001", "01110"],
|
|
31
|
+
7: ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
|
|
32
|
+
8: ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
|
|
33
|
+
9: ["01110", "10001", "10001", "01111", "00001", "00010", "01100"],
|
|
34
|
+
"-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"],
|
|
35
|
+
".": ["00000", "00000", "00000", "00000", "00000", "01100", "01100"],
|
|
36
|
+
" ": ["00000", "00000", "00000", "00000", "00000", "00000", "00000"],
|
|
37
|
+
};
|
|
38
|
+
const SPRITES = {
|
|
39
|
+
jellyA: ["0001111000", "0011111100", "0110110110", "1111111111", "1111111111", "0010110100", "0100000010", "0010000100"],
|
|
40
|
+
jellyB: ["0001111000", "0011111100", "0110110110", "1111111111", "1111111111", "0010110100", "0001001000", "0010000100"],
|
|
41
|
+
eyeA: ["0011110000", "0111111000", "1101101100", "1111111100", "0110011000", "1100001100"].map((r) => r.slice(0, 8)),
|
|
42
|
+
eyeB: ["0011110000", "0111111000", "1101101100", "1111111100", "0110011000", "0011110000"].map((r) => r.slice(0, 8)),
|
|
43
|
+
ship: ["00000100000", "00001110000", "00001110000", "01111111110", "11111111111", "11011111011", "10000100001"],
|
|
44
|
+
boom: ["1000100010", "0100100100", "0010001000", "1100000011", "0010001000", "0100100100", "1000100010"],
|
|
45
|
+
};
|
|
46
|
+
const n = (v) => Math.round(v * 100) / 100;
|
|
47
|
+
function bitmap(rows, x, y, s = 1) {
|
|
48
|
+
let d = "";
|
|
49
|
+
rows.forEach((row, r) => {
|
|
50
|
+
let c = 0;
|
|
51
|
+
while (c < row.length) {
|
|
52
|
+
if (row[c] === "1") {
|
|
53
|
+
let e = c;
|
|
54
|
+
while (e < row.length && row[e] === "1") e++;
|
|
55
|
+
d += `M${n(x + c * s)} ${n(y + r * s)}h${n((e - c) * s)}v${n(s)}h${n(-(e - c) * s)}z`;
|
|
56
|
+
c = e;
|
|
57
|
+
} else c++;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return d;
|
|
61
|
+
}
|
|
62
|
+
function text(str, x, y, s = 1, adv = 8) {
|
|
63
|
+
return [...String(str)].map((ch, i) => bitmap(FONT[ch] ?? FONT[" "], x + i * adv * s, y, s)).join("");
|
|
64
|
+
}
|
|
65
|
+
const centred = (str, s = 1, adv = 8) => n((W - (String(str).length * adv - (adv - 5)) * s) / 2);
|
|
66
|
+
|
|
67
|
+
const FORMATION = [];
|
|
68
|
+
for (let row = 0; row < 3; row++) for (let col = 0; col < 6; col++) FORMATION.push({ row, col, x: 34 + col * 26, y: 92 + row * 18, kind: row === 0 ? "eye" : "jelly" });
|
|
69
|
+
const SHIP_Y = 250;
|
|
70
|
+
const px = (d, extra = "") => `<path class="ac-px"${extra} d="${d}"/>`;
|
|
71
|
+
|
|
72
|
+
function svg() {
|
|
73
|
+
const out = [];
|
|
74
|
+
const hatch = [];
|
|
75
|
+
for (let x = 0; x <= W; x += 16) hatch.push(`M${Math.min(x, W - 1)} 0h1v${H}h-1z`);
|
|
76
|
+
for (let y = 0; y <= H; y += 16) hatch.push(`M0 ${Math.min(y, H - 1)}h${W}v1h${-W}z`);
|
|
77
|
+
out.push(`<g data-mode="test" style="display:none">`);
|
|
78
|
+
out.push(`<path class="ac-px ac-dim" d="${hatch.join("")}"/>`);
|
|
79
|
+
out.push(px(text("SELF TEST", centred("SELF TEST"), 56), ` data-step="0"`));
|
|
80
|
+
out.push(px(text("RAM OK", 56, 104), ` data-step="1"`));
|
|
81
|
+
out.push(px(text("ROM OK", 56, 120), ` data-step="2"`));
|
|
82
|
+
out.push(px(text("Z80 3.072 MHZ", 56, 152), ` data-step="3"`));
|
|
83
|
+
out.push(`</g>`);
|
|
84
|
+
out.push(`<g data-mode="attract" style="display:none">`);
|
|
85
|
+
out.push(px(text("1UP", 24, 8) + text("HI-SCORE", 80, 8) + text("2UP", 176, 8)));
|
|
86
|
+
out.push(px(text("00000", 16, 20), ` data-part="score"`));
|
|
87
|
+
out.push(px(text("12500", 92, 20)));
|
|
88
|
+
out.push(px(text("ANATOMY", centred("ANATOMY", 2), 44, 2)));
|
|
89
|
+
out.push(`<g data-part="formation">`);
|
|
90
|
+
FORMATION.forEach((a, i) => {
|
|
91
|
+
const [fa, fb] = a.kind === "eye" ? [SPRITES.eyeA, SPRITES.eyeB] : [SPRITES.jellyA, SPRITES.jellyB];
|
|
92
|
+
const ox = a.kind === "eye" ? 1 : 0;
|
|
93
|
+
out.push(`<g data-alien="${i}">${px(bitmap(fa, a.x + ox, a.y), ' data-frame="a"')}${px(bitmap(fb, a.x + ox, a.y), ' data-frame="b" style="display:none"')}</g>`);
|
|
94
|
+
});
|
|
95
|
+
out.push(`</g>`);
|
|
96
|
+
out.push(px(bitmap(SPRITES.boom, 0, 0), ` data-part="boom" style="display:none"`));
|
|
97
|
+
out.push(px("M0 0h1v5h-1z", ` data-part="shot" style="display:none"`));
|
|
98
|
+
out.push(px(bitmap(SPRITES.ship, -5.5, 0), ` data-part="ship" transform="translate(112 ${SHIP_Y})"`));
|
|
99
|
+
out.push(px(`M8 ${SHIP_Y + 12}h${W - 16}v1h${-(W - 16)}z`));
|
|
100
|
+
out.push(px(text("INSERT COIN", centred("INSERT COIN"), 200), ` data-part="insert"`));
|
|
101
|
+
out.push(px(text("PUSH START", centred("PUSH START"), 200), ` data-part="start" style="display:none"`));
|
|
102
|
+
out.push(px(text("CREDIT 0", 120, 274), ` data-part="credit"`));
|
|
103
|
+
out.push(`</g>`);
|
|
104
|
+
const k = 58 / W;
|
|
105
|
+
const scan = [];
|
|
106
|
+
for (let i = 0; i <= 112; i++) scan.push(`M${n(-29 + i * 0.518)} -37.3V37.3`);
|
|
107
|
+
return [
|
|
108
|
+
`<g data-part="raster"><g transform="translate(-29 -37.3) scale(${n(k * 10000) / 10000})">${out.join("")}</g>`,
|
|
109
|
+
`<path class="ac-scan" d="${scan.join("")}"/></g>`,
|
|
110
|
+
`<ellipse class="ac-dot" data-part="dot" cx="0" cy="0" rx="1.2" ry="1.2" style="opacity:0"/>`,
|
|
111
|
+
].join("");
|
|
112
|
+
}
|
|
113
|
+
return { W, H, FONT, SPRITES, FORMATION, SHIP_Y, bitmap, text, centred, svg };
|
|
114
|
+
})();
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import * as k from "../../kit/iso-kit.mjs";
|
|
5
|
+
import { kitScript } from "../../scripts/inline-kit.mjs";
|
|
6
|
+
|
|
7
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const theme = process.argv.includes("--light") ? "light" : "dark";
|
|
9
|
+
|
|
10
|
+
const WIDTH = 600;
|
|
11
|
+
const HEIGHT = 440;
|
|
12
|
+
|
|
13
|
+
const PLATE = { x: -100, y: -84, w: 210, d: 242, r: 10 };
|
|
14
|
+
const PLATE_H = 10;
|
|
15
|
+
const FOOT_H = 5;
|
|
16
|
+
|
|
17
|
+
const CASE = { x: -62, y: -62, w: 124, d: 118, r: 8 };
|
|
18
|
+
const FRONT = CASE.y + CASE.d;
|
|
19
|
+
const SIDE = CASE.x + CASE.w;
|
|
20
|
+
const PLINTH_H = 5;
|
|
21
|
+
const TOP = 170;
|
|
22
|
+
const SEAM = FRONT - 12;
|
|
23
|
+
|
|
24
|
+
const BEZEL = { x0: -48, x1: 48, z0: 68, z1: 158, r: 6, depth: 1.6 };
|
|
25
|
+
const CRT = { x0: -40, x1: 40, z0: 77, z1: 150, r: 8, depth: 5 };
|
|
26
|
+
const GLASS_Y = FRONT - BEZEL.depth - CRT.depth;
|
|
27
|
+
const CRT_MID = (CRT.z0 + CRT.z1) / 2;
|
|
28
|
+
const SLOT = { x0: 8, x1: 46, z0: 45, z1: 50.5, r: 2.4, depth: 4 };
|
|
29
|
+
|
|
30
|
+
const KB = { x: -76, y: 84, w: 152, d: 58, r: 4 };
|
|
31
|
+
const KB_Z = 1.6;
|
|
32
|
+
const KB_TOP = 8.6;
|
|
33
|
+
const U = 9.2;
|
|
34
|
+
const KEYS_X = -U * 7.5;
|
|
35
|
+
const KEYS_Y = 88.5;
|
|
36
|
+
|
|
37
|
+
const ROWS = [
|
|
38
|
+
[["`", 1], ["1", 1], ["2", 1], ["3", 1], ["4", 1], ["5", 1], ["6", 1], ["7", 1], ["8", 1], ["9", 1], ["0", 1], ["-", 1], ["=", 1], ["Backspace", 2]],
|
|
39
|
+
[["Tab", 1.5], ["q", 1], ["w", 1], ["e", 1], ["r", 1], ["t", 1], ["y", 1], ["u", 1], ["i", 1], ["o", 1], ["p", 1], ["[", 1], ["]", 1], ["\\", 1.5]],
|
|
40
|
+
[["CapsLock", 1.75], ["a", 1], ["s", 1], ["d", 1], ["f", 1], ["g", 1], ["h", 1], ["j", 1], ["k", 1], ["l", 1], [";", 1], ["'", 1], ["Enter", 2.25]],
|
|
41
|
+
[["Shift", 2.25], ["z", 1], ["x", 1], ["c", 1], ["v", 1], ["b", 1], ["n", 1], ["m", 1], [",", 1], [".", 1], ["/", 1], ["ShiftRight", 2.75]],
|
|
42
|
+
[["Alt", 1.5], ["Meta", 1.5], [" ", 9], ["NumpadEnter", 1.5], ["AltRight", 1.5]],
|
|
43
|
+
];
|
|
44
|
+
const ROW_H = [5.6, 5.2, 4.8, 4.4, 4];
|
|
45
|
+
|
|
46
|
+
const PORT = { y: 28, z: 9 };
|
|
47
|
+
const COIL = { x: 80, z: 3.6, r: 3.2, from: 44, to: 84, turns: 17 };
|
|
48
|
+
|
|
49
|
+
const P = k.fitProjection(
|
|
50
|
+
[...k.boxCorners(PLATE, -PLATE_H - FOOT_H, 0), ...k.boxCorners(CASE, 0, TOP)],
|
|
51
|
+
WIDTH,
|
|
52
|
+
HEIGHT,
|
|
53
|
+
{ pad: 30, azimuth: 56 },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const at = (point) => k.iso(point, P);
|
|
57
|
+
const S = (paths, style) => k.solidSvg(paths, style);
|
|
58
|
+
const L = (d, style) => k.lineSvg(d, style);
|
|
59
|
+
const D = (points, style) => k.dotsSvg(points, style);
|
|
60
|
+
const byDepth = (points) => [...points].sort((a, b) => k.depthOf([a[0], a[1], 0], P) - k.depthOf([b[0], b[1], 0], P));
|
|
61
|
+
const planeOf = (y) => k.sideMatrix([0, y], 0, P, "left");
|
|
62
|
+
const flat = (x0, z0, x1, z1, r, steps = 6) => k.pathOf(k.roundedPlan({ x: x0, y: z0, w: x1 - x0, d: z1 - z0, r }, steps).map(([u, z]) => [u, -z]), true);
|
|
63
|
+
const onFront = (points, y = FRONT) => k.pathOf(points.map(([x, z]) => at([x, y, z])));
|
|
64
|
+
const sideSlot = (y, z0, z1, half = 0.8) => k.pathOf(k.roundedPlan({ x: y - half, y: z0, w: half * 2, d: z1 - z0, r: half }, 4).map(([u, z]) => at([SIDE, u, z])), true);
|
|
65
|
+
|
|
66
|
+
let clips = 0;
|
|
67
|
+
function recess({ x0, x1, z0, z1, r, depth }, y, back = "", backClass = "pc-well", inside = "") {
|
|
68
|
+
const id = `pc-clip-${clips++}`;
|
|
69
|
+
const ring = k.roundedPlan({ x: x0, y: z0, w: x1 - x0, d: z1 - z0, r }, 6);
|
|
70
|
+
const cx = (x0 + x1) / 2;
|
|
71
|
+
const cz = (z0 + z1) / 2;
|
|
72
|
+
const { sinA } = k.cameraOf(P);
|
|
73
|
+
const tops = [];
|
|
74
|
+
const sides = [[], [], [], []];
|
|
75
|
+
ring.forEach(([ax, az], index) => {
|
|
76
|
+
const [bx, bz] = ring[(index + 1) % ring.length];
|
|
77
|
+
const length = Math.hypot(bx - ax, bz - az) || 1;
|
|
78
|
+
let [nx, nz] = [(bz - az) / length, -(bx - ax) / length];
|
|
79
|
+
if (nx * (cx - (ax + bx) / 2) + nz * (cz - (az + bz) / 2) < 0) [nx, nz] = [-nx, -nz];
|
|
80
|
+
if (nx <= 0 && nz <= 0) return;
|
|
81
|
+
const quad = k.pathOf([at([ax, y, az]), at([bx, y, bz]), at([bx, y - depth, bz]), at([ax, y - depth, az])], true);
|
|
82
|
+
if (nz >= nx) tops.push(quad);
|
|
83
|
+
else sides[Math.min(3, Math.max(0, Math.floor(((nx * -sinA + 1) / 2) * 4)))].push(quad);
|
|
84
|
+
});
|
|
85
|
+
const shape = flat(x0, z0, x1, z1, r);
|
|
86
|
+
return [
|
|
87
|
+
`<clipPath id="${id}"><path transform="${planeOf(y)}" d="${shape}"/></clipPath>`,
|
|
88
|
+
`<g clip-path="url(#${id})">`,
|
|
89
|
+
`<path class="iso-top" d="${tops.join("")}"/>`,
|
|
90
|
+
...sides.map((d, shade) => (d.length ? `<path class="iso-shade" data-shade="${shade}" d="${d.join("")}"/>` : "")),
|
|
91
|
+
`<g transform="${planeOf(y - depth)}"><path class="${backClass}" d="${shape}"/>${back}<path class="iso-line" data-tone="faint" d="${shape}"/></g>`,
|
|
92
|
+
inside,
|
|
93
|
+
`</g>`,
|
|
94
|
+
`<g transform="${planeOf(y)}"><path class="iso-line" data-tone="lo" d="${shape}"/></g>`,
|
|
95
|
+
].join("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function plate() {
|
|
99
|
+
const out = [`<path class="iso-halo" d="${k.haloOf(PLATE, -PLATE_H - FOOT_H, PLATE_H, P)}"/>`];
|
|
100
|
+
for (const [x, y] of byDepth(k.corners(PLATE, 22))) {
|
|
101
|
+
out.push(S(k.cylinder(x, y, 7, -PLATE_H - FOOT_H, 1.6, P, 28), { tone: "lo" }));
|
|
102
|
+
out.push(S(k.cylinder(x, y, 5.4, -PLATE_H - FOOT_H + 1.6, FOOT_H - 1.6, P, 28), { tone: "mid" }));
|
|
103
|
+
}
|
|
104
|
+
out.push(S(k.slabOf(PLATE, -PLATE_H, PLATE_H, P, 8, 1.6), { tone: "mid" }));
|
|
105
|
+
out.push(L(k.planOutline(k.insetPlan(PLATE, 5), 0, P), { tone: "lo" }));
|
|
106
|
+
const screws = k.corners(PLATE, 10);
|
|
107
|
+
out.push(L(screws.map(([x, y]) => k.ring(x, y, 2.2, 0, P, 16)).join(""), { tone: "lo" }));
|
|
108
|
+
out.push(D(screws.map(([x, y]) => at([x, y, 0])), { size: 0.5 }));
|
|
109
|
+
const label = { x: -88, y: 144, w: 40, d: 9, r: 1.2 };
|
|
110
|
+
out.push(S(k.slabOf(label, 0, 0.8, P, 3), { tone: "lo", crease: "none" }));
|
|
111
|
+
out.push(L([30, 22].map((w, i) => k.lineOnTop([label.x + 4, label.y + 3 + i * 3], [label.x + 4 + w, label.y + 3 + i * 3], 0.8, P)).join(""), { tone: "lo", free: true }));
|
|
112
|
+
out.push(D([at([label.x + label.w - 3, label.y + label.d / 2, 0.8])], { size: 0.45 }));
|
|
113
|
+
return out.join("");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function screen() {
|
|
117
|
+
const H = 19;
|
|
118
|
+
const left = -H * 0.61;
|
|
119
|
+
const base = CRT_MID + 2;
|
|
120
|
+
const glyph = (points) => k.pathOf(points.map(([gx, gy]) => [left + gx * H, -(base + gy * H)]), true);
|
|
121
|
+
const logo =
|
|
122
|
+
glyph([[0, 0], [0.37, 1], [0.55, 1], [0.92, 0], [0.71, 0], [0.63, 0.23], [0.29, 0.23], [0.21, 0]]) +
|
|
123
|
+
glyph([[0.35, 0.41], [0.57, 0.41], [0.46, 0.72]]) +
|
|
124
|
+
glyph([[0.68, 1], [0.89, 1], [1.26, 0], [1.05, 0]]);
|
|
125
|
+
const lines = [];
|
|
126
|
+
for (let z = CRT.z0 + 1.2; z < CRT.z1; z += 1.6) lines.push(`M${CRT.x0} ${(-z).toFixed(1)}H${CRT.x1}`);
|
|
127
|
+
const glow = flat(-30, CRT.z0 + 8, 30, CRT.z1 - 8, 26, 10);
|
|
128
|
+
const textX = CRT.x0 + 8;
|
|
129
|
+
const textZ = CRT.z0 + 14;
|
|
130
|
+
const crt = [
|
|
131
|
+
`<path class="pc-glow" d="${glow}"/>`,
|
|
132
|
+
`<path class="pc-scan" d="${lines.join("")}"/>`,
|
|
133
|
+
`<path class="pc-logo" d="${logo}"/>`,
|
|
134
|
+
`<text class="pc-text" data-part="text" x="${textX}" y="${-textZ}" font-size="5.2">> </text>`,
|
|
135
|
+
`<rect class="pc-cursor" data-part="cursor" x="${textX + 6.6}" y="${-textZ - 4.2}" width="3" height="5"/>`,
|
|
136
|
+
].join("");
|
|
137
|
+
const back = [
|
|
138
|
+
`<g data-part="crt" transform="">${crt}</g>`,
|
|
139
|
+
`<path class="pc-beam" data-part="beam" d="M${CRT.x0 + 4} ${-CRT_MID}H${CRT.x1 - 4}" style="opacity:0"/>`,
|
|
140
|
+
`<circle class="pc-dot" data-part="dot" cx="0" cy="${-CRT_MID}" r="1.4" style="opacity:0"/>`,
|
|
141
|
+
`<path class="iso-line" data-tone="lo" d="M${CRT.x0 + 6} ${-(CRT.z1 - 16)}Q${CRT.x0 + 7} ${-(CRT.z1 - 7)} ${CRT.x0 + 16} ${-(CRT.z1 - 6)}"/>`,
|
|
142
|
+
].join("");
|
|
143
|
+
return recess(BEZEL, FRONT, "", "pc-bezel", recess(CRT, FRONT - BEZEL.depth, back, "pc-glass"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function computer() {
|
|
147
|
+
const out = [`<path class="iso-halo" d="${k.haloOf(CASE, 0, 8, P)}"/>`];
|
|
148
|
+
out.push(S(k.slabOf(k.insetPlan(CASE, 3), 0, PLINTH_H, P, 8), { tone: "lo" }));
|
|
149
|
+
out.push(S(k.slabOf(CASE, PLINTH_H, TOP - PLINTH_H, P, 8, 1.6), { tone: "mid" }));
|
|
150
|
+
const handle = { x: -42, y: CASE.y + 7, w: 84, d: 11, r: 5.5 };
|
|
151
|
+
out.push(L(k.planOutline(handle, TOP, P, 8), { tone: "lo" }));
|
|
152
|
+
out.push(L(k.planOutline(k.insetPlan(handle, 2.2), TOP, P, 8), { tone: "faint" }));
|
|
153
|
+
out.push(L(Array.from({ length: 7 }, (_, i) => k.planOutline({ x: -34, y: CASE.y + 27.3 + i * 3.4, w: 68, d: 1.4, r: 0.7 }, TOP, P, 4)).join(""), { tone: "faint" }));
|
|
154
|
+
out.push(L(k.lineOnTop([CASE.x, SEAM], [SIDE, SEAM], TOP, P) + k.segment([SIDE, SEAM, TOP], [SIDE, SEAM, PLINTH_H], P), { tone: "lo" }));
|
|
155
|
+
const rowOf = (from, to) => Array.from({ length: Math.floor((to - from) / 3.2 + 1e-9) + 1 }, (_, i) => from + i * 3.2);
|
|
156
|
+
out.push(L(rowOf(CASE.y + 10, CASE.y + 52).map((y) => sideSlot(y, TOP - 48, TOP - 14)).join(""), { tone: "faint" }));
|
|
157
|
+
out.push(L(rowOf(CASE.y + 10, CASE.y + 40).map((y) => sideSlot(y, 20, 30)).join(""), { tone: "faint" }));
|
|
158
|
+
out.push(`<g transform="${planeOf(FRONT)}"><path class="iso-line" data-tone="faint" d="${flat(CASE.x + 4, PLINTH_H + 4, SIDE - 4, TOP - 4, 5)}"/></g>`);
|
|
159
|
+
out.push(screen());
|
|
160
|
+
out.push(recess(SLOT, FRONT, `<path class="iso-line" data-tone="lo" d="M${SLOT.x0} ${-(SLOT.z0 + 1.6)}H${SLOT.x1}"/>`, "pc-dark"));
|
|
161
|
+
out.push(D([at([SLOT.x1 + 4, FRONT, SLOT.z0 - 1])], { size: 0.5, tone: "mid" }));
|
|
162
|
+
out.push(`<g transform="${planeOf(FRONT)}">${L(Array.from({ length: 9 }, (_, i) => flat(-46.7 + i * 3, 42, -45.3 + i * 3, 54, 0.7, 4)).join(""), { tone: "lo" })}</g>`);
|
|
163
|
+
out.push(`<g data-part="led">${D([at([-46, FRONT, 36])], { size: 0.8, tone: "hi" })}</g>`);
|
|
164
|
+
out.push(rocker());
|
|
165
|
+
out.push(port());
|
|
166
|
+
return `<g class="pc-computer" data-part="computer">${out.join("")}</g>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function rocker() {
|
|
170
|
+
const y = CASE.y + 8;
|
|
171
|
+
const out = [S(k.slabOf({ x: SIDE, y, w: 1.2, d: 12, r: 1 }, 12, 15, P, 3), { tone: "mid" })];
|
|
172
|
+
const half = (z, proud) => S(k.slabOf({ x: SIDE + 1.2, y: y + 1.5, w: proud ? 2.4 : 0.8, d: 9, r: 0.6 }, z, 5.6, P, 2), { tone: "mid" });
|
|
173
|
+
out.push(`<g data-rocker="on">${half(13, true)}${half(20.4, false)}</g>`);
|
|
174
|
+
out.push(`<g data-rocker="off" style="display:none">${half(20.4, true)}${half(13, false)}</g>`);
|
|
175
|
+
return out.join("");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function port() {
|
|
179
|
+
const out = [];
|
|
180
|
+
out.push(S(k.slabOf({ x: SIDE - 0.4, y: PORT.y - 1, w: 1.4, d: 10, r: 1 }, PORT.z - 1, 9, P, 3), { tone: "lo" }));
|
|
181
|
+
out.push(S(k.slabOf({ x: SIDE + 1, y: PORT.y, w: 4.4, d: 8, r: 1 }, PORT.z, 7, P, 3, 0.4), { tone: "mid" }));
|
|
182
|
+
out.push(S(k.slabOf({ x: SIDE + 5.4, y: PORT.y + 2, w: 5, d: 4, r: 1.6 }, PORT.z + 1.5, 4, P, 4), { tone: "mid" }));
|
|
183
|
+
out.push(D([at([SIDE + 3.2, PORT.y + 1.6, PORT.z + 7]), at([SIDE + 3.2, PORT.y + 6.4, PORT.z + 7])], { size: 0.4 }));
|
|
184
|
+
return out.join("");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const keyList = [];
|
|
188
|
+
function keyboard() {
|
|
189
|
+
const out = [`<path class="iso-halo" d="${k.haloOf(KB, 0, 4, P)}"/>`];
|
|
190
|
+
for (const [x, y] of byDepth(k.corners(KB, 8))) out.push(S(k.cylinder(x, y, 2.8, 0, KB_Z, P, 16), { tone: "lo" }));
|
|
191
|
+
out.push(S(k.slabOf(KB, KB_Z, KB_TOP - KB_Z, P, 6, 1), { tone: "mid" }));
|
|
192
|
+
const well = { x: KEYS_X - 1.4, y: KEYS_Y - 1.4, w: U * 15 + 2.8, d: U * 5 + 2.8, r: 2 };
|
|
193
|
+
out.push(L(k.planOutline(well, KB_TOP, P, 4), { tone: "lo" }));
|
|
194
|
+
out.push(D([at([KB.x + 4, KB.y + 2.4, KB_TOP]), at([KB.x + KB.w - 4, KB.y + 2.4, KB_TOP])], { size: 0.45 }));
|
|
195
|
+
ROWS.forEach((row, r) => {
|
|
196
|
+
let x = KEYS_X;
|
|
197
|
+
for (const [code, width] of row) {
|
|
198
|
+
const plan = { x: x + 0.7, y: KEYS_Y + r * U + 0.7, w: width * U - 1.4, d: U - 1.4, r: 1.4 };
|
|
199
|
+
const h = ROW_H[r];
|
|
200
|
+
const index = keyList.length;
|
|
201
|
+
keyList.push(code);
|
|
202
|
+
const dish = code === " " ? k.planOutline(k.insetPlan(plan, 1.8), KB_TOP + h, P, 3) : k.planOutline(k.insetPlan(plan, 1.6), KB_TOP + h, P, 3);
|
|
203
|
+
out.push(`<g class="pc-key" data-key="${index}">${S(k.slabOf(plan, KB_TOP, h, P, 3, 0.4), { tone: "mid", crease: "faint" })}${L(dish, { tone: "faint" })}</g>`);
|
|
204
|
+
x += width * U;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
out.push(S(k.slabOf({ x: KB.x + KB.w, y: 90, w: 4, d: 4, r: 1.5 }, 2.5, 4, P, 4), { tone: "mid" }));
|
|
208
|
+
return out.join("");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function cable() {
|
|
212
|
+
const out = [];
|
|
213
|
+
const curve = (points) => k.pathOf(points.map((point) => at(point)));
|
|
214
|
+
const start = [COIL.x + COIL.r, COIL.from, COIL.z];
|
|
215
|
+
const end = [COIL.x + COIL.r, COIL.to, COIL.z];
|
|
216
|
+
out.push(`<path class="iso-line pc-cord" data-tone="mid" d="${curve([[SIDE + 10.4, PORT.y + 4, PORT.z + 3.5], [SIDE + 14, PORT.y + 6, PORT.z + 2.4], [SIDE + 17, PORT.y + 9.5, PORT.z - 1], [SIDE + 19.6, PORT.y + 13, COIL.z + 1.6], start])}"/>`);
|
|
217
|
+
const spring = k.coil(COIL.from, COIL.to, [COIL.x, COIL.z], COIL.r, COIL.turns, "y", P);
|
|
218
|
+
out.push(L(spring.back, { tone: "lo" }), L(spring.front, { tone: "mid" }));
|
|
219
|
+
out.push(`<path class="iso-line pc-cord" data-tone="mid" d="${curve([end, [COIL.x + 2.6, COIL.to + 3, COIL.z + 0.2], [KB.x + KB.w + 6.4, 91.4, 4.4], [KB.x + KB.w + 4, 92, 4.5]])}"/>`);
|
|
220
|
+
return out.join("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const svg = k.figureSvg({
|
|
224
|
+
width: WIDTH,
|
|
225
|
+
height: HEIGHT,
|
|
226
|
+
label:
|
|
227
|
+
"An all-in-one desk computer in the spirit of 1984 on a bench plate: a tall case with a CRT set into its front showing the Anthropic mark and a typing prompt, a floppy slot and speaker grille below, vents and a rocker switch on the side, and a keyboard joined to a side port by a coiled cord.",
|
|
228
|
+
body: [plate(), computer(), keyboard(), cable()],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const PHRASES = ["hello, claude", "what is a crt?", "draw it isometric"];
|
|
232
|
+
|
|
233
|
+
const live = `
|
|
234
|
+
const P = ${JSON.stringify(P)};
|
|
235
|
+
const KEYS = ${JSON.stringify(keyList)};
|
|
236
|
+
const PHRASES = ${JSON.stringify(PHRASES)};
|
|
237
|
+
const MAX_CHARS = 20, CRT_MID = ${CRT_MID}, TEXT_X = ${CRT.x0 + 8};
|
|
238
|
+
const SHIFTED = { "~": "\`", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9", ")": "0", "_": "-", "+": "=", "{": "[", "}": "]", "|": "\\\\", ":": ";", '"': "'", "<": ",", ">": ".", "?": "/" };
|
|
239
|
+
const stage = document.querySelector(".iso-stage");
|
|
240
|
+
const svg = stage.querySelector("svg");
|
|
241
|
+
const keyEls = KEYS.map((_, i) => svg.querySelector('[data-key="' + i + '"]'));
|
|
242
|
+
const textEl = svg.querySelector('[data-part="text"]');
|
|
243
|
+
const cursor = svg.querySelector('[data-part="cursor"]');
|
|
244
|
+
const crt = svg.querySelector('[data-part="crt"]');
|
|
245
|
+
const beam = svg.querySelector('[data-part="beam"]');
|
|
246
|
+
const dot = svg.querySelector('[data-part="dot"]');
|
|
247
|
+
const glass = svg.querySelector(".pc-glass");
|
|
248
|
+
const led = svg.querySelector('[data-part="led"]');
|
|
249
|
+
const rockerOn = svg.querySelector('[data-rocker="on"]');
|
|
250
|
+
const rockerOff = svg.querySelector('[data-rocker="off"]');
|
|
251
|
+
const readout = document.querySelector("[data-readout]");
|
|
252
|
+
const status = document.querySelector("[data-status]");
|
|
253
|
+
const still = matchMedia("(prefers-reduced-motion: reduce)");
|
|
254
|
+
const DOWN = 2.2;
|
|
255
|
+
const down = axisVector(P, [0, 0, -1]);
|
|
256
|
+
|
|
257
|
+
const keyState = KEYS.map(() => ({ at: 0, held: 0 }));
|
|
258
|
+
const active = new Set();
|
|
259
|
+
let line = "", lastKey = "", power = 1, powerClock = 9, keysTyped = 0;
|
|
260
|
+
let frame = 0, previous = 0, visible = false, touring = !still.matches, idleTimer = 0;
|
|
261
|
+
let tour = { phrase: 0, step: "type", index: 0, wait: 1.4 };
|
|
262
|
+
|
|
263
|
+
const indexOf = (code) => KEYS.indexOf(code);
|
|
264
|
+
function press(code, hold = 0.09) {
|
|
265
|
+
const i = indexOf(code);
|
|
266
|
+
if (i < 0) return;
|
|
267
|
+
keyState[i].held = hold;
|
|
268
|
+
active.add(i);
|
|
269
|
+
run();
|
|
270
|
+
}
|
|
271
|
+
function type(char) {
|
|
272
|
+
if (!power) return;
|
|
273
|
+
const lower = char.toLowerCase();
|
|
274
|
+
const code = SHIFTED[char] || lower;
|
|
275
|
+
if (indexOf(code) < 0) return;
|
|
276
|
+
if (SHIFTED[char] || char !== lower) press("Shift", 0.14);
|
|
277
|
+
press(code);
|
|
278
|
+
keysTyped++;
|
|
279
|
+
lastKey = char === " " ? "space" : char;
|
|
280
|
+
if (line.length < MAX_CHARS) line += char;
|
|
281
|
+
write();
|
|
282
|
+
}
|
|
283
|
+
function erase() { if (!power) return; press("Backspace"); lastKey = "backspace"; line = line.slice(0, -1); write(); }
|
|
284
|
+
function enter() { if (!power) return; press("Enter"); lastKey = "return"; line = ""; write(); }
|
|
285
|
+
function write() {
|
|
286
|
+
textEl.textContent = "> " + line;
|
|
287
|
+
cursor.setAttribute("x", String(TEXT_X + textEl.getComputedTextLength() + 0.4));
|
|
288
|
+
const text = power ? "on · " + line.length + " chars" + (lastKey ? " · key " + lastKey : "") : "off";
|
|
289
|
+
if (readout.textContent !== text) readout.textContent = text;
|
|
290
|
+
}
|
|
291
|
+
function setPower(next) {
|
|
292
|
+
if (next === power) return;
|
|
293
|
+
power = next;
|
|
294
|
+
powerClock = still.matches ? 9 : 0;
|
|
295
|
+
rockerOn.style.display = power ? "" : "none";
|
|
296
|
+
rockerOff.style.display = power ? "none" : "";
|
|
297
|
+
led.style.opacity = power ? "" : "0.15";
|
|
298
|
+
status.textContent = power ? "Computer on." : "Computer off.";
|
|
299
|
+
write();
|
|
300
|
+
run();
|
|
301
|
+
}
|
|
302
|
+
const ease = (t) => 1 - Math.pow(1 - Math.min(1, Math.max(0, t)), 3);
|
|
303
|
+
function crtFrame() {
|
|
304
|
+
const t = powerClock;
|
|
305
|
+
let sx = 1, sy = 1, beamAt = 0, dotAt = 0, lit = power;
|
|
306
|
+
if (power) {
|
|
307
|
+
if (t < 0.14) { sx = ease(t / 0.14); sy = 0.012; beamAt = 1; }
|
|
308
|
+
else if (t < 0.36) { sy = 0.012 + 0.988 * ease((t - 0.14) / 0.22); beamAt = 1 - ease((t - 0.14) / 0.22); }
|
|
309
|
+
} else {
|
|
310
|
+
if (t < 0.1) { sy = 1 - 0.988 * ease(t / 0.1); beamAt = ease(t / 0.1); lit = 1; }
|
|
311
|
+
else if (t < 0.26) { sy = 0.012; sx = 1 - ease((t - 0.1) / 0.16); beamAt = 1; lit = 1; }
|
|
312
|
+
else { sx = 0; sy = 0; dotAt = Math.max(0, 1 - (t - 0.26) / 0.5); }
|
|
313
|
+
}
|
|
314
|
+
crt.setAttribute("transform", "translate(0 " + -CRT_MID + ") scale(" + sx.toFixed(3) + " " + sy.toFixed(3) + ") translate(0 " + CRT_MID + ")");
|
|
315
|
+
beam.setAttribute("transform", "translate(0 " + -CRT_MID + ") scale(" + Math.max(sx, 0.001).toFixed(3) + " 1) translate(0 " + CRT_MID + ")");
|
|
316
|
+
beam.style.opacity = beamAt.toFixed(2);
|
|
317
|
+
dot.style.opacity = dotAt.toFixed(2);
|
|
318
|
+
glass.toggleAttribute("data-on", !!lit);
|
|
319
|
+
return t > 0.8;
|
|
320
|
+
}
|
|
321
|
+
function tourStep(dt) {
|
|
322
|
+
tour.wait -= dt;
|
|
323
|
+
if (tour.wait > 0) return;
|
|
324
|
+
const phrase = PHRASES[tour.phrase];
|
|
325
|
+
if (tour.step === "type") {
|
|
326
|
+
if (tour.index < phrase.length) { type(phrase[tour.index++]); tour.wait = 0.09 + Math.random() * 0.11 + (phrase[tour.index - 1] === " " ? 0.08 : 0); }
|
|
327
|
+
else { tour.step = "erase"; tour.wait = 2.4; }
|
|
328
|
+
} else if (tour.step === "erase") {
|
|
329
|
+
if (line.length) { erase(); tour.wait = 0.055; }
|
|
330
|
+
else if (tour.phrase === PHRASES.length - 1) { tour.step = "off"; tour.wait = 0.9; }
|
|
331
|
+
else { tour.phrase++; tour.index = 0; tour.step = "type"; tour.wait = 0.7; }
|
|
332
|
+
} else if (tour.step === "off") { setPower(0); tour.step = "on"; tour.wait = 1.6; }
|
|
333
|
+
else if (tour.step === "on") { setPower(1); tour = { phrase: 0, step: "type", index: 0, wait: 1.4 }; }
|
|
334
|
+
}
|
|
335
|
+
function tick(now) {
|
|
336
|
+
const dt = previous ? Math.min((now - previous) / 1000, 1 / 30) : 1 / 60;
|
|
337
|
+
previous = now;
|
|
338
|
+
const calm = still.matches;
|
|
339
|
+
if (touring) tourStep(dt);
|
|
340
|
+
powerClock += dt;
|
|
341
|
+
const settledCrt = crtFrame();
|
|
342
|
+
for (const i of active) {
|
|
343
|
+
const s = keyState[i];
|
|
344
|
+
s.held = Math.max(0, s.held - dt);
|
|
345
|
+
const want = s.held > 0 ? DOWN : 0;
|
|
346
|
+
s.at = calm ? want : s.at + (want - s.at) * (1 - Math.exp(-dt / (want ? 0.025 : 0.07)));
|
|
347
|
+
if (Math.abs(want - s.at) < 0.01) s.at = want;
|
|
348
|
+
keyEls[i].setAttribute("transform", "translate(" + (down[0] * s.at).toFixed(2) + " " + (down[1] * s.at).toFixed(2) + ")");
|
|
349
|
+
keyEls[i].toggleAttribute("data-down", s.at > 0.6);
|
|
350
|
+
if (!s.held && s.at === 0) active.delete(i);
|
|
351
|
+
}
|
|
352
|
+
const busy = touring || active.size || !settledCrt;
|
|
353
|
+
frame = visible && busy ? requestAnimationFrame(tick) : 0;
|
|
354
|
+
}
|
|
355
|
+
function run() { if (!frame && visible) { previous = 0; frame = requestAnimationFrame(tick); } }
|
|
356
|
+
function takeOver(switching = false) {
|
|
357
|
+
if (touring) { touring = false; tour = { phrase: 0, step: "type", index: 0, wait: 1.4 }; line = ""; if (!switching) setPower(1); write(); }
|
|
358
|
+
clearTimeout(idleTimer);
|
|
359
|
+
idleTimer = setTimeout(() => { if (still.matches) return; touring = true; line = ""; setPower(1); write(); run(); }, 9000);
|
|
360
|
+
}
|
|
361
|
+
new IntersectionObserver((entries) => { visible = entries[entries.length - 1].isIntersecting; if (visible) run(); }, { rootMargin: "120px 0px" }).observe(stage);
|
|
362
|
+
stage.addEventListener("keydown", (event) => {
|
|
363
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
364
|
+
const key = event.key;
|
|
365
|
+
if (key === "Tab") return;
|
|
366
|
+
takeOver(key === "Escape");
|
|
367
|
+
if (key === "Escape") { event.preventDefault(); setPower(power ? 0 : 1); return; }
|
|
368
|
+
if (key === "Backspace") { event.preventDefault(); erase(); return; }
|
|
369
|
+
if (key === "Enter") { event.preventDefault(); enter(); return; }
|
|
370
|
+
if (key === "Shift") { press(event.code === "ShiftRight" ? "ShiftRight" : "Shift", 0.14); return; }
|
|
371
|
+
if (key.length === 1) { event.preventDefault(); type(key); }
|
|
372
|
+
});
|
|
373
|
+
svg.addEventListener("click", (event) => {
|
|
374
|
+
const key = event.target.closest("[data-key]");
|
|
375
|
+
if (key) {
|
|
376
|
+
takeOver();
|
|
377
|
+
const code = KEYS[Number(key.dataset.key)];
|
|
378
|
+
if (code.length === 1) type(code);
|
|
379
|
+
else if (code === "Backspace") erase();
|
|
380
|
+
else if (code === "Enter" || code === "NumpadEnter") enter();
|
|
381
|
+
else press(code);
|
|
382
|
+
stage.focus({ preventScroll: true });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (event.target.closest('[data-part="computer"]')) { takeOver(true); setPower(power ? 0 : 1); stage.focus({ preventScroll: true }); }
|
|
386
|
+
});
|
|
387
|
+
write();
|
|
388
|
+
crtFrame();
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
const css = `<style>
|
|
392
|
+
.pc-well{fill:var(--anatomy-shade-1)}
|
|
393
|
+
.pc-bezel{fill:var(--anatomy-top)}
|
|
394
|
+
.pc-dark{fill:var(--anatomy-shade-0)}
|
|
395
|
+
.pc-glass{fill:var(--anatomy-shade-0);transition:fill .5s var(--anatomy-ease)}
|
|
396
|
+
.pc-glass[data-on]{fill:var(--anatomy-lit-shade)}
|
|
397
|
+
.pc-glow{fill:var(--anatomy-lit);opacity:.07;filter:blur(6px)}
|
|
398
|
+
.pc-scan{fill:none;stroke:var(--anatomy-faint);stroke-width:.5px;vector-effect:non-scaling-stroke;opacity:.7}
|
|
399
|
+
.pc-logo{fill:var(--anatomy-lit);fill-rule:evenodd}
|
|
400
|
+
.pc-text{fill:var(--anatomy-lit);font-family:var(--anatomy-mono);letter-spacing:.02em}
|
|
401
|
+
.pc-cursor{fill:var(--anatomy-lit);animation:pc-blink 1.06s steps(1,end) infinite}
|
|
402
|
+
.pc-beam{fill:none;stroke:var(--anatomy-lit);stroke-width:1.4px;vector-effect:non-scaling-stroke;stroke-linecap:round}
|
|
403
|
+
.pc-dot{fill:var(--anatomy-lit)}
|
|
404
|
+
.pc-cord{stroke-width:1.1px}
|
|
405
|
+
.pc-key,.pc-computer{cursor:pointer}
|
|
406
|
+
.pc-key[data-down] .iso-edge{stroke:var(--anatomy-hi)}
|
|
407
|
+
[data-part="led"]{transition:opacity .4s var(--anatomy-ease)}
|
|
408
|
+
@keyframes pc-blink{50%{opacity:0}}
|
|
409
|
+
@media (prefers-reduced-motion:reduce){.pc-cursor{animation:none}}
|
|
410
|
+
.pc-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
|
|
411
|
+
.iso-stage{touch-action:pan-y}
|
|
412
|
+
.iso-stage:focus{outline:none}
|
|
413
|
+
.iso-stage:focus-visible{outline:1px solid var(--anatomy-muted);outline-offset:6px}
|
|
414
|
+
</style>`;
|
|
415
|
+
|
|
416
|
+
const body =
|
|
417
|
+
css +
|
|
418
|
+
k.plateHtml({
|
|
419
|
+
fig: "Fig 2",
|
|
420
|
+
title: "Desk computer",
|
|
421
|
+
hint: "Type on it · click it to switch",
|
|
422
|
+
readout: "on · 0 chars",
|
|
423
|
+
keys: [
|
|
424
|
+
{ mark: "lit", label: "The screen: the Anthropic mark and what you type" },
|
|
425
|
+
{ mark: "raised", label: "Fifty-eight keys; each one goes down as it is typed" },
|
|
426
|
+
{ mark: "edge", label: "A coiled cord from the keyboard to the side port" },
|
|
427
|
+
],
|
|
428
|
+
caption:
|
|
429
|
+
"An all-in-one desk computer in the spirit of 1984: a CRT set into a tall case, a floppy slot and speaker grille under it, vents and a rocker switch on the side. Focus it and type: each key you press goes down on the keyboard and the character lands on the screen beneath the Anthropic mark. Backspace and Return work, Escape or a click on the case switches it off and the picture collapses to a line and a dot, as a CRT does.",
|
|
430
|
+
body: `<div class="iso-stage" tabindex="0" role="group" aria-label="Desk computer. Type to write on its screen; Escape switches the power.">${svg}<span class="pc-sr" role="status" aria-live="polite" data-status></span></div>`,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const name = theme === "light" ? "desk-computer-light.html" : "desk-computer.html";
|
|
434
|
+
writeFileSync(join(HERE, name), k.pageHtml({ title: "Desk computer", theme, body, script: kitScript() + live }));
|
|
435
|
+
console.log("wrote", name);
|