Skip to content

Commit 25b0db1

Browse files
author
b
committed
feat: pulse-hit letter blocks and yaw-only field turn
1 parent 608ccbf commit 25b0db1

File tree

1 file changed

+148
-4
lines changed

1 file changed

+148
-4
lines changed

src/components/CircuitBackground.vue

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { onMounted, onUnmounted, ref } from 'vue';
33
import * as THREE from 'three';
4+
import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
45
56
type BackgroundMode = 'circuit' | 'legacy';
67
@@ -20,6 +21,7 @@ interface PulseMarker {
2021
phase: number;
2122
flashRate: number;
2223
flashPhase: number;
24+
lastProgress: number;
2325
}
2426
2527
interface CircuitLine {
@@ -35,6 +37,13 @@ interface CircuitLine {
3537
baseThickness: number;
3638
}
3739
40+
interface LetterBlock {
41+
group: THREE.Group;
42+
labelTexture: THREE.CanvasTexture;
43+
labelMaterial: THREE.MeshBasicMaterial;
44+
bornAt: number;
45+
}
46+
3847
let scene: THREE.Scene | null = null;
3948
let camera: THREE.PerspectiveCamera | null = null;
4049
let renderer: THREE.WebGLRenderer | null = null;
@@ -57,13 +66,27 @@ const CENTER_DEPTH = -74;
5766
const CIRCUIT_COUNT = 68;
5867
const PULSE_SPEED_BASE = 0.032;
5968
const PULSE_SPEED_FLOW_GAIN = 0.0022;
69+
const BLOCK_SEQUENCE = 'PROMPTEXECUTION';
70+
const BLOCK_COLS = 8;
71+
const BLOCK_ROWS = 3;
72+
const BLOCK_SIZE = 2.5;
73+
const BLOCK_GAP = 0.26;
74+
const MAX_BLOCKS = BLOCK_COLS * BLOCK_ROWS * 4;
6075
6176
const PIPE_AXIS = new THREE.Vector3(0, 1, 0);
6277
const cameraToGroup = new THREE.Vector3();
6378
const colorNear = new THREE.Color(0xc8ffff);
6479
const focalPoint = new THREE.Vector3(0, 0, 0);
6580
const focalTarget = new THREE.Vector3(0, 0, 0);
81+
const cameraLookTarget = new THREE.Vector3(0, 0, 0);
82+
const blockGeometry = new RoundedBoxGeometry(1, 1, 1, 5, 0.2);
83+
const blockLabelGeometry = new THREE.PlaneGeometry(0.62, 0.62);
84+
const blockAssemblyOffset = new THREE.Vector3(0, -19, CENTER_DEPTH + 26);
85+
const blockLookAt = new THREE.Vector3();
6686
let flowTravel = 0;
87+
let blockAssemblyGroup: THREE.Group | null = null;
88+
let blockCursor = 0;
89+
const letterBlocks: LetterBlock[] = [];
6790
6891
function rand(min: number, max: number) {
6992
return min + Math.random() * (max - min);
@@ -177,6 +200,115 @@ function createPipeSegment(start: THREE.Vector3, end: THREE.Vector3, material: T
177200
return mesh;
178201
}
179202
203+
function createLetterTexture(letter: string) {
204+
const canvas = document.createElement('canvas');
205+
canvas.width = 128;
206+
canvas.height = 128;
207+
const ctx = canvas.getContext('2d');
208+
if (ctx) {
209+
ctx.fillStyle = '#fff6a5';
210+
ctx.fillRect(0, 0, canvas.width, canvas.height);
211+
ctx.fillStyle = '#2a2300';
212+
ctx.font = 'bold 86px "Courier New", monospace';
213+
ctx.textAlign = 'center';
214+
ctx.textBaseline = 'middle';
215+
ctx.fillText(letter, canvas.width / 2, canvas.height / 2 + 2);
216+
}
217+
const texture = new THREE.CanvasTexture(canvas);
218+
texture.colorSpace = THREE.SRGBColorSpace;
219+
texture.needsUpdate = true;
220+
return texture;
221+
}
222+
223+
function createLetterBlock(letter: string, bornAt: number): LetterBlock {
224+
const group = new THREE.Group();
225+
const blockMesh = new THREE.Mesh(
226+
blockGeometry,
227+
new THREE.MeshStandardMaterial({
228+
color: 0xffde4f,
229+
emissive: 0x9c7300,
230+
emissiveIntensity: 0.24,
231+
metalness: 0.09,
232+
roughness: 0.46,
233+
}),
234+
);
235+
blockMesh.scale.setScalar(BLOCK_SIZE);
236+
group.add(blockMesh);
237+
238+
const labelTexture = createLetterTexture(letter);
239+
const labelMaterial = new THREE.MeshBasicMaterial({
240+
map: labelTexture,
241+
transparent: true,
242+
depthWrite: false,
243+
});
244+
const label = new THREE.Mesh(blockLabelGeometry, labelMaterial);
245+
label.scale.setScalar(BLOCK_SIZE * 0.95);
246+
label.position.set(0, 0, BLOCK_SIZE * 0.515);
247+
group.add(label);
248+
249+
return { group, labelTexture, labelMaterial, bornAt };
250+
}
251+
252+
function layoutLetterBlocks() {
253+
for (let i = 0; i < letterBlocks.length; i += 1) {
254+
const col = i % BLOCK_COLS;
255+
const row = Math.floor(i / BLOCK_COLS) % BLOCK_ROWS;
256+
const layer = Math.floor(i / (BLOCK_COLS * BLOCK_ROWS));
257+
const x = (col - (BLOCK_COLS - 1) * 0.5) * (BLOCK_SIZE + BLOCK_GAP);
258+
const y = row * (BLOCK_SIZE + BLOCK_GAP);
259+
const z = layer * (BLOCK_SIZE * 0.78 + BLOCK_GAP * 0.6);
260+
letterBlocks[i].group.position.set(x, y, z);
261+
}
262+
}
263+
264+
function removeLetterBlock(block: LetterBlock) {
265+
if (!blockAssemblyGroup) return;
266+
blockAssemblyGroup.remove(block.group);
267+
for (const child of block.group.children) {
268+
const mesh = child as THREE.Mesh;
269+
if (mesh.material && !(Array.isArray(mesh.material))) {
270+
if (mesh.material !== block.labelMaterial) {
271+
mesh.material.dispose();
272+
}
273+
}
274+
}
275+
block.labelMaterial.dispose();
276+
block.labelTexture.dispose();
277+
}
278+
279+
function spawnLetterBlock(elapsed: number) {
280+
if (!scene || !blockAssemblyGroup) return;
281+
const letter = BLOCK_SEQUENCE[blockCursor % BLOCK_SEQUENCE.length];
282+
blockCursor += 1;
283+
const block = createLetterBlock(letter, elapsed);
284+
block.group.scale.setScalar(0.14);
285+
blockAssemblyGroup.add(block.group);
286+
letterBlocks.push(block);
287+
if (letterBlocks.length > MAX_BLOCKS) {
288+
const oldest = letterBlocks.shift();
289+
if (oldest) {
290+
removeLetterBlock(oldest);
291+
}
292+
}
293+
layoutLetterBlocks();
294+
}
295+
296+
function updateLetterBlocks(elapsed: number) {
297+
if (!blockAssemblyGroup || !camera) return;
298+
299+
blockAssemblyGroup.position.copy(focalPoint).add(blockAssemblyOffset);
300+
blockLookAt.copy(camera.position);
301+
blockAssemblyGroup.lookAt(blockLookAt);
302+
blockAssemblyGroup.rotateY(Math.PI);
303+
304+
for (const block of letterBlocks) {
305+
const age = elapsed - block.bornAt;
306+
const grow = THREE.MathUtils.clamp(age * 5.4, 0, 1);
307+
const bounce = grow < 1 ? 1 + Math.sin(grow * Math.PI * 2.3) * 0.11 * (1 - grow) : 1;
308+
block.group.scale.setScalar(grow * bounce);
309+
}
310+
}
311+
180312
function addCenterHole() {
181313
if (!scene) return;
182314
@@ -213,6 +345,9 @@ function addCenterHole() {
213345
function createCircuitFlow() {
214346
if (!scene) return;
215347
348+
blockAssemblyGroup = new THREE.Group();
349+
scene.add(blockAssemblyGroup);
350+
216351
for (let i = 0; i < CIRCUIT_COUNT; i += 1) {
217352
const pathPoints = createOrthogonalPath();
218353
const sampler = createSampler(pathPoints);
@@ -280,12 +415,14 @@ function createCircuitFlow() {
280415
281416
group.add(core);
282417
group.add(aura);
418+
const normalizedPhase = (((headPhase - k * sequenceGap) % 1) + 1) % 1;
283419
pulses.push({
284420
core,
285421
aura,
286-
phase: headPhase - k * sequenceGap,
422+
phase: normalizedPhase,
287423
flashRate: rand(5.2, 13.8),
288424
flashPhase: rand(0, Math.PI * 2),
425+
lastProgress: normalizedPhase,
289426
});
290427
}
291428
}
@@ -349,10 +486,11 @@ function animate() {
349486
pointerParallax.lerp(pointerTarget, 0.035);
350487
focalTarget.set(pointerTarget.x * 30, pointerTarget.y * 18, 0);
351488
focalPoint.lerp(focalTarget, 0.1);
352-
camera.position.x = pointerParallax.x * 2.4;
353-
camera.position.y = pointerParallax.y * 1.8;
489+
camera.position.x = pointerParallax.x * 2.8;
490+
camera.position.y = 0;
354491
camera.position.z = 72;
355-
camera.lookAt(focalPoint);
492+
cameraLookTarget.set(focalPoint.x * 1.12, 0, CENTER_DEPTH * 0.26);
493+
camera.lookAt(cameraLookTarget);
356494
357495
if (BACKGROUND_MODE === 'circuit') {
358496
for (const circuit of circuits) {
@@ -380,6 +518,11 @@ function animate() {
380518
for (const pulse of circuit.pulses) {
381519
let progress = (elapsed * linePulseSpeed + pulse.phase) % 1;
382520
if (progress < 0) progress += 1;
521+
const wrappedAtEnd = progress < pulse.lastProgress;
522+
pulse.lastProgress = progress;
523+
if (wrappedAtEnd) {
524+
spawnLetterBlock(elapsed);
525+
}
383526
if (proximity > 0.58) {
384527
const jumpStep = THREE.MathUtils.lerp(0.028, 0.11, (proximity - 0.58) / 0.42);
385528
progress = Math.round(progress / jumpStep) * jumpStep;
@@ -418,6 +561,7 @@ function animate() {
418561
if (centerHole) {
419562
centerHole.position.set(focalPoint.x, focalPoint.y, CENTER_DEPTH);
420563
}
564+
updateLetterBlocks(elapsed);
421565
} else if (legacyStars) {
422566
legacyStars.rotation.y += delta * 0.03;
423567
legacyStars.rotation.x = Math.sin(elapsed * 0.08) * 0.08;

0 commit comments

Comments
 (0)