3d-spinner 0.9.3 → 0.9.5

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.
Files changed (62) hide show
  1. package/dist/animations/charged-orb.js +4 -2
  2. package/dist/animations/ghost-train.d.ts +72 -0
  3. package/dist/animations/ghost-train.js +249 -0
  4. package/dist/animations/grid-assembly.d.ts +11 -8
  5. package/dist/animations/grid-assembly.js +44 -30
  6. package/dist/animations/rocket-launch.d.ts +58 -0
  7. package/dist/animations/rocket-launch.js +375 -0
  8. package/dist/animations/spin.d.ts +3 -1
  9. package/dist/animations/spin.js +6 -1
  10. package/dist/cjs/animations/charged-orb.cjs +170 -28
  11. package/dist/cjs/animations/grid-assembly.cjs +192 -172
  12. package/dist/cjs/animations/object-motion.cjs +156 -24
  13. package/dist/cjs/animations/particles.cjs +167 -26
  14. package/dist/cjs/animations/spin.cjs +169 -27
  15. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +215 -55
  16. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +51 -13
  17. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +56 -6
  18. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +55 -6
  19. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +64 -11
  20. package/dist/cjs/prefabs/prefabs.cjs +817 -205
  21. package/dist/engines/little-3d-engine/core/geometry.d.ts +15 -2
  22. package/dist/engines/little-3d-engine/core/geometry.js +25 -3
  23. package/dist/engines/little-3d-engine/core/light.d.ts +28 -4
  24. package/dist/engines/little-3d-engine/core/light.js +48 -7
  25. package/dist/engines/little-3d-engine/core/mesh.d.ts +33 -0
  26. package/dist/engines/little-3d-engine/core/mesh.js +12 -0
  27. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +2 -2
  28. package/dist/engines/little-3d-engine/little-3d-engine.js +1 -1
  29. package/dist/engines/little-3d-engine/loaders/obj.d.ts +8 -7
  30. package/dist/engines/little-3d-engine/loaders/obj.js +74 -20
  31. package/dist/engines/little-3d-engine/renderers/canvas2d.js +23 -1
  32. package/dist/engines/little-3d-engine/renderers/webgl.js +34 -5
  33. package/dist/engines/little-3d-engine/renderers/webgpu.js +43 -10
  34. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +8 -3
  35. package/dist/engines/little-3d-engine/shapes/complex/plane.js +10 -4
  36. package/dist/engines/little-3d-engine/shapes/primitives/cube.d.ts +3 -2
  37. package/dist/engines/little-3d-engine/shapes/primitives/cube.js +4 -2
  38. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.d.ts +3 -2
  39. package/dist/engines/little-3d-engine/shapes/primitives/octahedron.js +4 -2
  40. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.d.ts +3 -2
  41. package/dist/engines/little-3d-engine/shapes/primitives/pyramid.js +4 -2
  42. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +3 -2
  43. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +4 -2
  44. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.d.ts +3 -2
  45. package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.js +4 -2
  46. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.d.ts +3 -2
  47. package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.js +4 -2
  48. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.d.ts +3 -2
  49. package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.js +4 -2
  50. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.d.ts +3 -2
  51. package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.js +4 -2
  52. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.d.ts +3 -2
  53. package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.js +4 -2
  54. package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +14 -2
  55. package/dist/engines/little-3d-engine/textures/dynamic/star.js +26 -11
  56. package/dist/prefabs/ghost-train.d.ts +5 -4
  57. package/dist/prefabs/ghost-train.js +17 -24
  58. package/dist/prefabs/rocket-launch.d.ts +7 -5
  59. package/dist/prefabs/rocket-launch.js +10 -53
  60. package/dist/umd/spinner.global.js +877 -167
  61. package/dist/umd/spinner.global.min.js +58 -17
  62. package/package.json +2 -2
@@ -136,7 +136,10 @@ function expandToTriangles(mesh) {
136
136
  const positions = new Float32Array(triangles * 9);
137
137
  const normals = new Float32Array(triangles * 9);
138
138
  const colors = new Float32Array(triangles * 9);
139
+ const emissives = new Float32Array(triangles * 9);
140
+ const speculars = new Float32Array(triangles * 12);
139
141
  let o = 0;
142
+ let so = 0;
140
143
  for (const face of mesh.faces) {
141
144
  const v0 = mesh.vertices[face.indices[0]];
142
145
  const v1 = mesh.vertices[face.indices[1]];
@@ -146,6 +149,15 @@ function expandToTriangles(mesh) {
146
149
  const cr = r / 255;
147
150
  const cg = g / 255;
148
151
  const cb = b / 255;
152
+ const emissive = face.material?.emissive;
153
+ const er = emissive ? emissive[0] : 0;
154
+ const eg = emissive ? emissive[1] : 0;
155
+ const eb = emissive ? emissive[2] : 0;
156
+ const specular = face.material?.specular;
157
+ const sr = specular ? specular[0] : 0;
158
+ const sg = specular ? specular[1] : 0;
159
+ const sb = specular ? specular[2] : 0;
160
+ const sn = specular ? face.material?.shininess ?? 32 : 1;
149
161
  for (let k = 1; k < face.indices.length - 1; k++) {
150
162
  const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
151
163
  for (const index of tri) {
@@ -159,11 +171,19 @@ function expandToTriangles(mesh) {
159
171
  colors[o] = cr;
160
172
  colors[o + 1] = cg;
161
173
  colors[o + 2] = cb;
174
+ emissives[o] = er;
175
+ emissives[o + 1] = eg;
176
+ emissives[o + 2] = eb;
177
+ speculars[so] = sr;
178
+ speculars[so + 1] = sg;
179
+ speculars[so + 2] = sb;
180
+ speculars[so + 3] = sn;
162
181
  o += 3;
182
+ so += 4;
163
183
  }
164
184
  }
165
185
  }
166
- return { positions, normals, colors, count: positions.length / 3 };
186
+ return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
167
187
  }
168
188
  function midpoint(a, b) {
169
189
  return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
@@ -201,13 +221,42 @@ var init_geometry = __esm({
201
221
  function clamp01(value) {
202
222
  return Math.min(1, Math.max(0, value));
203
223
  }
204
- function shadeColor(normal, color, light) {
224
+ function clamp255(value) {
225
+ return Math.round(Math.min(255, Math.max(0, value)));
226
+ }
227
+ function shade(normal, color, light, surface) {
205
228
  const lambert = Math.max(0, dot(normal, light.toLight));
206
229
  const brightness = clamp01(light.ambient + light.intensity * lambert);
207
- const [r, g, b] = parseColor(color);
208
- return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
209
- b * brightness
210
- )})`;
230
+ const [baseR, baseG, baseB] = parseColor(color);
231
+ let r = baseR * brightness;
232
+ let g = baseG * brightness;
233
+ let b = baseB * brightness;
234
+ const material = surface?.material;
235
+ const specular = material?.specular;
236
+ const viewDir = surface?.viewDir;
237
+ if (specular && viewDir && lambert > 0) {
238
+ const half = normalize({
239
+ x: light.toLight.x + viewDir.x,
240
+ y: light.toLight.y + viewDir.y,
241
+ z: light.toLight.z + viewDir.z
242
+ });
243
+ const shininess = material?.shininess ?? 32;
244
+ const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
245
+ r += highlight * specular[0];
246
+ g += highlight * specular[1];
247
+ b += highlight * specular[2];
248
+ }
249
+ const emissive = material?.emissive;
250
+ if (emissive) {
251
+ r += emissive[0] * 255;
252
+ g += emissive[1] * 255;
253
+ b += emissive[2] * 255;
254
+ }
255
+ return [clamp255(r), clamp255(g), clamp255(b)];
256
+ }
257
+ function shadeColor(normal, color, light, surface) {
258
+ const [r, g, b] = shade(normal, color, light, surface);
259
+ return `rgb(${r}, ${g}, ${b})`;
211
260
  }
212
261
  var DEFAULTS2, Light;
213
262
  var init_light = __esm({
@@ -230,8 +279,8 @@ var init_light = __esm({
230
279
  };
231
280
  }
232
281
  /** Convenience wrapper around {@link shadeColor} using this light. */
233
- shade(normal, color) {
234
- return shadeColor(normal, color, this.params);
282
+ shade(normal, color, surface) {
283
+ return shadeColor(normal, color, this.params, surface);
235
284
  }
236
285
  };
237
286
  }
@@ -271,28 +320,51 @@ var init_webgl = __esm({
271
320
  in vec3 aPos;
272
321
  in vec3 aNormal;
273
322
  in vec3 aColor;
323
+ in vec3 aEmissive;
324
+ in vec4 aSpecular;
274
325
  uniform mat4 uViewProj;
275
326
  uniform mat4 uModel;
276
327
  out vec3 vNormal;
277
328
  out vec3 vColor;
329
+ out vec3 vEmissive;
330
+ out vec4 vSpecular;
331
+ out vec3 vWorldPos;
278
332
  void main() {
279
333
  vNormal = mat3(uModel) * aNormal;
280
334
  vColor = aColor;
281
- gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
335
+ vEmissive = aEmissive;
336
+ vSpecular = aSpecular;
337
+ vec4 world = uModel * vec4(aPos, 1.0);
338
+ vWorldPos = world.xyz;
339
+ gl_Position = uViewProj * world;
282
340
  }`;
283
341
  FRAGMENT_SHADER = `#version 300 es
284
342
  precision mediump float;
285
343
  in vec3 vNormal;
286
344
  in vec3 vColor;
345
+ in vec3 vEmissive;
346
+ in vec4 vSpecular;
347
+ in vec3 vWorldPos;
287
348
  uniform vec3 uToLight;
349
+ uniform vec3 uEye;
288
350
  uniform float uIntensity;
289
351
  uniform float uAmbient;
290
352
  uniform float uOpacity;
291
353
  out vec4 fragColor;
292
354
  void main() {
293
- float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
355
+ vec3 normal = normalize(vNormal);
356
+ vec3 toLight = normalize(uToLight);
357
+ float lambert = max(dot(normal, toLight), 0.0);
294
358
  float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
295
- fragColor = vec4(vColor * brightness, uOpacity);
359
+ vec3 lit = vColor * brightness;
360
+ if (lambert > 0.0) {
361
+ vec3 viewDir = normalize(uEye - vWorldPos);
362
+ vec3 halfVec = normalize(toLight + viewDir);
363
+ float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
364
+ lit += highlight * vSpecular.xyz;
365
+ }
366
+ lit += vEmissive;
367
+ fragColor = vec4(lit, uOpacity);
296
368
  }`;
297
369
  WebGLRenderer = class {
298
370
  constructor(options = {}) {
@@ -313,9 +385,12 @@ void main() {
313
385
  aPos: gl.getAttribLocation(this.program, "aPos"),
314
386
  aNormal: gl.getAttribLocation(this.program, "aNormal"),
315
387
  aColor: gl.getAttribLocation(this.program, "aColor"),
388
+ aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
389
+ aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
316
390
  uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
317
391
  uModel: gl.getUniformLocation(this.program, "uModel"),
318
392
  uToLight: gl.getUniformLocation(this.program, "uToLight"),
393
+ uEye: gl.getUniformLocation(this.program, "uEye"),
319
394
  uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
320
395
  uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
321
396
  uOpacity: gl.getUniformLocation(this.program, "uOpacity")
@@ -339,17 +414,19 @@ void main() {
339
414
  const data = expandToTriangles(mesh);
340
415
  const vao = gl.createVertexArray();
341
416
  gl.bindVertexArray(vao);
342
- const attribute = (location, array) => {
417
+ const attribute = (location, array, size = 3) => {
343
418
  if (location < 0) return;
344
419
  const buffer = gl.createBuffer();
345
420
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
346
421
  gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
347
422
  gl.enableVertexAttribArray(location);
348
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
423
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
349
424
  };
350
425
  attribute(loc.aPos, data.positions);
351
426
  attribute(loc.aNormal, data.normals);
352
427
  attribute(loc.aColor, data.colors);
428
+ attribute(loc.aEmissive, data.emissives);
429
+ attribute(loc.aSpecular, data.speculars, 4);
353
430
  gl.bindVertexArray(null);
354
431
  const result = { vao, count: data.count };
355
432
  this.cache.set(mesh, result);
@@ -364,6 +441,7 @@ void main() {
364
441
  gl.useProgram(this.program);
365
442
  gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
366
443
  gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
444
+ gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
367
445
  gl.uniform1f(loc.uIntensity, frame.light.intensity);
368
446
  gl.uniform1f(loc.uAmbient, frame.light.ambient);
369
447
  gl.disable(gl.BLEND);
@@ -438,6 +516,7 @@ struct Uniforms {
438
516
  model: mat4x4<f32>,
439
517
  toLight: vec4<f32>,
440
518
  params: vec4<f32>,
519
+ eye: vec4<f32>,
441
520
  };
442
521
  @group(0) @binding(0) var<uniform> u: Uniforms;
443
522
 
@@ -445,23 +524,40 @@ struct VSOut {
445
524
  @builtin(position) position: vec4<f32>,
446
525
  @location(0) normal: vec3<f32>,
447
526
  @location(1) color: vec3<f32>,
527
+ @location(2) emissive: vec3<f32>,
528
+ @location(3) specular: vec4<f32>,
529
+ @location(4) worldPos: vec3<f32>,
448
530
  };
449
531
 
450
532
  @vertex
451
- fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
533
+ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>, @location(3) emissive: vec3<f32>, @location(4) specular: vec4<f32>) -> VSOut {
452
534
  var out: VSOut;
453
535
  let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
454
536
  out.normal = m * normal;
455
537
  out.color = color;
456
- out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
538
+ out.emissive = emissive;
539
+ out.specular = specular;
540
+ let world = u.model * vec4<f32>(pos, 1.0);
541
+ out.worldPos = world.xyz;
542
+ out.position = u.viewProj * world;
457
543
  return out;
458
544
  }
459
545
 
460
546
  @fragment
461
547
  fn fs(in: VSOut) -> @location(0) vec4<f32> {
462
- let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
548
+ let normal = normalize(in.normal);
549
+ let toLight = normalize(u.toLight.xyz);
550
+ let lambert = max(dot(normal, toLight), 0.0);
463
551
  let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
464
- return vec4<f32>(in.color * brightness, u.params.z);
552
+ var lit = in.color * brightness;
553
+ if (lambert > 0.0) {
554
+ let viewDir = normalize(u.eye.xyz - in.worldPos);
555
+ let halfVec = normalize(toLight + viewDir);
556
+ let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
557
+ lit = lit + highlight * in.specular.xyz;
558
+ }
559
+ lit = lit + in.emissive;
560
+ return vec4<f32>(lit, u.params.z);
465
561
  }
466
562
  `;
467
563
  CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
@@ -502,13 +598,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
502
598
  {
503
599
  binding: 0,
504
600
  visibility: stage.VERTEX | stage.FRAGMENT,
505
- buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
601
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
506
602
  }
507
603
  ]
508
604
  });
509
- const vertexBuffer = (location) => ({
510
- arrayStride: 12,
511
- attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
605
+ const vertexBuffer = (location, components = 3) => ({
606
+ arrayStride: components * 4,
607
+ attributes: [
608
+ { shaderLocation: location, offset: 0, format: `float32x${components}` }
609
+ ]
512
610
  });
513
611
  const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
514
612
  const blend = {
@@ -524,7 +622,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
524
622
  vertex: {
525
623
  module: module2,
526
624
  entryPoint: "vs",
527
- buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
625
+ buffers: [
626
+ vertexBuffer(0),
627
+ vertexBuffer(1),
628
+ vertexBuffer(2),
629
+ vertexBuffer(3),
630
+ vertexBuffer(4, 4)
631
+ ]
528
632
  },
529
633
  fragment: {
530
634
  module: module2,
@@ -577,6 +681,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
577
681
  position: upload(data.positions),
578
682
  normal: upload(data.normals),
579
683
  color: upload(data.colors),
684
+ emissive: upload(data.emissives),
685
+ specular: upload(data.speculars),
580
686
  count: data.count
581
687
  };
582
688
  this.cache.set(mesh, result);
@@ -628,7 +734,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
628
734
  const bindGroup = this.device.createBindGroup({
629
735
  layout,
630
736
  entries: [
631
- { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
737
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
632
738
  ]
633
739
  });
634
740
  draws.forEach((draw, i) => {
@@ -637,6 +743,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
637
743
  data.set(draw.item.model, 16);
638
744
  data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
639
745
  data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
746
+ data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
640
747
  this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
641
748
  });
642
749
  const encoder = this.device.createCommandEncoder();
@@ -663,6 +770,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
663
770
  pass.setVertexBuffer(0, mesh.position);
664
771
  pass.setVertexBuffer(1, mesh.normal);
665
772
  pass.setVertexBuffer(2, mesh.color);
773
+ pass.setVertexBuffer(3, mesh.emissive);
774
+ pass.setVertexBuffer(4, mesh.specular);
666
775
  pass.draw(mesh.count);
667
776
  });
668
777
  pass.end();
@@ -674,6 +783,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
674
783
  mesh.position.destroy?.();
675
784
  mesh.normal.destroy?.();
676
785
  mesh.color.destroy?.();
786
+ mesh.emissive.destroy?.();
787
+ mesh.specular.destroy?.();
677
788
  }
678
789
  this.cache.clear();
679
790
  this.uniformBuffer?.destroy?.();
@@ -756,9 +867,30 @@ var init_canvas2d = __esm({
756
867
  depth += dot(d, d);
757
868
  }
758
869
  depth /= face.indices.length;
870
+ let surface;
871
+ const material = face.material;
872
+ if (material) {
873
+ if (material.specular) {
874
+ let cx = 0;
875
+ let cy = 0;
876
+ let cz = 0;
877
+ for (const i of face.indices) {
878
+ cx += world[i].x;
879
+ cy += world[i].y;
880
+ cz += world[i].z;
881
+ }
882
+ const inv = 1 / face.indices.length;
883
+ const viewDir = normalize(
884
+ subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
885
+ );
886
+ surface = { material, viewDir };
887
+ } else {
888
+ surface = { material };
889
+ }
890
+ }
759
891
  polygons.push({
760
892
  points,
761
- color: shadeColor(normal, face.color, frame.light),
893
+ color: shadeColor(normal, face.color, frame.light, surface),
762
894
  depth,
763
895
  opacity: faceOpacity
764
896
  });
@@ -1624,6 +1756,12 @@ init_light();
1624
1756
  init_math();
1625
1757
 
1626
1758
  // src/engines/little-3d-engine/core/mesh.ts
1759
+ function attachMaterial(mesh, material) {
1760
+ if (material) {
1761
+ for (const face of mesh.faces) face.material = material;
1762
+ }
1763
+ return mesh;
1764
+ }
1627
1765
  function transform(init) {
1628
1766
  return {
1629
1767
  position: init?.position ?? { x: 0, y: 0, z: 0 },
@@ -1638,7 +1776,7 @@ init_light();
1638
1776
 
1639
1777
  // src/engines/little-3d-engine/shapes/primitives/cube.ts
1640
1778
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1641
- function cube(size = 1, colors = DEFAULT_COLORS) {
1779
+ function cube(size = 1, colors = DEFAULT_COLORS, material) {
1642
1780
  const h = size / 2;
1643
1781
  const vertices = [
1644
1782
  { x: -h, y: -h, z: h },
@@ -1658,12 +1796,12 @@ function cube(size = 1, colors = DEFAULT_COLORS) {
1658
1796
  { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1659
1797
  { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1660
1798
  ];
1661
- return { vertices, faces };
1799
+ return attachMaterial({ vertices, faces }, material);
1662
1800
  }
1663
1801
 
1664
1802
  // src/engines/little-3d-engine/shapes/primitives/quad.ts
1665
1803
  var DEFAULT_COLORS2 = ["#3b82f6"];
1666
- function quad(size = 1, colors = DEFAULT_COLORS2) {
1804
+ function quad(size = 1, colors = DEFAULT_COLORS2, material) {
1667
1805
  const s = size / 2;
1668
1806
  const vertices = [
1669
1807
  { x: -s, y: -s, z: 0 },
@@ -1671,12 +1809,15 @@ function quad(size = 1, colors = DEFAULT_COLORS2) {
1671
1809
  { x: s, y: s, z: 0 },
1672
1810
  { x: -s, y: s, z: 0 }
1673
1811
  ];
1674
- return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1812
+ return attachMaterial(
1813
+ { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
1814
+ material
1815
+ );
1675
1816
  }
1676
1817
 
1677
1818
  // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1678
1819
  var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1679
- function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1820
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
1680
1821
  const s = size / 2;
1681
1822
  const vertices = [
1682
1823
  { x: s, y: s, z: s },
@@ -1690,46 +1831,12 @@ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1690
1831
  { indices: [0, 2, 3], color: colors[2 % colors.length] },
1691
1832
  { indices: [1, 3, 2], color: colors[3 % colors.length] }
1692
1833
  ];
1693
- return { vertices, faces };
1694
- }
1695
-
1696
- // src/engines/little-3d-engine/shapes/primitives/octahedron.ts
1697
- var DEFAULT_COLORS4 = [
1698
- "#3b82f6",
1699
- "#8b5cf6",
1700
- "#ec4899",
1701
- "#f59e0b",
1702
- "#10b981",
1703
- "#ef4444",
1704
- "#06b6d4",
1705
- "#eab308"
1706
- ];
1707
- function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1708
- const r = size / 2;
1709
- const vertices = [
1710
- { x: r, y: 0, z: 0 },
1711
- { x: -r, y: 0, z: 0 },
1712
- { x: 0, y: r, z: 0 },
1713
- { x: 0, y: -r, z: 0 },
1714
- { x: 0, y: 0, z: r },
1715
- { x: 0, y: 0, z: -r }
1716
- ];
1717
- const faces = [
1718
- { indices: [4, 0, 2], color: colors[0 % colors.length] },
1719
- { indices: [4, 2, 1], color: colors[1 % colors.length] },
1720
- { indices: [4, 1, 3], color: colors[2 % colors.length] },
1721
- { indices: [4, 3, 0], color: colors[3 % colors.length] },
1722
- { indices: [5, 2, 0], color: colors[4 % colors.length] },
1723
- { indices: [5, 1, 2], color: colors[5 % colors.length] },
1724
- { indices: [5, 3, 1], color: colors[6 % colors.length] },
1725
- { indices: [5, 0, 3], color: colors[7 % colors.length] }
1726
- ];
1727
- return { vertices, faces };
1834
+ return attachMaterial({ vertices, faces }, material);
1728
1835
  }
1729
1836
 
1730
1837
  // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1731
- var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1732
- function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1838
+ var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1839
+ function pyramid(size = 1, colors = DEFAULT_COLORS4, material) {
1733
1840
  const h = size / 2;
1734
1841
  const vertices = [
1735
1842
  { x: -h, y: -h, z: h },
@@ -1745,12 +1852,12 @@ function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1745
1852
  { indices: [4, 2, 3], color: colors[3 % colors.length] },
1746
1853
  { indices: [4, 3, 0], color: colors[4 % colors.length] }
1747
1854
  ];
1748
- return { vertices, faces };
1855
+ return attachMaterial({ vertices, faces }, material);
1749
1856
  }
1750
1857
 
1751
1858
  // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
1752
1859
  init_geometry();
1753
- var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1860
+ var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1754
1861
  var T = (1 + Math.sqrt(5)) / 2;
1755
1862
  var SEED_VERTICES = [
1756
1863
  { x: -1, y: T, z: 0 },
@@ -1788,17 +1895,20 @@ var SEED_FACES = [
1788
1895
  [8, 6, 7],
1789
1896
  [9, 8, 1]
1790
1897
  ];
1791
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1792
- return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1898
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5, material) {
1899
+ return attachMaterial(
1900
+ sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
1901
+ material
1902
+ );
1793
1903
  }
1794
1904
 
1795
1905
  // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
1796
1906
  init_geometry();
1797
1907
 
1798
1908
  // src/engines/little-3d-engine/shapes/complex/plane.ts
1799
- var DEFAULT_COLORS7 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1800
- function planeMesh(colors = DEFAULT_COLORS7) {
1801
- return {
1909
+ var DEFAULT_COLORS6 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1910
+ function planeMesh(colors = DEFAULT_COLORS6, material) {
1911
+ return attachMaterial({
1802
1912
  vertices: [
1803
1913
  { x: 0.9, y: 0, z: 0 },
1804
1914
  { x: -0.2, y: 0, z: 0.82 },
@@ -1811,22 +1921,22 @@ function planeMesh(colors = DEFAULT_COLORS7) {
1811
1921
  { x: -0.52, y: 0.38, z: 0 }
1812
1922
  ],
1813
1923
  faces: [
1814
- { indices: [6, 1, 0], color: colors[0] ?? DEFAULT_COLORS7[0] },
1815
- { indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS7[3] },
1816
- { indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS7[1] },
1817
- { indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS7[2] },
1818
- { indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS7[3] },
1819
- { indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS7[0] },
1820
- { indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS7[1] },
1821
- { indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS7[2] },
1822
- { indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS7[1] },
1823
- { indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS7[2] },
1824
- { indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS7[1] },
1825
- { indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS7[2] },
1826
- { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS7[0] },
1827
- { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS7[1] }
1924
+ { indices: [6, 1, 0], color: colors[0] ?? DEFAULT_COLORS6[0] },
1925
+ { indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS6[3] },
1926
+ { indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS6[1] },
1927
+ { indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS6[2] },
1928
+ { indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS6[3] },
1929
+ { indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS6[0] },
1930
+ { indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS6[1] },
1931
+ { indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS6[2] },
1932
+ { indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS6[1] },
1933
+ { indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS6[2] },
1934
+ { indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS6[1] },
1935
+ { indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS6[2] },
1936
+ { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS6[0] },
1937
+ { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS6[1] }
1828
1938
  ]
1829
- };
1939
+ }, material);
1830
1940
  }
1831
1941
 
1832
1942
  // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
@@ -1839,18 +1949,30 @@ function canvasTexture(draw, size = 96) {
1839
1949
  }
1840
1950
 
1841
1951
  // src/engines/little-3d-engine/textures/dynamic/star.ts
1842
- function starTexture() {
1952
+ function drawStar(ctx) {
1953
+ ctx.save();
1954
+ ctx.translate(48, 48);
1955
+ ctx.fillStyle = "#fff";
1956
+ ctx.beginPath();
1957
+ for (let index = 0; index < 10; index++) {
1958
+ const radius = index % 2 === 0 ? 43 : 16;
1959
+ const angle = index * Math.PI / 5 - Math.PI / 2;
1960
+ ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
1961
+ }
1962
+ ctx.closePath();
1963
+ ctx.fill();
1964
+ ctx.restore();
1965
+ }
1966
+ function starTexture(options = {}) {
1967
+ const glow = Math.max(0, options.glow ?? 0);
1843
1968
  return canvasTexture((ctx) => {
1844
- ctx.translate(48, 48);
1845
- ctx.fillStyle = "#fff";
1846
- ctx.beginPath();
1847
- for (let index = 0; index < 10; index++) {
1848
- const radius = index % 2 === 0 ? 43 : 16;
1849
- const angle = index * Math.PI / 5 - Math.PI / 2;
1850
- ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
1969
+ if (glow > 0) {
1970
+ ctx.save();
1971
+ ctx.filter = `blur(${glow}px)`;
1972
+ drawStar(ctx);
1973
+ ctx.restore();
1851
1974
  }
1852
- ctx.closePath();
1853
- ctx.fill();
1975
+ drawStar(ctx);
1854
1976
  });
1855
1977
  }
1856
1978
 
@@ -2034,7 +2156,7 @@ var Little3dEngine = class {
2034
2156
  };
2035
2157
 
2036
2158
  // src/animations/particles.ts
2037
- var DEFAULT_COLORS8 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
2159
+ var DEFAULT_COLORS7 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
2038
2160
  var FADE_IN_END = 0.15;
2039
2161
  var FADE_OUT_START = 0.6;
2040
2162
  function rand01(seed, index, salt) {
@@ -2129,7 +2251,7 @@ var ParticlesAnimation = class {
2129
2251
  this.exitAt = Infinity;
2130
2252
  this.finished = false;
2131
2253
  this.field = particleField(options);
2132
- this.colors = options.colors ?? DEFAULT_COLORS8;
2254
+ this.colors = options.colors ?? DEFAULT_COLORS7;
2133
2255
  this.backend = options.backend;
2134
2256
  this.texture = options.texture;
2135
2257
  this.labelContent = options.label;
@@ -2274,6 +2396,7 @@ var REENTER_STAGGER_MS = 45;
2274
2396
  var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
2275
2397
  var CENTER_POP_OUT_MS = 420;
2276
2398
  var PARKED = { x: 0, y: 0, z: 50 };
2399
+ var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
2277
2400
  var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
2278
2401
  var MINI_COLORS = [
2279
2402
  ["#e0f2fe", "#bae6fd", "#7dd3fc"],
@@ -2302,9 +2425,9 @@ var ChargedOrbAnimation = class {
2302
2425
  backend: this.backend,
2303
2426
  camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
2304
2427
  });
2305
- this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
2428
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
2306
2429
  for (let i = 0; i < MINIS; i++) {
2307
- const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
2430
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
2308
2431
  this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
2309
2432
  }
2310
2433
  this.engine = engine;
@@ -2952,46 +3075,250 @@ function squareMotion(options = {}) {
2952
3075
  };
2953
3076
  }
2954
3077
 
3078
+ // src/animations/ghost-train.ts
3079
+ var MAX_CARS = 50;
3080
+ var CAMERA_Z2 = 3;
3081
+ var FOV = 55 * Math.PI / 180;
3082
+ var HALF_HEIGHT = Math.tan(FOV / 2) * CAMERA_Z2;
3083
+ var RUN_GAP_MS = 130;
3084
+ var POP_MS = 320;
3085
+ var SAMPLE_MS2 = 8;
3086
+ var TURN_RATE = 0.4 * Math.PI / 180;
3087
+ var MAX_OUTRO_MS = 4e3;
3088
+ var WARP_ACCEL = 1e3;
3089
+ var TRAIL_OUTRO_MS = 1200;
3090
+ var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
3091
+ var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
3092
+ var CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
3093
+ var WORLD_UP2 = { x: 0, y: 1, z: 0 };
3094
+ function clamp014(value) {
3095
+ return Math.max(0, Math.min(1, value));
3096
+ }
3097
+ function orientationFor2(forward) {
3098
+ const fwd = normalize(forward);
3099
+ let right = cross(fwd, WORLD_UP2);
3100
+ if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
3101
+ right = normalize(right);
3102
+ const up = cross(right, fwd);
3103
+ const w = normalize(cross(fwd, up));
3104
+ return {
3105
+ x: Math.atan2(cross(w, fwd).z, w.z),
3106
+ y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
3107
+ z: Math.atan2(fwd.y, fwd.x)
3108
+ };
3109
+ }
3110
+ function rotateToward(from, to, maxRad) {
3111
+ const a = normalize(from);
3112
+ const b = normalize(to);
3113
+ const d = Math.max(-1, Math.min(1, dot(a, b)));
3114
+ const angle = Math.acos(d);
3115
+ if (angle <= maxRad || angle < 1e-4) return b;
3116
+ const sin = Math.sin(angle);
3117
+ if (sin < 1e-4) return b;
3118
+ const t = maxRad / angle;
3119
+ const w1 = Math.sin((1 - t) * angle) / sin;
3120
+ const w2 = Math.sin(t * angle) / sin;
3121
+ return normalize({ x: a.x * w1 + b.x * w2, y: a.y * w1 + b.y * w2, z: a.z * w1 + b.z * w2 });
3122
+ }
3123
+ var GhostTrainAnimation = class {
3124
+ constructor(options = {}) {
3125
+ this.cars = [];
3126
+ this.appear = new Array(MAX_CARS).fill(0);
3127
+ this.headings = new Array(MAX_CARS).fill(void 0);
3128
+ this.aspect = 16 / 9;
3129
+ this.enterAt = Infinity;
3130
+ this.outroAt = Infinity;
3131
+ this.carsAtOutro = 0;
3132
+ this.exitPathTime = 0;
3133
+ // lead car's path-time at blast-off (the escape switch point)
3134
+ this.exitPoint = { x: 0, y: 0, z: 0 };
3135
+ this.exitDir = { x: 1, y: 0, z: 0 };
3136
+ // shared escape direction, outward from the track
3137
+ this.exitSpeed = 1e-3;
3138
+ // path-units per path-millisecond at the switch (keeps speed continuous)
3139
+ this.lastNow = 0;
3140
+ this.finished = false;
3141
+ this.motion = options.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
3142
+ this.size = options.size ?? 0.15;
3143
+ this.backend = options.backend;
3144
+ this.labelContent = options.label;
3145
+ this.fadeLabel = options.fadeLabel ?? true;
3146
+ }
3147
+ mount(target) {
3148
+ if (!target.style.position) target.style.position = "relative";
3149
+ const engine = new Little3dEngine({
3150
+ backend: this.backend,
3151
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
3152
+ });
3153
+ const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
3154
+ for (let i = 0; i < MAX_CARS; i++) {
3155
+ this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
3156
+ }
3157
+ this.engine = engine;
3158
+ engine.mount(target).catch((error) => {
3159
+ target.textContent = error instanceof Error ? error.message : String(error);
3160
+ });
3161
+ const measure = () => {
3162
+ if (target.clientWidth > 0 && target.clientHeight > 0) {
3163
+ this.aspect = target.clientWidth / target.clientHeight;
3164
+ }
3165
+ };
3166
+ measure();
3167
+ this.observer = new ResizeObserver(measure);
3168
+ this.observer.observe(target);
3169
+ this.label = mountAnimationLabel(target, this.labelContent);
3170
+ if (this.fadeLabel) this.label.setOpacity(0);
3171
+ }
3172
+ enter(now) {
3173
+ if (this.enterAt === Infinity) this.enterAt = now;
3174
+ }
3175
+ exit(now) {
3176
+ if (this.outroAt !== Infinity || this.enterAt === Infinity) return;
3177
+ this.outroAt = now;
3178
+ this.carsAtOutro = this.appear.filter((a) => a > 0.5).length;
3179
+ this.exitPathTime = now - this.enterAt;
3180
+ const from = this.motion.positionAt(this.exitPathTime);
3181
+ const velocity = subtract(
3182
+ this.motion.positionAt(this.exitPathTime + 1),
3183
+ this.motion.positionAt(this.exitPathTime - 1)
3184
+ );
3185
+ const speed = Math.hypot(velocity.x, velocity.y, velocity.z);
3186
+ this.exitPoint = from;
3187
+ if (speed > 1e-6) this.exitSpeed = speed / 2;
3188
+ this.exitDir = speed > 1e-6 ? normalize(velocity) : { x: 1, y: 0, z: 0 };
3189
+ }
3190
+ isFinished() {
3191
+ return this.finished;
3192
+ }
3193
+ /** Milliseconds the lead car keeps moving into the outro; feed a trail layer's `outroMs`. */
3194
+ get outroDurationMs() {
3195
+ return TRAIL_OUTRO_MS;
3196
+ }
3197
+ /**
3198
+ * A {@link MotionController} following the lead car's actual position, through
3199
+ * laps and the accelerating escape. Feed it to a particle layer's `emitter`
3200
+ * so the star trail stays behind the train.
3201
+ */
3202
+ trailEmitter() {
3203
+ return {
3204
+ positionAt: (t) => this.enterAt === Infinity ? this.motion.positionAt(t) : this.pathPosition(t - this.enterAt + this.warp(t))
3205
+ };
3206
+ }
3207
+ render(now, frame) {
3208
+ if (!this.engine || !this.label) return;
3209
+ for (const car of this.cars) car.transform.scale = 0;
3210
+ if (this.enterAt === Infinity) {
3211
+ this.engine.render();
3212
+ return;
3213
+ }
3214
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
3215
+ this.lastNow = now;
3216
+ const want = this.outroAt !== Infinity ? this.carsAtOutro : Math.min(MAX_CARS, Math.round(frame.progress * MAX_CARS));
3217
+ const halfWidth = HALF_HEIGHT * this.aspect;
3218
+ const warp = this.warp(now);
3219
+ let anyOnScreen = false;
3220
+ for (let k = 0; k < MAX_CARS; k++) {
3221
+ const target = k < want ? 1 : 0;
3222
+ this.appear[k] = clamp014(this.appear[k] + Math.sign(target - this.appear[k]) * (dt / POP_MS));
3223
+ if (this.appear[k] <= 0) {
3224
+ this.headings[k] = void 0;
3225
+ continue;
3226
+ }
3227
+ const p = now - this.enterAt - k * RUN_GAP_MS + warp;
3228
+ const position = this.pathPosition(p);
3229
+ if (Math.abs(position.x) > halfWidth + this.size || Math.abs(position.y) > HALF_HEIGHT + this.size) {
3230
+ continue;
3231
+ }
3232
+ const ahead = subtract(this.pathPosition(p + SAMPLE_MS2), position);
3233
+ const targetDir = Math.hypot(ahead.x, ahead.y, ahead.z) > 1e-5 ? ahead : this.headings[k] ?? { x: 1, y: 0, z: 0 };
3234
+ this.headings[k] = this.headings[k] ? rotateToward(this.headings[k], targetDir, TURN_RATE * dt) : normalize(targetDir);
3235
+ const orientation = orientationFor2(this.headings[k]);
3236
+ const transform2 = this.cars[k].transform;
3237
+ transform2.position.x = position.x;
3238
+ transform2.position.y = position.y;
3239
+ transform2.position.z = position.z;
3240
+ transform2.rotation.x = orientation.x;
3241
+ transform2.rotation.y = orientation.y;
3242
+ transform2.rotation.z = orientation.z;
3243
+ transform2.scale = this.size * easeOutBack(this.appear[k]);
3244
+ anyOnScreen = true;
3245
+ }
3246
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
3247
+ if (this.fadeLabel) {
3248
+ this.label.setOpacity(animationLabelOpacity(now, this.enterAt, POP_MS, this.outroAt, TRAIL_OUTRO_MS));
3249
+ }
3250
+ if (this.outroAt !== Infinity && now > this.outroAt + 300 && (!anyOnScreen || now >= this.outroAt + MAX_OUTRO_MS)) {
3251
+ this.finished = true;
3252
+ }
3253
+ this.engine.render();
3254
+ }
3255
+ destroy() {
3256
+ this.observer?.disconnect();
3257
+ this.observer = void 0;
3258
+ this.label?.container.remove();
3259
+ this.label = void 0;
3260
+ this.engine?.destroy();
3261
+ this.engine = void 0;
3262
+ this.cars.length = 0;
3263
+ }
3264
+ /** Extra path-time every car has accelerated forward by, `now` ms into the outro. */
3265
+ warp(now) {
3266
+ if (this.outroAt === Infinity) return 0;
3267
+ const seconds = (now - this.outroAt) / 1e3;
3268
+ return 0.5 * WARP_ACCEL * seconds * seconds;
3269
+ }
3270
+ /**
3271
+ * The single trajectory every car rides, sampled at path-time `p`: the track
3272
+ * up to the exit switch point, then a straight escape outward. Because the
3273
+ * switch point and direction are shared, all cars follow the exact same path.
3274
+ */
3275
+ pathPosition(p) {
3276
+ if (this.outroAt === Infinity || p <= this.exitPathTime) {
3277
+ return this.motion.positionAt(p);
3278
+ }
3279
+ const distance = this.exitSpeed * (p - this.exitPathTime);
3280
+ return {
3281
+ x: this.exitPoint.x + this.exitDir.x * distance,
3282
+ y: this.exitPoint.y + this.exitDir.y * distance,
3283
+ z: this.exitPoint.z + this.exitDir.z * distance
3284
+ };
3285
+ }
3286
+ };
3287
+
2955
3288
  // src/prefabs/ghost-train.ts
2956
3289
  function ghostTrain(options = {}) {
2957
- const motion = options.object?.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
2958
3290
  const particles = options.particles ?? {};
2959
- const object = new ObjectMotionAnimation({
2960
- mesh: () => cube(1, ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"]),
2961
- motion,
2962
- size: 0.3,
2963
- transparency: { mode: "two-sided", opacity: 0.55 },
2964
- tail: { count: 4, gapMs: 240 },
2965
- backend: options.backend,
2966
- ...options.object,
2967
- label: options.object?.label
3291
+ const train = new GhostTrainAnimation({
3292
+ motion: options.object?.motion,
3293
+ backend: options.backend
2968
3294
  });
2969
3295
  const trail = new ParticlesAnimation({
2970
3296
  rate: 30,
2971
3297
  lifeMs: 1700,
2972
- size: 0.13,
2973
- speed: 0.07,
3298
+ size: 0.15,
3299
+ speed: 0.11,
2974
3300
  colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
2975
- texture: particles.texture ?? starTexture(),
2976
- emitter: object.trailEmitter(),
2977
- outroMs: object.outroDurationMs,
3301
+ texture: particles.texture ?? starTexture({ glow: 5 }),
3302
+ emitter: train.trailEmitter(),
3303
+ outroMs: train.outroDurationMs,
2978
3304
  seed: 17,
2979
3305
  backend: options.backend,
2980
3306
  ...particles,
2981
3307
  label: options.label ?? particles.label,
2982
3308
  fadeLabel: options.fadeLabel ?? particles.fadeLabel
2983
3309
  });
2984
- return progressSpinner(new CompositeAnimation([trail, object]), options);
3310
+ return progressSpinner(new CompositeAnimation([trail, train]), options);
2985
3311
  }
2986
3312
 
2987
3313
  // src/animations/grid-assembly.ts
2988
3314
  var GRID = 5;
2989
3315
  var COUNT = GRID * GRID;
2990
- var CAMERA_Z2 = 4;
2991
- var FOV = 55 * Math.PI / 180;
3316
+ var CAMERA_Z3 = 4;
3317
+ var FOV2 = 55 * Math.PI / 180;
2992
3318
  var TWO_PI2 = Math.PI * 2;
2993
3319
  var INTRO_MS = 900;
2994
3320
  var INTRO_STAGGER_MS = 60;
3321
+ var INTRO_DONE_MS = (COUNT - 1) * INTRO_STAGGER_MS + INTRO_MS;
2995
3322
  var GATE_DOCK = 0.35;
2996
3323
  var GATE_UNDOCK = 0.65;
2997
3324
  var EXIT_HURRY2 = 2.5;
@@ -3000,20 +3327,16 @@ var SPIN_MS = 380;
3000
3327
  var SPIN_STAGGER_MS = 80;
3001
3328
  var HOLD_MS = 1e3;
3002
3329
  var COLLAPSE_MS = 700;
3003
- var POP_MS = 170;
3330
+ var COLLAPSE_SPREAD_MS = 500;
3331
+ var POP_MS2 = 170;
3004
3332
  var LABEL_FADE_MS = 600;
3005
- var DEFAULT_MESHES = [
3006
- () => cube(1, ["#60a5fa", "#3b82f6", "#2563eb", "#38bdf8", "#0ea5e9", "#1d4ed8"]),
3007
- () => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
3008
- () => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
3009
- () => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
3010
- () => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
3011
- ];
3012
- function clamp014(value) {
3333
+ var CUBE_COLORS = ["#8397c6", "#7186b8", "#6176a8", "#93a6cf", "#556a9c", "#7a8fc0"];
3334
+ var DEFAULT_MESHES = [() => cube(1, CUBE_COLORS)];
3335
+ function clamp015(value) {
3013
3336
  return Math.max(0, Math.min(1, value));
3014
3337
  }
3015
3338
  function smooth01(value) {
3016
- const x = clamp014(value);
3339
+ const x = clamp015(value);
3017
3340
  return x * x * (3 - 2 * x);
3018
3341
  }
3019
3342
  function resolveMesh2(mesh) {
@@ -3036,6 +3359,9 @@ var GridAssemblyAnimation = class {
3036
3359
  this.dockedAt = new Array(COUNT).fill(Infinity);
3037
3360
  this.tumbleX = [];
3038
3361
  this.tumbleY = [];
3362
+ this.collapseDelay = [];
3363
+ this.popStarted = new Array(COUNT).fill(false);
3364
+ this.maxCollapseDelay = 0;
3039
3365
  this.fades = [];
3040
3366
  this.slots = [];
3041
3367
  this.aspect = 16 / 9;
@@ -3044,7 +3370,6 @@ var GridAssemblyAnimation = class {
3044
3370
  this.allDockedAt = Infinity;
3045
3371
  this.collapseAt = Infinity;
3046
3372
  this.lastNow = 0;
3047
- this.popFading = false;
3048
3373
  this.finished = false;
3049
3374
  const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
3050
3375
  this.meshes = sources.map(resolveMesh2);
@@ -3061,13 +3386,15 @@ var GridAssemblyAnimation = class {
3061
3386
  this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
3062
3387
  this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
3063
3388
  this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
3389
+ this.collapseDelay.push(hash01(i, 7) * COLLAPSE_SPREAD_MS);
3064
3390
  }
3391
+ this.maxCollapseDelay = Math.max(...this.collapseDelay);
3065
3392
  }
3066
3393
  mount(target) {
3067
3394
  if (!target.style.position) target.style.position = "relative";
3068
3395
  const engine = new Little3dEngine({
3069
3396
  backend: this.backend,
3070
- camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
3397
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
3071
3398
  });
3072
3399
  for (let i = 0; i < COUNT; i++) {
3073
3400
  this.fades.push({ mode: "one-sided", opacity: 1 });
@@ -3122,7 +3449,7 @@ var GridAssemblyAnimation = class {
3122
3449
  COLLAPSE_MS
3123
3450
  ));
3124
3451
  }
3125
- if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS + POP_MS) {
3452
+ if (this.collapseAt !== Infinity && now >= this.collapseAt + this.maxCollapseDelay + COLLAPSE_MS + POP_MS2) {
3126
3453
  this.finished = true;
3127
3454
  }
3128
3455
  this.engine.render();
@@ -3139,7 +3466,8 @@ var GridAssemblyAnimation = class {
3139
3466
  }
3140
3467
  updateBlends(dt, progress, now) {
3141
3468
  const exiting = this.exitAt !== Infinity;
3142
- const want = exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
3469
+ const ringComplete = now - this.enterAt >= INTRO_DONE_MS;
3470
+ const want = !ringComplete ? 0 : exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
3143
3471
  const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
3144
3472
  for (let i = 0; i < COUNT; i++) {
3145
3473
  const target = i < want ? 1 : 0;
@@ -3164,14 +3492,14 @@ var GridAssemblyAnimation = class {
3164
3492
  }
3165
3493
  renderStory(now, dt) {
3166
3494
  const t = now - this.enterAt;
3167
- const halfHeight = Math.tan(FOV / 2) * CAMERA_Z2;
3495
+ const halfHeight = Math.tan(FOV2 / 2) * CAMERA_Z3;
3168
3496
  const halfWidth = halfHeight * this.aspect;
3169
3497
  const radius = Math.max(0.6, Math.min(halfWidth, halfHeight) - this.size * 0.55);
3170
3498
  const spawnFactor = (Math.max(halfWidth, halfHeight) * 1.25 + this.size * 2) / radius;
3171
3499
  for (let i = 0; i < COUNT; i++) {
3172
3500
  const transform2 = this.handles[i].transform;
3173
3501
  const eased = smooth01(this.blends[i]);
3174
- const introT = clamp014((t - i * INTRO_STAGGER_MS) / INTRO_MS);
3502
+ const introT = clamp015((t - i * INTRO_STAGGER_MS) / INTRO_MS);
3175
3503
  const reach = 1 + (spawnFactor - 1) * (1 - easeOutCubic(introT));
3176
3504
  const angle = Math.PI / 2 - i / COUNT * TWO_PI2 - t / this.orbitPeriodMs * TWO_PI2;
3177
3505
  const orbitX = Math.cos(angle) * radius * reach;
@@ -3202,25 +3530,30 @@ var GridAssemblyAnimation = class {
3202
3530
  if (!this.captured) {
3203
3531
  this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
3204
3532
  }
3205
- const u = clamp014((now - this.collapseAt) / COLLAPSE_MS);
3206
- const pull = easeInCubic(u);
3207
3533
  for (let i = 0; i < COUNT; i++) {
3208
3534
  const transform2 = this.handles[i].transform;
3209
3535
  const from = this.captured[i];
3536
+ const local = now - this.collapseAt - this.collapseDelay[i];
3537
+ if (local <= 0) {
3538
+ transform2.position.x = from.x;
3539
+ transform2.position.y = from.y;
3540
+ transform2.position.z = from.z;
3541
+ transform2.scale = this.size;
3542
+ continue;
3543
+ }
3544
+ const pull = easeInCubic(clamp015(local / COLLAPSE_MS));
3210
3545
  transform2.position.x = from.x * (1 - pull);
3211
3546
  transform2.position.y = from.y * (1 - pull);
3212
3547
  transform2.position.z = from.z * (1 - pull);
3213
3548
  transform2.scale = this.size * (1 - 0.99 * pull);
3214
- }
3215
- if (u >= 1) {
3216
- if (!this.popFading) {
3217
- this.popFading = true;
3218
- for (let i = 0; i < COUNT; i++) this.handles[i].transparency = this.fades[i];
3219
- }
3220
- const v = clamp014((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
3221
- for (let i = 0; i < COUNT; i++) {
3549
+ if (local >= COLLAPSE_MS) {
3550
+ if (!this.popStarted[i]) {
3551
+ this.popStarted[i] = true;
3552
+ this.handles[i].transparency = this.fades[i];
3553
+ }
3554
+ const v = clamp015((local - COLLAPSE_MS) / POP_MS2);
3222
3555
  this.fades[i].opacity = 1 - v;
3223
- this.handles[i].transform.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
3556
+ transform2.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
3224
3557
  }
3225
3558
  }
3226
3559
  }
@@ -3320,68 +3653,347 @@ function pulsingStarfield(options = {}) {
3320
3653
  }), options);
3321
3654
  }
3322
3655
 
3323
- // src/motion/circle.ts
3324
- function circleMotion(options = {}) {
3325
- const radius = options.radius ?? 1.3;
3326
- const periodMs = options.periodMs ?? 3e3;
3327
- const tilt = options.tilt ?? 0.5;
3328
- const direction = options.direction ?? 1;
3329
- const cosTilt = Math.cos(tilt);
3330
- const sinTilt = Math.sin(tilt);
3331
- return {
3332
- positionAt(t) {
3333
- const angle = direction * t / periodMs * Math.PI * 2;
3334
- const x = radius * Math.cos(angle);
3335
- const flatY = radius * Math.sin(angle);
3336
- return { x, y: flatY * cosTilt, z: flatY * sinTilt };
3337
- }
3338
- };
3656
+ // src/animations/rocket-launch.ts
3657
+ var ROCKETS = 20;
3658
+ var CAMERA_Z4 = 3;
3659
+ var FOV3 = 55 * Math.PI / 180;
3660
+ var HALF_HEIGHT2 = Math.tan(FOV3 / 2) * CAMERA_Z4;
3661
+ var SIZE = 0.12;
3662
+ var ROW_Y = -0.5;
3663
+ var PARTICLE_Z = 0.3;
3664
+ var SLIDE_MS = 460;
3665
+ var SLIDE_GATE = 0.45;
3666
+ var EXIT_HURRY3 = 2.5;
3667
+ var LAUNCH_SPREAD_MS = 620;
3668
+ var ASCENT_G = 5.2;
3669
+ var FINISH_PAD_MS = 2e3;
3670
+ var TURNERS = 3;
3671
+ var TURN_MIN_Y = 0.2;
3672
+ var TURN_MAX_Y = 0.8;
3673
+ var TURN_MIN_DEG = 30;
3674
+ var TURN_MAX_DEG = 50;
3675
+ var DEG = Math.PI / 180;
3676
+ var SMOKE_LIFE_MS = 1400;
3677
+ var SMOKE_GAP_MS = 320;
3678
+ var SMOKE_RISE = 0.55;
3679
+ var SMOKE_SIZE = 0.17;
3680
+ var SMOKE_PEAK = 0.16;
3681
+ var SMOKE_POOL = 104;
3682
+ var FIRE_LIFE_MS = 420;
3683
+ var FIRE_GAP_MS = 55;
3684
+ var FIRE_ON_MS = 950;
3685
+ var FIRE_TRAIL = 0.25;
3686
+ var FIRE_SPREAD = 0.09;
3687
+ var FIRE_SIZE = 0.15;
3688
+ var FIRE_PEAK = 0.9;
3689
+ var FIRE_POOL = 140;
3690
+ var SMOKE_COLORS = ["#e2e8f0", "#cbd5e1"];
3691
+ var FIRE_COLORS = ["#fef3c7", "#fde047", "#fb923c", "#ef4444"];
3692
+ var ROCKET_COLORS = ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"];
3693
+ function clamp016(value) {
3694
+ return Math.max(0, Math.min(1, value));
3339
3695
  }
3340
-
3341
- // src/prefabs/rocket-launch.ts
3342
- var launchUp = ({ delta, position, velocity, durationMs }) => {
3343
- const coast = durationMs * delta * (1 - 0.5 * delta);
3344
- const climb = 5.5 * easeInCubic(delta);
3345
- return {
3346
- position: {
3347
- x: position.x + (velocity?.x ?? 0) * coast,
3348
- y: position.y + (velocity?.y ?? 0) * coast + climb,
3349
- z: position.z + (velocity?.z ?? 0) * coast
3696
+ function smoothstep2(edge0, edge1, value) {
3697
+ const x = clamp016((value - edge0) / (edge1 - edge0));
3698
+ return x * x * (3 - 2 * x);
3699
+ }
3700
+ function hash012(index, salt) {
3701
+ let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
3702
+ h = Math.imul(h ^ h >>> 16, 73244475);
3703
+ h ^= h >>> 16;
3704
+ return (h >>> 0) / 4294967296;
3705
+ }
3706
+ function puffTexture(coreAlpha, coreStop) {
3707
+ return canvasTexture((ctx) => {
3708
+ const g = ctx.createRadialGradient(48, 48, 1, 48, 48, 47);
3709
+ g.addColorStop(0, `rgba(255,255,255,${coreAlpha})`);
3710
+ g.addColorStop(coreStop, `rgba(255,255,255,${coreAlpha * 0.6})`);
3711
+ g.addColorStop(1, "rgba(255,255,255,0)");
3712
+ ctx.fillStyle = g;
3713
+ ctx.fillRect(0, 0, 96, 96);
3714
+ });
3715
+ }
3716
+ var RocketLaunchAnimation = class {
3717
+ constructor(options = {}) {
3718
+ this.rockets = [];
3719
+ this.smoke = [];
3720
+ this.fire = [];
3721
+ this.smokeFades = [];
3722
+ this.fireFades = [];
3723
+ this.blends = new Array(ROCKETS).fill(0);
3724
+ this.groundedAt = new Array(ROCKETS).fill(Infinity);
3725
+ // Per-rocket veer parameters (turnS = Infinity for a rocket that climbs straight).
3726
+ this.turnS = new Array(ROCKETS).fill(Infinity);
3727
+ this.turnDir = [];
3728
+ this.turnRoll = new Array(ROCKETS).fill(0);
3729
+ this.stagger = new Array(ROCKETS).fill(0);
3730
+ this.aspect = 16 / 9;
3731
+ this.enterAt = Infinity;
3732
+ this.exitAt = Infinity;
3733
+ this.launchedAt = Infinity;
3734
+ this.lastNow = 0;
3735
+ this.finished = false;
3736
+ this.backend = options.backend;
3737
+ this.labelContent = options.label;
3738
+ this.fadeLabel = options.fadeLabel ?? true;
3739
+ for (let i = 0; i < ROCKETS; i++) {
3740
+ this.turnDir.push({ x: 0, y: 1 });
3741
+ this.stagger[i] = hash012(i, 7) * LAUNCH_SPREAD_MS;
3350
3742
  }
3351
- };
3743
+ const order = Array.from({ length: ROCKETS }, (_, i) => i).sort(
3744
+ (a, b) => hash012(a, 11) - hash012(b, 11)
3745
+ );
3746
+ for (const i of order.slice(0, TURNERS)) {
3747
+ const height = TURN_MIN_Y + hash012(i, 12) * (TURN_MAX_Y - TURN_MIN_Y);
3748
+ const angle = (TURN_MIN_DEG + hash012(i, 13) * (TURN_MAX_DEG - TURN_MIN_DEG)) * DEG;
3749
+ const sign = hash012(i, 14) < 0.5 ? -1 : 1;
3750
+ this.turnS[i] = height - ROW_Y;
3751
+ this.turnDir[i] = { x: sign * Math.sin(angle), y: Math.cos(angle) };
3752
+ this.turnRoll[i] = -sign * angle;
3753
+ }
3754
+ }
3755
+ mount(target) {
3756
+ if (!target.style.position) target.style.position = "relative";
3757
+ const smokeMeshes = SMOKE_COLORS.map((color) => quad(1, [color]));
3758
+ const fireMeshes = FIRE_COLORS.map((color) => quad(1, [color]));
3759
+ const smokeTexture = puffTexture(0.85, 0.5);
3760
+ const fireTexture = puffTexture(1, 0.32);
3761
+ const backend = async (rendererOptions) => {
3762
+ const renderer = this.backend === "webgpu" ? new (await Promise.resolve().then(() => (init_webgpu_textured(), webgpu_textured_exports))).WebGPUTexturedRenderer(rendererOptions) : this.backend === "webgl" ? new (await Promise.resolve().then(() => (init_webgl_textured(), webgl_textured_exports))).WebGLTexturedRenderer(rendererOptions) : new (await Promise.resolve().then(() => (init_canvas2d_textured(), canvas2d_textured_exports))).Canvas2DTexturedRenderer(rendererOptions);
3763
+ for (const mesh of smokeMeshes) renderer.setTexture(mesh, smokeTexture);
3764
+ for (const mesh of fireMeshes) renderer.setTexture(mesh, fireTexture);
3765
+ return renderer;
3766
+ };
3767
+ const engine = new Little3dEngine({
3768
+ backend,
3769
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z4 }, fov: FOV3 }
3770
+ });
3771
+ const rocketMesh = pyramid(1, ROCKET_COLORS);
3772
+ for (let i = 0; i < ROCKETS; i++) this.rockets.push(engine.add(rocketMesh, { scale: 0 }));
3773
+ for (let s = 0; s < SMOKE_POOL; s++) {
3774
+ const fade = { mode: "one-sided", opacity: 0 };
3775
+ this.smokeFades.push(fade);
3776
+ this.smoke.push(engine.add(smokeMeshes[s % smokeMeshes.length], { scale: 0, transparency: fade }));
3777
+ }
3778
+ for (let f = 0; f < FIRE_POOL; f++) {
3779
+ const fade = { mode: "one-sided", opacity: 0 };
3780
+ this.fireFades.push(fade);
3781
+ this.fire.push(engine.add(fireMeshes[f % fireMeshes.length], { scale: 0, transparency: fade }));
3782
+ }
3783
+ this.engine = engine;
3784
+ engine.mount(target).catch((error) => {
3785
+ target.textContent = error instanceof Error ? error.message : String(error);
3786
+ });
3787
+ const measure = () => {
3788
+ if (target.clientWidth > 0 && target.clientHeight > 0) {
3789
+ this.aspect = target.clientWidth / target.clientHeight;
3790
+ }
3791
+ };
3792
+ measure();
3793
+ this.observer = new ResizeObserver(measure);
3794
+ this.observer.observe(target);
3795
+ this.label = mountAnimationLabel(target, this.labelContent);
3796
+ if (this.fadeLabel) this.label.setOpacity(0);
3797
+ }
3798
+ enter(now) {
3799
+ if (this.enterAt === Infinity) this.enterAt = now;
3800
+ }
3801
+ exit(now) {
3802
+ if (this.exitAt === Infinity) this.exitAt = now;
3803
+ }
3804
+ isFinished() {
3805
+ return this.finished;
3806
+ }
3807
+ render(now, frame) {
3808
+ if (!this.engine || !this.label) return;
3809
+ for (const handle of this.rockets) handle.transform.scale = 0;
3810
+ for (const fade of this.smokeFades) fade.opacity = 0;
3811
+ for (const fade of this.fireFades) fade.opacity = 0;
3812
+ for (const handle of this.smoke) handle.transform.scale = 0;
3813
+ for (const handle of this.fire) handle.transform.scale = 0;
3814
+ if (this.enterAt === Infinity) {
3815
+ this.engine.render();
3816
+ return;
3817
+ }
3818
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
3819
+ this.lastNow = now;
3820
+ const exiting = this.exitAt !== Infinity;
3821
+ this.updateBlends(dt, frame.progress, now, exiting);
3822
+ if (exiting && this.launchedAt === Infinity && this.blends.every((blend) => blend >= 1)) {
3823
+ this.launchedAt = now;
3824
+ }
3825
+ const launched = this.launchedAt !== Infinity;
3826
+ const halfWidth = HALF_HEIGHT2 * this.aspect;
3827
+ const rowHalf = Math.min(halfWidth * 0.8, 1.18);
3828
+ const spawnX = halfWidth + 0.6;
3829
+ let smokeCursor = 0;
3830
+ let fireCursor = 0;
3831
+ for (let i = 0; i < ROCKETS; i++) {
3832
+ const homeX = -rowHalf + 2 * rowHalf * i / (ROCKETS - 1);
3833
+ const launchAt = launched ? this.launchedAt + this.stagger[i] : Infinity;
3834
+ const la = now - launchAt;
3835
+ if (launched && la >= 0) {
3836
+ this.renderAscent(i, homeX, la, halfWidth);
3837
+ fireCursor = this.emitFire(i, homeX, la, fireCursor);
3838
+ continue;
3839
+ }
3840
+ const blend = this.blends[i];
3841
+ if (blend <= 0) continue;
3842
+ const transform2 = this.rockets[i].transform;
3843
+ transform2.position.x = spawnX + (homeX - spawnX) * easeOutBack(blend);
3844
+ transform2.position.y = ROW_Y;
3845
+ transform2.position.z = 0;
3846
+ transform2.rotation.x = 0;
3847
+ transform2.rotation.y = 0;
3848
+ transform2.rotation.z = 0;
3849
+ transform2.scale = SIZE * smoothstep2(0, 0.6, blend);
3850
+ if (this.groundedAt[i] !== Infinity) {
3851
+ smokeCursor = this.emitSmoke(i, homeX, now, launchAt, smokeCursor);
3852
+ }
3853
+ }
3854
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
3855
+ if (this.fadeLabel) {
3856
+ this.label.setOpacity(animationLabelOpacity(
3857
+ now,
3858
+ this.enterAt,
3859
+ SLIDE_MS,
3860
+ this.launchedAt,
3861
+ LAUNCH_SPREAD_MS
3862
+ ));
3863
+ }
3864
+ if (launched && now >= this.launchedAt + LAUNCH_SPREAD_MS + FINISH_PAD_MS) {
3865
+ this.finished = true;
3866
+ }
3867
+ this.engine.render();
3868
+ }
3869
+ destroy() {
3870
+ this.observer?.disconnect();
3871
+ this.observer = void 0;
3872
+ this.label?.container.remove();
3873
+ this.label = void 0;
3874
+ this.engine?.destroy();
3875
+ this.engine = void 0;
3876
+ this.rockets.length = 0;
3877
+ this.smoke.length = 0;
3878
+ this.fire.length = 0;
3879
+ this.smokeFades.length = 0;
3880
+ this.fireFades.length = 0;
3881
+ }
3882
+ updateBlends(dt, progress, now, exiting) {
3883
+ const want = exiting ? ROCKETS : Math.min(ROCKETS, Math.round(progress * ROCKETS));
3884
+ const rate = dt / SLIDE_MS * (exiting ? EXIT_HURRY3 : 1);
3885
+ for (let i = 0; i < ROCKETS; i++) {
3886
+ const target = i < want ? 1 : 0;
3887
+ const blend = this.blends[i];
3888
+ if (target > blend && (i === 0 || this.blends[i - 1] >= SLIDE_GATE)) {
3889
+ this.blends[i] = Math.min(1, blend + rate);
3890
+ } else if (target < blend && (i === ROCKETS - 1 || this.blends[i + 1] <= 1 - SLIDE_GATE)) {
3891
+ this.blends[i] = Math.max(0, blend - rate);
3892
+ }
3893
+ if (this.blends[i] >= 1) {
3894
+ if (this.groundedAt[i] === Infinity) this.groundedAt[i] = now;
3895
+ } else {
3896
+ this.groundedAt[i] = Infinity;
3897
+ }
3898
+ }
3899
+ }
3900
+ /** Along-track distance climbed `la` ms after this rocket's own blast-off. */
3901
+ ascentDistance(la) {
3902
+ const seconds = la / 1e3;
3903
+ return 0.5 * ASCENT_G * seconds * seconds;
3904
+ }
3905
+ /** Rocket center, nose direction, and roll `la` ms into its climb. */
3906
+ ascentPose(i, homeX, la) {
3907
+ const s = this.ascentDistance(la);
3908
+ const turnS = this.turnS[i];
3909
+ if (s <= turnS) {
3910
+ return { pos: { x: homeX, y: ROW_Y + s }, dir: { x: 0, y: 1 }, roll: 0 };
3911
+ }
3912
+ const post = s - turnS;
3913
+ const dir = this.turnDir[i];
3914
+ return {
3915
+ pos: { x: homeX + dir.x * post, y: ROW_Y + turnS + dir.y * post },
3916
+ dir,
3917
+ roll: this.turnRoll[i]
3918
+ };
3919
+ }
3920
+ renderAscent(i, homeX, la, halfWidth) {
3921
+ const { pos, roll } = this.ascentPose(i, homeX, la);
3922
+ if (pos.y > HALF_HEIGHT2 + 0.4 || Math.abs(pos.x) > halfWidth + 0.4) return;
3923
+ const transform2 = this.rockets[i].transform;
3924
+ transform2.position.x = pos.x;
3925
+ transform2.position.y = pos.y;
3926
+ transform2.position.z = 0;
3927
+ transform2.rotation.x = 0;
3928
+ transform2.rotation.y = 0;
3929
+ transform2.rotation.z = roll;
3930
+ transform2.scale = SIZE;
3931
+ }
3932
+ emitFire(i, homeX, la, cursor) {
3933
+ const gap = FIRE_GAP_MS;
3934
+ const last = Math.min(Math.floor(la / gap), Math.floor(FIRE_ON_MS / gap));
3935
+ const first = Math.max(0, Math.ceil((la - FIRE_LIFE_MS) / gap));
3936
+ for (let n = first; n <= last; n++) {
3937
+ if (cursor >= this.fire.length) return cursor;
3938
+ const emitLa = n * gap;
3939
+ const age = la - emitLa;
3940
+ if (age < 0 || age >= FIRE_LIFE_MS) continue;
3941
+ const life = age / FIRE_LIFE_MS;
3942
+ const seconds = age / 1e3;
3943
+ const pose = this.ascentPose(i, homeX, emitLa);
3944
+ const back = { x: -pose.dir.x, y: -pose.dir.y };
3945
+ const perp = { x: -pose.dir.y, y: pose.dir.x };
3946
+ const lat = (hash012(i * 97 + n, 1) - 0.5) * FIRE_SPREAD;
3947
+ const baseX = pose.pos.x + back.x * SIZE * 0.5;
3948
+ const baseY = pose.pos.y + back.y * SIZE * 0.5;
3949
+ const transform2 = this.fire[cursor].transform;
3950
+ transform2.position.x = baseX + back.x * FIRE_TRAIL * seconds + perp.x * lat;
3951
+ transform2.position.y = baseY + back.y * FIRE_TRAIL * seconds + perp.y * lat - 0.12 * seconds * seconds;
3952
+ transform2.position.z = PARTICLE_Z;
3953
+ transform2.rotation.z = hash012(i * 97 + n, 2) * Math.PI * 2;
3954
+ transform2.scale = FIRE_SIZE * (0.7 + 0.5 * hash012(i * 97 + n, 3)) * (1 - 0.55 * life);
3955
+ this.fireFades[cursor].opacity = FIRE_PEAK * smoothstep2(0, 0.15, life) * (1 - smoothstep2(0.55, 1, life));
3956
+ cursor++;
3957
+ }
3958
+ return cursor;
3959
+ }
3960
+ emitSmoke(i, homeX, now, launchAt, cursor) {
3961
+ const start = this.groundedAt[i];
3962
+ const tr = now - start;
3963
+ const gap = SMOKE_GAP_MS;
3964
+ const emitUntil = Number.isFinite(launchAt) ? launchAt - start : tr;
3965
+ const last = Math.min(Math.floor(tr / gap), Math.floor(emitUntil / gap));
3966
+ const first = Math.max(0, Math.ceil((tr - SMOKE_LIFE_MS) / gap));
3967
+ const baseY = ROW_Y - SIZE * 0.4;
3968
+ for (let n = first; n <= last; n++) {
3969
+ if (cursor >= this.smoke.length) return cursor;
3970
+ const age = tr - n * gap;
3971
+ if (age < 0 || age >= SMOKE_LIFE_MS) continue;
3972
+ const life = age / SMOKE_LIFE_MS;
3973
+ const drift = (hash012(i * 131 + n, 1) - 0.5) * 0.14;
3974
+ const transform2 = this.smoke[cursor].transform;
3975
+ transform2.position.x = homeX + drift * life;
3976
+ transform2.position.y = baseY + SMOKE_RISE * life;
3977
+ transform2.position.z = PARTICLE_Z;
3978
+ transform2.rotation.z = hash012(i * 131 + n, 2) * Math.PI * 2;
3979
+ transform2.scale = SMOKE_SIZE * (0.5 + 0.8 * life) * (0.7 + 0.6 * hash012(i * 131 + n, 3));
3980
+ this.smokeFades[cursor].opacity = SMOKE_PEAK * smoothstep2(0, 0.25, life) * (1 - smoothstep2(0.5, 1, life));
3981
+ cursor++;
3982
+ }
3983
+ return cursor;
3984
+ }
3352
3985
  };
3986
+
3987
+ // src/prefabs/rocket-launch.ts
3353
3988
  function rocketLaunch(options = {}) {
3354
- const motion = options.object?.motion ?? circleMotion({ radius: 0.55, periodMs: 7e3, tilt: 0.15 });
3355
- const particles = options.particles ?? {};
3356
- const object = new ObjectMotionAnimation({
3357
- mesh: () => pyramid(1, ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"]),
3358
- motion,
3359
- size: 0.44,
3360
- facing: "+y",
3361
- outro: { transition: launchUp, durationMs: 1300 },
3362
- backend: options.backend,
3363
- ...options.object,
3364
- label: options.object?.label
3365
- });
3366
- const exhaust = new ParticlesAnimation({
3367
- rate: 46,
3368
- lifeMs: 1100,
3369
- size: 0.18,
3370
- speed: 0.5,
3371
- direction: { x: 0, y: -1, z: 0 },
3372
- spread: 0.3,
3373
- gravity: { x: 0, y: -0.6, z: 0 },
3374
- colors: ["#fde047", "#fb923c", "#ef4444", "#fef3c7"],
3375
- texture: particles.texture ?? shineTexture(),
3376
- emitter: object.trailEmitter(),
3377
- outroMs: object.outroDurationMs,
3378
- seed: 9,
3379
- backend: options.backend,
3380
- ...particles,
3381
- label: options.label ?? particles.label,
3382
- fadeLabel: options.fadeLabel ?? particles.fadeLabel
3383
- });
3384
- return progressSpinner(new CompositeAnimation([exhaust, object]), options);
3989
+ return progressSpinner(
3990
+ new RocketLaunchAnimation({
3991
+ backend: options.backend,
3992
+ label: options.label,
3993
+ fadeLabel: options.fadeLabel
3994
+ }),
3995
+ options
3996
+ );
3385
3997
  }
3386
3998
 
3387
3999
  // src/motion/wander.ts