11<script setup lang="ts">
22import { onMounted , onUnmounted , ref } from ' vue' ;
33import * as THREE from ' three' ;
4+ import { RoundedBoxGeometry } from ' three/examples/jsm/geometries/RoundedBoxGeometry.js' ;
45
56type BackgroundMode = ' circuit' | ' legacy' ;
67
@@ -20,6 +21,7 @@ interface PulseMarker {
2021 phase: number ;
2122 flashRate: number ;
2223 flashPhase: number ;
24+ lastProgress: number ;
2325}
2426
2527interface 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+
3847let scene: THREE .Scene | null = null ;
3948let camera: THREE .PerspectiveCamera | null = null ;
4049let renderer: THREE .WebGLRenderer | null = null ;
@@ -57,13 +66,27 @@ const CENTER_DEPTH = -74;
5766const CIRCUIT_COUNT = 68 ;
5867const PULSE_SPEED_BASE = 0.032 ;
5968const 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
6176const PIPE_AXIS = new THREE .Vector3 (0 , 1 , 0 );
6277const cameraToGroup = new THREE .Vector3 ();
6378const colorNear = new THREE .Color (0xc8ffff );
6479const focalPoint = new THREE .Vector3 (0 , 0 , 0 );
6580const 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 ();
6686let flowTravel = 0 ;
87+ let blockAssemblyGroup: THREE .Group | null = null ;
88+ let blockCursor = 0 ;
89+ const letterBlocks: LetterBlock [] = [];
6790
6891function 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+
180312function addCenterHole() {
181313 if (! scene ) return ;
182314
@@ -213,6 +345,9 @@ function addCenterHole() {
213345function 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