3d-spinner 0.9.2 → 0.9.4

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 (101) hide show
  1. package/README.md +66 -4
  2. package/dist/animation-label.d.ts +8 -0
  3. package/dist/animation-label.js +47 -0
  4. package/dist/animation.d.ts +2 -0
  5. package/dist/animations/charged-orb.d.ts +55 -0
  6. package/dist/animations/charged-orb.js +229 -0
  7. package/dist/animations/ghost-train.d.ts +72 -0
  8. package/dist/animations/ghost-train.js +247 -0
  9. package/dist/animations/grid-assembly.d.ts +70 -0
  10. package/dist/animations/grid-assembly.js +282 -0
  11. package/dist/animations/object-motion.d.ts +19 -7
  12. package/dist/animations/object-motion.js +38 -32
  13. package/dist/animations/particles.d.ts +110 -0
  14. package/dist/animations/particles.js +209 -0
  15. package/dist/animations/rocket-launch.d.ts +58 -0
  16. package/dist/animations/rocket-launch.js +375 -0
  17. package/dist/cjs/animations/charged-orb.cjs +1334 -0
  18. package/dist/cjs/animations/grid-assembly.cjs +1380 -0
  19. package/dist/cjs/animations/object-motion.cjs +124 -43
  20. package/dist/cjs/animations/particles.cjs +1940 -0
  21. package/dist/cjs/animations/spin.cjs +4 -3
  22. package/dist/cjs/composite-animation.cjs +58 -0
  23. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +143 -22
  24. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +30 -1
  25. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +345 -0
  26. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +528 -0
  27. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +712 -0
  28. package/dist/cjs/prefabs/prefabs.cjs +3916 -0
  29. package/dist/composite-animation.d.ts +18 -0
  30. package/dist/composite-animation.js +39 -0
  31. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +17 -12
  32. package/dist/engines/little-3d-engine/little-3d-engine.js +13 -8
  33. package/dist/engines/little-3d-engine/loaders/obj.d.ts +12 -4
  34. package/dist/engines/little-3d-engine/loaders/obj.js +44 -5
  35. package/dist/engines/little-3d-engine/renderer.d.ts +7 -1
  36. package/dist/engines/little-3d-engine/renderer.js +2 -0
  37. package/dist/engines/little-3d-engine/renderers/canvas2d-textured.d.ts +20 -0
  38. package/dist/engines/little-3d-engine/renderers/canvas2d-textured.js +135 -0
  39. package/dist/engines/little-3d-engine/renderers/textured-helpers.d.ts +9 -0
  40. package/dist/engines/little-3d-engine/renderers/textured-helpers.js +35 -0
  41. package/dist/engines/little-3d-engine/renderers/webgl-textured.d.ts +43 -0
  42. package/dist/engines/little-3d-engine/renderers/webgl-textured.js +211 -0
  43. package/dist/engines/little-3d-engine/renderers/webgpu-textured.d.ts +45 -0
  44. package/dist/engines/little-3d-engine/renderers/webgpu-textured.js +300 -0
  45. package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +11 -6
  46. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +3 -0
  47. package/dist/engines/little-3d-engine/shapes/complex/plane.js +33 -0
  48. package/dist/engines/little-3d-engine/shapes/{cube.d.ts → primitives/cube.d.ts} +1 -1
  49. package/dist/engines/little-3d-engine/shapes/{octahedron.d.ts → primitives/octahedron.d.ts} +1 -1
  50. package/dist/engines/little-3d-engine/shapes/{pyramid.d.ts → primitives/pyramid.d.ts} +1 -1
  51. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +9 -0
  52. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +18 -0
  53. package/dist/engines/little-3d-engine/shapes/{cube-sphere.d.ts → primitives/spheres/cube-sphere.d.ts} +1 -1
  54. package/dist/engines/little-3d-engine/shapes/{icosphere.d.ts → primitives/spheres/icosphere.d.ts} +1 -1
  55. package/dist/engines/little-3d-engine/shapes/{icosphere.js → primitives/spheres/icosphere.js} +1 -1
  56. package/dist/engines/little-3d-engine/shapes/{octa-sphere.d.ts → primitives/spheres/octa-sphere.d.ts} +1 -1
  57. package/dist/engines/little-3d-engine/shapes/{octa-sphere.js → primitives/spheres/octa-sphere.js} +1 -1
  58. package/dist/engines/little-3d-engine/shapes/{uv-sphere.d.ts → primitives/spheres/uv-sphere.d.ts} +1 -1
  59. package/dist/engines/little-3d-engine/shapes/{tetrahedron.d.ts → primitives/tetrahedron.d.ts} +1 -1
  60. package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.d.ts +1 -0
  61. package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.js +8 -0
  62. package/dist/engines/little-3d-engine/textures/dynamic/shine.d.ts +2 -0
  63. package/dist/engines/little-3d-engine/textures/dynamic/shine.js +14 -0
  64. package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +14 -0
  65. package/dist/engines/little-3d-engine/textures/dynamic/star.js +31 -0
  66. package/dist/engines/little-3d-engine/textures/dynamic/streak.d.ts +5 -0
  67. package/dist/engines/little-3d-engine/textures/dynamic/streak.js +20 -0
  68. package/dist/index.d.ts +2 -1
  69. package/dist/prefabs/charged-orb.d.ts +17 -0
  70. package/dist/prefabs/charged-orb.js +37 -0
  71. package/dist/prefabs/crystal-comet.d.ts +4 -0
  72. package/dist/prefabs/crystal-comet.js +41 -0
  73. package/dist/prefabs/ghost-train.d.ts +10 -0
  74. package/dist/prefabs/ghost-train.js +37 -0
  75. package/dist/prefabs/grid-assembly.d.ts +13 -0
  76. package/dist/prefabs/grid-assembly.js +15 -0
  77. package/dist/prefabs/monochrome-streak.d.ts +4 -0
  78. package/dist/prefabs/monochrome-streak.js +25 -0
  79. package/dist/prefabs/plane-star-trail.d.ts +4 -0
  80. package/dist/prefabs/plane-star-trail.js +40 -0
  81. package/dist/prefabs/prefabs.d.ts +10 -0
  82. package/dist/prefabs/prefabs.js +9 -0
  83. package/dist/prefabs/pulsing-starfield.d.ts +4 -0
  84. package/dist/prefabs/pulsing-starfield.js +27 -0
  85. package/dist/prefabs/rocket-launch.d.ts +10 -0
  86. package/dist/prefabs/rocket-launch.js +16 -0
  87. package/dist/prefabs/spinner.d.ts +7 -0
  88. package/dist/prefabs/spinner.js +17 -0
  89. package/dist/prefabs/star-swarm.d.ts +4 -0
  90. package/dist/prefabs/star-swarm.js +27 -0
  91. package/dist/prefabs/types.d.ts +46 -0
  92. package/dist/prefabs/types.js +1 -0
  93. package/dist/umd/spinner.global.js +2407 -85
  94. package/dist/umd/spinner.global.min.js +60 -5
  95. package/package.json +43 -2
  96. /package/dist/engines/little-3d-engine/shapes/{cube.js → primitives/cube.js} +0 -0
  97. /package/dist/engines/little-3d-engine/shapes/{octahedron.js → primitives/octahedron.js} +0 -0
  98. /package/dist/engines/little-3d-engine/shapes/{pyramid.js → primitives/pyramid.js} +0 -0
  99. /package/dist/engines/little-3d-engine/shapes/{cube-sphere.js → primitives/spheres/cube-sphere.js} +0 -0
  100. /package/dist/engines/little-3d-engine/shapes/{uv-sphere.js → primitives/spheres/uv-sphere.js} +0 -0
  101. /package/dist/engines/little-3d-engine/shapes/{tetrahedron.js → primitives/tetrahedron.js} +0 -0
@@ -825,6 +825,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
825
825
  return opaque.concat(transparent);
826
826
  }
827
827
  async function createRenderer(backend, options = {}) {
828
+ if (typeof backend === "function") return backend(options);
828
829
  switch (backend) {
829
830
  case "webgl":
830
831
  return new (await Promise.resolve().then(() => (init_webgl(), webgl_exports))).WebGLRenderer(options);
@@ -845,19 +846,678 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
845
846
  }
846
847
  });
847
848
 
849
+ // src/engines/little-3d-engine/renderers/textured-helpers.ts
850
+ function planarUVs(mesh) {
851
+ let minX = Infinity;
852
+ let minY = Infinity;
853
+ let maxX = -Infinity;
854
+ let maxY = -Infinity;
855
+ for (const v of mesh.vertices) {
856
+ minX = Math.min(minX, v.x);
857
+ minY = Math.min(minY, v.y);
858
+ maxX = Math.max(maxX, v.x);
859
+ maxY = Math.max(maxY, v.y);
860
+ }
861
+ const width = maxX - minX || 1;
862
+ const height = maxY - minY || 1;
863
+ let triangles = 0;
864
+ for (const face of mesh.faces) triangles += Math.max(0, face.indices.length - 2);
865
+ const uvs = new Float32Array(triangles * 6);
866
+ let o = 0;
867
+ for (const face of mesh.faces) {
868
+ for (let k = 1; k < face.indices.length - 1; k++) {
869
+ for (const index of [face.indices[0], face.indices[k], face.indices[k + 1]]) {
870
+ const v = mesh.vertices[index];
871
+ uvs[o] = (v.x - minX) / width;
872
+ uvs[o + 1] = 1 - (v.y - minY) / height;
873
+ o += 2;
874
+ }
875
+ }
876
+ }
877
+ return uvs;
878
+ }
879
+ var init_textured_helpers = __esm({
880
+ "src/engines/little-3d-engine/renderers/textured-helpers.ts"() {
881
+ "use strict";
882
+ }
883
+ });
884
+
885
+ // src/engines/little-3d-engine/renderers/webgpu-textured.ts
886
+ var webgpu_textured_exports = {};
887
+ __export(webgpu_textured_exports, {
888
+ WebGPUTexturedRenderer: () => WebGPUTexturedRenderer
889
+ });
890
+ function itemOpacity(transparency) {
891
+ if (!transparency) return 1;
892
+ if (transparency.mode === "two-sided") return resolveTwoSidedOpacity(transparency).front;
893
+ return opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
894
+ }
895
+ var WGSL2, CLIP_Z_FIX2, UNIFORM_STRIDE2, WebGPUTexturedRenderer;
896
+ var init_webgpu_textured = __esm({
897
+ "src/engines/little-3d-engine/renderers/webgpu-textured.ts"() {
898
+ "use strict";
899
+ init_geometry();
900
+ init_math();
901
+ init_renderer();
902
+ init_textured_helpers();
903
+ init_webgpu();
904
+ WGSL2 = `
905
+ struct Uniforms {
906
+ viewProj: mat4x4<f32>,
907
+ model: mat4x4<f32>,
908
+ params: vec4<f32>,
909
+ };
910
+ @group(0) @binding(0) var<uniform> u: Uniforms;
911
+ @group(0) @binding(1) var tex: texture_2d<f32>;
912
+ @group(0) @binding(2) var samp: sampler;
913
+
914
+ struct VSOut {
915
+ @builtin(position) position: vec4<f32>,
916
+ @location(0) uv: vec2<f32>,
917
+ @location(1) color: vec3<f32>,
918
+ };
919
+
920
+ @vertex
921
+ fn vs(@location(0) pos: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) color: vec3<f32>) -> VSOut {
922
+ var out: VSOut;
923
+ out.uv = uv;
924
+ out.color = color;
925
+ out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
926
+ return out;
927
+ }
928
+
929
+ @fragment
930
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
931
+ let t = textureSample(tex, samp, in.uv);
932
+ return vec4<f32>(t.rgb * in.color, t.a * u.params.x);
933
+ }
934
+ `;
935
+ CLIP_Z_FIX2 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
936
+ UNIFORM_STRIDE2 = 256;
937
+ WebGPUTexturedRenderer = class extends WebGPURenderer {
938
+ constructor() {
939
+ super(...arguments);
940
+ this.texturedCapacity = 0;
941
+ this.sources = /* @__PURE__ */ new Map();
942
+ this.textures = /* @__PURE__ */ new Map();
943
+ this.retired = [];
944
+ this.texturedBuffers = /* @__PURE__ */ new Map();
945
+ this.bindGroups = /* @__PURE__ */ new Map();
946
+ }
947
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
948
+ setTexture(mesh, source) {
949
+ this.sources.set(mesh, source);
950
+ }
951
+ async init(canvas) {
952
+ await super.init(canvas);
953
+ const device = this.device;
954
+ if (!device) return;
955
+ const format = navigator.gpu.getPreferredCanvasFormat();
956
+ const module = device.createShaderModule({ code: WGSL2 });
957
+ const stage = globalThis.GPUShaderStage;
958
+ const layout = device.createBindGroupLayout({
959
+ entries: [
960
+ {
961
+ binding: 0,
962
+ visibility: stage.VERTEX | stage.FRAGMENT,
963
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 144 }
964
+ },
965
+ { binding: 1, visibility: stage.FRAGMENT, texture: {} },
966
+ { binding: 2, visibility: stage.FRAGMENT, sampler: {} }
967
+ ]
968
+ });
969
+ const vertexBuffer = (location, components) => ({
970
+ arrayStride: components * 4,
971
+ attributes: [{ shaderLocation: location, offset: 0, format: `float32x${components}` }]
972
+ });
973
+ this.texturedPipeline = device.createRenderPipeline({
974
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
975
+ vertex: {
976
+ module,
977
+ entryPoint: "vs",
978
+ buffers: [vertexBuffer(0, 3), vertexBuffer(1, 2), vertexBuffer(2, 3)]
979
+ },
980
+ fragment: {
981
+ module,
982
+ entryPoint: "fs",
983
+ targets: [
984
+ {
985
+ format,
986
+ blend: {
987
+ color: {
988
+ srcFactor: "src-alpha",
989
+ dstFactor: "one-minus-src-alpha",
990
+ operation: "add"
991
+ },
992
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
993
+ }
994
+ }
995
+ ]
996
+ },
997
+ primitive: { topology: "triangle-list", cullMode: "back", frontFace: "ccw" },
998
+ depthStencil: {
999
+ format: "depth24plus",
1000
+ depthWriteEnabled: false,
1001
+ depthCompare: "less"
1002
+ }
1003
+ });
1004
+ this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
1005
+ }
1006
+ textureFor(mesh) {
1007
+ const cached = this.textures.get(mesh);
1008
+ if (cached) return cached;
1009
+ const device = this.device;
1010
+ const usage = globalThis.GPUTextureUsage;
1011
+ const white = device.createTexture({
1012
+ size: { width: 1, height: 1 },
1013
+ format: "rgba8unorm",
1014
+ usage: usage.TEXTURE_BINDING | usage.COPY_DST
1015
+ });
1016
+ device.queue.writeTexture(
1017
+ { texture: white },
1018
+ new Uint8Array([255, 255, 255, 255]),
1019
+ {},
1020
+ { width: 1, height: 1 }
1021
+ );
1022
+ this.textures.set(mesh, white);
1023
+ const upload = async (source2) => {
1024
+ const image = source2 instanceof HTMLImageElement ? await createImageBitmap(source2) : source2;
1025
+ if (this.destroyed || !this.device || this.textures.get(mesh) !== white) return;
1026
+ const width = image.width || 1;
1027
+ const height = image.height || 1;
1028
+ const texture = this.device.createTexture({
1029
+ size: { width, height },
1030
+ format: "rgba8unorm",
1031
+ usage: usage.TEXTURE_BINDING | usage.COPY_DST | usage.RENDER_ATTACHMENT
1032
+ });
1033
+ this.device.queue.copyExternalImageToTexture(
1034
+ { source: image },
1035
+ { texture },
1036
+ { width, height }
1037
+ );
1038
+ this.retired.push(white);
1039
+ this.textures.set(mesh, texture);
1040
+ this.bindGroups.delete(mesh);
1041
+ };
1042
+ const source = this.sources.get(mesh);
1043
+ if (typeof source === "string") {
1044
+ const image = new Image();
1045
+ image.onload = () => void upload(image);
1046
+ image.src = source;
1047
+ } else {
1048
+ void upload(source);
1049
+ }
1050
+ return this.textures.get(mesh);
1051
+ }
1052
+ buffersFor(mesh) {
1053
+ const cached = this.texturedBuffers.get(mesh);
1054
+ if (cached) return cached;
1055
+ const data = expandToTriangles(mesh);
1056
+ const usage = globalThis.GPUBufferUsage.VERTEX | globalThis.GPUBufferUsage.COPY_DST;
1057
+ const upload = (array) => {
1058
+ const buffer = this.device.createBuffer({ size: array.byteLength, usage });
1059
+ this.device.queue.writeBuffer(buffer, 0, array);
1060
+ return buffer;
1061
+ };
1062
+ const result = {
1063
+ position: upload(data.positions),
1064
+ uv: upload(planarUVs(mesh)),
1065
+ color: upload(data.colors),
1066
+ count: data.count
1067
+ };
1068
+ this.texturedBuffers.set(mesh, result);
1069
+ return result;
1070
+ }
1071
+ bindGroupFor(mesh) {
1072
+ const texture = this.textureFor(mesh);
1073
+ const cached = this.bindGroups.get(mesh);
1074
+ if (cached && cached.buffer === this.texturedUniforms && cached.texture === texture) {
1075
+ return cached.group;
1076
+ }
1077
+ const group = this.device.createBindGroup({
1078
+ layout: this.texturedPipeline.getBindGroupLayout(0),
1079
+ entries: [
1080
+ { binding: 0, resource: { buffer: this.texturedUniforms, offset: 0, size: 144 } },
1081
+ { binding: 1, resource: texture.createView() },
1082
+ { binding: 2, resource: this.sampler }
1083
+ ]
1084
+ });
1085
+ this.bindGroups.set(mesh, { group, buffer: this.texturedUniforms, texture });
1086
+ return group;
1087
+ }
1088
+ render(frame) {
1089
+ const plain = [];
1090
+ const textured = [];
1091
+ for (const item of frame.items) {
1092
+ (this.sources.has(item.mesh) ? textured : plain).push(item);
1093
+ }
1094
+ super.render(textured.length ? { ...frame, items: plain } : frame);
1095
+ if (!textured.length) return;
1096
+ if (this.destroyed || !this.device || !this.context || !this.texturedPipeline) return;
1097
+ if (frame.width === 0 || frame.height === 0) return;
1098
+ this.ensureDepth();
1099
+ if (textured.length > this.texturedCapacity || !this.texturedUniforms) {
1100
+ this.texturedUniforms?.destroy?.();
1101
+ this.texturedUniforms = this.device.createBuffer({
1102
+ size: textured.length * UNIFORM_STRIDE2,
1103
+ usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST
1104
+ });
1105
+ this.texturedCapacity = textured.length;
1106
+ }
1107
+ const viewProj = multiply(CLIP_Z_FIX2, frame.viewProjection);
1108
+ textured.forEach((item, i) => {
1109
+ const data = new Float32Array(UNIFORM_STRIDE2 / 4);
1110
+ data.set(viewProj, 0);
1111
+ data.set(item.model, 16);
1112
+ data.set([itemOpacity(item.transparency), 0, 0, 0], 32);
1113
+ this.device.queue.writeBuffer(this.texturedUniforms, i * UNIFORM_STRIDE2, data);
1114
+ });
1115
+ const cleared = plain.length > 0;
1116
+ const encoder = this.device.createCommandEncoder();
1117
+ const pass = encoder.beginRenderPass({
1118
+ colorAttachments: [
1119
+ {
1120
+ view: this.context.getCurrentTexture().createView(),
1121
+ clearValue: this.clearValue,
1122
+ loadOp: cleared ? "load" : "clear",
1123
+ storeOp: "store"
1124
+ }
1125
+ ],
1126
+ depthStencilAttachment: {
1127
+ view: this.depthTexture.createView(),
1128
+ depthClearValue: 1,
1129
+ depthLoadOp: cleared ? "load" : "clear",
1130
+ depthStoreOp: "store"
1131
+ }
1132
+ });
1133
+ pass.setPipeline(this.texturedPipeline);
1134
+ textured.forEach((item, i) => {
1135
+ const mesh = this.buffersFor(item.mesh);
1136
+ pass.setBindGroup(0, this.bindGroupFor(item.mesh), [i * UNIFORM_STRIDE2]);
1137
+ pass.setVertexBuffer(0, mesh.position);
1138
+ pass.setVertexBuffer(1, mesh.uv);
1139
+ pass.setVertexBuffer(2, mesh.color);
1140
+ pass.draw(mesh.count);
1141
+ });
1142
+ pass.end();
1143
+ this.device.queue.submit([encoder.finish()]);
1144
+ }
1145
+ destroy() {
1146
+ for (const texture of this.textures.values()) texture.destroy?.();
1147
+ for (const texture of this.retired.splice(0)) texture.destroy?.();
1148
+ for (const buffers of this.texturedBuffers.values()) {
1149
+ buffers.position.destroy?.();
1150
+ buffers.uv.destroy?.();
1151
+ buffers.color.destroy?.();
1152
+ }
1153
+ this.textures.clear();
1154
+ this.texturedBuffers.clear();
1155
+ this.bindGroups.clear();
1156
+ this.sources.clear();
1157
+ this.texturedUniforms?.destroy?.();
1158
+ this.texturedUniforms = void 0;
1159
+ this.texturedPipeline = void 0;
1160
+ this.sampler = void 0;
1161
+ super.destroy();
1162
+ }
1163
+ };
1164
+ }
1165
+ });
1166
+
1167
+ // src/engines/little-3d-engine/renderers/webgl-textured.ts
1168
+ var webgl_textured_exports = {};
1169
+ __export(webgl_textured_exports, {
1170
+ WebGLTexturedRenderer: () => WebGLTexturedRenderer
1171
+ });
1172
+ function compile2(gl, type, source) {
1173
+ const shader = gl.createShader(type);
1174
+ gl.shaderSource(shader, source);
1175
+ gl.compileShader(shader);
1176
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1177
+ throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
1178
+ }
1179
+ return shader;
1180
+ }
1181
+ function link2(gl) {
1182
+ const program = gl.createProgram();
1183
+ gl.attachShader(program, compile2(gl, gl.VERTEX_SHADER, VERTEX_SHADER2));
1184
+ gl.attachShader(program, compile2(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER2));
1185
+ gl.linkProgram(program);
1186
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1187
+ throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
1188
+ }
1189
+ return program;
1190
+ }
1191
+ function itemOpacity2(transparency) {
1192
+ if (!transparency) return 1;
1193
+ if (transparency.mode === "two-sided") return resolveTwoSidedOpacity(transparency).front;
1194
+ return opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
1195
+ }
1196
+ var VERTEX_SHADER2, FRAGMENT_SHADER2, WebGLTexturedRenderer;
1197
+ var init_webgl_textured = __esm({
1198
+ "src/engines/little-3d-engine/renderers/webgl-textured.ts"() {
1199
+ "use strict";
1200
+ init_geometry();
1201
+ init_renderer();
1202
+ init_textured_helpers();
1203
+ init_webgl();
1204
+ VERTEX_SHADER2 = `#version 300 es
1205
+ in vec3 aPos;
1206
+ in vec2 aUV;
1207
+ in vec3 aColor;
1208
+ uniform mat4 uViewProj;
1209
+ uniform mat4 uModel;
1210
+ out vec2 vUV;
1211
+ out vec3 vColor;
1212
+ void main() {
1213
+ vUV = aUV;
1214
+ vColor = aColor;
1215
+ gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
1216
+ }`;
1217
+ FRAGMENT_SHADER2 = `#version 300 es
1218
+ precision mediump float;
1219
+ in vec2 vUV;
1220
+ in vec3 vColor;
1221
+ uniform sampler2D uTexture;
1222
+ uniform float uOpacity;
1223
+ out vec4 fragColor;
1224
+ void main() {
1225
+ vec4 t = texture(uTexture, vUV);
1226
+ fragColor = vec4(t.rgb * vColor, t.a * uOpacity);
1227
+ }`;
1228
+ WebGLTexturedRenderer = class {
1229
+ constructor(options = {}) {
1230
+ this.sources = /* @__PURE__ */ new Map();
1231
+ this.textures = /* @__PURE__ */ new Map();
1232
+ this.buffers = /* @__PURE__ */ new Map();
1233
+ this.inner = new WebGLRenderer(options);
1234
+ }
1235
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
1236
+ setTexture(mesh, source) {
1237
+ this.sources.set(mesh, source);
1238
+ }
1239
+ init(canvas) {
1240
+ this.inner.init(canvas);
1241
+ const gl = canvas.getContext("webgl2");
1242
+ this.gl = gl;
1243
+ this.program = link2(gl);
1244
+ this.locations = {
1245
+ aPos: gl.getAttribLocation(this.program, "aPos"),
1246
+ aUV: gl.getAttribLocation(this.program, "aUV"),
1247
+ aColor: gl.getAttribLocation(this.program, "aColor"),
1248
+ uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
1249
+ uModel: gl.getUniformLocation(this.program, "uModel"),
1250
+ uTexture: gl.getUniformLocation(this.program, "uTexture"),
1251
+ uOpacity: gl.getUniformLocation(this.program, "uOpacity")
1252
+ };
1253
+ }
1254
+ resize() {
1255
+ this.inner.resize();
1256
+ }
1257
+ textureFor(mesh) {
1258
+ const cached = this.textures.get(mesh);
1259
+ if (cached) return cached;
1260
+ const gl = this.gl;
1261
+ const texture = gl.createTexture();
1262
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1263
+ gl.texImage2D(
1264
+ gl.TEXTURE_2D,
1265
+ 0,
1266
+ gl.RGBA,
1267
+ 1,
1268
+ 1,
1269
+ 0,
1270
+ gl.RGBA,
1271
+ gl.UNSIGNED_BYTE,
1272
+ new Uint8Array([255, 255, 255, 255])
1273
+ );
1274
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1275
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1276
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1277
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1278
+ this.textures.set(mesh, texture);
1279
+ const upload = (image) => {
1280
+ if (!this.gl || this.textures.get(mesh) !== texture) return;
1281
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
1282
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image);
1283
+ };
1284
+ const source = this.sources.get(mesh);
1285
+ if (typeof source === "string") {
1286
+ const image = new Image();
1287
+ image.onload = () => upload(image);
1288
+ image.src = source;
1289
+ } else {
1290
+ upload(source);
1291
+ }
1292
+ return texture;
1293
+ }
1294
+ buffersFor(mesh) {
1295
+ const cached = this.buffers.get(mesh);
1296
+ if (cached) return cached;
1297
+ const gl = this.gl;
1298
+ const loc = this.locations;
1299
+ const data = expandToTriangles(mesh);
1300
+ const vao = gl.createVertexArray();
1301
+ gl.bindVertexArray(vao);
1302
+ const attribute = (location, array, size) => {
1303
+ if (location < 0) return;
1304
+ const buffer = gl.createBuffer();
1305
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1306
+ gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
1307
+ gl.enableVertexAttribArray(location);
1308
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
1309
+ };
1310
+ attribute(loc.aPos, data.positions, 3);
1311
+ attribute(loc.aColor, data.colors, 3);
1312
+ attribute(loc.aUV, planarUVs(mesh), 2);
1313
+ gl.bindVertexArray(null);
1314
+ const result = { vao, count: data.count };
1315
+ this.buffers.set(mesh, result);
1316
+ return result;
1317
+ }
1318
+ render(frame) {
1319
+ const plain = [];
1320
+ const textured = [];
1321
+ for (const item of frame.items) {
1322
+ (this.sources.has(item.mesh) ? textured : plain).push(item);
1323
+ }
1324
+ this.inner.render(textured.length ? { ...frame, items: plain } : frame);
1325
+ if (!textured.length) return;
1326
+ const gl = this.gl;
1327
+ const loc = this.locations;
1328
+ if (!gl || !this.program || !loc) return;
1329
+ gl.useProgram(this.program);
1330
+ gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
1331
+ gl.uniform1i(loc.uTexture, 0);
1332
+ gl.activeTexture(gl.TEXTURE0);
1333
+ gl.enable(gl.BLEND);
1334
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1335
+ gl.depthMask(false);
1336
+ for (const item of textured) {
1337
+ const buffers = this.buffersFor(item.mesh);
1338
+ gl.bindTexture(gl.TEXTURE_2D, this.textureFor(item.mesh));
1339
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
1340
+ gl.uniform1f(loc.uOpacity, itemOpacity2(item.transparency));
1341
+ gl.bindVertexArray(buffers.vao);
1342
+ gl.drawArrays(gl.TRIANGLES, 0, buffers.count);
1343
+ }
1344
+ gl.depthMask(true);
1345
+ gl.disable(gl.BLEND);
1346
+ gl.bindVertexArray(null);
1347
+ }
1348
+ destroy() {
1349
+ const gl = this.gl;
1350
+ if (gl) {
1351
+ for (const texture of this.textures.values()) gl.deleteTexture(texture);
1352
+ for (const buffers of this.buffers.values()) gl.deleteVertexArray(buffers.vao);
1353
+ if (this.program) gl.deleteProgram(this.program);
1354
+ }
1355
+ this.textures.clear();
1356
+ this.buffers.clear();
1357
+ this.sources.clear();
1358
+ this.gl = void 0;
1359
+ this.program = void 0;
1360
+ this.locations = void 0;
1361
+ this.inner.destroy();
1362
+ }
1363
+ };
1364
+ }
1365
+ });
1366
+
1367
+ // src/engines/little-3d-engine/renderers/canvas2d-textured.ts
1368
+ var canvas2d_textured_exports = {};
1369
+ __export(canvas2d_textured_exports, {
1370
+ Canvas2DTexturedRenderer: () => Canvas2DTexturedRenderer
1371
+ });
1372
+ function imageSize(source) {
1373
+ if (source instanceof HTMLImageElement) {
1374
+ return source.complete && source.naturalWidth > 0 ? { width: source.naturalWidth, height: source.naturalHeight } : void 0;
1375
+ }
1376
+ if (source instanceof HTMLVideoElement) {
1377
+ return source.readyState >= 2 ? { width: source.videoWidth, height: source.videoHeight } : void 0;
1378
+ }
1379
+ if (source instanceof SVGImageElement) {
1380
+ const width = source.width.baseVal.value;
1381
+ const height = source.height.baseVal.value;
1382
+ return width > 0 && height > 0 ? { width, height } : void 0;
1383
+ }
1384
+ if (typeof VideoFrame !== "undefined" && source instanceof VideoFrame) {
1385
+ return { width: source.displayWidth, height: source.displayHeight };
1386
+ }
1387
+ const sized = source;
1388
+ return sized.width > 0 && sized.height > 0 ? { width: sized.width, height: sized.height } : void 0;
1389
+ }
1390
+ function drawMappedTriangle(ctx, image, source, target) {
1391
+ const [s0, s1, s2] = source;
1392
+ const [d0, d1, d2] = target;
1393
+ const determinant = s0.x * (s1.y - s2.y) + s1.x * (s2.y - s0.y) + s2.x * (s0.y - s1.y);
1394
+ if (Math.abs(determinant) < 1e-8) return;
1395
+ const a = (d0.x * (s1.y - s2.y) + d1.x * (s2.y - s0.y) + d2.x * (s0.y - s1.y)) / determinant;
1396
+ const c = (d0.x * (s2.x - s1.x) + d1.x * (s0.x - s2.x) + d2.x * (s1.x - s0.x)) / determinant;
1397
+ const e = (d0.x * (s1.x * s2.y - s2.x * s1.y) + d1.x * (s2.x * s0.y - s0.x * s2.y) + d2.x * (s0.x * s1.y - s1.x * s0.y)) / determinant;
1398
+ const b = (d0.y * (s1.y - s2.y) + d1.y * (s2.y - s0.y) + d2.y * (s0.y - s1.y)) / determinant;
1399
+ const d = (d0.y * (s2.x - s1.x) + d1.y * (s0.x - s2.x) + d2.y * (s1.x - s0.x)) / determinant;
1400
+ const f = (d0.y * (s1.x * s2.y - s2.x * s1.y) + d1.y * (s2.x * s0.y - s0.x * s2.y) + d2.y * (s0.x * s1.y - s1.x * s0.y)) / determinant;
1401
+ ctx.save();
1402
+ ctx.beginPath();
1403
+ ctx.moveTo(d0.x, d0.y);
1404
+ ctx.lineTo(d1.x, d1.y);
1405
+ ctx.lineTo(d2.x, d2.y);
1406
+ ctx.closePath();
1407
+ ctx.clip();
1408
+ ctx.transform(a, b, c, d, e, f);
1409
+ ctx.drawImage(image, 0, 0);
1410
+ ctx.restore();
1411
+ }
1412
+ var Canvas2DTexturedRenderer;
1413
+ var init_canvas2d_textured = __esm({
1414
+ "src/engines/little-3d-engine/renderers/canvas2d-textured.ts"() {
1415
+ "use strict";
1416
+ init_math();
1417
+ init_renderer();
1418
+ init_canvas2d();
1419
+ Canvas2DTexturedRenderer = class {
1420
+ constructor(options = {}) {
1421
+ this.sources = /* @__PURE__ */ new Map();
1422
+ this.loaded = /* @__PURE__ */ new Map();
1423
+ this.dpr = 1;
1424
+ this.inner = new Canvas2DRenderer(options);
1425
+ }
1426
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
1427
+ setTexture(mesh, source) {
1428
+ this.sources.set(mesh, source);
1429
+ if (typeof source === "string" && !this.loaded.has(source)) {
1430
+ const image = new Image();
1431
+ image.src = source;
1432
+ this.loaded.set(source, image);
1433
+ }
1434
+ }
1435
+ init(canvas) {
1436
+ this.inner.init(canvas);
1437
+ this.ctx = canvas.getContext("2d") ?? void 0;
1438
+ }
1439
+ resize(cssWidth, cssHeight, dpr) {
1440
+ this.dpr = dpr;
1441
+ this.inner.resize(cssWidth, cssHeight, dpr);
1442
+ }
1443
+ drawable(mesh) {
1444
+ const source = this.sources.get(mesh);
1445
+ return typeof source === "string" ? this.loaded.get(source) : source;
1446
+ }
1447
+ render(frame) {
1448
+ const plain = frame.items.filter((item) => {
1449
+ if (!this.sources.has(item.mesh)) return true;
1450
+ const source = this.drawable(item.mesh);
1451
+ return !source || !imageSize(source);
1452
+ });
1453
+ this.inner.render({ ...frame, items: plain });
1454
+ const ctx = this.ctx;
1455
+ if (!ctx) return;
1456
+ ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1457
+ const tinted = /* @__PURE__ */ new Map();
1458
+ for (const item of frame.items) {
1459
+ const source = this.drawable(item.mesh);
1460
+ if (!source) continue;
1461
+ const size = imageSize(source);
1462
+ if (!size) continue;
1463
+ let image = tinted.get(item.mesh);
1464
+ if (!image) {
1465
+ image = document.createElement("canvas");
1466
+ image.width = size.width;
1467
+ image.height = size.height;
1468
+ const tint = image.getContext("2d");
1469
+ if (!tint) continue;
1470
+ tint.drawImage(source, 0, 0, size.width, size.height);
1471
+ tint.globalCompositeOperation = "source-in";
1472
+ tint.fillStyle = item.mesh.faces[0]?.color ?? "#ffffff";
1473
+ tint.fillRect(0, 0, size.width, size.height);
1474
+ tinted.set(item.mesh, image);
1475
+ }
1476
+ const world = item.mesh.vertices.map((vertex) => transformAffine(item.model, vertex));
1477
+ const projected = world.map((vertex) => {
1478
+ const ndc = transformPoint(frame.viewProjection, vertex);
1479
+ return { x: (ndc.x * 0.5 + 0.5) * frame.width, y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height };
1480
+ });
1481
+ const face = item.mesh.faces[0];
1482
+ if (!face || face.indices.length !== 4) continue;
1483
+ const [a, b, c, d] = face.indices.map((index) => projected[index]);
1484
+ ctx.globalAlpha = item.transparency?.mode === "one-sided" ? opacity(item.transparency.opacity, DEFAULT_ONE_SIDED_OPACITY) : 1;
1485
+ drawMappedTriangle(ctx, image, [{ x: 0, y: size.height }, { x: size.width, y: size.height }, { x: size.width, y: 0 }], [a, b, c]);
1486
+ drawMappedTriangle(ctx, image, [{ x: 0, y: size.height }, { x: size.width, y: 0 }, { x: 0, y: 0 }], [a, c, d]);
1487
+ }
1488
+ ctx.globalAlpha = 1;
1489
+ }
1490
+ destroy() {
1491
+ this.inner.destroy();
1492
+ this.ctx = void 0;
1493
+ this.sources.clear();
1494
+ this.loaded.clear();
1495
+ }
1496
+ };
1497
+ }
1498
+ });
1499
+
848
1500
  // <stdin>
849
1501
  var stdin_exports = {};
850
1502
  __export(stdin_exports, {
851
1503
  Camera: () => Camera,
1504
+ ChargedOrbAnimation: () => ChargedOrbAnimation,
1505
+ CompositeAnimation: () => CompositeAnimation,
1506
+ GridAssemblyAnimation: () => GridAssemblyAnimation,
852
1507
  Light: () => Light,
853
1508
  Little3dEngine: () => Little3dEngine,
854
1509
  LittleTweenEngine: () => LittleTweenEngine,
855
1510
  ObjectMotionAnimation: () => ObjectMotionAnimation,
1511
+ ParticlesAnimation: () => ParticlesAnimation,
856
1512
  SpinAnimation: () => SpinAnimation,
1513
+ WebGLTexturedRenderer: () => WebGLTexturedRenderer,
1514
+ WebGPUTexturedRenderer: () => WebGPUTexturedRenderer,
857
1515
  centerAndScaleMesh: () => centerAndScaleMesh,
1516
+ chargedOrb: () => chargedOrb,
858
1517
  circleMotion: () => circleMotion,
859
1518
  createSpinner: () => createSpinner,
860
1519
  cross: () => cross,
1520
+ crystalComet: () => crystalComet,
861
1521
  cube: () => cube,
862
1522
  cubeSphere: () => cubeSphere,
863
1523
  cubic: () => cubic,
@@ -897,22 +1557,35 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
897
1557
  enterFromObjectDirection: () => enterFromObjectDirection,
898
1558
  expandToTriangles: () => expandToTriangles,
899
1559
  figureEightMotion: () => figureEightMotion,
1560
+ ghostTrain: () => ghostTrain,
1561
+ gridAssembly: () => gridAssembly,
900
1562
  grow: () => grow,
901
1563
  icosphere: () => icosphere,
902
1564
  leaveInObjectDirection: () => leaveInObjectDirection,
903
1565
  linear: () => linear,
1566
+ monochromeStreak: () => monochromeStreak,
904
1567
  normalize: () => normalize,
905
1568
  octaSphere: () => octaSphere,
906
1569
  octahedron: () => octahedron,
907
1570
  orderRenderItems: () => orderRenderItems,
908
1571
  parseObj: () => parseObj,
1572
+ particleField: () => particleField,
1573
+ planeMesh: () => planeMesh,
1574
+ planeStarTrail: () => planeStarTrail,
1575
+ pulsingStarfield: () => pulsingStarfield,
909
1576
  pyramid: () => pyramid,
1577
+ quad: () => quad,
910
1578
  quadratic: () => quadratic,
911
1579
  quartic: () => quartic,
912
1580
  quintic: () => quintic,
1581
+ rocketLaunch: () => rocketLaunch,
913
1582
  scale: () => scale,
1583
+ shineTexture: () => shineTexture,
914
1584
  shrink: () => shrink,
915
1585
  squareMotion: () => squareMotion,
1586
+ starSwarm: () => starSwarm,
1587
+ starTexture: () => starTexture,
1588
+ streakTexture: () => streakTexture,
916
1589
  subtract: () => subtract,
917
1590
  tetrahedron: () => tetrahedron,
918
1591
  transform: () => transform,
@@ -1068,7 +1741,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1068
1741
  init_renderer();
1069
1742
  init_light();
1070
1743
 
1071
- // src/engines/little-3d-engine/shapes/cube.ts
1744
+ // src/engines/little-3d-engine/shapes/primitives/cube.ts
1072
1745
  var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1073
1746
  function cube(size = 1, colors = DEFAULT_COLORS) {
1074
1747
  const h = size / 2;
@@ -1093,9 +1766,22 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1093
1766
  return { vertices, faces };
1094
1767
  }
1095
1768
 
1096
- // src/engines/little-3d-engine/shapes/tetrahedron.ts
1097
- var DEFAULT_COLORS2 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1098
- function tetrahedron(size = 1, colors = DEFAULT_COLORS2) {
1769
+ // src/engines/little-3d-engine/shapes/primitives/quad.ts
1770
+ var DEFAULT_COLORS2 = ["#3b82f6"];
1771
+ function quad(size = 1, colors = DEFAULT_COLORS2) {
1772
+ const s = size / 2;
1773
+ const vertices = [
1774
+ { x: -s, y: -s, z: 0 },
1775
+ { x: s, y: -s, z: 0 },
1776
+ { x: s, y: s, z: 0 },
1777
+ { x: -s, y: s, z: 0 }
1778
+ ];
1779
+ return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1780
+ }
1781
+
1782
+ // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1783
+ var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1784
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1099
1785
  const s = size / 2;
1100
1786
  const vertices = [
1101
1787
  { x: s, y: s, z: s },
@@ -1112,8 +1798,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1112
1798
  return { vertices, faces };
1113
1799
  }
1114
1800
 
1115
- // src/engines/little-3d-engine/shapes/octahedron.ts
1116
- var DEFAULT_COLORS3 = [
1801
+ // src/engines/little-3d-engine/shapes/primitives/octahedron.ts
1802
+ var DEFAULT_COLORS4 = [
1117
1803
  "#3b82f6",
1118
1804
  "#8b5cf6",
1119
1805
  "#ec4899",
@@ -1123,7 +1809,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1123
1809
  "#06b6d4",
1124
1810
  "#eab308"
1125
1811
  ];
1126
- function octahedron(size = 1, colors = DEFAULT_COLORS3) {
1812
+ function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1127
1813
  const r = size / 2;
1128
1814
  const vertices = [
1129
1815
  { x: r, y: 0, z: 0 },
@@ -1146,9 +1832,9 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1146
1832
  return { vertices, faces };
1147
1833
  }
1148
1834
 
1149
- // src/engines/little-3d-engine/shapes/pyramid.ts
1150
- var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1151
- function pyramid(size = 1, colors = DEFAULT_COLORS4) {
1835
+ // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1836
+ var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1837
+ function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1152
1838
  const h = size / 2;
1153
1839
  const vertices = [
1154
1840
  { x: -h, y: -h, z: h },
@@ -1167,9 +1853,9 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1167
1853
  return { vertices, faces };
1168
1854
  }
1169
1855
 
1170
- // src/engines/little-3d-engine/shapes/uv-sphere.ts
1171
- var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1172
- function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS5) {
1856
+ // src/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.ts
1857
+ var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1858
+ function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1173
1859
  const r = size / 2;
1174
1860
  const d = Math.max(1, Math.floor(detail));
1175
1861
  const slices = Math.max(4, d * 4);
@@ -1211,9 +1897,9 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1211
1897
  return { vertices, faces };
1212
1898
  }
1213
1899
 
1214
- // src/engines/little-3d-engine/shapes/icosphere.ts
1900
+ // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
1215
1901
  init_geometry();
1216
- var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1902
+ var DEFAULT_COLORS7 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1217
1903
  var T = (1 + Math.sqrt(5)) / 2;
1218
1904
  var SEED_VERTICES = [
1219
1905
  { x: -1, y: T, z: 0 },
@@ -1251,13 +1937,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1251
1937
  [8, 6, 7],
1252
1938
  [9, 8, 1]
1253
1939
  ];
1254
- function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1940
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
1255
1941
  return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1256
1942
  }
1257
1943
 
1258
- // src/engines/little-3d-engine/shapes/octa-sphere.ts
1944
+ // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
1259
1945
  init_geometry();
1260
- var DEFAULT_COLORS7 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1946
+ var DEFAULT_COLORS8 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1261
1947
  var SEED_VERTICES2 = [
1262
1948
  { x: 1, y: 0, z: 0 },
1263
1949
  { x: -1, y: 0, z: 0 },
@@ -1276,12 +1962,12 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1276
1962
  [5, 3, 1],
1277
1963
  [5, 0, 3]
1278
1964
  ];
1279
- function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
1965
+ function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
1280
1966
  return sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors);
1281
1967
  }
1282
1968
 
1283
- // src/engines/little-3d-engine/shapes/cube-sphere.ts
1284
- var DEFAULT_COLORS8 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1969
+ // src/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.ts
1970
+ var DEFAULT_COLORS9 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1285
1971
  var CUBE_FACES = [
1286
1972
  { normal: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
1287
1973
  { normal: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] },
@@ -1290,7 +1976,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1290
1976
  { normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
1291
1977
  { normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
1292
1978
  ];
1293
- function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
1979
+ function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
1294
1980
  const r = size / 2;
1295
1981
  const n = Math.max(1, Math.floor(detail));
1296
1982
  const vertices = [];
@@ -1319,7 +2005,109 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1319
2005
  }
1320
2006
  }
1321
2007
  }
1322
- return { vertices, faces };
2008
+ return { vertices, faces };
2009
+ }
2010
+
2011
+ // src/engines/little-3d-engine/shapes/complex/plane.ts
2012
+ var DEFAULT_COLORS10 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
2013
+ function planeMesh(colors = DEFAULT_COLORS10) {
2014
+ return {
2015
+ vertices: [
2016
+ { x: 0.9, y: 0, z: 0 },
2017
+ { x: -0.2, y: 0, z: 0.82 },
2018
+ { x: -0.55, y: 0, z: 0.16 },
2019
+ { x: -0.72, y: 0, z: 0 },
2020
+ { x: -0.55, y: 0, z: -0.16 },
2021
+ { x: -0.2, y: 0, z: -0.82 },
2022
+ { x: -0.08, y: 0.12, z: 0 },
2023
+ { x: -0.08, y: -0.1, z: 0 },
2024
+ { x: -0.52, y: 0.38, z: 0 }
2025
+ ],
2026
+ faces: [
2027
+ { indices: [6, 1, 0], color: colors[0] ?? DEFAULT_COLORS10[0] },
2028
+ { indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS10[3] },
2029
+ { indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS10[1] },
2030
+ { indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS10[2] },
2031
+ { indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS10[3] },
2032
+ { indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS10[0] },
2033
+ { indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS10[1] },
2034
+ { indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS10[2] },
2035
+ { indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS10[1] },
2036
+ { indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS10[2] },
2037
+ { indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS10[1] },
2038
+ { indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS10[2] },
2039
+ { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS10[0] },
2040
+ { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS10[1] }
2041
+ ]
2042
+ };
2043
+ }
2044
+
2045
+ // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
2046
+ function canvasTexture(draw, size = 96) {
2047
+ const canvas = document.createElement("canvas");
2048
+ canvas.width = canvas.height = size;
2049
+ const ctx = canvas.getContext("2d");
2050
+ if (ctx) draw(ctx);
2051
+ return canvas;
2052
+ }
2053
+
2054
+ // src/engines/little-3d-engine/textures/dynamic/star.ts
2055
+ function drawStar(ctx) {
2056
+ ctx.save();
2057
+ ctx.translate(48, 48);
2058
+ ctx.fillStyle = "#fff";
2059
+ ctx.beginPath();
2060
+ for (let index = 0; index < 10; index++) {
2061
+ const radius = index % 2 === 0 ? 43 : 16;
2062
+ const angle = index * Math.PI / 5 - Math.PI / 2;
2063
+ ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
2064
+ }
2065
+ ctx.closePath();
2066
+ ctx.fill();
2067
+ ctx.restore();
2068
+ }
2069
+ function starTexture(options = {}) {
2070
+ const glow = Math.max(0, options.glow ?? 0);
2071
+ return canvasTexture((ctx) => {
2072
+ if (glow > 0) {
2073
+ ctx.save();
2074
+ ctx.filter = `blur(${glow}px)`;
2075
+ drawStar(ctx);
2076
+ ctx.restore();
2077
+ }
2078
+ drawStar(ctx);
2079
+ });
2080
+ }
2081
+
2082
+ // src/engines/little-3d-engine/textures/dynamic/shine.ts
2083
+ function shineTexture() {
2084
+ return canvasTexture((ctx) => {
2085
+ const halo = ctx.createRadialGradient(48, 48, 1, 48, 48, 46);
2086
+ halo.addColorStop(0, "rgba(255,255,255,1)");
2087
+ halo.addColorStop(0.08, "rgba(255,255,255,1)");
2088
+ halo.addColorStop(0.22, "rgba(210,240,255,0.7)");
2089
+ halo.addColorStop(0.55, "rgba(120,200,255,0.22)");
2090
+ halo.addColorStop(1, "rgba(80,160,255,0)");
2091
+ ctx.fillStyle = halo;
2092
+ ctx.fillRect(0, 0, 96, 96);
2093
+ });
2094
+ }
2095
+
2096
+ // src/engines/little-3d-engine/textures/dynamic/streak.ts
2097
+ function streakTexture() {
2098
+ return canvasTexture((ctx) => {
2099
+ const gradient = ctx.createLinearGradient(5, 0, 91, 0);
2100
+ gradient.addColorStop(0, "rgba(255,255,255,0)");
2101
+ gradient.addColorStop(0.7, "rgba(255,255,255,0.4)");
2102
+ gradient.addColorStop(1, "rgba(255,255,255,1)");
2103
+ ctx.strokeStyle = gradient;
2104
+ ctx.lineWidth = 3.5;
2105
+ ctx.lineCap = "round";
2106
+ ctx.beginPath();
2107
+ ctx.moveTo(5, 48);
2108
+ ctx.lineTo(91, 48);
2109
+ ctx.stroke();
2110
+ });
1323
2111
  }
1324
2112
 
1325
2113
  // src/engines/little-3d-engine/little-3d-engine.ts
@@ -1882,6 +2670,48 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1882
2670
  }
1883
2671
  };
1884
2672
 
2673
+ // src/animation-label.ts
2674
+ var LABEL_STYLE2 = [
2675
+ "position:absolute",
2676
+ "inset:0",
2677
+ "display:flex",
2678
+ "align-items:center",
2679
+ "justify-content:center",
2680
+ "pointer-events:none",
2681
+ "font:700 1.6rem/1 system-ui,sans-serif",
2682
+ "letter-spacing:0.02em",
2683
+ "color:rgba(255,255,255,0.9)",
2684
+ "text-shadow:0 1px 10px rgba(0,0,0,0.6)",
2685
+ "z-index:1"
2686
+ ].join(";");
2687
+ function animationLabelOpacity(now, enterAt, introDurationMs, exitAt, outroDurationMs) {
2688
+ if (enterAt === Infinity) return 0;
2689
+ const intro = introDurationMs <= 0 ? 1 : Math.max(0, Math.min(1, (now - enterAt) / introDurationMs));
2690
+ const outro = exitAt === Infinity ? 1 : outroDurationMs <= 0 ? 0 : Math.max(0, Math.min(1, 1 - (now - exitAt) / outroDurationMs));
2691
+ return Math.min(intro, outro);
2692
+ }
2693
+ function mountAnimationLabel(target, content) {
2694
+ var _a;
2695
+ const container = document.createElement("div");
2696
+ container.style.cssText = LABEL_STYLE2;
2697
+ container.setAttribute("role", "status");
2698
+ if (typeof content === "string") container.textContent = content;
2699
+ else if (content) {
2700
+ (_a = content.style).pointerEvents || (_a.pointerEvents = "auto");
2701
+ container.appendChild(content);
2702
+ }
2703
+ target.appendChild(container);
2704
+ return {
2705
+ container,
2706
+ setText(value) {
2707
+ if (typeof content !== "object") container.textContent = value;
2708
+ },
2709
+ setOpacity(value) {
2710
+ container.style.opacity = String(value);
2711
+ }
2712
+ };
2713
+ }
2714
+
1885
2715
  // src/animations/object-motion.ts
1886
2716
  init_math();
1887
2717
 
@@ -1940,19 +2770,6 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1940
2770
  }
1941
2771
 
1942
2772
  // src/animations/object-motion.ts
1943
- var LABEL_STYLE2 = [
1944
- "position:absolute",
1945
- "inset:0",
1946
- "display:flex",
1947
- "align-items:center",
1948
- "justify-content:center",
1949
- "pointer-events:none",
1950
- "font:700 1.6rem/1 system-ui,sans-serif",
1951
- "letter-spacing:0.02em",
1952
- "color:rgba(255,255,255,0.9)",
1953
- "text-shadow:0 1px 10px rgba(0,0,0,0.6)",
1954
- "z-index:1"
1955
- ].join(";");
1956
2773
  var WORLD_UP = { x: 0, y: 1, z: 0 };
1957
2774
  var DEFAULT_INTRO_MS = 2100;
1958
2775
  var DEFAULT_OUTRO_MS = 2100;
@@ -1975,6 +2792,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
1975
2792
  return typeof mesh === "function" ? mesh() : mesh;
1976
2793
  }
1977
2794
  function applyColor2(mesh, color) {
2795
+ if (color === void 0) return mesh;
1978
2796
  return { vertices: mesh.vertices, faces: mesh.faces.map((face) => ({ ...face, color })) };
1979
2797
  }
1980
2798
  function faceForward(mesh, facing) {
@@ -2076,15 +2894,16 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2076
2894
  this.outroDirection = { x: 1, y: 0, z: 0 };
2077
2895
  const centered = centerAndScaleMesh(resolveMesh2(options.mesh), options.size ?? 1);
2078
2896
  const facing = faceForward(centered, options.facing ?? "+x");
2079
- this.mesh = applyColor2(facing, options.color ?? "#cbd5e1");
2897
+ this.mesh = applyColor2(facing, options.color);
2080
2898
  this.motion = options.motion;
2081
2899
  this.backend = options.backend;
2082
2900
  this.transparency = options.transparency;
2083
- this.labelText = options.label;
2901
+ this.labelContent = options.label;
2902
+ this.fadeLabel = options.fadeLabel ?? true;
2084
2903
  this.tailCount = Math.max(0, Math.floor(options.tail?.count ?? 0));
2085
2904
  this.tailGap = Math.max(0, options.tail?.gapMs ?? 0);
2086
- this.intro = resolveTransition(options.intro, grow(), DEFAULT_INTRO_MS);
2087
- this.outro = resolveTransition(options.outro, shrink(), DEFAULT_OUTRO_MS);
2905
+ this.intro = resolveTransition(options.intro, enterFromObjectDirection(), DEFAULT_INTRO_MS);
2906
+ this.outro = resolveTransition(options.outro, leaveInObjectDirection(), DEFAULT_OUTRO_MS);
2088
2907
  const rotation = options.rotation;
2089
2908
  this.rotationOffset = { x: rotation?.x ?? 0, y: rotation?.y ?? 0, z: rotation?.z ?? 0 };
2090
2909
  this.rotationSpin = {
@@ -2095,7 +2914,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2095
2914
  this.hasExtraRotation = this.rotationOffset.x !== 0 || this.rotationOffset.y !== 0 || this.rotationOffset.z !== 0 || this.rotationSpin.x !== 0 || this.rotationSpin.y !== 0 || this.rotationSpin.z !== 0;
2096
2915
  }
2097
2916
  mount(target) {
2098
- target.style.position = "relative";
2917
+ if (!target.style.position) target.style.position = "relative";
2099
2918
  const engine = new Little3dEngine({
2100
2919
  backend: this.backend,
2101
2920
  camera: { position: { x: 0, y: 0, z: 3 } }
@@ -2109,11 +2928,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2109
2928
  engine.mount(target).catch((error) => {
2110
2929
  target.textContent = error instanceof Error ? error.message : String(error);
2111
2930
  });
2112
- const label = document.createElement("div");
2113
- label.style.cssText = LABEL_STYLE2;
2114
- label.setAttribute("role", "status");
2115
- target.appendChild(label);
2116
- this.label = label;
2931
+ this.label = mountAnimationLabel(target, this.labelContent);
2932
+ if (this.fadeLabel) this.label.setOpacity(0);
2117
2933
  }
2118
2934
  enter(now) {
2119
2935
  if (this.started) return;
@@ -2130,6 +2946,19 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2130
2946
  isFinished() {
2131
2947
  return this.finished;
2132
2948
  }
2949
+ /** Milliseconds the fly-out takes; used to align a following particle trail's outro. */
2950
+ get outroDurationMs() {
2951
+ return this.outro.durationMs;
2952
+ }
2953
+ /**
2954
+ * A {@link MotionController} that follows the object's *actual* position, including
2955
+ * the intro fly-in and outro fly-out (it falls back to the raw motion path before
2956
+ * {@link enter} and once idle). Feed it to a particle layer's `emitter` so the
2957
+ * particles trail the object through its transitions instead of the bare path.
2958
+ */
2959
+ trailEmitter() {
2960
+ return { positionAt: (t) => this.sampleAt(t)?.position ?? this.motion.positionAt(t) };
2961
+ }
2133
2962
  render(now, frame) {
2134
2963
  if (!this.engine || !this.label) return;
2135
2964
  if (this.outroStart !== Infinity && now >= this.outroStart + this.outro.durationMs + this.tailCount * this.tailGap) {
@@ -2172,11 +3001,20 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2172
3001
  transform2.rotation.y = euler.y;
2173
3002
  transform2.rotation.z = euler.z;
2174
3003
  }
2175
- this.label.textContent = frame.indeterminate ? this.labelText ?? "" : `${Math.round(frame.progress * 100)}%`;
3004
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
3005
+ if (this.fadeLabel) {
3006
+ this.label.setOpacity(animationLabelOpacity(
3007
+ now,
3008
+ this.started ? this.introStart : Infinity,
3009
+ this.intro.durationMs,
3010
+ this.outroStart,
3011
+ this.outro.durationMs
3012
+ ));
3013
+ }
2176
3014
  this.engine.render();
2177
3015
  }
2178
3016
  destroy() {
2179
- this.label?.remove();
3017
+ this.label?.container.remove();
2180
3018
  this.label = void 0;
2181
3019
  this.engine?.destroy();
2182
3020
  this.engine = void 0;
@@ -2209,7 +3047,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2209
3047
  const delta = transition.durationMs === 0 ? 1 : clamp013(elapsedMs / transition.durationMs);
2210
3048
  const input2 = this.transitionInput(phase, delta, elapsedMs, transition.durationMs, start);
2211
3049
  const output = transition.transition(input2);
2212
- return this.applyTransitionOutput(input2, output);
3050
+ return this.applyTransitionOutput(input2, output, phase, t);
2213
3051
  }
2214
3052
  transitionInput(phase, delta, elapsedMs, durationMs, start) {
2215
3053
  if (phase === "intro") {
@@ -2237,33 +3075,790 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2237
3075
  phase
2238
3076
  };
2239
3077
  }
2240
- applyTransitionOutput(input2, output) {
2241
- return {
2242
- position: output.position ?? input2.position,
2243
- size: output.size ?? input2.size ?? 1,
2244
- orientation: output.orientation
2245
- };
3078
+ applyTransitionOutput(input2, output, phase, t) {
3079
+ return {
3080
+ position: output.position ?? (phase === "intro" ? this.motion.positionAt(t) : input2.position),
3081
+ size: output.size ?? input2.size ?? 1,
3082
+ orientation: output.orientation
3083
+ };
3084
+ }
3085
+ };
3086
+
3087
+ // src/animations/particles.ts
3088
+ var DEFAULT_COLORS11 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
3089
+ var FADE_IN_END = 0.15;
3090
+ var FADE_OUT_START = 0.6;
3091
+ function rand01(seed, index, salt) {
3092
+ let h = (seed ^ Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
3093
+ h = Math.imul(h ^ h >>> 16, 73244475);
3094
+ h = Math.imul(h ^ h >>> 16, 73244475);
3095
+ h ^= h >>> 16;
3096
+ return (h >>> 0) / 4294967296;
3097
+ }
3098
+ function smoothstep(edge0, edge1, value) {
3099
+ const x = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
3100
+ return x * x * (3 - 2 * x);
3101
+ }
3102
+ function positiveFinite(value, name) {
3103
+ if (!Number.isFinite(value) || value <= 0) {
3104
+ throw new RangeError(`3d-spinner: ${name} must be a finite number greater than zero.`);
3105
+ }
3106
+ return value;
3107
+ }
3108
+ function emitBasis(direction) {
3109
+ const d = normalize(direction);
3110
+ const helper = Math.abs(d.y) < 0.99 ? { x: 0, y: 1, z: 0 } : { x: 1, y: 0, z: 0 };
3111
+ const right = normalize(cross(helper, d));
3112
+ return { d, right, up: cross(d, right) };
3113
+ }
3114
+ function particleField(options = {}) {
3115
+ const rate = positiveFinite(options.rate ?? 20, "rate");
3116
+ const lifeMs = positiveFinite(options.lifeMs ?? 1800, "lifeMs");
3117
+ const size = options.size ?? 0.16;
3118
+ const speed = options.speed ?? 0.6;
3119
+ const gravity = options.gravity;
3120
+ const spread = options.spread ?? 0.5;
3121
+ const peak = Math.max(0, Math.min(1, options.opacity ?? 0.9));
3122
+ const spin = options.spin ?? 2e-3;
3123
+ const alignToMotion = options.alignToMotion ?? false;
3124
+ const seed = options.seed ?? 1;
3125
+ const basis = options.direction && emitBasis(options.direction);
3126
+ const spawnGapMs = 1e3 / rate;
3127
+ const directionOf = (index) => {
3128
+ const u = rand01(seed, index, 0);
3129
+ const phi = 2 * Math.PI * rand01(seed, index, 1);
3130
+ if (!basis) {
3131
+ const z = 2 * u - 1;
3132
+ const r = Math.sqrt(Math.max(0, 1 - z * z));
3133
+ return { x: r * Math.cos(phi), y: r * Math.sin(phi), z };
3134
+ }
3135
+ const cos = 1 - u * (1 - Math.cos(spread));
3136
+ const sin = Math.sqrt(Math.max(0, 1 - cos * cos));
3137
+ const { d, right, up } = basis;
3138
+ return {
3139
+ x: d.x * cos + (right.x * Math.cos(phi) + up.x * Math.sin(phi)) * sin,
3140
+ y: d.y * cos + (right.y * Math.cos(phi) + up.y * Math.sin(phi)) * sin,
3141
+ z: d.z * cos + (right.z * Math.cos(phi) + up.z * Math.sin(phi)) * sin
3142
+ };
3143
+ };
3144
+ return {
3145
+ maxLive: Math.ceil(lifeMs * rate / 1e3) + 1,
3146
+ spawnGapMs,
3147
+ lifeMs,
3148
+ sample(index, t) {
3149
+ if (index < 0) return void 0;
3150
+ const age = t - index * spawnGapMs;
3151
+ if (age < 0 || age >= lifeMs) return void 0;
3152
+ const seconds = age / 1e3;
3153
+ const dir = directionOf(index);
3154
+ const particleSpeed = speed * (0.6 + 0.8 * rand01(seed, index, 2));
3155
+ const travel = particleSpeed * seconds;
3156
+ const pull = gravity ? 0.5 * seconds * seconds : 0;
3157
+ const life = age / lifeMs;
3158
+ const roll = alignToMotion ? Math.atan2(
3159
+ dir.y * particleSpeed + (gravity?.y ?? 0) * seconds,
3160
+ dir.x * particleSpeed + (gravity?.x ?? 0) * seconds
3161
+ ) : 2 * Math.PI * rand01(seed, index, 3) + (2 * rand01(seed, index, 4) - 1) * spin * age;
3162
+ return {
3163
+ position: {
3164
+ x: dir.x * travel + (gravity ? gravity.x * pull : 0),
3165
+ y: dir.y * travel + (gravity ? gravity.y * pull : 0),
3166
+ z: dir.z * travel + (gravity ? gravity.z * pull : 0)
3167
+ },
3168
+ roll,
3169
+ size: size * (0.7 + 0.6 * rand01(seed, index, 5)),
3170
+ opacity: peak * smoothstep(0, FADE_IN_END, life) * (1 - smoothstep(FADE_OUT_START, 1, life))
3171
+ };
3172
+ }
3173
+ };
3174
+ }
3175
+ var ParticlesAnimation = class {
3176
+ constructor(options = {}) {
3177
+ this.handles = [];
3178
+ this.fades = [];
3179
+ this.enterAt = Infinity;
3180
+ this.exitAt = Infinity;
3181
+ this.finished = false;
3182
+ this.field = particleField(options);
3183
+ this.colors = options.colors ?? DEFAULT_COLORS11;
3184
+ this.backend = options.backend;
3185
+ this.texture = options.texture;
3186
+ this.labelContent = options.label;
3187
+ this.fadeLabel = options.fadeLabel ?? true;
3188
+ this.emitter = options.emitter;
3189
+ this.outroMs = Math.max(0, options.outroMs ?? 0);
3190
+ }
3191
+ mount(target) {
3192
+ if (!target.style.position) target.style.position = "relative";
3193
+ const meshes = this.colors.map((color) => quad(1, [color]));
3194
+ const texture = this.texture;
3195
+ const backend = texture ? async (rendererOptions) => {
3196
+ 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);
3197
+ for (const mesh of meshes) renderer.setTexture(mesh, texture);
3198
+ return renderer;
3199
+ } : this.backend;
3200
+ const engine = new Little3dEngine({
3201
+ backend,
3202
+ camera: { position: { x: 0, y: 0, z: 3 } },
3203
+ light: { intensity: 0, ambient: 1 }
3204
+ });
3205
+ for (let slot = 0; slot < this.field.maxLive; slot++) {
3206
+ const fade = { mode: "one-sided", opacity: 0 };
3207
+ this.fades.push(fade);
3208
+ this.handles.push(engine.add(meshes[slot % meshes.length], { scale: 0, transparency: fade }));
3209
+ }
3210
+ this.engine = engine;
3211
+ engine.mount(target).catch((error) => {
3212
+ target.textContent = error instanceof Error ? error.message : String(error);
3213
+ });
3214
+ this.label = mountAnimationLabel(target, this.labelContent);
3215
+ if (this.fadeLabel) this.label.setOpacity(0);
3216
+ }
3217
+ enter(now) {
3218
+ if (this.enterAt === Infinity) this.enterAt = now;
3219
+ }
3220
+ exit(now) {
3221
+ if (this.exitAt === Infinity) this.exitAt = now;
3222
+ }
3223
+ isFinished() {
3224
+ return this.finished;
3225
+ }
3226
+ render(now, frame) {
3227
+ if (!this.engine || !this.label) return;
3228
+ if (this.exitAt !== Infinity && now >= this.exitAt + this.outroMs + this.field.lifeMs) this.finished = true;
3229
+ for (const handle of this.handles) handle.transform.scale = 0;
3230
+ if (this.enterAt !== Infinity) {
3231
+ const t = now - this.enterAt;
3232
+ const gap = this.field.spawnGapMs;
3233
+ let first = Math.max(0, Math.ceil((t - this.field.lifeMs) / gap));
3234
+ let last = Math.floor(t / gap);
3235
+ if (this.exitAt !== Infinity) {
3236
+ last = Math.min(last, Math.floor((this.exitAt - this.enterAt + this.outroMs) / gap));
3237
+ }
3238
+ first = Math.max(first, last - this.field.maxLive + 1);
3239
+ for (let index = first; index <= last; index++) {
3240
+ const sample = this.field.sample(index, t);
3241
+ if (!sample) continue;
3242
+ const slot = index % this.handles.length;
3243
+ const transform2 = this.handles[slot].transform;
3244
+ const origin = this.emitter?.positionAt(this.enterAt + index * gap);
3245
+ transform2.position.x = sample.position.x + (origin?.x ?? 0);
3246
+ transform2.position.y = sample.position.y + (origin?.y ?? 0);
3247
+ transform2.position.z = sample.position.z + (origin?.z ?? 0);
3248
+ transform2.rotation.z = sample.roll;
3249
+ transform2.scale = sample.size;
3250
+ this.fades[slot].opacity = sample.opacity;
3251
+ }
3252
+ }
3253
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
3254
+ if (this.fadeLabel) {
3255
+ this.label.setOpacity(animationLabelOpacity(
3256
+ now,
3257
+ this.enterAt,
3258
+ this.field.lifeMs * FADE_IN_END,
3259
+ this.exitAt,
3260
+ this.field.lifeMs
3261
+ ));
3262
+ }
3263
+ this.engine.render();
3264
+ }
3265
+ destroy() {
3266
+ this.label?.container.remove();
3267
+ this.label = void 0;
3268
+ this.engine?.destroy();
3269
+ this.engine = void 0;
3270
+ this.handles.length = 0;
3271
+ this.fades.length = 0;
3272
+ }
3273
+ };
3274
+
3275
+ // src/animations/charged-orb.ts
3276
+ var MINIS = 10;
3277
+ var CAMERA_Z = 3;
3278
+ var CENTER_SCALE = 0.76;
3279
+ var MINI_SCALE = 0.36;
3280
+ var MINI_TRANSPARENCY = { mode: "two-sided", frontOpacity: 0.68, backOpacity: 0.87 };
3281
+ var ORBIT_RADIUS = 1.2;
3282
+ var TILT = 0.8;
3283
+ var TWO_PI = Math.PI * 2;
3284
+ var CENTER_POP_MS = 500;
3285
+ var LAUNCH_MS = 550;
3286
+ var EXIT_HURRY = 2.5;
3287
+ var SPREAD_TAU_MS = 250;
3288
+ var EXTRA_PAUSE_MS = 250;
3289
+ var EXTRA_SPIN_MS = 1300;
3290
+ var REENTER_MS = 600;
3291
+ var REENTER_STAGGER_MS = 45;
3292
+ var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
3293
+ var CENTER_POP_OUT_MS = 420;
3294
+ var PARKED = { x: 0, y: 0, z: 50 };
3295
+ var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
3296
+ var MINI_COLORS = [
3297
+ ["#e0f2fe", "#bae6fd", "#7dd3fc"],
3298
+ ["#c7d2fe", "#a5b4fc", "#818cf8"],
3299
+ ["#a5f3fc", "#67e8f9", "#22d3ee"]
3300
+ ];
3301
+ function clamp014(value) {
3302
+ return Math.max(0, Math.min(1, value));
3303
+ }
3304
+ var ChargedOrbAnimation = class {
3305
+ constructor(options = {}) {
3306
+ this.minis = [];
3307
+ this.blends = new Array(MINIS).fill(0);
3308
+ this.offsets = new Array(MINIS).fill(0);
3309
+ this.enterAt = Infinity;
3310
+ this.exitAt = Infinity;
3311
+ this.allOutAt = Infinity;
3312
+ this.lastNow = 0;
3313
+ this.finished = false;
3314
+ this.orbitPeriodMs = options.orbitPeriodMs ?? 6e3;
3315
+ this.backend = options.backend;
3316
+ }
3317
+ mount(target) {
3318
+ if (!target.style.position) target.style.position = "relative";
3319
+ const engine = new Little3dEngine({
3320
+ backend: this.backend,
3321
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
3322
+ });
3323
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
3324
+ for (let i = 0; i < MINIS; i++) {
3325
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
3326
+ this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
3327
+ }
3328
+ this.engine = engine;
3329
+ engine.mount(target).catch((error) => {
3330
+ target.textContent = error instanceof Error ? error.message : String(error);
3331
+ });
3332
+ }
3333
+ enter(now) {
3334
+ if (this.enterAt === Infinity) this.enterAt = now;
3335
+ }
3336
+ exit(now) {
3337
+ if (this.exitAt === Infinity) this.exitAt = now;
3338
+ }
3339
+ isFinished() {
3340
+ return this.finished;
3341
+ }
3342
+ /** Milliseconds after {@link exit} during which the satellites are still flying. */
3343
+ get outroEmitMs() {
3344
+ return EXTRA_PAUSE_MS + EXTRA_SPIN_MS + CENTER_POP_OUT_AT;
3345
+ }
3346
+ /**
3347
+ * A {@link MotionController} that cycles across the live satellites, one
3348
+ * spawn slot per orb, so a particle layer emits one stream per satellite.
3349
+ * `spawnGapMs` must match the particle layer's emission gap (`1000 / rate`).
3350
+ */
3351
+ satelliteEmitter(spawnGapMs) {
3352
+ return {
3353
+ positionAt: (t) => {
3354
+ const live = [];
3355
+ for (let i = 0; i < MINIS; i++) {
3356
+ const sample = this.miniSample(i, t);
3357
+ if (sample) live.push(sample.position);
3358
+ }
3359
+ if (live.length === 0) return PARKED;
3360
+ const slot = Math.abs(Math.floor(t / spawnGapMs)) % live.length;
3361
+ return live[slot];
3362
+ }
3363
+ };
3364
+ }
3365
+ render(now, frame) {
3366
+ if (!this.engine || !this.center) return;
3367
+ if (this.enterAt === Infinity) {
3368
+ this.center.transform.scale = 0;
3369
+ for (const mini of this.minis) mini.transform.scale = 0;
3370
+ this.engine.render();
3371
+ return;
3372
+ }
3373
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
3374
+ this.lastNow = now;
3375
+ this.updateBlends(dt, frame.progress, now);
3376
+ this.updateSpread(dt);
3377
+ const t = now - this.enterAt;
3378
+ this.center.transform.scale = this.centerScale(now, t);
3379
+ this.center.transform.rotation.x = t * 2e-4;
3380
+ this.center.transform.rotation.y = t * 5e-4;
3381
+ for (let i = 0; i < MINIS; i++) {
3382
+ const transform2 = this.minis[i].transform;
3383
+ const sample = this.miniSample(i, now);
3384
+ if (!sample) {
3385
+ transform2.scale = 0;
3386
+ continue;
3387
+ }
3388
+ transform2.position.x = sample.position.x;
3389
+ transform2.position.y = sample.position.y;
3390
+ transform2.position.z = sample.position.z;
3391
+ transform2.scale = sample.scale;
3392
+ transform2.rotation.y = t * 12e-4;
3393
+ }
3394
+ this.engine.render();
3395
+ }
3396
+ destroy() {
3397
+ this.engine?.destroy();
3398
+ this.engine = void 0;
3399
+ this.center = void 0;
3400
+ this.minis.length = 0;
3401
+ }
3402
+ updateBlends(dt, progress, now) {
3403
+ const exiting = this.exitAt !== Infinity;
3404
+ const want = exiting ? MINIS : Math.min(MINIS, Math.floor(progress * MINIS + 1e-9));
3405
+ const rate = dt / LAUNCH_MS * (exiting ? EXIT_HURRY : 1);
3406
+ for (let i = 0; i < MINIS; i++) {
3407
+ const target = i < want ? 1 : 0;
3408
+ const blend = this.blends[i];
3409
+ if (target > blend && (i === 0 || this.blends[i - 1] >= 0.6)) {
3410
+ this.blends[i] = Math.min(1, blend + rate);
3411
+ if (blend === 0) this.offsets[i] = this.slotAngle(i);
3412
+ } else if (target < blend && (i === MINIS - 1 || this.blends[i + 1] <= 0.4)) {
3413
+ this.blends[i] = Math.max(0, blend - rate);
3414
+ }
3415
+ }
3416
+ if (exiting && this.allOutAt === Infinity && this.blends.every((blend) => blend >= 1)) {
3417
+ this.allOutAt = now;
3418
+ }
3419
+ }
3420
+ updateSpread(dt) {
3421
+ const ease2 = 1 - Math.exp(-dt / SPREAD_TAU_MS);
3422
+ for (let i = 0; i < MINIS; i++) {
3423
+ if (this.blends[i] <= 0) continue;
3424
+ this.offsets[i] += (this.slotAngle(i) - this.offsets[i]) * ease2;
3425
+ }
3426
+ }
3427
+ slotAngle(index) {
3428
+ const launched = Math.max(1, this.blends.filter((blend) => blend > 0).length);
3429
+ return TWO_PI * Math.min(index, launched - 1) / launched;
3430
+ }
3431
+ baseAngleAt(t) {
3432
+ let angle = -TWO_PI * (t - this.enterAt) / this.orbitPeriodMs;
3433
+ if (this.allOutAt !== Infinity) {
3434
+ const u = clamp014((t - this.allOutAt - EXTRA_PAUSE_MS) / EXTRA_SPIN_MS);
3435
+ angle -= TWO_PI * easeInOutCubic(u);
3436
+ }
3437
+ return angle;
3438
+ }
3439
+ reenterStart() {
3440
+ return this.allOutAt + EXTRA_PAUSE_MS + EXTRA_SPIN_MS;
3441
+ }
3442
+ miniSample(index, t) {
3443
+ const blend = this.blends[index];
3444
+ if (blend <= 0) return void 0;
3445
+ let radial = easeOutCubic(blend);
3446
+ let scale2 = MINI_SCALE * easeOutBack(blend);
3447
+ if (this.allOutAt !== Infinity) {
3448
+ const start = this.reenterStart() + index * REENTER_STAGGER_MS;
3449
+ const pull = easeInCubic(clamp014((t - start) / REENTER_MS));
3450
+ if (pull >= 1) return void 0;
3451
+ radial *= 1 - pull;
3452
+ scale2 *= 1 - pull;
3453
+ }
3454
+ const angle = this.baseAngleAt(t) + this.offsets[index];
3455
+ const flat = Math.sin(angle) * ORBIT_RADIUS * radial;
3456
+ return {
3457
+ position: {
3458
+ x: Math.cos(angle) * ORBIT_RADIUS * radial,
3459
+ y: flat * Math.cos(TILT),
3460
+ z: flat * Math.sin(TILT)
3461
+ },
3462
+ scale: scale2
3463
+ };
3464
+ }
3465
+ centerScale(now, t) {
3466
+ if (this.allOutAt !== Infinity) {
3467
+ const w = clamp014((now - this.reenterStart() - CENTER_POP_OUT_AT) / CENTER_POP_OUT_MS);
3468
+ if (w >= 1) {
3469
+ this.finished = true;
3470
+ return 0;
3471
+ }
3472
+ if (w > 0) {
3473
+ return CENTER_SCALE * (w < 0.35 ? 1 + 0.18 * easeOutQuad(w / 0.35) : 1.18 * (1 - easeInQuad((w - 0.35) / 0.65)));
3474
+ }
3475
+ }
3476
+ return CENTER_SCALE * easeOutBack(clamp014(t / CENTER_POP_MS));
3477
+ }
3478
+ };
3479
+
3480
+ // src/animations/grid-assembly.ts
3481
+ var GRID = 5;
3482
+ var COUNT = GRID * GRID;
3483
+ var CAMERA_Z2 = 4;
3484
+ var FOV = 55 * Math.PI / 180;
3485
+ var TWO_PI2 = Math.PI * 2;
3486
+ var INTRO_MS = 900;
3487
+ var INTRO_STAGGER_MS = 60;
3488
+ var INTRO_DONE_MS = (COUNT - 1) * INTRO_STAGGER_MS + INTRO_MS;
3489
+ var GATE_DOCK = 0.35;
3490
+ var GATE_UNDOCK = 0.65;
3491
+ var EXIT_HURRY2 = 2.5;
3492
+ var SPIN_EVERY_MS = 2e3;
3493
+ var SPIN_MS = 380;
3494
+ var SPIN_STAGGER_MS = 80;
3495
+ var HOLD_MS = 1e3;
3496
+ var COLLAPSE_MS = 700;
3497
+ var COLLAPSE_SPREAD_MS = 500;
3498
+ var POP_MS = 170;
3499
+ var LABEL_FADE_MS = 600;
3500
+ var CUBE_COLORS = ["#8397c6", "#7186b8", "#6176a8", "#93a6cf", "#556a9c", "#7a8fc0"];
3501
+ var DEFAULT_MESHES = [() => cube(1, CUBE_COLORS)];
3502
+ function clamp015(value) {
3503
+ return Math.max(0, Math.min(1, value));
3504
+ }
3505
+ function smooth01(value) {
3506
+ const x = clamp015(value);
3507
+ return x * x * (3 - 2 * x);
3508
+ }
3509
+ function resolveMesh3(mesh) {
3510
+ return typeof mesh === "function" ? mesh() : mesh;
3511
+ }
3512
+ function wrapAngle(angle) {
3513
+ const wrapped = angle - TWO_PI2 * Math.floor(angle / TWO_PI2);
3514
+ return wrapped > Math.PI ? wrapped - TWO_PI2 : wrapped;
3515
+ }
3516
+ function hash01(index, salt) {
3517
+ let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
3518
+ h = Math.imul(h ^ h >>> 16, 73244475);
3519
+ h ^= h >>> 16;
3520
+ return (h >>> 0) / 4294967296;
3521
+ }
3522
+ var GridAssemblyAnimation = class {
3523
+ constructor(options = {}) {
3524
+ this.handles = [];
3525
+ this.blends = new Array(COUNT).fill(0);
3526
+ this.dockedAt = new Array(COUNT).fill(Infinity);
3527
+ this.tumbleX = [];
3528
+ this.tumbleY = [];
3529
+ this.collapseDelay = [];
3530
+ this.popStarted = new Array(COUNT).fill(false);
3531
+ this.maxCollapseDelay = 0;
3532
+ this.fades = [];
3533
+ this.slots = [];
3534
+ this.aspect = 16 / 9;
3535
+ this.enterAt = Infinity;
3536
+ this.exitAt = Infinity;
3537
+ this.allDockedAt = Infinity;
3538
+ this.collapseAt = Infinity;
3539
+ this.lastNow = 0;
3540
+ this.finished = false;
3541
+ const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
3542
+ this.meshes = sources.map(resolveMesh3);
3543
+ this.size = options.size ?? 0.34;
3544
+ this.orbitPeriodMs = options.orbitPeriodMs ?? 9e3;
3545
+ this.dockMs = options.dockMs ?? 800;
3546
+ this.backend = options.backend;
3547
+ this.labelContent = options.label;
3548
+ this.fadeLabel = options.fadeLabel ?? true;
3549
+ const spacing = this.size + (options.gap ?? 0.12);
3550
+ for (let i = 0; i < COUNT; i++) {
3551
+ const row = Math.floor(i / GRID);
3552
+ const col = i % GRID;
3553
+ this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
3554
+ this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
3555
+ this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
3556
+ this.collapseDelay.push(hash01(i, 7) * COLLAPSE_SPREAD_MS);
3557
+ }
3558
+ this.maxCollapseDelay = Math.max(...this.collapseDelay);
3559
+ }
3560
+ mount(target) {
3561
+ if (!target.style.position) target.style.position = "relative";
3562
+ const engine = new Little3dEngine({
3563
+ backend: this.backend,
3564
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
3565
+ });
3566
+ for (let i = 0; i < COUNT; i++) {
3567
+ this.fades.push({ mode: "one-sided", opacity: 1 });
3568
+ this.handles.push(engine.add(this.meshes[i % this.meshes.length], { scale: 0 }));
3569
+ }
3570
+ this.engine = engine;
3571
+ engine.mount(target).catch((error) => {
3572
+ target.textContent = error instanceof Error ? error.message : String(error);
3573
+ });
3574
+ const measure = () => {
3575
+ if (target.clientWidth > 0 && target.clientHeight > 0) {
3576
+ this.aspect = target.clientWidth / target.clientHeight;
3577
+ }
3578
+ };
3579
+ measure();
3580
+ this.observer = new ResizeObserver(measure);
3581
+ this.observer.observe(target);
3582
+ this.label = mountAnimationLabel(target, this.labelContent);
3583
+ if (this.fadeLabel) this.label.setOpacity(0);
3584
+ }
3585
+ enter(now) {
3586
+ if (this.enterAt === Infinity) this.enterAt = now;
3587
+ }
3588
+ exit(now) {
3589
+ if (this.exitAt === Infinity) this.exitAt = now;
3590
+ }
3591
+ isFinished() {
3592
+ return this.finished;
3593
+ }
3594
+ render(now, frame) {
3595
+ if (!this.engine || !this.label) return;
3596
+ if (this.enterAt === Infinity) {
3597
+ for (const handle of this.handles) handle.transform.scale = 0;
3598
+ this.engine.render();
3599
+ return;
3600
+ }
3601
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
3602
+ this.lastNow = now;
3603
+ this.updateBlends(dt, frame.progress, now);
3604
+ if (this.exitAt !== Infinity && this.allDockedAt !== Infinity && this.collapseAt === Infinity) {
3605
+ this.collapseAt = Math.max(this.exitAt, this.allDockedAt) + HOLD_MS;
3606
+ }
3607
+ if (now >= this.collapseAt) this.renderCollapse(now);
3608
+ else this.renderStory(now, dt);
3609
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
3610
+ if (this.fadeLabel) {
3611
+ this.label.setOpacity(animationLabelOpacity(
3612
+ now,
3613
+ this.enterAt,
3614
+ LABEL_FADE_MS,
3615
+ this.collapseAt,
3616
+ COLLAPSE_MS
3617
+ ));
3618
+ }
3619
+ if (this.collapseAt !== Infinity && now >= this.collapseAt + this.maxCollapseDelay + COLLAPSE_MS + POP_MS) {
3620
+ this.finished = true;
3621
+ }
3622
+ this.engine.render();
3623
+ }
3624
+ destroy() {
3625
+ this.observer?.disconnect();
3626
+ this.observer = void 0;
3627
+ this.label?.container.remove();
3628
+ this.label = void 0;
3629
+ this.engine?.destroy();
3630
+ this.engine = void 0;
3631
+ this.handles.length = 0;
3632
+ this.fades.length = 0;
3633
+ }
3634
+ updateBlends(dt, progress, now) {
3635
+ const exiting = this.exitAt !== Infinity;
3636
+ const ringComplete = now - this.enterAt >= INTRO_DONE_MS;
3637
+ const want = !ringComplete ? 0 : exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
3638
+ const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
3639
+ for (let i = 0; i < COUNT; i++) {
3640
+ const target = i < want ? 1 : 0;
3641
+ const blend = this.blends[i];
3642
+ if (target > blend && (i === 0 || this.blends[i - 1] >= GATE_DOCK)) {
3643
+ this.blends[i] = Math.min(1, blend + rate);
3644
+ } else if (target < blend && (i === COUNT - 1 || this.blends[i + 1] <= GATE_UNDOCK)) {
3645
+ this.blends[i] = Math.max(0, blend - rate);
3646
+ }
3647
+ if (this.blends[i] >= 1) {
3648
+ if (this.dockedAt[i] === Infinity) this.dockedAt[i] = now;
3649
+ } else {
3650
+ this.dockedAt[i] = Infinity;
3651
+ }
3652
+ }
3653
+ const allDocked = want === COUNT && this.blends.every((blend) => blend >= 1);
3654
+ if (allDocked) {
3655
+ if (this.allDockedAt === Infinity) this.allDockedAt = now;
3656
+ } else if (!exiting) {
3657
+ this.allDockedAt = Infinity;
3658
+ }
3659
+ }
3660
+ renderStory(now, dt) {
3661
+ const t = now - this.enterAt;
3662
+ const halfHeight = Math.tan(FOV / 2) * CAMERA_Z2;
3663
+ const halfWidth = halfHeight * this.aspect;
3664
+ const radius = Math.max(0.6, Math.min(halfWidth, halfHeight) - this.size * 0.55);
3665
+ const spawnFactor = (Math.max(halfWidth, halfHeight) * 1.25 + this.size * 2) / radius;
3666
+ for (let i = 0; i < COUNT; i++) {
3667
+ const transform2 = this.handles[i].transform;
3668
+ const eased = smooth01(this.blends[i]);
3669
+ const introT = clamp015((t - i * INTRO_STAGGER_MS) / INTRO_MS);
3670
+ const reach = 1 + (spawnFactor - 1) * (1 - easeOutCubic(introT));
3671
+ const angle = Math.PI / 2 - i / COUNT * TWO_PI2 - t / this.orbitPeriodMs * TWO_PI2;
3672
+ const orbitX = Math.cos(angle) * radius * reach;
3673
+ const orbitY = Math.sin(angle) * radius * reach;
3674
+ const slot = this.slots[i];
3675
+ transform2.position.x = orbitX + (slot.x - orbitX) * eased;
3676
+ transform2.position.y = orbitY + (slot.y - orbitY) * eased;
3677
+ transform2.position.z = 0;
3678
+ transform2.scale = this.size;
3679
+ if (this.blends[i] === 0) {
3680
+ this.tumbleX[i] = wrapAngle(this.tumbleX[i] + (4e-4 + 8e-4 * hash01(i, 1)) * dt);
3681
+ this.tumbleY[i] = wrapAngle(this.tumbleY[i] + (6e-4 + 1e-3 * hash01(i, 3)) * dt);
3682
+ }
3683
+ transform2.rotation.z = 0;
3684
+ if (this.blends[i] >= 1) {
3685
+ const phase = (t + i * SPIN_STAGGER_MS) % SPIN_EVERY_MS;
3686
+ const spinning = phase < SPIN_MS && now - phase >= this.dockedAt[i];
3687
+ transform2.rotation.x = 0;
3688
+ transform2.rotation.y = spinning ? TWO_PI2 * easeInOutCubic(phase / SPIN_MS) : 0;
3689
+ } else {
3690
+ const free = 1 - eased;
3691
+ transform2.rotation.x = this.tumbleX[i] * free;
3692
+ transform2.rotation.y = this.tumbleY[i] * free;
3693
+ }
3694
+ }
3695
+ }
3696
+ renderCollapse(now) {
3697
+ if (!this.captured) {
3698
+ this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
3699
+ }
3700
+ for (let i = 0; i < COUNT; i++) {
3701
+ const transform2 = this.handles[i].transform;
3702
+ const from = this.captured[i];
3703
+ const local = now - this.collapseAt - this.collapseDelay[i];
3704
+ if (local <= 0) {
3705
+ transform2.position.x = from.x;
3706
+ transform2.position.y = from.y;
3707
+ transform2.position.z = from.z;
3708
+ transform2.scale = this.size;
3709
+ continue;
3710
+ }
3711
+ const pull = easeInCubic(clamp015(local / COLLAPSE_MS));
3712
+ transform2.position.x = from.x * (1 - pull);
3713
+ transform2.position.y = from.y * (1 - pull);
3714
+ transform2.position.z = from.z * (1 - pull);
3715
+ transform2.scale = this.size * (1 - 0.99 * pull);
3716
+ if (local >= COLLAPSE_MS) {
3717
+ if (!this.popStarted[i]) {
3718
+ this.popStarted[i] = true;
3719
+ this.handles[i].transparency = this.fades[i];
3720
+ }
3721
+ const v = clamp015((local - COLLAPSE_MS) / POP_MS);
3722
+ this.fades[i].opacity = 1 - v;
3723
+ transform2.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
3724
+ }
3725
+ }
3726
+ }
3727
+ };
3728
+
3729
+ // src/composite-animation.ts
3730
+ var CompositeAnimation = class {
3731
+ constructor(layers) {
3732
+ this.elements = [];
3733
+ this.layers = layers.map((layer) => "animation" in layer ? layer : { animation: layer });
3734
+ }
3735
+ mount(target) {
3736
+ target.style.position = "relative";
3737
+ for (const [index, layer] of this.layers.entries()) {
3738
+ const element = document.createElement("div");
3739
+ element.style.cssText = `position:absolute;inset:0;z-index:${layer.zIndex ?? index}`;
3740
+ target.appendChild(element);
3741
+ this.elements.push(element);
3742
+ layer.animation.mount(element);
3743
+ }
3744
+ }
3745
+ enter(now) {
3746
+ for (const layer of this.layers) layer.animation.enter(now);
3747
+ }
3748
+ exit(now) {
3749
+ for (const layer of this.layers) layer.animation.exit(now);
3750
+ }
3751
+ render(now, frame) {
3752
+ for (const layer of this.layers) layer.animation.render(now, frame);
3753
+ }
3754
+ isFinished() {
3755
+ return this.layers.every((layer) => layer.animation.isFinished());
3756
+ }
3757
+ destroy() {
3758
+ for (const layer of this.layers) layer.animation.destroy();
3759
+ for (const element of this.elements) element.remove();
3760
+ this.elements.length = 0;
2246
3761
  }
2247
3762
  };
2248
3763
 
2249
- // src/motion/circle.ts
2250
- function circleMotion(options = {}) {
2251
- const radius = options.radius ?? 1.3;
2252
- const periodMs = options.periodMs ?? 3e3;
2253
- const tilt = options.tilt ?? 0.5;
2254
- const direction = options.direction ?? 1;
2255
- const cosTilt = Math.cos(tilt);
2256
- const sinTilt = Math.sin(tilt);
3764
+ // src/prefabs/spinner.ts
3765
+ function spinner(animation, options) {
3766
+ return {
3767
+ type: "indeterminate",
3768
+ animation,
3769
+ loop: options.loop,
3770
+ periodMs: options.periodMs
3771
+ };
3772
+ }
3773
+ function progressSpinner(animation, options) {
3774
+ return {
3775
+ type: "progress",
3776
+ animation,
3777
+ progress: options.progress ?? 1e-3,
3778
+ timeout: options.timeout,
3779
+ until: options.until
3780
+ };
3781
+ }
3782
+
3783
+ // src/prefabs/charged-orb.ts
3784
+ function chargedOrb(options = {}) {
3785
+ const particles = options.particles ?? {};
3786
+ const rate = particles.rate ?? 60;
3787
+ const orb = new ChargedOrbAnimation({
3788
+ backend: options.backend,
3789
+ ...options.orb
3790
+ });
3791
+ const streams = new ParticlesAnimation({
3792
+ rate,
3793
+ lifeMs: 1200,
3794
+ size: 0.12,
3795
+ speed: 0.05,
3796
+ colors: ["#ffffff", "#a5f3fc", "#818cf8"],
3797
+ texture: particles.texture ?? shineTexture(),
3798
+ emitter: orb.satelliteEmitter(1e3 / rate),
3799
+ outroMs: orb.outroEmitMs,
3800
+ seed: 5,
3801
+ backend: options.backend,
3802
+ ...particles,
3803
+ label: options.label ?? particles.label,
3804
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3805
+ });
3806
+ return progressSpinner(new CompositeAnimation([orb, streams]), options);
3807
+ }
3808
+
3809
+ // src/motion/figure-eight.ts
3810
+ var LOOP_X = 1.5;
3811
+ var LOOP_Y = 1;
3812
+ var LOOP_Z = 1.05;
3813
+ function figureEightMotion(options = {}) {
3814
+ const size = options.size ?? 1;
3815
+ const periodMs = options.periodMs ?? 3600;
2257
3816
  return {
2258
3817
  positionAt(t) {
2259
- const angle = direction * t / periodMs * Math.PI * 2;
2260
- const x = radius * Math.cos(angle);
2261
- const flatY = radius * Math.sin(angle);
2262
- return { x, y: flatY * cosTilt, z: flatY * sinTilt };
3818
+ const a = t / periodMs * Math.PI * 2;
3819
+ return {
3820
+ x: size * LOOP_X * Math.sin(a),
3821
+ y: size * LOOP_Y * Math.sin(a) * Math.cos(a),
3822
+ z: size * LOOP_Z * Math.cos(a)
3823
+ };
2263
3824
  }
2264
3825
  };
2265
3826
  }
2266
3827
 
3828
+ // src/prefabs/crystal-comet.ts
3829
+ function crystalComet(options = {}) {
3830
+ const motion = options.object?.motion ?? figureEightMotion({ size: 0.66, periodMs: 7200 });
3831
+ const particles = options.particles ?? {};
3832
+ const object = new ObjectMotionAnimation({
3833
+ mesh: () => tetrahedron(1, ["#f0f9ff", "#7dd3fc", "#818cf8", "#e879f9"]),
3834
+ motion,
3835
+ size: 0.42,
3836
+ rotation: { spinX: 2e-3, spinY: 3e-3 },
3837
+ backend: options.backend,
3838
+ ...options.object,
3839
+ label: options.object?.label
3840
+ });
3841
+ const animation = new CompositeAnimation([
3842
+ new ParticlesAnimation({
3843
+ rate: 44,
3844
+ lifeMs: 2300,
3845
+ size: 0.25,
3846
+ speed: 0.08,
3847
+ colors: ["#ffffff", "#bae6fd", "#818cf8"],
3848
+ texture: particles.texture ?? shineTexture(),
3849
+ emitter: object.trailEmitter(),
3850
+ outroMs: object.outroDurationMs,
3851
+ seed: 28,
3852
+ backend: options.backend,
3853
+ ...particles,
3854
+ label: options.label ?? particles.label ?? "Polishing pixels",
3855
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3856
+ }),
3857
+ object
3858
+ ]);
3859
+ return spinner(animation, options);
3860
+ }
3861
+
2267
3862
  // src/motion/square.ts
2268
3863
  function squareMotion(options = {}) {
2269
3864
  const half = (options.size ?? 2.4) / 2;
@@ -2298,24 +3893,676 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2298
3893
  };
2299
3894
  }
2300
3895
 
2301
- // src/motion/figure-eight.ts
2302
- var LOOP_X = 1.5;
2303
- var LOOP_Y = 1;
2304
- var LOOP_Z = 1.05;
2305
- function figureEightMotion(options = {}) {
2306
- const size = options.size ?? 1;
2307
- const periodMs = options.periodMs ?? 3600;
3896
+ // src/animations/ghost-train.ts
3897
+ var MAX_CARS = 50;
3898
+ var CAMERA_Z3 = 3;
3899
+ var FOV2 = 55 * Math.PI / 180;
3900
+ var HALF_HEIGHT = Math.tan(FOV2 / 2) * CAMERA_Z3;
3901
+ var RUN_GAP_MS = 130;
3902
+ var POP_MS2 = 320;
3903
+ var SAMPLE_MS2 = 8;
3904
+ var TURN_RATE = 0.4 * Math.PI / 180;
3905
+ var MAX_OUTRO_MS = 4e3;
3906
+ var WARP_ACCEL = 1e3;
3907
+ var TRAIL_OUTRO_MS = 1200;
3908
+ var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
3909
+ var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
3910
+ var WORLD_UP2 = { x: 0, y: 1, z: 0 };
3911
+ function clamp016(value) {
3912
+ return Math.max(0, Math.min(1, value));
3913
+ }
3914
+ function orientationFor2(forward) {
3915
+ const fwd = normalize(forward);
3916
+ let right = cross(fwd, WORLD_UP2);
3917
+ if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
3918
+ right = normalize(right);
3919
+ const up = cross(right, fwd);
3920
+ const w = normalize(cross(fwd, up));
2308
3921
  return {
2309
- positionAt(t) {
2310
- const a = t / periodMs * Math.PI * 2;
2311
- return {
2312
- x: size * LOOP_X * Math.sin(a),
2313
- y: size * LOOP_Y * Math.sin(a) * Math.cos(a),
2314
- z: size * LOOP_Z * Math.cos(a)
2315
- };
2316
- }
3922
+ x: Math.atan2(cross(w, fwd).z, w.z),
3923
+ y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
3924
+ z: Math.atan2(fwd.y, fwd.x)
2317
3925
  };
2318
3926
  }
3927
+ function rotateToward(from, to, maxRad) {
3928
+ const a = normalize(from);
3929
+ const b = normalize(to);
3930
+ const d = Math.max(-1, Math.min(1, dot(a, b)));
3931
+ const angle = Math.acos(d);
3932
+ if (angle <= maxRad || angle < 1e-4) return b;
3933
+ const sin = Math.sin(angle);
3934
+ if (sin < 1e-4) return b;
3935
+ const t = maxRad / angle;
3936
+ const w1 = Math.sin((1 - t) * angle) / sin;
3937
+ const w2 = Math.sin(t * angle) / sin;
3938
+ return normalize({ x: a.x * w1 + b.x * w2, y: a.y * w1 + b.y * w2, z: a.z * w1 + b.z * w2 });
3939
+ }
3940
+ var GhostTrainAnimation = class {
3941
+ constructor(options = {}) {
3942
+ this.cars = [];
3943
+ this.appear = new Array(MAX_CARS).fill(0);
3944
+ this.headings = new Array(MAX_CARS).fill(void 0);
3945
+ this.aspect = 16 / 9;
3946
+ this.enterAt = Infinity;
3947
+ this.outroAt = Infinity;
3948
+ this.carsAtOutro = 0;
3949
+ this.exitPathTime = 0;
3950
+ // lead car's path-time at blast-off (the escape switch point)
3951
+ this.exitPoint = { x: 0, y: 0, z: 0 };
3952
+ this.exitDir = { x: 1, y: 0, z: 0 };
3953
+ // shared escape direction, outward from the track
3954
+ this.exitSpeed = 1e-3;
3955
+ // path-units per path-millisecond at the switch (keeps speed continuous)
3956
+ this.lastNow = 0;
3957
+ this.finished = false;
3958
+ this.motion = options.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
3959
+ this.size = options.size ?? 0.15;
3960
+ this.backend = options.backend;
3961
+ this.labelContent = options.label;
3962
+ this.fadeLabel = options.fadeLabel ?? true;
3963
+ }
3964
+ mount(target) {
3965
+ if (!target.style.position) target.style.position = "relative";
3966
+ const engine = new Little3dEngine({
3967
+ backend: this.backend,
3968
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
3969
+ });
3970
+ const mesh = cube(1, CAR_COLORS);
3971
+ for (let i = 0; i < MAX_CARS; i++) {
3972
+ this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
3973
+ }
3974
+ this.engine = engine;
3975
+ engine.mount(target).catch((error) => {
3976
+ target.textContent = error instanceof Error ? error.message : String(error);
3977
+ });
3978
+ const measure = () => {
3979
+ if (target.clientWidth > 0 && target.clientHeight > 0) {
3980
+ this.aspect = target.clientWidth / target.clientHeight;
3981
+ }
3982
+ };
3983
+ measure();
3984
+ this.observer = new ResizeObserver(measure);
3985
+ this.observer.observe(target);
3986
+ this.label = mountAnimationLabel(target, this.labelContent);
3987
+ if (this.fadeLabel) this.label.setOpacity(0);
3988
+ }
3989
+ enter(now) {
3990
+ if (this.enterAt === Infinity) this.enterAt = now;
3991
+ }
3992
+ exit(now) {
3993
+ if (this.outroAt !== Infinity || this.enterAt === Infinity) return;
3994
+ this.outroAt = now;
3995
+ this.carsAtOutro = this.appear.filter((a) => a > 0.5).length;
3996
+ this.exitPathTime = now - this.enterAt;
3997
+ const from = this.motion.positionAt(this.exitPathTime);
3998
+ const velocity = subtract(
3999
+ this.motion.positionAt(this.exitPathTime + 1),
4000
+ this.motion.positionAt(this.exitPathTime - 1)
4001
+ );
4002
+ const speed = Math.hypot(velocity.x, velocity.y, velocity.z);
4003
+ this.exitPoint = from;
4004
+ if (speed > 1e-6) this.exitSpeed = speed / 2;
4005
+ this.exitDir = speed > 1e-6 ? normalize(velocity) : { x: 1, y: 0, z: 0 };
4006
+ }
4007
+ isFinished() {
4008
+ return this.finished;
4009
+ }
4010
+ /** Milliseconds the lead car keeps moving into the outro; feed a trail layer's `outroMs`. */
4011
+ get outroDurationMs() {
4012
+ return TRAIL_OUTRO_MS;
4013
+ }
4014
+ /**
4015
+ * A {@link MotionController} following the lead car's actual position, through
4016
+ * laps and the accelerating escape. Feed it to a particle layer's `emitter`
4017
+ * so the star trail stays behind the train.
4018
+ */
4019
+ trailEmitter() {
4020
+ return {
4021
+ positionAt: (t) => this.enterAt === Infinity ? this.motion.positionAt(t) : this.pathPosition(t - this.enterAt + this.warp(t))
4022
+ };
4023
+ }
4024
+ render(now, frame) {
4025
+ if (!this.engine || !this.label) return;
4026
+ for (const car of this.cars) car.transform.scale = 0;
4027
+ if (this.enterAt === Infinity) {
4028
+ this.engine.render();
4029
+ return;
4030
+ }
4031
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
4032
+ this.lastNow = now;
4033
+ const want = this.outroAt !== Infinity ? this.carsAtOutro : Math.min(MAX_CARS, Math.round(frame.progress * MAX_CARS));
4034
+ const halfWidth = HALF_HEIGHT * this.aspect;
4035
+ const warp = this.warp(now);
4036
+ let anyOnScreen = false;
4037
+ for (let k = 0; k < MAX_CARS; k++) {
4038
+ const target = k < want ? 1 : 0;
4039
+ this.appear[k] = clamp016(this.appear[k] + Math.sign(target - this.appear[k]) * (dt / POP_MS2));
4040
+ if (this.appear[k] <= 0) {
4041
+ this.headings[k] = void 0;
4042
+ continue;
4043
+ }
4044
+ const p = now - this.enterAt - k * RUN_GAP_MS + warp;
4045
+ const position = this.pathPosition(p);
4046
+ if (Math.abs(position.x) > halfWidth + this.size || Math.abs(position.y) > HALF_HEIGHT + this.size) {
4047
+ continue;
4048
+ }
4049
+ const ahead = subtract(this.pathPosition(p + SAMPLE_MS2), position);
4050
+ const targetDir = Math.hypot(ahead.x, ahead.y, ahead.z) > 1e-5 ? ahead : this.headings[k] ?? { x: 1, y: 0, z: 0 };
4051
+ this.headings[k] = this.headings[k] ? rotateToward(this.headings[k], targetDir, TURN_RATE * dt) : normalize(targetDir);
4052
+ const orientation = orientationFor2(this.headings[k]);
4053
+ const transform2 = this.cars[k].transform;
4054
+ transform2.position.x = position.x;
4055
+ transform2.position.y = position.y;
4056
+ transform2.position.z = position.z;
4057
+ transform2.rotation.x = orientation.x;
4058
+ transform2.rotation.y = orientation.y;
4059
+ transform2.rotation.z = orientation.z;
4060
+ transform2.scale = this.size * easeOutBack(this.appear[k]);
4061
+ anyOnScreen = true;
4062
+ }
4063
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
4064
+ if (this.fadeLabel) {
4065
+ this.label.setOpacity(animationLabelOpacity(now, this.enterAt, POP_MS2, this.outroAt, TRAIL_OUTRO_MS));
4066
+ }
4067
+ if (this.outroAt !== Infinity && now > this.outroAt + 300 && (!anyOnScreen || now >= this.outroAt + MAX_OUTRO_MS)) {
4068
+ this.finished = true;
4069
+ }
4070
+ this.engine.render();
4071
+ }
4072
+ destroy() {
4073
+ this.observer?.disconnect();
4074
+ this.observer = void 0;
4075
+ this.label?.container.remove();
4076
+ this.label = void 0;
4077
+ this.engine?.destroy();
4078
+ this.engine = void 0;
4079
+ this.cars.length = 0;
4080
+ }
4081
+ /** Extra path-time every car has accelerated forward by, `now` ms into the outro. */
4082
+ warp(now) {
4083
+ if (this.outroAt === Infinity) return 0;
4084
+ const seconds = (now - this.outroAt) / 1e3;
4085
+ return 0.5 * WARP_ACCEL * seconds * seconds;
4086
+ }
4087
+ /**
4088
+ * The single trajectory every car rides, sampled at path-time `p`: the track
4089
+ * up to the exit switch point, then a straight escape outward. Because the
4090
+ * switch point and direction are shared, all cars follow the exact same path.
4091
+ */
4092
+ pathPosition(p) {
4093
+ if (this.outroAt === Infinity || p <= this.exitPathTime) {
4094
+ return this.motion.positionAt(p);
4095
+ }
4096
+ const distance = this.exitSpeed * (p - this.exitPathTime);
4097
+ return {
4098
+ x: this.exitPoint.x + this.exitDir.x * distance,
4099
+ y: this.exitPoint.y + this.exitDir.y * distance,
4100
+ z: this.exitPoint.z + this.exitDir.z * distance
4101
+ };
4102
+ }
4103
+ };
4104
+
4105
+ // src/prefabs/ghost-train.ts
4106
+ function ghostTrain(options = {}) {
4107
+ const particles = options.particles ?? {};
4108
+ const train = new GhostTrainAnimation({
4109
+ motion: options.object?.motion,
4110
+ backend: options.backend
4111
+ });
4112
+ const trail = new ParticlesAnimation({
4113
+ rate: 30,
4114
+ lifeMs: 1700,
4115
+ size: 0.15,
4116
+ speed: 0.11,
4117
+ colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
4118
+ texture: particles.texture ?? starTexture({ glow: 5 }),
4119
+ emitter: train.trailEmitter(),
4120
+ outroMs: train.outroDurationMs,
4121
+ seed: 17,
4122
+ backend: options.backend,
4123
+ ...particles,
4124
+ label: options.label ?? particles.label,
4125
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
4126
+ });
4127
+ return progressSpinner(new CompositeAnimation([trail, train]), options);
4128
+ }
4129
+
4130
+ // src/prefabs/grid-assembly.ts
4131
+ function gridAssembly(options = {}) {
4132
+ return progressSpinner(
4133
+ new GridAssemblyAnimation({
4134
+ backend: options.backend,
4135
+ label: options.label,
4136
+ fadeLabel: options.fadeLabel,
4137
+ ...options.assembly
4138
+ }),
4139
+ options
4140
+ );
4141
+ }
4142
+
4143
+ // src/prefabs/monochrome-streak.ts
4144
+ function monochromeStreak(options = {}) {
4145
+ const particles = options.particles ?? {};
4146
+ return spinner(new ParticlesAnimation({
4147
+ rate: 70,
4148
+ lifeMs: 2800,
4149
+ size: 0.38,
4150
+ speed: 1.35,
4151
+ direction: { x: 0, y: 1, z: 0 },
4152
+ spread: 0.62,
4153
+ gravity: { x: 0, y: -1.45, z: 0 },
4154
+ colors: ["#fff", "#000"],
4155
+ texture: particles.texture ?? streakTexture(),
4156
+ spin: 0,
4157
+ alignToMotion: true,
4158
+ seed: 37,
4159
+ backend: options.backend,
4160
+ ...particles,
4161
+ label: options.label ?? particles.label ?? "Loading...",
4162
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
4163
+ }), options);
4164
+ }
4165
+
4166
+ // src/prefabs/plane-star-trail.ts
4167
+ function planeStarTrail(options = {}) {
4168
+ const motion = options.object?.motion ?? figureEightMotion({ size: 0.72, periodMs: 6200 });
4169
+ const particles = options.particles ?? {};
4170
+ const object = new ObjectMotionAnimation({
4171
+ mesh: planeMesh,
4172
+ motion,
4173
+ size: 0.48,
4174
+ backend: options.backend,
4175
+ ...options.object,
4176
+ label: options.object?.label
4177
+ });
4178
+ const animation = new CompositeAnimation([
4179
+ new ParticlesAnimation({
4180
+ rate: 34,
4181
+ lifeMs: 1900,
4182
+ size: 0.16,
4183
+ speed: 0.11,
4184
+ colors: ["#fde047", "#f472b6", "#7dd3fc"],
4185
+ texture: particles.texture ?? starTexture(),
4186
+ emitter: object.trailEmitter(),
4187
+ outroMs: object.outroDurationMs,
4188
+ seed: 11,
4189
+ backend: options.backend,
4190
+ ...particles,
4191
+ label: options.label ?? particles.label ?? "Flying in...",
4192
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
4193
+ }),
4194
+ object
4195
+ ]);
4196
+ return spinner(animation, options);
4197
+ }
4198
+
4199
+ // src/prefabs/pulsing-starfield.ts
4200
+ function pulsingLabel() {
4201
+ const label = document.createElement("div");
4202
+ label.innerHTML = `<style>
4203
+ @keyframes spinner-prefab-pulse { 0%,100% { color:#fff; transform:scale(1); } 50% { color:#93c5fd; transform:scale(1.06); } }
4204
+ </style><div style="animation:spinner-prefab-pulse 2.4s ease-in-out infinite;font-size:2rem">Loading the good stuff</div>`;
4205
+ return label;
4206
+ }
4207
+ function pulsingStarfield(options = {}) {
4208
+ const particles = options.particles ?? {};
4209
+ return spinner(new ParticlesAnimation({
4210
+ rate: 48,
4211
+ lifeMs: 4200,
4212
+ size: 0.3,
4213
+ speed: 0.34,
4214
+ colors: ["#ffffff", "#dbeafe", "#93c5fd", "#c4b5fd"],
4215
+ texture: particles.texture ?? shineTexture(),
4216
+ seed: 71,
4217
+ backend: options.backend,
4218
+ ...particles,
4219
+ label: options.label ?? particles.label ?? pulsingLabel(),
4220
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
4221
+ }), options);
4222
+ }
4223
+
4224
+ // src/animations/rocket-launch.ts
4225
+ var ROCKETS = 20;
4226
+ var CAMERA_Z4 = 3;
4227
+ var FOV3 = 55 * Math.PI / 180;
4228
+ var HALF_HEIGHT2 = Math.tan(FOV3 / 2) * CAMERA_Z4;
4229
+ var SIZE = 0.12;
4230
+ var ROW_Y = -0.5;
4231
+ var PARTICLE_Z = 0.3;
4232
+ var SLIDE_MS = 460;
4233
+ var SLIDE_GATE = 0.45;
4234
+ var EXIT_HURRY3 = 2.5;
4235
+ var LAUNCH_SPREAD_MS = 620;
4236
+ var ASCENT_G = 5.2;
4237
+ var FINISH_PAD_MS = 2e3;
4238
+ var TURNERS = 3;
4239
+ var TURN_MIN_Y = 0.2;
4240
+ var TURN_MAX_Y = 0.8;
4241
+ var TURN_MIN_DEG = 30;
4242
+ var TURN_MAX_DEG = 50;
4243
+ var DEG = Math.PI / 180;
4244
+ var SMOKE_LIFE_MS = 1400;
4245
+ var SMOKE_GAP_MS = 320;
4246
+ var SMOKE_RISE = 0.55;
4247
+ var SMOKE_SIZE = 0.17;
4248
+ var SMOKE_PEAK = 0.16;
4249
+ var SMOKE_POOL = 104;
4250
+ var FIRE_LIFE_MS = 420;
4251
+ var FIRE_GAP_MS = 55;
4252
+ var FIRE_ON_MS = 950;
4253
+ var FIRE_TRAIL = 0.25;
4254
+ var FIRE_SPREAD = 0.09;
4255
+ var FIRE_SIZE = 0.15;
4256
+ var FIRE_PEAK = 0.9;
4257
+ var FIRE_POOL = 140;
4258
+ var SMOKE_COLORS = ["#e2e8f0", "#cbd5e1"];
4259
+ var FIRE_COLORS = ["#fef3c7", "#fde047", "#fb923c", "#ef4444"];
4260
+ var ROCKET_COLORS = ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"];
4261
+ function clamp017(value) {
4262
+ return Math.max(0, Math.min(1, value));
4263
+ }
4264
+ function smoothstep2(edge0, edge1, value) {
4265
+ const x = clamp017((value - edge0) / (edge1 - edge0));
4266
+ return x * x * (3 - 2 * x);
4267
+ }
4268
+ function hash012(index, salt) {
4269
+ let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
4270
+ h = Math.imul(h ^ h >>> 16, 73244475);
4271
+ h ^= h >>> 16;
4272
+ return (h >>> 0) / 4294967296;
4273
+ }
4274
+ function puffTexture(coreAlpha, coreStop) {
4275
+ return canvasTexture((ctx) => {
4276
+ const g = ctx.createRadialGradient(48, 48, 1, 48, 48, 47);
4277
+ g.addColorStop(0, `rgba(255,255,255,${coreAlpha})`);
4278
+ g.addColorStop(coreStop, `rgba(255,255,255,${coreAlpha * 0.6})`);
4279
+ g.addColorStop(1, "rgba(255,255,255,0)");
4280
+ ctx.fillStyle = g;
4281
+ ctx.fillRect(0, 0, 96, 96);
4282
+ });
4283
+ }
4284
+ var RocketLaunchAnimation = class {
4285
+ constructor(options = {}) {
4286
+ this.rockets = [];
4287
+ this.smoke = [];
4288
+ this.fire = [];
4289
+ this.smokeFades = [];
4290
+ this.fireFades = [];
4291
+ this.blends = new Array(ROCKETS).fill(0);
4292
+ this.groundedAt = new Array(ROCKETS).fill(Infinity);
4293
+ // Per-rocket veer parameters (turnS = Infinity for a rocket that climbs straight).
4294
+ this.turnS = new Array(ROCKETS).fill(Infinity);
4295
+ this.turnDir = [];
4296
+ this.turnRoll = new Array(ROCKETS).fill(0);
4297
+ this.stagger = new Array(ROCKETS).fill(0);
4298
+ this.aspect = 16 / 9;
4299
+ this.enterAt = Infinity;
4300
+ this.exitAt = Infinity;
4301
+ this.launchedAt = Infinity;
4302
+ this.lastNow = 0;
4303
+ this.finished = false;
4304
+ this.backend = options.backend;
4305
+ this.labelContent = options.label;
4306
+ this.fadeLabel = options.fadeLabel ?? true;
4307
+ for (let i = 0; i < ROCKETS; i++) {
4308
+ this.turnDir.push({ x: 0, y: 1 });
4309
+ this.stagger[i] = hash012(i, 7) * LAUNCH_SPREAD_MS;
4310
+ }
4311
+ const order = Array.from({ length: ROCKETS }, (_, i) => i).sort(
4312
+ (a, b) => hash012(a, 11) - hash012(b, 11)
4313
+ );
4314
+ for (const i of order.slice(0, TURNERS)) {
4315
+ const height = TURN_MIN_Y + hash012(i, 12) * (TURN_MAX_Y - TURN_MIN_Y);
4316
+ const angle = (TURN_MIN_DEG + hash012(i, 13) * (TURN_MAX_DEG - TURN_MIN_DEG)) * DEG;
4317
+ const sign = hash012(i, 14) < 0.5 ? -1 : 1;
4318
+ this.turnS[i] = height - ROW_Y;
4319
+ this.turnDir[i] = { x: sign * Math.sin(angle), y: Math.cos(angle) };
4320
+ this.turnRoll[i] = -sign * angle;
4321
+ }
4322
+ }
4323
+ mount(target) {
4324
+ if (!target.style.position) target.style.position = "relative";
4325
+ const smokeMeshes = SMOKE_COLORS.map((color) => quad(1, [color]));
4326
+ const fireMeshes = FIRE_COLORS.map((color) => quad(1, [color]));
4327
+ const smokeTexture = puffTexture(0.85, 0.5);
4328
+ const fireTexture = puffTexture(1, 0.32);
4329
+ const backend = async (rendererOptions) => {
4330
+ 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);
4331
+ for (const mesh of smokeMeshes) renderer.setTexture(mesh, smokeTexture);
4332
+ for (const mesh of fireMeshes) renderer.setTexture(mesh, fireTexture);
4333
+ return renderer;
4334
+ };
4335
+ const engine = new Little3dEngine({
4336
+ backend,
4337
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z4 }, fov: FOV3 }
4338
+ });
4339
+ const rocketMesh = pyramid(1, ROCKET_COLORS);
4340
+ for (let i = 0; i < ROCKETS; i++) this.rockets.push(engine.add(rocketMesh, { scale: 0 }));
4341
+ for (let s = 0; s < SMOKE_POOL; s++) {
4342
+ const fade = { mode: "one-sided", opacity: 0 };
4343
+ this.smokeFades.push(fade);
4344
+ this.smoke.push(engine.add(smokeMeshes[s % smokeMeshes.length], { scale: 0, transparency: fade }));
4345
+ }
4346
+ for (let f = 0; f < FIRE_POOL; f++) {
4347
+ const fade = { mode: "one-sided", opacity: 0 };
4348
+ this.fireFades.push(fade);
4349
+ this.fire.push(engine.add(fireMeshes[f % fireMeshes.length], { scale: 0, transparency: fade }));
4350
+ }
4351
+ this.engine = engine;
4352
+ engine.mount(target).catch((error) => {
4353
+ target.textContent = error instanceof Error ? error.message : String(error);
4354
+ });
4355
+ const measure = () => {
4356
+ if (target.clientWidth > 0 && target.clientHeight > 0) {
4357
+ this.aspect = target.clientWidth / target.clientHeight;
4358
+ }
4359
+ };
4360
+ measure();
4361
+ this.observer = new ResizeObserver(measure);
4362
+ this.observer.observe(target);
4363
+ this.label = mountAnimationLabel(target, this.labelContent);
4364
+ if (this.fadeLabel) this.label.setOpacity(0);
4365
+ }
4366
+ enter(now) {
4367
+ if (this.enterAt === Infinity) this.enterAt = now;
4368
+ }
4369
+ exit(now) {
4370
+ if (this.exitAt === Infinity) this.exitAt = now;
4371
+ }
4372
+ isFinished() {
4373
+ return this.finished;
4374
+ }
4375
+ render(now, frame) {
4376
+ if (!this.engine || !this.label) return;
4377
+ for (const handle of this.rockets) handle.transform.scale = 0;
4378
+ for (const fade of this.smokeFades) fade.opacity = 0;
4379
+ for (const fade of this.fireFades) fade.opacity = 0;
4380
+ for (const handle of this.smoke) handle.transform.scale = 0;
4381
+ for (const handle of this.fire) handle.transform.scale = 0;
4382
+ if (this.enterAt === Infinity) {
4383
+ this.engine.render();
4384
+ return;
4385
+ }
4386
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
4387
+ this.lastNow = now;
4388
+ const exiting = this.exitAt !== Infinity;
4389
+ this.updateBlends(dt, frame.progress, now, exiting);
4390
+ if (exiting && this.launchedAt === Infinity && this.blends.every((blend) => blend >= 1)) {
4391
+ this.launchedAt = now;
4392
+ }
4393
+ const launched = this.launchedAt !== Infinity;
4394
+ const halfWidth = HALF_HEIGHT2 * this.aspect;
4395
+ const rowHalf = Math.min(halfWidth * 0.8, 1.18);
4396
+ const spawnX = halfWidth + 0.6;
4397
+ let smokeCursor = 0;
4398
+ let fireCursor = 0;
4399
+ for (let i = 0; i < ROCKETS; i++) {
4400
+ const homeX = -rowHalf + 2 * rowHalf * i / (ROCKETS - 1);
4401
+ const launchAt = launched ? this.launchedAt + this.stagger[i] : Infinity;
4402
+ const la = now - launchAt;
4403
+ if (launched && la >= 0) {
4404
+ this.renderAscent(i, homeX, la, halfWidth);
4405
+ fireCursor = this.emitFire(i, homeX, la, fireCursor);
4406
+ continue;
4407
+ }
4408
+ const blend = this.blends[i];
4409
+ if (blend <= 0) continue;
4410
+ const transform2 = this.rockets[i].transform;
4411
+ transform2.position.x = spawnX + (homeX - spawnX) * easeOutBack(blend);
4412
+ transform2.position.y = ROW_Y;
4413
+ transform2.position.z = 0;
4414
+ transform2.rotation.x = 0;
4415
+ transform2.rotation.y = 0;
4416
+ transform2.rotation.z = 0;
4417
+ transform2.scale = SIZE * smoothstep2(0, 0.6, blend);
4418
+ if (this.groundedAt[i] !== Infinity) {
4419
+ smokeCursor = this.emitSmoke(i, homeX, now, launchAt, smokeCursor);
4420
+ }
4421
+ }
4422
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
4423
+ if (this.fadeLabel) {
4424
+ this.label.setOpacity(animationLabelOpacity(
4425
+ now,
4426
+ this.enterAt,
4427
+ SLIDE_MS,
4428
+ this.launchedAt,
4429
+ LAUNCH_SPREAD_MS
4430
+ ));
4431
+ }
4432
+ if (launched && now >= this.launchedAt + LAUNCH_SPREAD_MS + FINISH_PAD_MS) {
4433
+ this.finished = true;
4434
+ }
4435
+ this.engine.render();
4436
+ }
4437
+ destroy() {
4438
+ this.observer?.disconnect();
4439
+ this.observer = void 0;
4440
+ this.label?.container.remove();
4441
+ this.label = void 0;
4442
+ this.engine?.destroy();
4443
+ this.engine = void 0;
4444
+ this.rockets.length = 0;
4445
+ this.smoke.length = 0;
4446
+ this.fire.length = 0;
4447
+ this.smokeFades.length = 0;
4448
+ this.fireFades.length = 0;
4449
+ }
4450
+ updateBlends(dt, progress, now, exiting) {
4451
+ const want = exiting ? ROCKETS : Math.min(ROCKETS, Math.round(progress * ROCKETS));
4452
+ const rate = dt / SLIDE_MS * (exiting ? EXIT_HURRY3 : 1);
4453
+ for (let i = 0; i < ROCKETS; i++) {
4454
+ const target = i < want ? 1 : 0;
4455
+ const blend = this.blends[i];
4456
+ if (target > blend && (i === 0 || this.blends[i - 1] >= SLIDE_GATE)) {
4457
+ this.blends[i] = Math.min(1, blend + rate);
4458
+ } else if (target < blend && (i === ROCKETS - 1 || this.blends[i + 1] <= 1 - SLIDE_GATE)) {
4459
+ this.blends[i] = Math.max(0, blend - rate);
4460
+ }
4461
+ if (this.blends[i] >= 1) {
4462
+ if (this.groundedAt[i] === Infinity) this.groundedAt[i] = now;
4463
+ } else {
4464
+ this.groundedAt[i] = Infinity;
4465
+ }
4466
+ }
4467
+ }
4468
+ /** Along-track distance climbed `la` ms after this rocket's own blast-off. */
4469
+ ascentDistance(la) {
4470
+ const seconds = la / 1e3;
4471
+ return 0.5 * ASCENT_G * seconds * seconds;
4472
+ }
4473
+ /** Rocket center, nose direction, and roll `la` ms into its climb. */
4474
+ ascentPose(i, homeX, la) {
4475
+ const s = this.ascentDistance(la);
4476
+ const turnS = this.turnS[i];
4477
+ if (s <= turnS) {
4478
+ return { pos: { x: homeX, y: ROW_Y + s }, dir: { x: 0, y: 1 }, roll: 0 };
4479
+ }
4480
+ const post = s - turnS;
4481
+ const dir = this.turnDir[i];
4482
+ return {
4483
+ pos: { x: homeX + dir.x * post, y: ROW_Y + turnS + dir.y * post },
4484
+ dir,
4485
+ roll: this.turnRoll[i]
4486
+ };
4487
+ }
4488
+ renderAscent(i, homeX, la, halfWidth) {
4489
+ const { pos, roll } = this.ascentPose(i, homeX, la);
4490
+ if (pos.y > HALF_HEIGHT2 + 0.4 || Math.abs(pos.x) > halfWidth + 0.4) return;
4491
+ const transform2 = this.rockets[i].transform;
4492
+ transform2.position.x = pos.x;
4493
+ transform2.position.y = pos.y;
4494
+ transform2.position.z = 0;
4495
+ transform2.rotation.x = 0;
4496
+ transform2.rotation.y = 0;
4497
+ transform2.rotation.z = roll;
4498
+ transform2.scale = SIZE;
4499
+ }
4500
+ emitFire(i, homeX, la, cursor) {
4501
+ const gap = FIRE_GAP_MS;
4502
+ const last = Math.min(Math.floor(la / gap), Math.floor(FIRE_ON_MS / gap));
4503
+ const first = Math.max(0, Math.ceil((la - FIRE_LIFE_MS) / gap));
4504
+ for (let n = first; n <= last; n++) {
4505
+ if (cursor >= this.fire.length) return cursor;
4506
+ const emitLa = n * gap;
4507
+ const age = la - emitLa;
4508
+ if (age < 0 || age >= FIRE_LIFE_MS) continue;
4509
+ const life = age / FIRE_LIFE_MS;
4510
+ const seconds = age / 1e3;
4511
+ const pose = this.ascentPose(i, homeX, emitLa);
4512
+ const back = { x: -pose.dir.x, y: -pose.dir.y };
4513
+ const perp = { x: -pose.dir.y, y: pose.dir.x };
4514
+ const lat = (hash012(i * 97 + n, 1) - 0.5) * FIRE_SPREAD;
4515
+ const baseX = pose.pos.x + back.x * SIZE * 0.5;
4516
+ const baseY = pose.pos.y + back.y * SIZE * 0.5;
4517
+ const transform2 = this.fire[cursor].transform;
4518
+ transform2.position.x = baseX + back.x * FIRE_TRAIL * seconds + perp.x * lat;
4519
+ transform2.position.y = baseY + back.y * FIRE_TRAIL * seconds + perp.y * lat - 0.12 * seconds * seconds;
4520
+ transform2.position.z = PARTICLE_Z;
4521
+ transform2.rotation.z = hash012(i * 97 + n, 2) * Math.PI * 2;
4522
+ transform2.scale = FIRE_SIZE * (0.7 + 0.5 * hash012(i * 97 + n, 3)) * (1 - 0.55 * life);
4523
+ this.fireFades[cursor].opacity = FIRE_PEAK * smoothstep2(0, 0.15, life) * (1 - smoothstep2(0.55, 1, life));
4524
+ cursor++;
4525
+ }
4526
+ return cursor;
4527
+ }
4528
+ emitSmoke(i, homeX, now, launchAt, cursor) {
4529
+ const start = this.groundedAt[i];
4530
+ const tr = now - start;
4531
+ const gap = SMOKE_GAP_MS;
4532
+ const emitUntil = Number.isFinite(launchAt) ? launchAt - start : tr;
4533
+ const last = Math.min(Math.floor(tr / gap), Math.floor(emitUntil / gap));
4534
+ const first = Math.max(0, Math.ceil((tr - SMOKE_LIFE_MS) / gap));
4535
+ const baseY = ROW_Y - SIZE * 0.4;
4536
+ for (let n = first; n <= last; n++) {
4537
+ if (cursor >= this.smoke.length) return cursor;
4538
+ const age = tr - n * gap;
4539
+ if (age < 0 || age >= SMOKE_LIFE_MS) continue;
4540
+ const life = age / SMOKE_LIFE_MS;
4541
+ const drift = (hash012(i * 131 + n, 1) - 0.5) * 0.14;
4542
+ const transform2 = this.smoke[cursor].transform;
4543
+ transform2.position.x = homeX + drift * life;
4544
+ transform2.position.y = baseY + SMOKE_RISE * life;
4545
+ transform2.position.z = PARTICLE_Z;
4546
+ transform2.rotation.z = hash012(i * 131 + n, 2) * Math.PI * 2;
4547
+ transform2.scale = SMOKE_SIZE * (0.5 + 0.8 * life) * (0.7 + 0.6 * hash012(i * 131 + n, 3));
4548
+ this.smokeFades[cursor].opacity = SMOKE_PEAK * smoothstep2(0, 0.25, life) * (1 - smoothstep2(0.5, 1, life));
4549
+ cursor++;
4550
+ }
4551
+ return cursor;
4552
+ }
4553
+ };
4554
+
4555
+ // src/prefabs/rocket-launch.ts
4556
+ function rocketLaunch(options = {}) {
4557
+ return progressSpinner(
4558
+ new RocketLaunchAnimation({
4559
+ backend: options.backend,
4560
+ label: options.label,
4561
+ fadeLabel: options.fadeLabel
4562
+ }),
4563
+ options
4564
+ );
4565
+ }
2319
4566
 
2320
4567
  // src/motion/wander.ts
2321
4568
  function mulberry32(seed) {
@@ -2358,16 +4605,87 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2358
4605
  };
2359
4606
  }
2360
4607
 
4608
+ // src/prefabs/star-swarm.ts
4609
+ function starSwarm(options = {}) {
4610
+ const particles = options.particles ?? {};
4611
+ const emitter = particles.emitter ?? wanderMotion({
4612
+ bounds: { x: 1.1, y: 0.72, z: 0.35 },
4613
+ periodMs: 7200,
4614
+ seed: 19
4615
+ });
4616
+ return spinner(new ParticlesAnimation({
4617
+ rate: 38,
4618
+ lifeMs: 2600,
4619
+ size: 0.15,
4620
+ speed: 0.17,
4621
+ colors: ["#fef08a", "#f9a8d4", "#a5f3fc"],
4622
+ texture: particles.texture ?? starTexture(),
4623
+ emitter,
4624
+ seed: 91,
4625
+ backend: options.backend,
4626
+ ...particles,
4627
+ label: options.label ?? particles.label ?? "Loading...",
4628
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
4629
+ }), options);
4630
+ }
4631
+
4632
+ // src/motion/circle.ts
4633
+ function circleMotion(options = {}) {
4634
+ const radius = options.radius ?? 1.3;
4635
+ const periodMs = options.periodMs ?? 3e3;
4636
+ const tilt = options.tilt ?? 0.5;
4637
+ const direction = options.direction ?? 1;
4638
+ const cosTilt = Math.cos(tilt);
4639
+ const sinTilt = Math.sin(tilt);
4640
+ return {
4641
+ positionAt(t) {
4642
+ const angle = direction * t / periodMs * Math.PI * 2;
4643
+ const x = radius * Math.cos(angle);
4644
+ const flatY = radius * Math.sin(angle);
4645
+ return { x, y: flatY * cosTilt, z: flatY * sinTilt };
4646
+ }
4647
+ };
4648
+ }
4649
+
4650
+ // <stdin>
4651
+ init_webgl_textured();
4652
+ init_webgpu_textured();
4653
+
2361
4654
  // src/engines/little-3d-engine/loaders/obj.ts
2362
- var DEFAULT_COLORS9 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
4655
+ var DEFAULT_COLORS12 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
4656
+ function channelToHex(value) {
4657
+ const channel = Number.parseFloat(value);
4658
+ if (!Number.isFinite(channel)) return void 0;
4659
+ return Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0");
4660
+ }
4661
+ function parseMtlColors(text) {
4662
+ const colors = /* @__PURE__ */ new Map();
4663
+ let material;
4664
+ for (const line of text.split("\n")) {
4665
+ const trimmed = line.trim();
4666
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
4667
+ const parts = trimmed.split(/\s+/);
4668
+ if (parts[0] === "newmtl") {
4669
+ material = parts.slice(1).join(" ");
4670
+ } else if (parts[0] === "Kd" && material) {
4671
+ const channels = parts.slice(1, 4).map(channelToHex);
4672
+ if (channels.length === 3 && channels.every((channel) => channel !== void 0)) {
4673
+ colors.set(material, `#${channels.join("")}`);
4674
+ }
4675
+ }
4676
+ }
4677
+ return colors;
4678
+ }
2363
4679
  function resolveIndex(token, vertexCount) {
2364
4680
  const n = parseInt(token, 10);
2365
4681
  return n < 0 ? vertexCount + n : n - 1;
2366
4682
  }
2367
4683
  function parseObj(text, options = {}) {
2368
- const colors = options.colors ?? DEFAULT_COLORS9;
4684
+ const colors = options.colors ?? DEFAULT_COLORS12;
4685
+ const materialColors = options.useMtlColors && options.mtl ? parseMtlColors(options.mtl) : void 0;
2369
4686
  const vertices = [];
2370
4687
  const faces = [];
4688
+ let material;
2371
4689
  for (const line of text.split("\n")) {
2372
4690
  const trimmed = line.trim();
2373
4691
  if (trimmed === "" || trimmed.startsWith("#")) continue;
@@ -2379,6 +4697,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2379
4697
  y: parseFloat(parts[2]),
2380
4698
  z: parseFloat(parts[3])
2381
4699
  });
4700
+ } else if (keyword === "usemtl") {
4701
+ material = parts.slice(1).join(" ");
2382
4702
  } else if (keyword === "f") {
2383
4703
  const indices = [];
2384
4704
  for (let i = 1; i < parts.length; i++) {
@@ -2386,7 +4706,9 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
2386
4706
  indices.push(resolveIndex(vertexToken, vertices.length));
2387
4707
  }
2388
4708
  if (indices.length >= 3) {
2389
- faces.push({ indices, color: colors[faces.length % colors.length] });
4709
+ const mtlColor = material ? materialColors?.get(material) : void 0;
4710
+ const color = mtlColor ?? (materialColors ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
4711
+ faces.push({ indices, color });
2390
4712
  }
2391
4713
  }
2392
4714
  }