@vibes.diy/prompts 14.3.36 → 14.3.38

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.
@@ -0,0 +1,161 @@
1
+ # Three.js — the first draft: one lit object turning in a viewport
2
+
3
+ A Three.js app's first draft is the whole page around the scene (title, copy,
4
+ a card holding the viewport, a control row) with one real, lit object turning
5
+ inside it. The seeded document names the scene and holds its color, and the
6
+ control row repaints the object live. Loading models, physics, particles,
7
+ camera controls and textures are the next pass. What the draft owes that pass
8
+ is the platform's own engine skeleton: the renderer, scene, camera, lights and
9
+ loop are built once inside one effect, sized to their host, and fully disposed
10
+ on unmount, and React talks to the running scene through a ref, so the next
11
+ pass adds meshes and systems to this file rather than rewriting it.
12
+
13
+ ### `App.jsx`
14
+ ```jsx
15
+ import React, { useEffect, useRef } from "react";
16
+ import { useFireproof } from "use-fireproof";
17
+ import * as THREE from "three";
18
+ import { Card, CardHeader, CardTitle, CardContent, Button, Badge } from "@vibes.diy/look";
19
+
20
+ // The finishes the control row offers, named once for the buttons and the material.
21
+ const FINISHES = [
22
+ { label: "Lagoon", color: "#3fa7d6" },
23
+ { label: "Marigold", color: "#f4b942" },
24
+ { label: "Orchid", color: "#c86bd8" },
25
+ { label: "Moss", color: "#6fae5b" },
26
+ ];
27
+
28
+ export default function App() {
29
+ const { database, useLiveQuery } = useFireproof("spinStudio");
30
+ const { docs: scenes } = useLiveQuery("type", { key: "scene" });
31
+ const scene = scenes[0];
32
+ const hostRef = useRef(null);
33
+ // The running engine reads the color from here each frame, so a change never rebuilds it.
34
+ const colorRef = useRef(scene?.color ?? FINISHES[0].color);
35
+
36
+ useEffect(() => {
37
+ if (scene?.color) colorRef.current = scene.color;
38
+ }, [scene?.color]);
39
+
40
+ // The engine, built once: renderer, scene, camera, lights, one mesh, loop, resize, cleanup.
41
+ useEffect(() => {
42
+ const host = hostRef.current;
43
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
44
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
45
+ renderer.setSize(host.clientWidth, host.clientHeight);
46
+ host.appendChild(renderer.domElement);
47
+
48
+ const world = new THREE.Scene();
49
+ const camera = new THREE.PerspectiveCamera(50, host.clientWidth / host.clientHeight, 0.1, 100);
50
+ camera.position.set(0, 1.2, 4.2);
51
+ camera.lookAt(0, 0, 0);
52
+
53
+ world.add(new THREE.HemisphereLight(0xffffff, 0x445066, 0.8));
54
+ const sun = new THREE.DirectionalLight(0xffffff, 1.4);
55
+ sun.position.set(3, 5, 2);
56
+ world.add(sun);
57
+
58
+ const geometry = new THREE.BoxGeometry(1.4, 1.4, 1.4);
59
+ const material = new THREE.MeshStandardMaterial({ color: colorRef.current, roughness: 0.35, metalness: 0.15 });
60
+ const mesh = new THREE.Mesh(geometry, material);
61
+ world.add(mesh);
62
+
63
+ const onResize = () => {
64
+ const w = host.clientWidth;
65
+ const h = host.clientHeight;
66
+ if (w === 0 || h === 0) return;
67
+ renderer.setSize(w, h);
68
+ camera.aspect = w / h;
69
+ camera.updateProjectionMatrix();
70
+ };
71
+ const observer = new window.ResizeObserver(onResize); // follows the host, not just the window
72
+ observer.observe(host);
73
+
74
+ const clock = new THREE.Clock();
75
+ let raf = 0;
76
+ const animate = () => {
77
+ const dt = Math.min(clock.getDelta(), 0.05);
78
+ mesh.rotation.x += dt * 0.5;
79
+ mesh.rotation.y += dt * 0.8;
80
+ material.color.set(colorRef.current);
81
+ renderer.render(world, camera);
82
+ raf = requestAnimationFrame(animate);
83
+ };
84
+ animate();
85
+
86
+ return () => {
87
+ cancelAnimationFrame(raf);
88
+ observer.disconnect();
89
+ geometry.dispose();
90
+ material.dispose();
91
+ renderer.dispose();
92
+ if (renderer.domElement.parentNode === host) host.removeChild(renderer.domElement);
93
+ };
94
+ }, []);
95
+
96
+ const paint = (color) => {
97
+ colorRef.current = color;
98
+ if (scene) database.put({ ...scene, color });
99
+ };
100
+
101
+ return (
102
+ <main className="min-h-screen bg-background text-foreground font-sans p-6">
103
+ <h1 className="text-3xl font-bold">{scene?.name ?? "Spin Studio"}</h1>
104
+ <p className="mt-1 text-muted-foreground">A little object on a turntable. Pick a finish and watch it catch the light.</p>
105
+
106
+ <Card className="mt-6">
107
+ <CardHeader>
108
+ <CardTitle>Turntable</CardTitle>
109
+ </CardHeader>
110
+ <CardContent>
111
+ <div ref={hostRef} className="aspect-video w-full rounded-lg overflow-hidden bg-muted" />
112
+ {/* TODO(next pass): load a GLTF model, orbit controls, textures, particles and physics into this same scene */}
113
+ <div className="mt-4 flex flex-wrap items-center gap-2">
114
+ {FINISHES.map((f) => (
115
+ <Button key={f.label} size="sm" variant={f.color === scene?.color ? "default" : "outline"} onClick={() => paint(f.color)}>
116
+ {f.label}
117
+ </Button>
118
+ ))}
119
+ <Badge variant="secondary" className="ml-auto">{FINISHES.find((f) => f.color === scene?.color)?.label ?? "Lagoon"}</Badge>
120
+ </div>
121
+ </CardContent>
122
+ </Card>
123
+ </main>
124
+ );
125
+ }
126
+ ```
127
+
128
+ ### `seed.json`
129
+ ```json
130
+ {
131
+ "spinStudio": [
132
+ { "key": "turntable", "type": "scene", "name": "Spin Studio", "color": "#3fa7d6" }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ Draft note: the next pass adds loaded models, orbit controls, textures, particles and physics to the same scene.
138
+
139
+ ## What stays the same in a Three.js draft
140
+
141
+ - **The engine is built once** in `useEffect(() => {...}, [])`: renderer,
142
+ scene, camera, lights, mesh and `requestAnimationFrame` loop all inside it.
143
+ React never rebuilds it; values React changes reach the loop through a ref
144
+ (`colorRef`), never through the effect's dependency list.
145
+ - **The cleanup is complete**: it cancels the frame, disconnects the resize
146
+ observer, disposes every geometry and material it made, disposes the
147
+ renderer, and removes the canvas from its host.
148
+ - **The viewport sizes to its host**, not the window: a `div` with a fixed
149
+ aspect (`aspect-video w-full rounded-lg overflow-hidden`) inside a Card, a
150
+ resize handler that updates the renderer size and the camera aspect, and
151
+ `setPixelRatio(Math.min(window.devicePixelRatio, 2))`.
152
+ - **The hello is one lit object**: a `BoxGeometry` with a
153
+ `MeshStandardMaterial` under a hemisphere light and a directional light,
154
+ turning on a clock delta. Standard materials need light; the draft always
155
+ ships both.
156
+ - **The page is complete around it**: title, a sentence of friendly copy, the
157
+ Card, and a control row of Buttons whose choice saves to the seeded
158
+ Fireproof document and shows up in the scene on the next frame.
159
+ - **What waits for the next pass** is named in the `TODO(next pass)` comment
160
+ and the draft note: models, controls, textures, particles, physics. The
161
+ draft draws none of it and imports nothing beyond `three` itself.
package/llms/three-js.js CHANGED
@@ -3,6 +3,7 @@ export const threeJsConfig = {
3
3
  label: "Three.js",
4
4
  description: "Three.js 3D graphics library for WebGL rendering: mesh geometry, materials, lighting, animation, cameras, textures, GLSL shaders, GLTF/OBJ/FBX model loading, physics, particle systems, post-processing, visual effects, 3js",
5
5
  cues: ["3d", "three.js", "threejs", "3js", "webgl", "shader", "shaders", "mesh", "gltf", "3d model", "3d scene"],
6
+ draftVariant: true,
6
7
  importModule: "three",
7
8
  importName: "THREE",
8
9
  importType: "namespace",
@@ -1 +1 @@
1
- {"version":3,"file":"three-js.js","sourceRoot":"","sources":["../../jsr/llms/three-js.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,UAAU;IACjB,WAAW,EACT,+NAA+N;IACjO,IAAI,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC;IAChH,YAAY,EAAE,OAAO;IACrB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,WAAW;CACxB,CAAC"}
1
+ {"version":3,"file":"three-js.js","sourceRoot":"","sources":["../../jsr/llms/three-js.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAc;IACtC,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,UAAU;IACjB,WAAW,EACT,+NAA+N;IACjO,IAAI,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC;IAGhH,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,OAAO;IACrB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,WAAW;CACxB,CAAC"}
@@ -1,12 +1,17 @@
1
- # Voxel worlds — the first draft: a viewport that plays
2
-
3
- A voxel app's first draft is a full-screen 3D viewport with a block world you
4
- can fly through and look around, drawn the fast way, with the HUD floating over
5
- it. Placing and breaking blocks, walking with collision, endless terrain,
6
- saving edits and sharing the world are the next pass. What the draft owes that
7
- pass is the right skeleton: the world lives in a flat `Map`, the terrain is
8
- drawn as one `InstancedMesh`, and the engine is built once inside an effect —
9
- so the next pass adds to this file rather than rewriting it.
1
+ # Voxel worlds — the first draft: one grass block, turning in the sky
2
+
3
+ A voxel app's first draft is a full-screen 3D viewport with the HUD floating
4
+ over it, and in that viewport ONE block: a pixel-art grass block, green on top
5
+ and dirt on the sides, turning slowly under a blue sky. The name card, the
6
+ hotbar, the palette, the seeded world document and the look are all finished;
7
+ the block is real, lit and drawn through the same instanced path the whole
8
+ world will use.
9
+ Terrain, placing and breaking, walking, saving edits and sharing the world are
10
+ the next pass. What the draft owes that pass is the right skeleton: the engine
11
+ is built once inside an effect, the block is ONE `InstancedMesh` with a
12
+ count of 1, the viewport is `fixed inset-0` with the controls on top of it,
13
+ and the palette is named once for the HUD and the blocks, so the next pass
14
+ raises the count and adds to this file rather than rewriting it.
10
15
 
11
16
  Unlike the page-shaped draft above, a world does not flow down the page: the
12
17
  viewport is `fixed inset-0` and the controls sit on top of it.
@@ -16,54 +21,36 @@ viewport is `fixed inset-0` and the controls sit on top of it.
16
21
  import React, { useEffect, useRef, useState } from "react";
17
22
  import { useFireproof } from "use-fireproof";
18
23
  import * as THREE from "three";
19
- import { Button, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
24
+ import { Button, Badge } from "@vibes.diy/look";
20
25
 
21
- // The block-in-hand palette (shared by materials and HUD swatches).
26
+ // The block-in-hand palette (shared by block colors and HUD swatches).
22
27
  const VOXEL_PALETTE = {
23
28
  grass: "#6fcf5b", dirt: "#8a5a3b", stone: "#9b9b9b", wood: "#b5793a", glass: "#9fe3ff", candy: "#ff6fb1",
24
29
  neon: "#39ff9e", water: "#2f8fe0", lava: "#ff5a2f", erase: "#ff4d6d", ink: "#1a1325", light: "#ffffff",
25
30
  };
26
- const HOTBAR = ["grass", "dirt", "stone", "wood"];
27
- const GAME_KEYS = new Set(["KeyW", "KeyA", "KeyS", "KeyD", "KeyC", "Space", "ShiftLeft"]);
28
- const SIZE = 24; // half-width of the starting island, in blocks
29
- const SEED = 7;
30
-
31
- const keyOf = (x, y, z) => `${x}|${y}|${z}`;
32
- const heightAt = (x, z) => Math.round(Math.sin((x + SEED) * 0.18) * 2 + Math.cos((z - SEED) * 0.15) * 2 + Math.sin((x + z) * 0.06) * 3) + 5;
33
-
34
- // The world model: every block, buried or not. Collision and picking read this, never the scene.
35
- function buildBlocks() {
36
- const blocks = new Map();
37
- for (let x = -SIZE; x <= SIZE; x++)
38
- for (let z = -SIZE; z <= SIZE; z++) {
39
- const h = heightAt(x, z);
40
- for (let y = 0; y <= h; y++) blocks.set(keyOf(x, y, z), y === h ? "grass" : y > h - 3 ? "dirt" : "stone");
31
+ const HOTBAR = ["grass", "dirt", "stone", "wood", "glass", "candy"];
32
+ const SKY = 0x8ecbff;
33
+
34
+ // A 16×16 pixel-art face, painted once. `fringe` rows of green sit on top of dirt for the sides.
35
+ function blockFace(kind) {
36
+ const canvas = document.createElement("canvas");
37
+ canvas.width = 16;
38
+ canvas.height = 16;
39
+ const ctx = canvas.getContext("2d");
40
+ const px = (x, y, color) => { ctx.fillStyle = color; ctx.fillRect(x, y, 1, 1); };
41
+ let n = kind.length * 7;
42
+ const rand = () => { n = (n * 9301 + 49297) % 233280; return n / 233280; };
43
+ for (let y = 0; y < 16; y++)
44
+ for (let x = 0; x < 16; x++) {
45
+ const fringe = kind === "side" && y < 3 + (x % 3 === 0 ? 1 : 0);
46
+ if (kind === "top" || fringe) px(x, y, rand() < 0.18 ? "#4fa83f" : VOXEL_PALETTE.grass);
47
+ else px(x, y, rand() < 0.2 ? "#6e4630" : VOXEL_PALETTE.dirt);
41
48
  }
42
- return blocks;
43
- }
44
-
45
- const NB = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
46
- const isHidden = (blocks, x, y, z) => NB.every(([dx, dy, dz]) => blocks.has(keyOf(x + dx, y + dy, z + dz)));
47
-
48
- // One shared cube, one shared material, one draw call: the exposed blocks as instances with a color each.
49
- const CUBE = new THREE.BoxGeometry(1, 1, 1);
50
- const WORLD_MAT = new THREE.MeshLambertMaterial({ color: 0xffffff });
51
- function buildWorldMesh(blocks) {
52
- const visible = [];
53
- for (const [k, type] of blocks) {
54
- const [x, y, z] = k.split("|").map(Number);
55
- if (!isHidden(blocks, x, y, z)) visible.push([x, y, z, type]);
56
- }
57
- const mesh = new THREE.InstancedMesh(CUBE, WORLD_MAT, visible.length);
58
- const m = new THREE.Matrix4();
59
- const c = new THREE.Color();
60
- visible.forEach(([x, y, z, type], i) => {
61
- mesh.setMatrixAt(i, m.makeTranslation(x + 0.5, y + 0.5, z + 0.5));
62
- mesh.setColorAt(i, c.set(VOXEL_PALETTE[type]));
63
- });
64
- mesh.instanceMatrix.needsUpdate = true;
65
- mesh.instanceColor.needsUpdate = true;
66
- return mesh;
49
+ const texture = new THREE.CanvasTexture(canvas);
50
+ texture.magFilter = THREE.NearestFilter;
51
+ texture.minFilter = THREE.NearestFilter;
52
+ texture.colorSpace = THREE.SRGBColorSpace;
53
+ return texture;
67
54
  }
68
55
 
69
56
  export default function App() {
@@ -71,11 +58,9 @@ export default function App() {
71
58
  const { docs: worlds } = useLiveQuery("type", { key: "world" });
72
59
  const world = worlds[0];
73
60
  const mountRef = useRef(null);
74
- const engineRef = useRef(null);
75
- const [locked, setLocked] = useState(false);
76
61
  const [held, setHeld] = useState(HOTBAR[0]);
77
62
 
78
- // The engine, built once: renderer, scene, world mesh, input, loop. React only draws the HUD over it.
63
+ // The engine, built once: renderer, scene, lights, the instanced block, the loop. React only draws the HUD over it.
79
64
  useEffect(() => {
80
65
  const mount = mountRef.current;
81
66
  const renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -84,85 +69,60 @@ export default function App() {
84
69
  mount.appendChild(renderer.domElement);
85
70
 
86
71
  const scene = new THREE.Scene();
87
- scene.background = new THREE.Color(0x8ecbff);
88
- scene.fog = new THREE.Fog(0x8ecbff, 40, 140);
89
- const camera = new THREE.PerspectiveCamera(75, mount.clientWidth / mount.clientHeight, 0.1, 500);
72
+ scene.background = new THREE.Color(SKY);
73
+ scene.fog = new THREE.Fog(SKY, 8, 30);
74
+ const camera = new THREE.PerspectiveCamera(60, mount.clientWidth / mount.clientHeight, 0.1, 100);
75
+ camera.position.set(0, 1.4, 3.2);
76
+ camera.lookAt(0, 0, 0);
90
77
  scene.add(new THREE.HemisphereLight(0xffffff, 0x8a5a3b, 0.9));
91
- const sun = new THREE.DirectionalLight(0xffffff, 1);
92
- sun.position.set(30, 60, 20);
78
+ const sun = new THREE.DirectionalLight(0xffffff, 1.2);
79
+ sun.position.set(4, 8, 5);
93
80
  scene.add(sun);
94
81
 
95
- const blocks = buildBlocks();
96
- const worldMesh = buildWorldMesh(blocks);
97
- scene.add(worldMesh);
98
-
99
- const state = { pos: new THREE.Vector3(0, heightAt(0, 0) + 3, 8), yaw: 0, pitch: -0.2, keys: {}, running: true };
100
- const canvas = renderer.domElement;
101
- const onLock = () => setLocked(document.pointerLockElement === canvas);
102
- const onMove = (e) => {
103
- if (document.pointerLockElement !== canvas) return;
104
- state.yaw -= e.movementX * 0.002;
105
- state.pitch = Math.max(-1.5, Math.min(1.5, state.pitch - e.movementY * 0.002));
106
- };
107
- const onKeyDown = (e) => {
108
- if (document.pointerLockElement !== canvas) return; // only while the world holds the mouse
109
- if (e.target.isContentEditable || e.target.closest("input, textarea")) return; // typing stays typing
110
- if (!GAME_KEYS.has(e.code)) return; // browser shortcuts stay browser shortcuts
111
- state.keys[e.code] = true;
112
- e.preventDefault(); // Space flies up, never scrolls the page
113
- };
114
- const onKeyUp = (e) => {
115
- state.keys[e.code] = false;
116
- };
117
- const onBlur = () => { state.keys = {}; };
82
+ // The hello: one grass block, drawn the way the whole world will be — one shared cube, six shared
83
+ // face materials (BoxGeometry face order is +x, -x, +y, -y, +z, -z), one InstancedMesh. The next pass
84
+ // raises the count and fills more matrices; it never adds a Mesh per block.
85
+ const textures = { top: blockFace("top"), side: blockFace("side"), bottom: blockFace("bottom") };
86
+ const faces = ["side", "side", "top", "bottom", "side", "side"];
87
+ const materials = faces.map((f) => new THREE.MeshLambertMaterial({ map: textures[f] }));
88
+ const cube = new THREE.BoxGeometry(1, 1, 1);
89
+ const blocks = new THREE.InstancedMesh(cube, materials, 1);
90
+ scene.add(blocks);
91
+ const matrix = new THREE.Matrix4();
92
+ const spin = new THREE.Quaternion();
93
+ const up = new THREE.Vector3(0, 1, 0);
94
+ const at = new THREE.Vector3();
95
+ const one = new THREE.Vector3(1, 1, 1);
96
+
118
97
  const onResize = () => {
119
98
  renderer.setSize(mount.clientWidth, mount.clientHeight);
120
99
  camera.aspect = mount.clientWidth / mount.clientHeight;
121
100
  camera.updateProjectionMatrix();
122
101
  };
123
- document.addEventListener("pointerlockchange", onLock);
124
- document.addEventListener("mousemove", onMove);
125
- window.addEventListener("keydown", onKeyDown);
126
- window.addEventListener("keyup", onKeyUp);
127
- window.addEventListener("blur", onBlur);
128
102
  window.addEventListener("resize", onResize);
129
- engineRef.current = { requestLock: () => canvas.requestPointerLock() };
130
103
 
131
104
  const clock = new THREE.Clock();
132
- const forward = new THREE.Vector3();
133
- const right = new THREE.Vector3();
134
105
  let raf;
135
106
  const loop = () => {
136
- if (!state.running) return;
137
- const dt = Math.min(clock.getDelta(), 0.05);
138
- const speed = (state.keys.ShiftLeft ? 18 : 9) * dt;
139
- forward.set(-Math.sin(state.yaw), 0, -Math.cos(state.yaw));
140
- right.set(forward.z, 0, -forward.x);
141
- if (state.keys.KeyW) state.pos.addScaledVector(forward, speed);
142
- if (state.keys.KeyS) state.pos.addScaledVector(forward, -speed);
143
- if (state.keys.KeyD) state.pos.addScaledVector(right, speed);
144
- if (state.keys.KeyA) state.pos.addScaledVector(right, -speed);
145
- if (state.keys.Space) state.pos.y += speed;
146
- if (state.keys.KeyC) state.pos.y -= speed;
147
- camera.position.copy(state.pos);
148
- camera.rotation.set(state.pitch, state.yaw, 0, "YXZ");
107
+ const t = clock.getElapsedTime();
108
+ spin.setFromAxisAngle(up, t * 0.5);
109
+ at.set(0, Math.sin(t * 1.2) * 0.06, 0);
110
+ blocks.setMatrixAt(0, matrix.compose(at, spin, one));
111
+ blocks.instanceMatrix.needsUpdate = true;
149
112
  renderer.render(scene, camera);
150
113
  raf = requestAnimationFrame(loop);
151
114
  };
152
115
  loop();
153
116
 
154
117
  return () => {
155
- state.running = false;
156
118
  cancelAnimationFrame(raf);
157
- document.removeEventListener("pointerlockchange", onLock);
158
- document.removeEventListener("mousemove", onMove);
159
- window.removeEventListener("keydown", onKeyDown);
160
- window.removeEventListener("keyup", onKeyUp);
161
- window.removeEventListener("blur", onBlur);
162
119
  window.removeEventListener("resize", onResize);
163
- worldMesh.dispose();
120
+ Object.values(textures).forEach((t) => t.dispose());
121
+ materials.forEach((m) => m.dispose());
122
+ blocks.dispose();
123
+ cube.dispose();
164
124
  renderer.dispose();
165
- mount.removeChild(canvas);
125
+ mount.removeChild(renderer.domElement);
166
126
  };
167
127
  }, []);
168
128
 
@@ -172,32 +132,21 @@ export default function App() {
172
132
 
173
133
  <div className="absolute left-4 top-4 rounded-lg bg-card/90 px-3 py-2 text-card-foreground shadow-md">
174
134
  <p className="font-bold">{world?.name ?? "Sky Island"}</p>
175
- <p className="text-muted-foreground">WASD fly · Space up · C down · Shift fast</p>
135
+ <p className="text-muted-foreground">Your first block is ready. Pick what goes in your hand.</p>
176
136
  </div>
177
137
 
138
+ <Badge className="absolute right-4 top-4">Holding {held}</Badge>
139
+
178
140
  <div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-lg bg-card/90 p-2 shadow-md">
179
141
  {HOTBAR.map((type) => (
180
142
  <Button key={type} size="sm" variant={type === held ? "default" : "outline"} onClick={() => setHeld(type)}
181
- style={{ backgroundColor: type === held ? VOXEL_PALETTE[type] : undefined }}>
143
+ aria-label={type}>
144
+ <span className="inline-block h-4 w-4 rounded-sm" style={{ backgroundColor: VOXEL_PALETTE[type] }} />
182
145
  {type}
183
146
  </Button>
184
147
  ))}
185
148
  </div>
186
- {/* TODO(next pass): place and break the held block with a voxel raycast, walk with gravity and collision, save edits as documents */}
187
-
188
- {!locked && (
189
- <button onClick={() => engineRef.current?.requestLock()} className="absolute inset-0 z-20 flex items-center justify-center bg-black/55">
190
- <Card className="max-w-sm">
191
- <CardHeader>
192
- <CardTitle>{world?.name ?? "Sky Island"}</CardTitle>
193
- </CardHeader>
194
- <CardContent>
195
- <p>Click to play</p>
196
- <p className="mt-2 text-muted-foreground">Mouse looks around. Esc gives the mouse back.</p>
197
- </CardContent>
198
- </Card>
199
- </button>
200
- )}
149
+ {/* TODO(next pass): the flat block map and instanced terrain, place and break the held block, walk with pointer lock, save edits as documents, share the world */}
201
150
  </div>
202
151
  );
203
152
  }
@@ -212,7 +161,7 @@ export default function App() {
212
161
  }
213
162
  ```
214
163
 
215
- Draft note: the next pass adds placing and breaking blocks, walking with gravity and collision, saved edits per player, and inviting friends into the world.
164
+ Draft note: the next pass adds the terrain around this block, placing and breaking with the held block, walking the world, saving edits, and sharing the world with friends.
216
165
 
217
166
  ## What stays the same in a voxel draft
218
167
 
@@ -221,24 +170,27 @@ Draft note: the next pass adds placing and breaking blocks, walking with gravity
221
170
  and every control is an `absolute` layer over it. Never the page-flow `main`
222
171
  with padding from the shape above.
223
172
  - **The engine is built once** in `useEffect(() => {...}, [])` with the
224
- renderer, scene, world mesh, listeners and loop inside it, `engineRef.current` for the few calls
225
- React makes in, and a cleanup that stops the loop, removes every listener
226
- and disposes the mesh and renderer. Per-frame values live in the `state`
227
- object, never in React state.
173
+ renderer, scene, lights, listeners and loop inside it, and a cleanup that
174
+ stops the loop, removes the listener and disposes every texture, material, the
175
+ instanced mesh, the geometry and the renderer. Per-frame values never live in React state.
228
176
  - **`setPixelRatio(Math.min(window.devicePixelRatio, 2))`**, a resize handler
229
- that updates the renderer size and the camera aspect, and a `Fog` that
230
- matches the sky color.
231
- - **The world is a flat `Map` keyed `"x|y|z"`**, and it is drawn as ONE
232
- `InstancedMesh` of the exposed blocks with one shared `BoxGeometry` and one
233
- shared material, the color per instance. Never a `THREE.Mesh` per block, and
234
- never a geometry or material created inside the block loop.
235
- - **Pointer lock comes from a "Click to play" overlay** whose visibility follows
236
- the real `pointerlockchange` state, so it returns when the player presses
237
- Esc. Mouse look clamps pitch to ±1.5 rad. A keydown calls `preventDefault`
238
- only when the canvas holds the lock, the target is not a text field, and the
239
- key is in the game's own set; held keys clear on blur.
177
+ that updates the renderer size and the camera aspect, a hemisphere plus a
178
+ directional light, and a `Fog` that matches the sky color.
179
+ - **The hello is one grass block, turning, through the fast path**: one shared
180
+ `BoxGeometry` and an array of six shared materials, each face a 16×16
181
+ `THREE.CanvasTexture` painted in code with `NearestFilter` so the pixels
182
+ stay crisp, drawn as `new THREE.InstancedMesh(cube, materials, 1)` and
183
+ turned each frame with `setMatrixAt(0, …)` plus
184
+ `instanceMatrix.needsUpdate = true`. No texture URLs, no loaders.
240
185
  - **The palette table** is the Blocktopia voxel palette, in full, named once
241
- for both the instance colors and the HUD swatches.
186
+ for the block colors and the HUD swatches.
187
+ - **The next pass keeps this skeleton**: the world becomes a flat `Map` keyed
188
+ `"x|y|z"`, drawn through the same ONE `InstancedMesh` of the exposed blocks
189
+ (the count raised, a matrix per block) with its shared geometry and
190
+ materials. Never a
191
+ `THREE.Mesh` per block, and never a geometry or material created inside the
192
+ block loop. Pointer lock comes from a click-to-play overlay whose visibility
193
+ follows the real lock state, and movement keys are the game's own set.
242
194
  - **What waits for the next pass** is named in the `TODO(next pass)` comment
243
- and the draft note: placing and breaking, collision and gravity, endless
244
- terrain, saving edits, sharing. The draft draws none of it.
195
+ and the draft note: terrain, placing and breaking, walking, saving edits,
196
+ sharing. The draft draws none of it.