3d-spinner 0.9.2 → 0.9.3

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 (97) 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/grid-assembly.d.ts +67 -0
  8. package/dist/animations/grid-assembly.js +268 -0
  9. package/dist/animations/object-motion.d.ts +19 -7
  10. package/dist/animations/object-motion.js +38 -32
  11. package/dist/animations/particles.d.ts +110 -0
  12. package/dist/animations/particles.js +209 -0
  13. package/dist/cjs/animations/charged-orb.cjs +1334 -0
  14. package/dist/cjs/animations/grid-assembly.cjs +1498 -0
  15. package/dist/cjs/animations/object-motion.cjs +124 -43
  16. package/dist/cjs/animations/particles.cjs +1940 -0
  17. package/dist/cjs/animations/spin.cjs +4 -3
  18. package/dist/cjs/composite-animation.cjs +58 -0
  19. package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +131 -22
  20. package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +30 -1
  21. package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +345 -0
  22. package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +528 -0
  23. package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +712 -0
  24. package/dist/cjs/prefabs/prefabs.cjs +3450 -0
  25. package/dist/composite-animation.d.ts +18 -0
  26. package/dist/composite-animation.js +39 -0
  27. package/dist/engines/little-3d-engine/little-3d-engine.d.ts +17 -12
  28. package/dist/engines/little-3d-engine/little-3d-engine.js +13 -8
  29. package/dist/engines/little-3d-engine/loaders/obj.d.ts +12 -4
  30. package/dist/engines/little-3d-engine/loaders/obj.js +44 -5
  31. package/dist/engines/little-3d-engine/renderer.d.ts +7 -1
  32. package/dist/engines/little-3d-engine/renderer.js +2 -0
  33. package/dist/engines/little-3d-engine/renderers/canvas2d-textured.d.ts +20 -0
  34. package/dist/engines/little-3d-engine/renderers/canvas2d-textured.js +135 -0
  35. package/dist/engines/little-3d-engine/renderers/textured-helpers.d.ts +9 -0
  36. package/dist/engines/little-3d-engine/renderers/textured-helpers.js +35 -0
  37. package/dist/engines/little-3d-engine/renderers/webgl-textured.d.ts +43 -0
  38. package/dist/engines/little-3d-engine/renderers/webgl-textured.js +211 -0
  39. package/dist/engines/little-3d-engine/renderers/webgpu-textured.d.ts +45 -0
  40. package/dist/engines/little-3d-engine/renderers/webgpu-textured.js +300 -0
  41. package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +11 -6
  42. package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +3 -0
  43. package/dist/engines/little-3d-engine/shapes/complex/plane.js +33 -0
  44. package/dist/engines/little-3d-engine/shapes/{cube.d.ts → primitives/cube.d.ts} +1 -1
  45. package/dist/engines/little-3d-engine/shapes/{octahedron.d.ts → primitives/octahedron.d.ts} +1 -1
  46. package/dist/engines/little-3d-engine/shapes/{pyramid.d.ts → primitives/pyramid.d.ts} +1 -1
  47. package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +9 -0
  48. package/dist/engines/little-3d-engine/shapes/primitives/quad.js +18 -0
  49. package/dist/engines/little-3d-engine/shapes/{cube-sphere.d.ts → primitives/spheres/cube-sphere.d.ts} +1 -1
  50. package/dist/engines/little-3d-engine/shapes/{icosphere.d.ts → primitives/spheres/icosphere.d.ts} +1 -1
  51. package/dist/engines/little-3d-engine/shapes/{icosphere.js → primitives/spheres/icosphere.js} +1 -1
  52. package/dist/engines/little-3d-engine/shapes/{octa-sphere.d.ts → primitives/spheres/octa-sphere.d.ts} +1 -1
  53. package/dist/engines/little-3d-engine/shapes/{octa-sphere.js → primitives/spheres/octa-sphere.js} +1 -1
  54. package/dist/engines/little-3d-engine/shapes/{uv-sphere.d.ts → primitives/spheres/uv-sphere.d.ts} +1 -1
  55. package/dist/engines/little-3d-engine/shapes/{tetrahedron.d.ts → primitives/tetrahedron.d.ts} +1 -1
  56. package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.d.ts +1 -0
  57. package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.js +8 -0
  58. package/dist/engines/little-3d-engine/textures/dynamic/shine.d.ts +2 -0
  59. package/dist/engines/little-3d-engine/textures/dynamic/shine.js +14 -0
  60. package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +2 -0
  61. package/dist/engines/little-3d-engine/textures/dynamic/star.js +16 -0
  62. package/dist/engines/little-3d-engine/textures/dynamic/streak.d.ts +5 -0
  63. package/dist/engines/little-3d-engine/textures/dynamic/streak.js +20 -0
  64. package/dist/index.d.ts +2 -1
  65. package/dist/prefabs/charged-orb.d.ts +17 -0
  66. package/dist/prefabs/charged-orb.js +37 -0
  67. package/dist/prefabs/crystal-comet.d.ts +4 -0
  68. package/dist/prefabs/crystal-comet.js +41 -0
  69. package/dist/prefabs/ghost-train.d.ts +9 -0
  70. package/dist/prefabs/ghost-train.js +44 -0
  71. package/dist/prefabs/grid-assembly.d.ts +13 -0
  72. package/dist/prefabs/grid-assembly.js +15 -0
  73. package/dist/prefabs/monochrome-streak.d.ts +4 -0
  74. package/dist/prefabs/monochrome-streak.js +25 -0
  75. package/dist/prefabs/plane-star-trail.d.ts +4 -0
  76. package/dist/prefabs/plane-star-trail.js +40 -0
  77. package/dist/prefabs/prefabs.d.ts +10 -0
  78. package/dist/prefabs/prefabs.js +9 -0
  79. package/dist/prefabs/pulsing-starfield.d.ts +4 -0
  80. package/dist/prefabs/pulsing-starfield.js +27 -0
  81. package/dist/prefabs/rocket-launch.d.ts +8 -0
  82. package/dist/prefabs/rocket-launch.js +59 -0
  83. package/dist/prefabs/spinner.d.ts +7 -0
  84. package/dist/prefabs/spinner.js +17 -0
  85. package/dist/prefabs/star-swarm.d.ts +4 -0
  86. package/dist/prefabs/star-swarm.js +27 -0
  87. package/dist/prefabs/types.d.ts +46 -0
  88. package/dist/prefabs/types.js +1 -0
  89. package/dist/umd/spinner.global.js +3055 -1251
  90. package/dist/umd/spinner.global.min.js +60 -5
  91. package/package.json +42 -1
  92. /package/dist/engines/little-3d-engine/shapes/{cube.js → primitives/cube.js} +0 -0
  93. /package/dist/engines/little-3d-engine/shapes/{octahedron.js → primitives/octahedron.js} +0 -0
  94. /package/dist/engines/little-3d-engine/shapes/{pyramid.js → primitives/pyramid.js} +0 -0
  95. /package/dist/engines/little-3d-engine/shapes/{cube-sphere.js → primitives/spheres/cube-sphere.js} +0 -0
  96. /package/dist/engines/little-3d-engine/shapes/{uv-sphere.js → primitives/spheres/uv-sphere.js} +0 -0
  97. /package/dist/engines/little-3d-engine/shapes/{tetrahedron.js → primitives/tetrahedron.js} +0 -0
@@ -0,0 +1,3450 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res, err) => function __init() {
7
+ if (err) throw err[0];
8
+ try {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ } catch (e) {
11
+ throw err = [e], e;
12
+ }
13
+ };
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+
28
+ // src/engines/little-3d-engine/core/math.ts
29
+ function subtract(a, b) {
30
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
31
+ }
32
+ function cross(a, b) {
33
+ return {
34
+ x: a.y * b.z - a.z * b.y,
35
+ y: a.z * b.x - a.x * b.z,
36
+ z: a.x * b.y - a.y * b.x
37
+ };
38
+ }
39
+ function dot(a, b) {
40
+ return a.x * b.x + a.y * b.y + a.z * b.z;
41
+ }
42
+ function scale(v, s) {
43
+ return { x: v.x * s, y: v.y * s, z: v.z * s };
44
+ }
45
+ function normalize(v) {
46
+ const length = Math.hypot(v.x, v.y, v.z);
47
+ if (length === 0) return { x: 0, y: 0, z: 0 };
48
+ return { x: v.x / length, y: v.y / length, z: v.z / length };
49
+ }
50
+ function multiply(a, b) {
51
+ const out = new Array(16);
52
+ for (let col = 0; col < 4; col++) {
53
+ for (let row = 0; row < 4; row++) {
54
+ let sum = 0;
55
+ for (let k = 0; k < 4; k++) {
56
+ sum += a[k * 4 + row] * b[col * 4 + k];
57
+ }
58
+ out[col * 4 + row] = sum;
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function translation(x, y, z) {
64
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
65
+ }
66
+ function scaleMatrix(s) {
67
+ return [s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1];
68
+ }
69
+ function rotationX(rad) {
70
+ const c = Math.cos(rad);
71
+ const s = Math.sin(rad);
72
+ return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
73
+ }
74
+ function rotationY(rad) {
75
+ const c = Math.cos(rad);
76
+ const s = Math.sin(rad);
77
+ return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
78
+ }
79
+ function rotationZ(rad) {
80
+ const c = Math.cos(rad);
81
+ const s = Math.sin(rad);
82
+ return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
83
+ }
84
+ function perspective(fovY, aspect, near, far) {
85
+ const f = 1 / Math.tan(fovY / 2);
86
+ const nf = 1 / (near - far);
87
+ return [
88
+ f / aspect,
89
+ 0,
90
+ 0,
91
+ 0,
92
+ 0,
93
+ f,
94
+ 0,
95
+ 0,
96
+ 0,
97
+ 0,
98
+ (far + near) * nf,
99
+ -1,
100
+ 0,
101
+ 0,
102
+ 2 * far * near * nf,
103
+ 0
104
+ ];
105
+ }
106
+ function transformPoint(m, p) {
107
+ const x = m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12];
108
+ const y = m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13];
109
+ const z = m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14];
110
+ const w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15] || 1;
111
+ return { x: x / w, y: y / w, z: z / w };
112
+ }
113
+ function transformAffine(m, p) {
114
+ return {
115
+ x: m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
116
+ y: m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
117
+ z: m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]
118
+ };
119
+ }
120
+ var init_math = __esm({
121
+ "src/engines/little-3d-engine/core/math.ts"() {
122
+ "use strict";
123
+ }
124
+ });
125
+
126
+ // src/engines/little-3d-engine/core/geometry.ts
127
+ function parseColor(color) {
128
+ const hex = color.trim().replace("#", "");
129
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
130
+ const n = parseInt(full, 16);
131
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
132
+ }
133
+ function expandToTriangles(mesh) {
134
+ let triangles = 0;
135
+ for (const face of mesh.faces) triangles += Math.max(0, face.indices.length - 2);
136
+ const positions = new Float32Array(triangles * 9);
137
+ const normals = new Float32Array(triangles * 9);
138
+ const colors = new Float32Array(triangles * 9);
139
+ let o = 0;
140
+ for (const face of mesh.faces) {
141
+ const v0 = mesh.vertices[face.indices[0]];
142
+ const v1 = mesh.vertices[face.indices[1]];
143
+ const v2 = mesh.vertices[face.indices[2]];
144
+ const normal = normalize(cross(subtract(v1, v0), subtract(v2, v0)));
145
+ const [r, g, b] = parseColor(face.color);
146
+ const cr = r / 255;
147
+ const cg = g / 255;
148
+ const cb = b / 255;
149
+ for (let k = 1; k < face.indices.length - 1; k++) {
150
+ const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
151
+ for (const index of tri) {
152
+ const v = mesh.vertices[index];
153
+ positions[o] = v.x;
154
+ positions[o + 1] = v.y;
155
+ positions[o + 2] = v.z;
156
+ normals[o] = normal.x;
157
+ normals[o + 1] = normal.y;
158
+ normals[o + 2] = normal.z;
159
+ colors[o] = cr;
160
+ colors[o + 1] = cg;
161
+ colors[o + 2] = cb;
162
+ o += 3;
163
+ }
164
+ }
165
+ }
166
+ return { positions, normals, colors, count: positions.length / 3 };
167
+ }
168
+ function midpoint(a, b) {
169
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
170
+ }
171
+ function sphereFromTriangles(seedVertices, seedFaces, size, detail, colors) {
172
+ const radius = size / 2;
173
+ let triangles = seedFaces.map((f) => f.map((i) => normalize(seedVertices[i])));
174
+ const levels = Math.max(0, Math.floor(detail) - 1);
175
+ for (let level = 0; level < levels; level++) {
176
+ const next = [];
177
+ for (const [a, b, c] of triangles) {
178
+ const ab = normalize(midpoint(a, b));
179
+ const bc = normalize(midpoint(b, c));
180
+ const ca = normalize(midpoint(c, a));
181
+ next.push([a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]);
182
+ }
183
+ triangles = next;
184
+ }
185
+ const vertices = [];
186
+ const faces = triangles.map((tri, i) => {
187
+ const base = vertices.length;
188
+ vertices.push(scale(tri[0], radius), scale(tri[1], radius), scale(tri[2], radius));
189
+ return { indices: [base, base + 1, base + 2], color: colors[i % colors.length] };
190
+ });
191
+ return { vertices, faces };
192
+ }
193
+ var init_geometry = __esm({
194
+ "src/engines/little-3d-engine/core/geometry.ts"() {
195
+ "use strict";
196
+ init_math();
197
+ }
198
+ });
199
+
200
+ // src/engines/little-3d-engine/core/light.ts
201
+ function clamp01(value) {
202
+ return Math.min(1, Math.max(0, value));
203
+ }
204
+ function shadeColor(normal, color, light) {
205
+ const lambert = Math.max(0, dot(normal, light.toLight));
206
+ const brightness = clamp01(light.ambient + light.intensity * lambert);
207
+ const [r, g, b] = parseColor(color);
208
+ return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
209
+ b * brightness
210
+ )})`;
211
+ }
212
+ var DEFAULTS2, Light;
213
+ var init_light = __esm({
214
+ "src/engines/little-3d-engine/core/light.ts"() {
215
+ "use strict";
216
+ init_math();
217
+ init_geometry();
218
+ DEFAULTS2 = {
219
+ direction: { x: -0.4, y: -0.7, z: -0.6 },
220
+ intensity: 0.85,
221
+ ambient: 0.25
222
+ };
223
+ Light = class {
224
+ constructor(options) {
225
+ this.options = { ...DEFAULTS2, ...options };
226
+ this.params = {
227
+ toLight: normalize(scale(this.options.direction, -1)),
228
+ intensity: this.options.intensity,
229
+ ambient: this.options.ambient
230
+ };
231
+ }
232
+ /** Convenience wrapper around {@link shadeColor} using this light. */
233
+ shade(normal, color) {
234
+ return shadeColor(normal, color, this.params);
235
+ }
236
+ };
237
+ }
238
+ });
239
+
240
+ // src/engines/little-3d-engine/renderers/webgl.ts
241
+ var webgl_exports = {};
242
+ __export(webgl_exports, {
243
+ WebGLRenderer: () => WebGLRenderer
244
+ });
245
+ function compile(gl, type, source) {
246
+ const shader = gl.createShader(type);
247
+ gl.shaderSource(shader, source);
248
+ gl.compileShader(shader);
249
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
250
+ throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
251
+ }
252
+ return shader;
253
+ }
254
+ function link(gl) {
255
+ const program = gl.createProgram();
256
+ gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER));
257
+ gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER));
258
+ gl.linkProgram(program);
259
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
260
+ throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
261
+ }
262
+ return program;
263
+ }
264
+ var VERTEX_SHADER, FRAGMENT_SHADER, WebGLRenderer;
265
+ var init_webgl = __esm({
266
+ "src/engines/little-3d-engine/renderers/webgl.ts"() {
267
+ "use strict";
268
+ init_geometry();
269
+ init_renderer();
270
+ VERTEX_SHADER = `#version 300 es
271
+ in vec3 aPos;
272
+ in vec3 aNormal;
273
+ in vec3 aColor;
274
+ uniform mat4 uViewProj;
275
+ uniform mat4 uModel;
276
+ out vec3 vNormal;
277
+ out vec3 vColor;
278
+ void main() {
279
+ vNormal = mat3(uModel) * aNormal;
280
+ vColor = aColor;
281
+ gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
282
+ }`;
283
+ FRAGMENT_SHADER = `#version 300 es
284
+ precision mediump float;
285
+ in vec3 vNormal;
286
+ in vec3 vColor;
287
+ uniform vec3 uToLight;
288
+ uniform float uIntensity;
289
+ uniform float uAmbient;
290
+ uniform float uOpacity;
291
+ out vec4 fragColor;
292
+ void main() {
293
+ float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
294
+ float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
295
+ fragColor = vec4(vColor * brightness, uOpacity);
296
+ }`;
297
+ WebGLRenderer = class {
298
+ constructor(options = {}) {
299
+ this.cache = /* @__PURE__ */ new Map();
300
+ if (options.background) {
301
+ const [r, g, b] = parseColor(options.background);
302
+ this.clearColor = [r / 255, g / 255, b / 255, 1];
303
+ } else {
304
+ this.clearColor = [0, 0, 0, 0];
305
+ }
306
+ }
307
+ init(canvas) {
308
+ const gl = canvas.getContext("webgl2");
309
+ if (!gl) throw new Error("3d-spinner: WebGL2 is not supported in this browser.");
310
+ this.gl = gl;
311
+ this.program = link(gl);
312
+ this.locations = {
313
+ aPos: gl.getAttribLocation(this.program, "aPos"),
314
+ aNormal: gl.getAttribLocation(this.program, "aNormal"),
315
+ aColor: gl.getAttribLocation(this.program, "aColor"),
316
+ uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
317
+ uModel: gl.getUniformLocation(this.program, "uModel"),
318
+ uToLight: gl.getUniformLocation(this.program, "uToLight"),
319
+ uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
320
+ uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
321
+ uOpacity: gl.getUniformLocation(this.program, "uOpacity")
322
+ };
323
+ gl.enable(gl.DEPTH_TEST);
324
+ gl.enable(gl.CULL_FACE);
325
+ gl.cullFace(gl.BACK);
326
+ gl.frontFace(gl.CCW);
327
+ }
328
+ resize() {
329
+ const gl = this.gl;
330
+ if (!gl) return;
331
+ const canvas = gl.canvas;
332
+ gl.viewport(0, 0, canvas.width, canvas.height);
333
+ }
334
+ buffers(mesh) {
335
+ const cached = this.cache.get(mesh);
336
+ if (cached) return cached;
337
+ const gl = this.gl;
338
+ const loc = this.locations;
339
+ const data = expandToTriangles(mesh);
340
+ const vao = gl.createVertexArray();
341
+ gl.bindVertexArray(vao);
342
+ const attribute = (location, array) => {
343
+ if (location < 0) return;
344
+ const buffer = gl.createBuffer();
345
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
346
+ gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
347
+ gl.enableVertexAttribArray(location);
348
+ gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
349
+ };
350
+ attribute(loc.aPos, data.positions);
351
+ attribute(loc.aNormal, data.normals);
352
+ attribute(loc.aColor, data.colors);
353
+ gl.bindVertexArray(null);
354
+ const result = { vao, count: data.count };
355
+ this.cache.set(mesh, result);
356
+ return result;
357
+ }
358
+ render(frame) {
359
+ const gl = this.gl;
360
+ const loc = this.locations;
361
+ if (!gl || !this.program || !loc) return;
362
+ gl.clearColor(...this.clearColor);
363
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
364
+ gl.useProgram(this.program);
365
+ gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
366
+ gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
367
+ gl.uniform1f(loc.uIntensity, frame.light.intensity);
368
+ gl.uniform1f(loc.uAmbient, frame.light.ambient);
369
+ gl.disable(gl.BLEND);
370
+ gl.depthMask(true);
371
+ gl.cullFace(gl.BACK);
372
+ for (const item of frame.items) {
373
+ if (item.transparency) continue;
374
+ const mesh = this.buffers(item.mesh);
375
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
376
+ gl.uniform1f(loc.uOpacity, 1);
377
+ gl.bindVertexArray(mesh.vao);
378
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
379
+ }
380
+ gl.enable(gl.BLEND);
381
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
382
+ gl.depthMask(false);
383
+ for (const item of frame.items) {
384
+ const transparency = item.transparency;
385
+ if (!transparency) continue;
386
+ const mesh = this.buffers(item.mesh);
387
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
388
+ gl.bindVertexArray(mesh.vao);
389
+ if (transparency.mode === "two-sided") {
390
+ const resolved = resolveTwoSidedOpacity(transparency);
391
+ gl.cullFace(gl.FRONT);
392
+ gl.uniform1f(loc.uOpacity, resolved.back);
393
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
394
+ gl.cullFace(gl.BACK);
395
+ gl.uniform1f(loc.uOpacity, resolved.front);
396
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
397
+ } else {
398
+ gl.cullFace(gl.BACK);
399
+ gl.uniform1f(loc.uOpacity, opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY));
400
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
401
+ }
402
+ }
403
+ gl.depthMask(true);
404
+ gl.disable(gl.BLEND);
405
+ gl.cullFace(gl.BACK);
406
+ gl.bindVertexArray(null);
407
+ }
408
+ destroy() {
409
+ const gl = this.gl;
410
+ if (gl) {
411
+ for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
412
+ if (this.program) gl.deleteProgram(this.program);
413
+ }
414
+ this.cache.clear();
415
+ this.gl = void 0;
416
+ this.program = void 0;
417
+ this.locations = void 0;
418
+ }
419
+ };
420
+ }
421
+ });
422
+
423
+ // src/engines/little-3d-engine/renderers/webgpu.ts
424
+ var webgpu_exports = {};
425
+ __export(webgpu_exports, {
426
+ WebGPURenderer: () => WebGPURenderer
427
+ });
428
+ var WGSL, CLIP_Z_FIX, UNIFORM_STRIDE, WebGPURenderer;
429
+ var init_webgpu = __esm({
430
+ "src/engines/little-3d-engine/renderers/webgpu.ts"() {
431
+ "use strict";
432
+ init_geometry();
433
+ init_math();
434
+ init_renderer();
435
+ WGSL = `
436
+ struct Uniforms {
437
+ viewProj: mat4x4<f32>,
438
+ model: mat4x4<f32>,
439
+ toLight: vec4<f32>,
440
+ params: vec4<f32>,
441
+ };
442
+ @group(0) @binding(0) var<uniform> u: Uniforms;
443
+
444
+ struct VSOut {
445
+ @builtin(position) position: vec4<f32>,
446
+ @location(0) normal: vec3<f32>,
447
+ @location(1) color: vec3<f32>,
448
+ };
449
+
450
+ @vertex
451
+ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
452
+ var out: VSOut;
453
+ let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
454
+ out.normal = m * normal;
455
+ out.color = color;
456
+ out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
457
+ return out;
458
+ }
459
+
460
+ @fragment
461
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
462
+ let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
463
+ let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
464
+ return vec4<f32>(in.color * brightness, u.params.z);
465
+ }
466
+ `;
467
+ CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
468
+ UNIFORM_STRIDE = 256;
469
+ WebGPURenderer = class {
470
+ constructor(options = {}) {
471
+ this.uniformCapacity = 0;
472
+ this.depthSize = "";
473
+ this.destroyed = false;
474
+ this.cache = /* @__PURE__ */ new Map();
475
+ if (options.background) {
476
+ const [r, g, b] = parseColor(options.background);
477
+ this.clearValue = { r: r / 255, g: g / 255, b: b / 255, a: 1 };
478
+ this.alphaMode = "opaque";
479
+ } else {
480
+ this.clearValue = { r: 0, g: 0, b: 0, a: 0 };
481
+ this.alphaMode = "premultiplied";
482
+ }
483
+ }
484
+ async init(canvas) {
485
+ const gpu = navigator.gpu;
486
+ if (!gpu) throw new Error("3d-spinner: WebGPU is not supported in this browser.");
487
+ const adapter = await gpu.requestAdapter();
488
+ if (!adapter) throw new Error("3d-spinner: no WebGPU adapter is available.");
489
+ const device = await adapter.requestDevice();
490
+ if (this.destroyed) {
491
+ device.destroy?.();
492
+ return;
493
+ }
494
+ const context = canvas.getContext("webgpu");
495
+ if (!context) throw new Error("3d-spinner: could not get a WebGPU canvas context.");
496
+ const format = gpu.getPreferredCanvasFormat();
497
+ context.configure({ device, format, alphaMode: this.alphaMode });
498
+ const module2 = device.createShaderModule({ code: WGSL });
499
+ const stage = globalThis.GPUShaderStage;
500
+ const layout = device.createBindGroupLayout({
501
+ entries: [
502
+ {
503
+ binding: 0,
504
+ visibility: stage.VERTEX | stage.FRAGMENT,
505
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
506
+ }
507
+ ]
508
+ });
509
+ const vertexBuffer = (location) => ({
510
+ arrayStride: 12,
511
+ attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
512
+ });
513
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
514
+ const blend = {
515
+ color: {
516
+ srcFactor: "src-alpha",
517
+ dstFactor: "one-minus-src-alpha",
518
+ operation: "add"
519
+ },
520
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
521
+ };
522
+ const pipeline = (cullMode, transparent) => device.createRenderPipeline({
523
+ layout: pipelineLayout,
524
+ vertex: {
525
+ module: module2,
526
+ entryPoint: "vs",
527
+ buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
528
+ },
529
+ fragment: {
530
+ module: module2,
531
+ entryPoint: "fs",
532
+ targets: [{ format, ...transparent ? { blend } : {} }]
533
+ },
534
+ primitive: { topology: "triangle-list", cullMode, frontFace: "ccw" },
535
+ depthStencil: {
536
+ format: "depth24plus",
537
+ depthWriteEnabled: !transparent,
538
+ depthCompare: "less"
539
+ }
540
+ });
541
+ this.pipeline = pipeline("back", false);
542
+ this.transparentBackPipeline = pipeline("front", true);
543
+ this.transparentFrontPipeline = pipeline("back", true);
544
+ this.canvas = canvas;
545
+ this.device = device;
546
+ this.context = context;
547
+ }
548
+ resize() {
549
+ this.ensureDepth();
550
+ }
551
+ ensureDepth() {
552
+ const canvas = this.canvas;
553
+ if (!this.device || !canvas) return;
554
+ const width = Math.max(1, canvas.width);
555
+ const height = Math.max(1, canvas.height);
556
+ const key = `${width}x${height}`;
557
+ if (key === this.depthSize && this.depthTexture) return;
558
+ this.depthTexture?.destroy?.();
559
+ this.depthTexture = this.device.createTexture({
560
+ size: { width, height },
561
+ format: "depth24plus",
562
+ usage: globalThis.GPUTextureUsage.RENDER_ATTACHMENT
563
+ });
564
+ this.depthSize = key;
565
+ }
566
+ buffers(mesh) {
567
+ const cached = this.cache.get(mesh);
568
+ if (cached) return cached;
569
+ const data = expandToTriangles(mesh);
570
+ const usage = globalThis.GPUBufferUsage.VERTEX | globalThis.GPUBufferUsage.COPY_DST;
571
+ const upload = (array) => {
572
+ const buffer = this.device.createBuffer({ size: array.byteLength, usage });
573
+ this.device.queue.writeBuffer(buffer, 0, array);
574
+ return buffer;
575
+ };
576
+ const result = {
577
+ position: upload(data.positions),
578
+ normal: upload(data.normals),
579
+ color: upload(data.colors),
580
+ count: data.count
581
+ };
582
+ this.cache.set(mesh, result);
583
+ return result;
584
+ }
585
+ ensureUniformCapacity(draws) {
586
+ if (draws <= this.uniformCapacity && this.uniformBuffer) return;
587
+ this.uniformBuffer?.destroy?.();
588
+ this.uniformBuffer = this.device.createBuffer({
589
+ size: Math.max(1, draws) * UNIFORM_STRIDE,
590
+ usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST
591
+ });
592
+ this.uniformCapacity = draws;
593
+ }
594
+ render(frame) {
595
+ if (this.destroyed || !this.device || !this.context || !this.pipeline) return;
596
+ if (frame.width === 0 || frame.height === 0 || frame.items.length === 0) return;
597
+ this.ensureDepth();
598
+ const draws = [];
599
+ for (const item of frame.items) {
600
+ if (!item.transparency) draws.push({ item, opacity: 1, pipeline: this.pipeline });
601
+ }
602
+ for (const item of frame.items) {
603
+ const transparency = item.transparency;
604
+ if (!transparency) continue;
605
+ if (transparency.mode === "two-sided") {
606
+ const resolved = resolveTwoSidedOpacity(transparency);
607
+ draws.push({
608
+ item,
609
+ opacity: resolved.back,
610
+ pipeline: this.transparentBackPipeline
611
+ });
612
+ draws.push({
613
+ item,
614
+ opacity: resolved.front,
615
+ pipeline: this.transparentFrontPipeline
616
+ });
617
+ } else {
618
+ draws.push({
619
+ item,
620
+ opacity: opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY),
621
+ pipeline: this.transparentFrontPipeline
622
+ });
623
+ }
624
+ }
625
+ this.ensureUniformCapacity(draws.length);
626
+ const viewProj = multiply(CLIP_Z_FIX, frame.viewProjection);
627
+ const layout = this.pipeline.getBindGroupLayout(0);
628
+ const bindGroup = this.device.createBindGroup({
629
+ layout,
630
+ entries: [
631
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
632
+ ]
633
+ });
634
+ draws.forEach((draw, i) => {
635
+ const data = new Float32Array(UNIFORM_STRIDE / 4);
636
+ data.set(viewProj, 0);
637
+ data.set(draw.item.model, 16);
638
+ data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
639
+ data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
640
+ this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
641
+ });
642
+ const encoder = this.device.createCommandEncoder();
643
+ const pass = encoder.beginRenderPass({
644
+ colorAttachments: [
645
+ {
646
+ view: this.context.getCurrentTexture().createView(),
647
+ clearValue: this.clearValue,
648
+ loadOp: "clear",
649
+ storeOp: "store"
650
+ }
651
+ ],
652
+ depthStencilAttachment: {
653
+ view: this.depthTexture.createView(),
654
+ depthClearValue: 1,
655
+ depthLoadOp: "clear",
656
+ depthStoreOp: "store"
657
+ }
658
+ });
659
+ draws.forEach((draw, i) => {
660
+ const mesh = this.buffers(draw.item.mesh);
661
+ pass.setPipeline(draw.pipeline);
662
+ pass.setBindGroup(0, bindGroup, [i * UNIFORM_STRIDE]);
663
+ pass.setVertexBuffer(0, mesh.position);
664
+ pass.setVertexBuffer(1, mesh.normal);
665
+ pass.setVertexBuffer(2, mesh.color);
666
+ pass.draw(mesh.count);
667
+ });
668
+ pass.end();
669
+ this.device.queue.submit([encoder.finish()]);
670
+ }
671
+ destroy() {
672
+ this.destroyed = true;
673
+ for (const mesh of this.cache.values()) {
674
+ mesh.position.destroy?.();
675
+ mesh.normal.destroy?.();
676
+ mesh.color.destroy?.();
677
+ }
678
+ this.cache.clear();
679
+ this.uniformBuffer?.destroy?.();
680
+ this.depthTexture?.destroy?.();
681
+ this.device?.destroy?.();
682
+ this.device = void 0;
683
+ this.context = void 0;
684
+ this.pipeline = void 0;
685
+ this.transparentBackPipeline = void 0;
686
+ this.transparentFrontPipeline = void 0;
687
+ this.uniformBuffer = void 0;
688
+ this.depthTexture = void 0;
689
+ this.canvas = void 0;
690
+ }
691
+ };
692
+ }
693
+ });
694
+
695
+ // src/engines/little-3d-engine/renderers/canvas2d.ts
696
+ var canvas2d_exports = {};
697
+ __export(canvas2d_exports, {
698
+ Canvas2DRenderer: () => Canvas2DRenderer
699
+ });
700
+ var Canvas2DRenderer;
701
+ var init_canvas2d = __esm({
702
+ "src/engines/little-3d-engine/renderers/canvas2d.ts"() {
703
+ "use strict";
704
+ init_light();
705
+ init_math();
706
+ init_renderer();
707
+ Canvas2DRenderer = class {
708
+ constructor(options = {}) {
709
+ this.options = options;
710
+ this.dpr = 1;
711
+ }
712
+ init(canvas) {
713
+ this.ctx = canvas.getContext("2d") ?? void 0;
714
+ }
715
+ resize(_cssWidth, _cssHeight, dpr) {
716
+ this.dpr = dpr;
717
+ this.ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
718
+ }
719
+ render(frame) {
720
+ const ctx = this.ctx;
721
+ if (!ctx) return;
722
+ if (this.options.background) {
723
+ ctx.fillStyle = this.options.background;
724
+ ctx.fillRect(0, 0, frame.width, frame.height);
725
+ } else {
726
+ ctx.clearRect(0, 0, frame.width, frame.height);
727
+ }
728
+ const polygons = [];
729
+ for (const item of frame.items) {
730
+ const world = item.mesh.vertices.map((v) => transformAffine(item.model, v));
731
+ const twoSidedOpacity = item.transparency?.mode === "two-sided" ? resolveTwoSidedOpacity(item.transparency) : void 0;
732
+ for (const face of item.mesh.faces) {
733
+ const a = world[face.indices[0]];
734
+ const b = world[face.indices[1]];
735
+ const c = world[face.indices[2]];
736
+ const normal = normalize(cross(subtract(b, a), subtract(c, a)));
737
+ const frontFacing = dot(normal, subtract(frame.eye, a)) > 0;
738
+ const transparency = item.transparency;
739
+ if (!frontFacing && transparency?.mode !== "two-sided") continue;
740
+ let faceOpacity = 1;
741
+ if (transparency?.mode === "one-sided") {
742
+ faceOpacity = opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
743
+ } else if (twoSidedOpacity) {
744
+ faceOpacity = frontFacing ? twoSidedOpacity.front : twoSidedOpacity.back;
745
+ }
746
+ const points = face.indices.map((i) => {
747
+ const ndc = transformPoint(frame.viewProjection, world[i]);
748
+ return {
749
+ x: (ndc.x * 0.5 + 0.5) * frame.width,
750
+ y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height
751
+ };
752
+ });
753
+ let depth = 0;
754
+ for (const i of face.indices) {
755
+ const d = subtract(world[i], frame.eye);
756
+ depth += dot(d, d);
757
+ }
758
+ depth /= face.indices.length;
759
+ polygons.push({
760
+ points,
761
+ color: shadeColor(normal, face.color, frame.light),
762
+ depth,
763
+ opacity: faceOpacity
764
+ });
765
+ }
766
+ }
767
+ polygons.sort((p, q) => q.depth - p.depth);
768
+ for (const poly of polygons) {
769
+ if (poly.points.length < 3) continue;
770
+ ctx.beginPath();
771
+ ctx.moveTo(poly.points[0].x, poly.points[0].y);
772
+ for (let i = 1; i < poly.points.length; i++) {
773
+ ctx.lineTo(poly.points[i].x, poly.points[i].y);
774
+ }
775
+ ctx.closePath();
776
+ ctx.fillStyle = poly.color;
777
+ ctx.strokeStyle = poly.color;
778
+ ctx.lineWidth = 1;
779
+ ctx.globalAlpha = poly.opacity;
780
+ ctx.fill();
781
+ ctx.stroke();
782
+ }
783
+ ctx.globalAlpha = 1;
784
+ }
785
+ destroy() {
786
+ this.ctx = void 0;
787
+ }
788
+ };
789
+ }
790
+ });
791
+
792
+ // src/engines/little-3d-engine/renderer.ts
793
+ function opacity(value, fallback) {
794
+ return Math.max(0, Math.min(1, value ?? fallback));
795
+ }
796
+ function resolveTwoSidedOpacity(transparency) {
797
+ const front = opacity(
798
+ transparency.frontOpacity ?? transparency.opacity,
799
+ DEFAULT_FRONT_OPACITY
800
+ );
801
+ const backFallback = transparency.opacity === void 0 ? DEFAULT_BACK_OPACITY : front * (2 / 3);
802
+ return {
803
+ front,
804
+ back: opacity(transparency.backOpacity, backFallback)
805
+ };
806
+ }
807
+ function orderRenderItems(items, eye) {
808
+ const opaque = [];
809
+ const transparent = [];
810
+ for (const item of items) {
811
+ (item.transparency ? transparent : opaque).push(item);
812
+ }
813
+ transparent.sort((a, b) => {
814
+ const ax = a.model[12] - eye.x;
815
+ const ay = a.model[13] - eye.y;
816
+ const az = a.model[14] - eye.z;
817
+ const bx = b.model[12] - eye.x;
818
+ const by = b.model[13] - eye.y;
819
+ const bz = b.model[14] - eye.z;
820
+ return bx * bx + by * by + bz * bz - (ax * ax + ay * ay + az * az);
821
+ });
822
+ return opaque.concat(transparent);
823
+ }
824
+ async function createRenderer(backend, options = {}) {
825
+ if (typeof backend === "function") return backend(options);
826
+ switch (backend) {
827
+ case "webgl":
828
+ return new (await Promise.resolve().then(() => (init_webgl(), webgl_exports))).WebGLRenderer(options);
829
+ case "webgpu":
830
+ return new (await Promise.resolve().then(() => (init_webgpu(), webgpu_exports))).WebGPURenderer(options);
831
+ case "canvas2d":
832
+ default:
833
+ return new (await Promise.resolve().then(() => (init_canvas2d(), canvas2d_exports))).Canvas2DRenderer(options);
834
+ }
835
+ }
836
+ var DEFAULT_ONE_SIDED_OPACITY, DEFAULT_BACK_OPACITY, DEFAULT_FRONT_OPACITY;
837
+ var init_renderer = __esm({
838
+ "src/engines/little-3d-engine/renderer.ts"() {
839
+ "use strict";
840
+ DEFAULT_ONE_SIDED_OPACITY = 0.35;
841
+ DEFAULT_BACK_OPACITY = 0.84;
842
+ DEFAULT_FRONT_OPACITY = 0.56;
843
+ }
844
+ });
845
+
846
+ // src/engines/little-3d-engine/renderers/textured-helpers.ts
847
+ function planarUVs(mesh) {
848
+ let minX = Infinity;
849
+ let minY = Infinity;
850
+ let maxX = -Infinity;
851
+ let maxY = -Infinity;
852
+ for (const v of mesh.vertices) {
853
+ minX = Math.min(minX, v.x);
854
+ minY = Math.min(minY, v.y);
855
+ maxX = Math.max(maxX, v.x);
856
+ maxY = Math.max(maxY, v.y);
857
+ }
858
+ const width = maxX - minX || 1;
859
+ const height = maxY - minY || 1;
860
+ let triangles = 0;
861
+ for (const face of mesh.faces) triangles += Math.max(0, face.indices.length - 2);
862
+ const uvs = new Float32Array(triangles * 6);
863
+ let o = 0;
864
+ for (const face of mesh.faces) {
865
+ for (let k = 1; k < face.indices.length - 1; k++) {
866
+ for (const index of [face.indices[0], face.indices[k], face.indices[k + 1]]) {
867
+ const v = mesh.vertices[index];
868
+ uvs[o] = (v.x - minX) / width;
869
+ uvs[o + 1] = 1 - (v.y - minY) / height;
870
+ o += 2;
871
+ }
872
+ }
873
+ }
874
+ return uvs;
875
+ }
876
+ var init_textured_helpers = __esm({
877
+ "src/engines/little-3d-engine/renderers/textured-helpers.ts"() {
878
+ "use strict";
879
+ }
880
+ });
881
+
882
+ // src/engines/little-3d-engine/renderers/webgpu-textured.ts
883
+ var webgpu_textured_exports = {};
884
+ __export(webgpu_textured_exports, {
885
+ WebGPUTexturedRenderer: () => WebGPUTexturedRenderer
886
+ });
887
+ function itemOpacity(transparency) {
888
+ if (!transparency) return 1;
889
+ if (transparency.mode === "two-sided") return resolveTwoSidedOpacity(transparency).front;
890
+ return opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
891
+ }
892
+ var WGSL2, CLIP_Z_FIX2, UNIFORM_STRIDE2, WebGPUTexturedRenderer;
893
+ var init_webgpu_textured = __esm({
894
+ "src/engines/little-3d-engine/renderers/webgpu-textured.ts"() {
895
+ "use strict";
896
+ init_geometry();
897
+ init_math();
898
+ init_renderer();
899
+ init_textured_helpers();
900
+ init_webgpu();
901
+ WGSL2 = `
902
+ struct Uniforms {
903
+ viewProj: mat4x4<f32>,
904
+ model: mat4x4<f32>,
905
+ params: vec4<f32>,
906
+ };
907
+ @group(0) @binding(0) var<uniform> u: Uniforms;
908
+ @group(0) @binding(1) var tex: texture_2d<f32>;
909
+ @group(0) @binding(2) var samp: sampler;
910
+
911
+ struct VSOut {
912
+ @builtin(position) position: vec4<f32>,
913
+ @location(0) uv: vec2<f32>,
914
+ @location(1) color: vec3<f32>,
915
+ };
916
+
917
+ @vertex
918
+ fn vs(@location(0) pos: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) color: vec3<f32>) -> VSOut {
919
+ var out: VSOut;
920
+ out.uv = uv;
921
+ out.color = color;
922
+ out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
923
+ return out;
924
+ }
925
+
926
+ @fragment
927
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
928
+ let t = textureSample(tex, samp, in.uv);
929
+ return vec4<f32>(t.rgb * in.color, t.a * u.params.x);
930
+ }
931
+ `;
932
+ CLIP_Z_FIX2 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
933
+ UNIFORM_STRIDE2 = 256;
934
+ WebGPUTexturedRenderer = class extends WebGPURenderer {
935
+ constructor() {
936
+ super(...arguments);
937
+ this.texturedCapacity = 0;
938
+ this.sources = /* @__PURE__ */ new Map();
939
+ this.textures = /* @__PURE__ */ new Map();
940
+ this.retired = [];
941
+ this.texturedBuffers = /* @__PURE__ */ new Map();
942
+ this.bindGroups = /* @__PURE__ */ new Map();
943
+ }
944
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
945
+ setTexture(mesh, source) {
946
+ this.sources.set(mesh, source);
947
+ }
948
+ async init(canvas) {
949
+ await super.init(canvas);
950
+ const device = this.device;
951
+ if (!device) return;
952
+ const format = navigator.gpu.getPreferredCanvasFormat();
953
+ const module2 = device.createShaderModule({ code: WGSL2 });
954
+ const stage = globalThis.GPUShaderStage;
955
+ const layout = device.createBindGroupLayout({
956
+ entries: [
957
+ {
958
+ binding: 0,
959
+ visibility: stage.VERTEX | stage.FRAGMENT,
960
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 144 }
961
+ },
962
+ { binding: 1, visibility: stage.FRAGMENT, texture: {} },
963
+ { binding: 2, visibility: stage.FRAGMENT, sampler: {} }
964
+ ]
965
+ });
966
+ const vertexBuffer = (location, components) => ({
967
+ arrayStride: components * 4,
968
+ attributes: [{ shaderLocation: location, offset: 0, format: `float32x${components}` }]
969
+ });
970
+ this.texturedPipeline = device.createRenderPipeline({
971
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
972
+ vertex: {
973
+ module: module2,
974
+ entryPoint: "vs",
975
+ buffers: [vertexBuffer(0, 3), vertexBuffer(1, 2), vertexBuffer(2, 3)]
976
+ },
977
+ fragment: {
978
+ module: module2,
979
+ entryPoint: "fs",
980
+ targets: [
981
+ {
982
+ format,
983
+ blend: {
984
+ color: {
985
+ srcFactor: "src-alpha",
986
+ dstFactor: "one-minus-src-alpha",
987
+ operation: "add"
988
+ },
989
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
990
+ }
991
+ }
992
+ ]
993
+ },
994
+ primitive: { topology: "triangle-list", cullMode: "back", frontFace: "ccw" },
995
+ depthStencil: {
996
+ format: "depth24plus",
997
+ depthWriteEnabled: false,
998
+ depthCompare: "less"
999
+ }
1000
+ });
1001
+ this.sampler = device.createSampler({ magFilter: "linear", minFilter: "linear" });
1002
+ }
1003
+ textureFor(mesh) {
1004
+ const cached = this.textures.get(mesh);
1005
+ if (cached) return cached;
1006
+ const device = this.device;
1007
+ const usage = globalThis.GPUTextureUsage;
1008
+ const white = device.createTexture({
1009
+ size: { width: 1, height: 1 },
1010
+ format: "rgba8unorm",
1011
+ usage: usage.TEXTURE_BINDING | usage.COPY_DST
1012
+ });
1013
+ device.queue.writeTexture(
1014
+ { texture: white },
1015
+ new Uint8Array([255, 255, 255, 255]),
1016
+ {},
1017
+ { width: 1, height: 1 }
1018
+ );
1019
+ this.textures.set(mesh, white);
1020
+ const upload = async (source2) => {
1021
+ const image = source2 instanceof HTMLImageElement ? await createImageBitmap(source2) : source2;
1022
+ if (this.destroyed || !this.device || this.textures.get(mesh) !== white) return;
1023
+ const width = image.width || 1;
1024
+ const height = image.height || 1;
1025
+ const texture = this.device.createTexture({
1026
+ size: { width, height },
1027
+ format: "rgba8unorm",
1028
+ usage: usage.TEXTURE_BINDING | usage.COPY_DST | usage.RENDER_ATTACHMENT
1029
+ });
1030
+ this.device.queue.copyExternalImageToTexture(
1031
+ { source: image },
1032
+ { texture },
1033
+ { width, height }
1034
+ );
1035
+ this.retired.push(white);
1036
+ this.textures.set(mesh, texture);
1037
+ this.bindGroups.delete(mesh);
1038
+ };
1039
+ const source = this.sources.get(mesh);
1040
+ if (typeof source === "string") {
1041
+ const image = new Image();
1042
+ image.onload = () => void upload(image);
1043
+ image.src = source;
1044
+ } else {
1045
+ void upload(source);
1046
+ }
1047
+ return this.textures.get(mesh);
1048
+ }
1049
+ buffersFor(mesh) {
1050
+ const cached = this.texturedBuffers.get(mesh);
1051
+ if (cached) return cached;
1052
+ const data = expandToTriangles(mesh);
1053
+ const usage = globalThis.GPUBufferUsage.VERTEX | globalThis.GPUBufferUsage.COPY_DST;
1054
+ const upload = (array) => {
1055
+ const buffer = this.device.createBuffer({ size: array.byteLength, usage });
1056
+ this.device.queue.writeBuffer(buffer, 0, array);
1057
+ return buffer;
1058
+ };
1059
+ const result = {
1060
+ position: upload(data.positions),
1061
+ uv: upload(planarUVs(mesh)),
1062
+ color: upload(data.colors),
1063
+ count: data.count
1064
+ };
1065
+ this.texturedBuffers.set(mesh, result);
1066
+ return result;
1067
+ }
1068
+ bindGroupFor(mesh) {
1069
+ const texture = this.textureFor(mesh);
1070
+ const cached = this.bindGroups.get(mesh);
1071
+ if (cached && cached.buffer === this.texturedUniforms && cached.texture === texture) {
1072
+ return cached.group;
1073
+ }
1074
+ const group = this.device.createBindGroup({
1075
+ layout: this.texturedPipeline.getBindGroupLayout(0),
1076
+ entries: [
1077
+ { binding: 0, resource: { buffer: this.texturedUniforms, offset: 0, size: 144 } },
1078
+ { binding: 1, resource: texture.createView() },
1079
+ { binding: 2, resource: this.sampler }
1080
+ ]
1081
+ });
1082
+ this.bindGroups.set(mesh, { group, buffer: this.texturedUniforms, texture });
1083
+ return group;
1084
+ }
1085
+ render(frame) {
1086
+ const plain = [];
1087
+ const textured = [];
1088
+ for (const item of frame.items) {
1089
+ (this.sources.has(item.mesh) ? textured : plain).push(item);
1090
+ }
1091
+ super.render(textured.length ? { ...frame, items: plain } : frame);
1092
+ if (!textured.length) return;
1093
+ if (this.destroyed || !this.device || !this.context || !this.texturedPipeline) return;
1094
+ if (frame.width === 0 || frame.height === 0) return;
1095
+ this.ensureDepth();
1096
+ if (textured.length > this.texturedCapacity || !this.texturedUniforms) {
1097
+ this.texturedUniforms?.destroy?.();
1098
+ this.texturedUniforms = this.device.createBuffer({
1099
+ size: textured.length * UNIFORM_STRIDE2,
1100
+ usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST
1101
+ });
1102
+ this.texturedCapacity = textured.length;
1103
+ }
1104
+ const viewProj = multiply(CLIP_Z_FIX2, frame.viewProjection);
1105
+ textured.forEach((item, i) => {
1106
+ const data = new Float32Array(UNIFORM_STRIDE2 / 4);
1107
+ data.set(viewProj, 0);
1108
+ data.set(item.model, 16);
1109
+ data.set([itemOpacity(item.transparency), 0, 0, 0], 32);
1110
+ this.device.queue.writeBuffer(this.texturedUniforms, i * UNIFORM_STRIDE2, data);
1111
+ });
1112
+ const cleared = plain.length > 0;
1113
+ const encoder = this.device.createCommandEncoder();
1114
+ const pass = encoder.beginRenderPass({
1115
+ colorAttachments: [
1116
+ {
1117
+ view: this.context.getCurrentTexture().createView(),
1118
+ clearValue: this.clearValue,
1119
+ loadOp: cleared ? "load" : "clear",
1120
+ storeOp: "store"
1121
+ }
1122
+ ],
1123
+ depthStencilAttachment: {
1124
+ view: this.depthTexture.createView(),
1125
+ depthClearValue: 1,
1126
+ depthLoadOp: cleared ? "load" : "clear",
1127
+ depthStoreOp: "store"
1128
+ }
1129
+ });
1130
+ pass.setPipeline(this.texturedPipeline);
1131
+ textured.forEach((item, i) => {
1132
+ const mesh = this.buffersFor(item.mesh);
1133
+ pass.setBindGroup(0, this.bindGroupFor(item.mesh), [i * UNIFORM_STRIDE2]);
1134
+ pass.setVertexBuffer(0, mesh.position);
1135
+ pass.setVertexBuffer(1, mesh.uv);
1136
+ pass.setVertexBuffer(2, mesh.color);
1137
+ pass.draw(mesh.count);
1138
+ });
1139
+ pass.end();
1140
+ this.device.queue.submit([encoder.finish()]);
1141
+ }
1142
+ destroy() {
1143
+ for (const texture of this.textures.values()) texture.destroy?.();
1144
+ for (const texture of this.retired.splice(0)) texture.destroy?.();
1145
+ for (const buffers of this.texturedBuffers.values()) {
1146
+ buffers.position.destroy?.();
1147
+ buffers.uv.destroy?.();
1148
+ buffers.color.destroy?.();
1149
+ }
1150
+ this.textures.clear();
1151
+ this.texturedBuffers.clear();
1152
+ this.bindGroups.clear();
1153
+ this.sources.clear();
1154
+ this.texturedUniforms?.destroy?.();
1155
+ this.texturedUniforms = void 0;
1156
+ this.texturedPipeline = void 0;
1157
+ this.sampler = void 0;
1158
+ super.destroy();
1159
+ }
1160
+ };
1161
+ }
1162
+ });
1163
+
1164
+ // src/engines/little-3d-engine/renderers/webgl-textured.ts
1165
+ var webgl_textured_exports = {};
1166
+ __export(webgl_textured_exports, {
1167
+ WebGLTexturedRenderer: () => WebGLTexturedRenderer
1168
+ });
1169
+ function compile2(gl, type, source) {
1170
+ const shader = gl.createShader(type);
1171
+ gl.shaderSource(shader, source);
1172
+ gl.compileShader(shader);
1173
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1174
+ throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
1175
+ }
1176
+ return shader;
1177
+ }
1178
+ function link2(gl) {
1179
+ const program = gl.createProgram();
1180
+ gl.attachShader(program, compile2(gl, gl.VERTEX_SHADER, VERTEX_SHADER2));
1181
+ gl.attachShader(program, compile2(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER2));
1182
+ gl.linkProgram(program);
1183
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1184
+ throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
1185
+ }
1186
+ return program;
1187
+ }
1188
+ function itemOpacity2(transparency) {
1189
+ if (!transparency) return 1;
1190
+ if (transparency.mode === "two-sided") return resolveTwoSidedOpacity(transparency).front;
1191
+ return opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
1192
+ }
1193
+ var VERTEX_SHADER2, FRAGMENT_SHADER2, WebGLTexturedRenderer;
1194
+ var init_webgl_textured = __esm({
1195
+ "src/engines/little-3d-engine/renderers/webgl-textured.ts"() {
1196
+ "use strict";
1197
+ init_geometry();
1198
+ init_renderer();
1199
+ init_textured_helpers();
1200
+ init_webgl();
1201
+ VERTEX_SHADER2 = `#version 300 es
1202
+ in vec3 aPos;
1203
+ in vec2 aUV;
1204
+ in vec3 aColor;
1205
+ uniform mat4 uViewProj;
1206
+ uniform mat4 uModel;
1207
+ out vec2 vUV;
1208
+ out vec3 vColor;
1209
+ void main() {
1210
+ vUV = aUV;
1211
+ vColor = aColor;
1212
+ gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
1213
+ }`;
1214
+ FRAGMENT_SHADER2 = `#version 300 es
1215
+ precision mediump float;
1216
+ in vec2 vUV;
1217
+ in vec3 vColor;
1218
+ uniform sampler2D uTexture;
1219
+ uniform float uOpacity;
1220
+ out vec4 fragColor;
1221
+ void main() {
1222
+ vec4 t = texture(uTexture, vUV);
1223
+ fragColor = vec4(t.rgb * vColor, t.a * uOpacity);
1224
+ }`;
1225
+ WebGLTexturedRenderer = class {
1226
+ constructor(options = {}) {
1227
+ this.sources = /* @__PURE__ */ new Map();
1228
+ this.textures = /* @__PURE__ */ new Map();
1229
+ this.buffers = /* @__PURE__ */ new Map();
1230
+ this.inner = new WebGLRenderer(options);
1231
+ }
1232
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
1233
+ setTexture(mesh, source) {
1234
+ this.sources.set(mesh, source);
1235
+ }
1236
+ init(canvas) {
1237
+ this.inner.init(canvas);
1238
+ const gl = canvas.getContext("webgl2");
1239
+ this.gl = gl;
1240
+ this.program = link2(gl);
1241
+ this.locations = {
1242
+ aPos: gl.getAttribLocation(this.program, "aPos"),
1243
+ aUV: gl.getAttribLocation(this.program, "aUV"),
1244
+ aColor: gl.getAttribLocation(this.program, "aColor"),
1245
+ uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
1246
+ uModel: gl.getUniformLocation(this.program, "uModel"),
1247
+ uTexture: gl.getUniformLocation(this.program, "uTexture"),
1248
+ uOpacity: gl.getUniformLocation(this.program, "uOpacity")
1249
+ };
1250
+ }
1251
+ resize() {
1252
+ this.inner.resize();
1253
+ }
1254
+ textureFor(mesh) {
1255
+ const cached = this.textures.get(mesh);
1256
+ if (cached) return cached;
1257
+ const gl = this.gl;
1258
+ const texture = gl.createTexture();
1259
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1260
+ gl.texImage2D(
1261
+ gl.TEXTURE_2D,
1262
+ 0,
1263
+ gl.RGBA,
1264
+ 1,
1265
+ 1,
1266
+ 0,
1267
+ gl.RGBA,
1268
+ gl.UNSIGNED_BYTE,
1269
+ new Uint8Array([255, 255, 255, 255])
1270
+ );
1271
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
1272
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
1273
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1274
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1275
+ this.textures.set(mesh, texture);
1276
+ const upload = (image) => {
1277
+ if (!this.gl || this.textures.get(mesh) !== texture) return;
1278
+ this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
1279
+ this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, image);
1280
+ };
1281
+ const source = this.sources.get(mesh);
1282
+ if (typeof source === "string") {
1283
+ const image = new Image();
1284
+ image.onload = () => upload(image);
1285
+ image.src = source;
1286
+ } else {
1287
+ upload(source);
1288
+ }
1289
+ return texture;
1290
+ }
1291
+ buffersFor(mesh) {
1292
+ const cached = this.buffers.get(mesh);
1293
+ if (cached) return cached;
1294
+ const gl = this.gl;
1295
+ const loc = this.locations;
1296
+ const data = expandToTriangles(mesh);
1297
+ const vao = gl.createVertexArray();
1298
+ gl.bindVertexArray(vao);
1299
+ const attribute = (location, array, size) => {
1300
+ if (location < 0) return;
1301
+ const buffer = gl.createBuffer();
1302
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1303
+ gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
1304
+ gl.enableVertexAttribArray(location);
1305
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
1306
+ };
1307
+ attribute(loc.aPos, data.positions, 3);
1308
+ attribute(loc.aColor, data.colors, 3);
1309
+ attribute(loc.aUV, planarUVs(mesh), 2);
1310
+ gl.bindVertexArray(null);
1311
+ const result = { vao, count: data.count };
1312
+ this.buffers.set(mesh, result);
1313
+ return result;
1314
+ }
1315
+ render(frame) {
1316
+ const plain = [];
1317
+ const textured = [];
1318
+ for (const item of frame.items) {
1319
+ (this.sources.has(item.mesh) ? textured : plain).push(item);
1320
+ }
1321
+ this.inner.render(textured.length ? { ...frame, items: plain } : frame);
1322
+ if (!textured.length) return;
1323
+ const gl = this.gl;
1324
+ const loc = this.locations;
1325
+ if (!gl || !this.program || !loc) return;
1326
+ gl.useProgram(this.program);
1327
+ gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
1328
+ gl.uniform1i(loc.uTexture, 0);
1329
+ gl.activeTexture(gl.TEXTURE0);
1330
+ gl.enable(gl.BLEND);
1331
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
1332
+ gl.depthMask(false);
1333
+ for (const item of textured) {
1334
+ const buffers = this.buffersFor(item.mesh);
1335
+ gl.bindTexture(gl.TEXTURE_2D, this.textureFor(item.mesh));
1336
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
1337
+ gl.uniform1f(loc.uOpacity, itemOpacity2(item.transparency));
1338
+ gl.bindVertexArray(buffers.vao);
1339
+ gl.drawArrays(gl.TRIANGLES, 0, buffers.count);
1340
+ }
1341
+ gl.depthMask(true);
1342
+ gl.disable(gl.BLEND);
1343
+ gl.bindVertexArray(null);
1344
+ }
1345
+ destroy() {
1346
+ const gl = this.gl;
1347
+ if (gl) {
1348
+ for (const texture of this.textures.values()) gl.deleteTexture(texture);
1349
+ for (const buffers of this.buffers.values()) gl.deleteVertexArray(buffers.vao);
1350
+ if (this.program) gl.deleteProgram(this.program);
1351
+ }
1352
+ this.textures.clear();
1353
+ this.buffers.clear();
1354
+ this.sources.clear();
1355
+ this.gl = void 0;
1356
+ this.program = void 0;
1357
+ this.locations = void 0;
1358
+ this.inner.destroy();
1359
+ }
1360
+ };
1361
+ }
1362
+ });
1363
+
1364
+ // src/engines/little-3d-engine/renderers/canvas2d-textured.ts
1365
+ var canvas2d_textured_exports = {};
1366
+ __export(canvas2d_textured_exports, {
1367
+ Canvas2DTexturedRenderer: () => Canvas2DTexturedRenderer
1368
+ });
1369
+ function imageSize(source) {
1370
+ if (source instanceof HTMLImageElement) {
1371
+ return source.complete && source.naturalWidth > 0 ? { width: source.naturalWidth, height: source.naturalHeight } : void 0;
1372
+ }
1373
+ if (source instanceof HTMLVideoElement) {
1374
+ return source.readyState >= 2 ? { width: source.videoWidth, height: source.videoHeight } : void 0;
1375
+ }
1376
+ if (source instanceof SVGImageElement) {
1377
+ const width = source.width.baseVal.value;
1378
+ const height = source.height.baseVal.value;
1379
+ return width > 0 && height > 0 ? { width, height } : void 0;
1380
+ }
1381
+ if (typeof VideoFrame !== "undefined" && source instanceof VideoFrame) {
1382
+ return { width: source.displayWidth, height: source.displayHeight };
1383
+ }
1384
+ const sized = source;
1385
+ return sized.width > 0 && sized.height > 0 ? { width: sized.width, height: sized.height } : void 0;
1386
+ }
1387
+ function drawMappedTriangle(ctx, image, source, target) {
1388
+ const [s0, s1, s2] = source;
1389
+ const [d0, d1, d2] = target;
1390
+ const determinant = s0.x * (s1.y - s2.y) + s1.x * (s2.y - s0.y) + s2.x * (s0.y - s1.y);
1391
+ if (Math.abs(determinant) < 1e-8) return;
1392
+ const a = (d0.x * (s1.y - s2.y) + d1.x * (s2.y - s0.y) + d2.x * (s0.y - s1.y)) / determinant;
1393
+ const c = (d0.x * (s2.x - s1.x) + d1.x * (s0.x - s2.x) + d2.x * (s1.x - s0.x)) / determinant;
1394
+ 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;
1395
+ const b = (d0.y * (s1.y - s2.y) + d1.y * (s2.y - s0.y) + d2.y * (s0.y - s1.y)) / determinant;
1396
+ const d = (d0.y * (s2.x - s1.x) + d1.y * (s0.x - s2.x) + d2.y * (s1.x - s0.x)) / determinant;
1397
+ 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;
1398
+ ctx.save();
1399
+ ctx.beginPath();
1400
+ ctx.moveTo(d0.x, d0.y);
1401
+ ctx.lineTo(d1.x, d1.y);
1402
+ ctx.lineTo(d2.x, d2.y);
1403
+ ctx.closePath();
1404
+ ctx.clip();
1405
+ ctx.transform(a, b, c, d, e, f);
1406
+ ctx.drawImage(image, 0, 0);
1407
+ ctx.restore();
1408
+ }
1409
+ var Canvas2DTexturedRenderer;
1410
+ var init_canvas2d_textured = __esm({
1411
+ "src/engines/little-3d-engine/renderers/canvas2d-textured.ts"() {
1412
+ "use strict";
1413
+ init_math();
1414
+ init_renderer();
1415
+ init_canvas2d();
1416
+ Canvas2DTexturedRenderer = class {
1417
+ constructor(options = {}) {
1418
+ this.sources = /* @__PURE__ */ new Map();
1419
+ this.loaded = /* @__PURE__ */ new Map();
1420
+ this.dpr = 1;
1421
+ this.inner = new Canvas2DRenderer(options);
1422
+ }
1423
+ /** Texture every instance of `mesh` with `source`. Call any time, also before init. */
1424
+ setTexture(mesh, source) {
1425
+ this.sources.set(mesh, source);
1426
+ if (typeof source === "string" && !this.loaded.has(source)) {
1427
+ const image = new Image();
1428
+ image.src = source;
1429
+ this.loaded.set(source, image);
1430
+ }
1431
+ }
1432
+ init(canvas) {
1433
+ this.inner.init(canvas);
1434
+ this.ctx = canvas.getContext("2d") ?? void 0;
1435
+ }
1436
+ resize(cssWidth, cssHeight, dpr) {
1437
+ this.dpr = dpr;
1438
+ this.inner.resize(cssWidth, cssHeight, dpr);
1439
+ }
1440
+ drawable(mesh) {
1441
+ const source = this.sources.get(mesh);
1442
+ return typeof source === "string" ? this.loaded.get(source) : source;
1443
+ }
1444
+ render(frame) {
1445
+ const plain = frame.items.filter((item) => {
1446
+ if (!this.sources.has(item.mesh)) return true;
1447
+ const source = this.drawable(item.mesh);
1448
+ return !source || !imageSize(source);
1449
+ });
1450
+ this.inner.render({ ...frame, items: plain });
1451
+ const ctx = this.ctx;
1452
+ if (!ctx) return;
1453
+ ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1454
+ const tinted = /* @__PURE__ */ new Map();
1455
+ for (const item of frame.items) {
1456
+ const source = this.drawable(item.mesh);
1457
+ if (!source) continue;
1458
+ const size = imageSize(source);
1459
+ if (!size) continue;
1460
+ let image = tinted.get(item.mesh);
1461
+ if (!image) {
1462
+ image = document.createElement("canvas");
1463
+ image.width = size.width;
1464
+ image.height = size.height;
1465
+ const tint = image.getContext("2d");
1466
+ if (!tint) continue;
1467
+ tint.drawImage(source, 0, 0, size.width, size.height);
1468
+ tint.globalCompositeOperation = "source-in";
1469
+ tint.fillStyle = item.mesh.faces[0]?.color ?? "#ffffff";
1470
+ tint.fillRect(0, 0, size.width, size.height);
1471
+ tinted.set(item.mesh, image);
1472
+ }
1473
+ const world = item.mesh.vertices.map((vertex) => transformAffine(item.model, vertex));
1474
+ const projected = world.map((vertex) => {
1475
+ const ndc = transformPoint(frame.viewProjection, vertex);
1476
+ return { x: (ndc.x * 0.5 + 0.5) * frame.width, y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height };
1477
+ });
1478
+ const face = item.mesh.faces[0];
1479
+ if (!face || face.indices.length !== 4) continue;
1480
+ const [a, b, c, d] = face.indices.map((index) => projected[index]);
1481
+ ctx.globalAlpha = item.transparency?.mode === "one-sided" ? opacity(item.transparency.opacity, DEFAULT_ONE_SIDED_OPACITY) : 1;
1482
+ drawMappedTriangle(ctx, image, [{ x: 0, y: size.height }, { x: size.width, y: size.height }, { x: size.width, y: 0 }], [a, b, c]);
1483
+ drawMappedTriangle(ctx, image, [{ x: 0, y: size.height }, { x: size.width, y: 0 }, { x: 0, y: 0 }], [a, c, d]);
1484
+ }
1485
+ ctx.globalAlpha = 1;
1486
+ }
1487
+ destroy() {
1488
+ this.inner.destroy();
1489
+ this.ctx = void 0;
1490
+ this.sources.clear();
1491
+ this.loaded.clear();
1492
+ }
1493
+ };
1494
+ }
1495
+ });
1496
+
1497
+ // src/prefabs/prefabs.ts
1498
+ var prefabs_exports = {};
1499
+ __export(prefabs_exports, {
1500
+ chargedOrb: () => chargedOrb,
1501
+ crystalComet: () => crystalComet,
1502
+ ghostTrain: () => ghostTrain,
1503
+ gridAssembly: () => gridAssembly,
1504
+ monochromeStreak: () => monochromeStreak,
1505
+ planeStarTrail: () => planeStarTrail,
1506
+ pulsingStarfield: () => pulsingStarfield,
1507
+ rocketLaunch: () => rocketLaunch,
1508
+ starSwarm: () => starSwarm
1509
+ });
1510
+ module.exports = __toCommonJS(prefabs_exports);
1511
+
1512
+ // src/composite-animation.ts
1513
+ var CompositeAnimation = class {
1514
+ constructor(layers) {
1515
+ this.elements = [];
1516
+ this.layers = layers.map((layer) => "animation" in layer ? layer : { animation: layer });
1517
+ }
1518
+ mount(target) {
1519
+ target.style.position = "relative";
1520
+ for (const [index, layer] of this.layers.entries()) {
1521
+ const element = document.createElement("div");
1522
+ element.style.cssText = `position:absolute;inset:0;z-index:${layer.zIndex ?? index}`;
1523
+ target.appendChild(element);
1524
+ this.elements.push(element);
1525
+ layer.animation.mount(element);
1526
+ }
1527
+ }
1528
+ enter(now) {
1529
+ for (const layer of this.layers) layer.animation.enter(now);
1530
+ }
1531
+ exit(now) {
1532
+ for (const layer of this.layers) layer.animation.exit(now);
1533
+ }
1534
+ render(now, frame) {
1535
+ for (const layer of this.layers) layer.animation.render(now, frame);
1536
+ }
1537
+ isFinished() {
1538
+ return this.layers.every((layer) => layer.animation.isFinished());
1539
+ }
1540
+ destroy() {
1541
+ for (const layer of this.layers) layer.animation.destroy();
1542
+ for (const element of this.elements) element.remove();
1543
+ this.elements.length = 0;
1544
+ }
1545
+ };
1546
+
1547
+ // src/animation-label.ts
1548
+ var LABEL_STYLE = [
1549
+ "position:absolute",
1550
+ "inset:0",
1551
+ "display:flex",
1552
+ "align-items:center",
1553
+ "justify-content:center",
1554
+ "pointer-events:none",
1555
+ "font:700 1.6rem/1 system-ui,sans-serif",
1556
+ "letter-spacing:0.02em",
1557
+ "color:rgba(255,255,255,0.9)",
1558
+ "text-shadow:0 1px 10px rgba(0,0,0,0.6)",
1559
+ "z-index:1"
1560
+ ].join(";");
1561
+ function animationLabelOpacity(now, enterAt, introDurationMs, exitAt, outroDurationMs) {
1562
+ if (enterAt === Infinity) return 0;
1563
+ const intro = introDurationMs <= 0 ? 1 : Math.max(0, Math.min(1, (now - enterAt) / introDurationMs));
1564
+ const outro = exitAt === Infinity ? 1 : outroDurationMs <= 0 ? 0 : Math.max(0, Math.min(1, 1 - (now - exitAt) / outroDurationMs));
1565
+ return Math.min(intro, outro);
1566
+ }
1567
+ function mountAnimationLabel(target, content) {
1568
+ var _a;
1569
+ const container = document.createElement("div");
1570
+ container.style.cssText = LABEL_STYLE;
1571
+ container.setAttribute("role", "status");
1572
+ if (typeof content === "string") container.textContent = content;
1573
+ else if (content) {
1574
+ (_a = content.style).pointerEvents || (_a.pointerEvents = "auto");
1575
+ container.appendChild(content);
1576
+ }
1577
+ target.appendChild(container);
1578
+ return {
1579
+ container,
1580
+ setText(value) {
1581
+ if (typeof content !== "object") container.textContent = value;
1582
+ },
1583
+ setOpacity(value) {
1584
+ container.style.opacity = String(value);
1585
+ }
1586
+ };
1587
+ }
1588
+
1589
+ // src/engines/little-3d-engine/core/camera.ts
1590
+ init_math();
1591
+ var DEFAULTS = {
1592
+ position: { x: 0, y: 0, z: 4 },
1593
+ fov: 55 * Math.PI / 180,
1594
+ near: 0.1,
1595
+ far: 100
1596
+ };
1597
+ var Camera = class {
1598
+ constructor(options) {
1599
+ this.options = { ...DEFAULTS, ...options };
1600
+ }
1601
+ /** Transform a world-space point into view (camera) space. */
1602
+ toView(p) {
1603
+ const { position } = this.options;
1604
+ return transformAffine(translation(-position.x, -position.y, -position.z), p);
1605
+ }
1606
+ /** Combined view-projection matrix for the given viewport aspect ratio. */
1607
+ viewProjection(aspect) {
1608
+ const { position, fov, near, far } = this.options;
1609
+ const view = translation(-position.x, -position.y, -position.z);
1610
+ const projection = perspective(fov, aspect, near, far);
1611
+ return multiply(projection, view);
1612
+ }
1613
+ /** Convert a normalized device coordinate (-1..1) to a pixel position. */
1614
+ toScreen(ndc, width, height) {
1615
+ return {
1616
+ x: (ndc.x * 0.5 + 0.5) * width,
1617
+ y: (1 - (ndc.y * 0.5 + 0.5)) * height
1618
+ };
1619
+ }
1620
+ };
1621
+
1622
+ // src/engines/little-3d-engine/little-3d-engine.ts
1623
+ init_light();
1624
+ init_math();
1625
+
1626
+ // src/engines/little-3d-engine/core/mesh.ts
1627
+ function transform(init) {
1628
+ return {
1629
+ position: init?.position ?? { x: 0, y: 0, z: 0 },
1630
+ rotation: init?.rotation ?? { x: 0, y: 0, z: 0 },
1631
+ scale: init?.scale ?? 1
1632
+ };
1633
+ }
1634
+
1635
+ // src/engines/little-3d-engine/little-3d-engine.ts
1636
+ init_renderer();
1637
+ init_light();
1638
+
1639
+ // src/engines/little-3d-engine/shapes/primitives/cube.ts
1640
+ var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1641
+ function cube(size = 1, colors = DEFAULT_COLORS) {
1642
+ const h = size / 2;
1643
+ const vertices = [
1644
+ { x: -h, y: -h, z: h },
1645
+ { x: h, y: -h, z: h },
1646
+ { x: h, y: h, z: h },
1647
+ { x: -h, y: h, z: h },
1648
+ { x: -h, y: -h, z: -h },
1649
+ { x: h, y: -h, z: -h },
1650
+ { x: h, y: h, z: -h },
1651
+ { x: -h, y: h, z: -h }
1652
+ ];
1653
+ const faces = [
1654
+ { indices: [0, 1, 2, 3], color: colors[0 % colors.length] },
1655
+ { indices: [5, 4, 7, 6], color: colors[1 % colors.length] },
1656
+ { indices: [3, 2, 6, 7], color: colors[2 % colors.length] },
1657
+ { indices: [4, 5, 1, 0], color: colors[3 % colors.length] },
1658
+ { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1659
+ { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1660
+ ];
1661
+ return { vertices, faces };
1662
+ }
1663
+
1664
+ // src/engines/little-3d-engine/shapes/primitives/quad.ts
1665
+ var DEFAULT_COLORS2 = ["#3b82f6"];
1666
+ function quad(size = 1, colors = DEFAULT_COLORS2) {
1667
+ const s = size / 2;
1668
+ const vertices = [
1669
+ { x: -s, y: -s, z: 0 },
1670
+ { x: s, y: -s, z: 0 },
1671
+ { x: s, y: s, z: 0 },
1672
+ { x: -s, y: s, z: 0 }
1673
+ ];
1674
+ return { vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] };
1675
+ }
1676
+
1677
+ // src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
1678
+ var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1679
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
1680
+ const s = size / 2;
1681
+ const vertices = [
1682
+ { x: s, y: s, z: s },
1683
+ { x: s, y: -s, z: -s },
1684
+ { x: -s, y: s, z: -s },
1685
+ { x: -s, y: -s, z: s }
1686
+ ];
1687
+ const faces = [
1688
+ { indices: [0, 1, 2], color: colors[0 % colors.length] },
1689
+ { indices: [0, 3, 1], color: colors[1 % colors.length] },
1690
+ { indices: [0, 2, 3], color: colors[2 % colors.length] },
1691
+ { indices: [1, 3, 2], color: colors[3 % colors.length] }
1692
+ ];
1693
+ return { vertices, faces };
1694
+ }
1695
+
1696
+ // src/engines/little-3d-engine/shapes/primitives/octahedron.ts
1697
+ var DEFAULT_COLORS4 = [
1698
+ "#3b82f6",
1699
+ "#8b5cf6",
1700
+ "#ec4899",
1701
+ "#f59e0b",
1702
+ "#10b981",
1703
+ "#ef4444",
1704
+ "#06b6d4",
1705
+ "#eab308"
1706
+ ];
1707
+ function octahedron(size = 1, colors = DEFAULT_COLORS4) {
1708
+ const r = size / 2;
1709
+ const vertices = [
1710
+ { x: r, y: 0, z: 0 },
1711
+ { x: -r, y: 0, z: 0 },
1712
+ { x: 0, y: r, z: 0 },
1713
+ { x: 0, y: -r, z: 0 },
1714
+ { x: 0, y: 0, z: r },
1715
+ { x: 0, y: 0, z: -r }
1716
+ ];
1717
+ const faces = [
1718
+ { indices: [4, 0, 2], color: colors[0 % colors.length] },
1719
+ { indices: [4, 2, 1], color: colors[1 % colors.length] },
1720
+ { indices: [4, 1, 3], color: colors[2 % colors.length] },
1721
+ { indices: [4, 3, 0], color: colors[3 % colors.length] },
1722
+ { indices: [5, 2, 0], color: colors[4 % colors.length] },
1723
+ { indices: [5, 1, 2], color: colors[5 % colors.length] },
1724
+ { indices: [5, 3, 1], color: colors[6 % colors.length] },
1725
+ { indices: [5, 0, 3], color: colors[7 % colors.length] }
1726
+ ];
1727
+ return { vertices, faces };
1728
+ }
1729
+
1730
+ // src/engines/little-3d-engine/shapes/primitives/pyramid.ts
1731
+ var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1732
+ function pyramid(size = 1, colors = DEFAULT_COLORS5) {
1733
+ const h = size / 2;
1734
+ const vertices = [
1735
+ { x: -h, y: -h, z: h },
1736
+ { x: h, y: -h, z: h },
1737
+ { x: h, y: -h, z: -h },
1738
+ { x: -h, y: -h, z: -h },
1739
+ { x: 0, y: h, z: 0 }
1740
+ ];
1741
+ const faces = [
1742
+ { indices: [0, 3, 2, 1], color: colors[0 % colors.length] },
1743
+ { indices: [4, 0, 1], color: colors[1 % colors.length] },
1744
+ { indices: [4, 1, 2], color: colors[2 % colors.length] },
1745
+ { indices: [4, 2, 3], color: colors[3 % colors.length] },
1746
+ { indices: [4, 3, 0], color: colors[4 % colors.length] }
1747
+ ];
1748
+ return { vertices, faces };
1749
+ }
1750
+
1751
+ // src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
1752
+ init_geometry();
1753
+ var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1754
+ var T = (1 + Math.sqrt(5)) / 2;
1755
+ var SEED_VERTICES = [
1756
+ { x: -1, y: T, z: 0 },
1757
+ { x: 1, y: T, z: 0 },
1758
+ { x: -1, y: -T, z: 0 },
1759
+ { x: 1, y: -T, z: 0 },
1760
+ { x: 0, y: -1, z: T },
1761
+ { x: 0, y: 1, z: T },
1762
+ { x: 0, y: -1, z: -T },
1763
+ { x: 0, y: 1, z: -T },
1764
+ { x: T, y: 0, z: -1 },
1765
+ { x: T, y: 0, z: 1 },
1766
+ { x: -T, y: 0, z: -1 },
1767
+ { x: -T, y: 0, z: 1 }
1768
+ ];
1769
+ var SEED_FACES = [
1770
+ [0, 11, 5],
1771
+ [0, 5, 1],
1772
+ [0, 1, 7],
1773
+ [0, 7, 10],
1774
+ [0, 10, 11],
1775
+ [1, 5, 9],
1776
+ [5, 11, 4],
1777
+ [11, 10, 2],
1778
+ [10, 7, 6],
1779
+ [7, 1, 8],
1780
+ [3, 9, 4],
1781
+ [3, 4, 2],
1782
+ [3, 2, 6],
1783
+ [3, 6, 8],
1784
+ [3, 8, 9],
1785
+ [4, 9, 5],
1786
+ [2, 4, 11],
1787
+ [6, 2, 10],
1788
+ [8, 6, 7],
1789
+ [9, 8, 1]
1790
+ ];
1791
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1792
+ return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1793
+ }
1794
+
1795
+ // src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
1796
+ init_geometry();
1797
+
1798
+ // src/engines/little-3d-engine/shapes/complex/plane.ts
1799
+ var DEFAULT_COLORS7 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1800
+ function planeMesh(colors = DEFAULT_COLORS7) {
1801
+ return {
1802
+ vertices: [
1803
+ { x: 0.9, y: 0, z: 0 },
1804
+ { x: -0.2, y: 0, z: 0.82 },
1805
+ { x: -0.55, y: 0, z: 0.16 },
1806
+ { x: -0.72, y: 0, z: 0 },
1807
+ { x: -0.55, y: 0, z: -0.16 },
1808
+ { x: -0.2, y: 0, z: -0.82 },
1809
+ { x: -0.08, y: 0.12, z: 0 },
1810
+ { x: -0.08, y: -0.1, z: 0 },
1811
+ { x: -0.52, y: 0.38, z: 0 }
1812
+ ],
1813
+ faces: [
1814
+ { indices: [6, 1, 0], color: colors[0] ?? DEFAULT_COLORS7[0] },
1815
+ { indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS7[3] },
1816
+ { indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS7[1] },
1817
+ { indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS7[2] },
1818
+ { indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS7[3] },
1819
+ { indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS7[0] },
1820
+ { indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS7[1] },
1821
+ { indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS7[2] },
1822
+ { indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS7[1] },
1823
+ { indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS7[2] },
1824
+ { indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS7[1] },
1825
+ { indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS7[2] },
1826
+ { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS7[0] },
1827
+ { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS7[1] }
1828
+ ]
1829
+ };
1830
+ }
1831
+
1832
+ // src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
1833
+ function canvasTexture(draw, size = 96) {
1834
+ const canvas = document.createElement("canvas");
1835
+ canvas.width = canvas.height = size;
1836
+ const ctx = canvas.getContext("2d");
1837
+ if (ctx) draw(ctx);
1838
+ return canvas;
1839
+ }
1840
+
1841
+ // src/engines/little-3d-engine/textures/dynamic/star.ts
1842
+ function starTexture() {
1843
+ return canvasTexture((ctx) => {
1844
+ ctx.translate(48, 48);
1845
+ ctx.fillStyle = "#fff";
1846
+ ctx.beginPath();
1847
+ for (let index = 0; index < 10; index++) {
1848
+ const radius = index % 2 === 0 ? 43 : 16;
1849
+ const angle = index * Math.PI / 5 - Math.PI / 2;
1850
+ ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
1851
+ }
1852
+ ctx.closePath();
1853
+ ctx.fill();
1854
+ });
1855
+ }
1856
+
1857
+ // src/engines/little-3d-engine/textures/dynamic/shine.ts
1858
+ function shineTexture() {
1859
+ return canvasTexture((ctx) => {
1860
+ const halo = ctx.createRadialGradient(48, 48, 1, 48, 48, 46);
1861
+ halo.addColorStop(0, "rgba(255,255,255,1)");
1862
+ halo.addColorStop(0.08, "rgba(255,255,255,1)");
1863
+ halo.addColorStop(0.22, "rgba(210,240,255,0.7)");
1864
+ halo.addColorStop(0.55, "rgba(120,200,255,0.22)");
1865
+ halo.addColorStop(1, "rgba(80,160,255,0)");
1866
+ ctx.fillStyle = halo;
1867
+ ctx.fillRect(0, 0, 96, 96);
1868
+ });
1869
+ }
1870
+
1871
+ // src/engines/little-3d-engine/textures/dynamic/streak.ts
1872
+ function streakTexture() {
1873
+ return canvasTexture((ctx) => {
1874
+ const gradient = ctx.createLinearGradient(5, 0, 91, 0);
1875
+ gradient.addColorStop(0, "rgba(255,255,255,0)");
1876
+ gradient.addColorStop(0.7, "rgba(255,255,255,0.4)");
1877
+ gradient.addColorStop(1, "rgba(255,255,255,1)");
1878
+ ctx.strokeStyle = gradient;
1879
+ ctx.lineWidth = 3.5;
1880
+ ctx.lineCap = "round";
1881
+ ctx.beginPath();
1882
+ ctx.moveTo(5, 48);
1883
+ ctx.lineTo(91, 48);
1884
+ ctx.stroke();
1885
+ });
1886
+ }
1887
+
1888
+ // src/engines/little-3d-engine/little-3d-engine.ts
1889
+ init_geometry();
1890
+ init_renderer();
1891
+ init_math();
1892
+ function modelMatrix(t) {
1893
+ const rotation = multiply(
1894
+ rotationZ(t.rotation.z),
1895
+ multiply(rotationY(t.rotation.y), rotationX(t.rotation.x))
1896
+ );
1897
+ return multiply(
1898
+ translation(t.position.x, t.position.y, t.position.z),
1899
+ multiply(rotation, scaleMatrix(t.scale))
1900
+ );
1901
+ }
1902
+ var Little3dEngine = class {
1903
+ constructor(options = {}) {
1904
+ this.scene = [];
1905
+ this.cssWidth = 0;
1906
+ this.cssHeight = 0;
1907
+ this.ready = false;
1908
+ this.generation = 0;
1909
+ this.rafId = 0;
1910
+ this.running = false;
1911
+ this.camera = new Camera(options.camera);
1912
+ this.light = new Light(options.light);
1913
+ this.backend = options.backend ?? "canvas2d";
1914
+ this.background = options.background;
1915
+ }
1916
+ /**
1917
+ * Create the canvas inside `target`, load the selected backend, and start
1918
+ * tracking size. Resolves once the renderer is ready; rejects if the backend
1919
+ * is unavailable. Drawing is a no-op until it resolves.
1920
+ */
1921
+ async mount(target) {
1922
+ const canvas = document.createElement("canvas");
1923
+ canvas.style.display = "block";
1924
+ canvas.style.width = "100%";
1925
+ canvas.style.height = "100%";
1926
+ target.appendChild(canvas);
1927
+ this.canvas = canvas;
1928
+ this.observer = new ResizeObserver(() => this.resize());
1929
+ this.observer.observe(canvas);
1930
+ this.resize();
1931
+ const generation = this.generation;
1932
+ const dropCanvas = () => {
1933
+ if (this.canvas !== canvas) return;
1934
+ this.observer?.disconnect();
1935
+ this.observer = void 0;
1936
+ canvas.remove();
1937
+ this.canvas = void 0;
1938
+ };
1939
+ try {
1940
+ const renderer = await createRenderer(this.backend, { background: this.background });
1941
+ if (generation !== this.generation) {
1942
+ renderer.destroy();
1943
+ dropCanvas();
1944
+ return;
1945
+ }
1946
+ await renderer.init(canvas);
1947
+ if (generation !== this.generation) {
1948
+ renderer.destroy();
1949
+ dropCanvas();
1950
+ return;
1951
+ }
1952
+ this.renderer = renderer;
1953
+ this.resize();
1954
+ this.ready = true;
1955
+ } catch (error) {
1956
+ dropCanvas();
1957
+ throw error;
1958
+ }
1959
+ }
1960
+ /** Add a mesh to the scene and return a handle for animating it. */
1961
+ add(mesh, init) {
1962
+ const entry = {
1963
+ mesh,
1964
+ transform: transform(init),
1965
+ transparency: init?.transparency,
1966
+ remove: () => {
1967
+ const i = this.scene.indexOf(entry);
1968
+ if (i >= 0) this.scene.splice(i, 1);
1969
+ }
1970
+ };
1971
+ this.scene.push(entry);
1972
+ return entry;
1973
+ }
1974
+ resize() {
1975
+ const canvas = this.canvas;
1976
+ if (!canvas) return;
1977
+ const dpr = window.devicePixelRatio || 1;
1978
+ this.cssWidth = canvas.clientWidth || canvas.parentElement?.clientWidth || 0;
1979
+ this.cssHeight = canvas.clientHeight || canvas.parentElement?.clientHeight || 0;
1980
+ canvas.width = Math.max(1, Math.round(this.cssWidth * dpr));
1981
+ canvas.height = Math.max(1, Math.round(this.cssHeight * dpr));
1982
+ this.renderer?.resize(this.cssWidth, this.cssHeight, dpr);
1983
+ }
1984
+ /** Draw a single frame from the current scene state. */
1985
+ render() {
1986
+ if (!this.ready || !this.renderer) return;
1987
+ const width = this.cssWidth;
1988
+ const height = this.cssHeight;
1989
+ if (width === 0 || height === 0) return;
1990
+ const items = this.scene.map((entry) => ({
1991
+ mesh: entry.mesh,
1992
+ model: modelMatrix(entry.transform),
1993
+ transparency: entry.transparency
1994
+ }));
1995
+ const eye = this.camera.options.position;
1996
+ this.renderer.render({
1997
+ items: orderRenderItems(items, eye),
1998
+ viewProjection: this.camera.viewProjection(width / height),
1999
+ eye,
2000
+ light: this.light.params,
2001
+ width,
2002
+ height
2003
+ });
2004
+ }
2005
+ /** Start an internal animation loop that calls {@link render} each frame. */
2006
+ start() {
2007
+ if (this.running) return;
2008
+ this.running = true;
2009
+ const loop = () => {
2010
+ if (!this.running) return;
2011
+ this.render();
2012
+ this.rafId = requestAnimationFrame(loop);
2013
+ };
2014
+ this.rafId = requestAnimationFrame(loop);
2015
+ }
2016
+ /** Stop the internal animation loop started by {@link start}. */
2017
+ stop() {
2018
+ this.running = false;
2019
+ if (this.rafId) cancelAnimationFrame(this.rafId);
2020
+ this.rafId = 0;
2021
+ }
2022
+ /** Stop animating, release the renderer, and remove the canvas. */
2023
+ destroy() {
2024
+ this.generation++;
2025
+ this.ready = false;
2026
+ this.stop();
2027
+ this.observer?.disconnect();
2028
+ this.observer = void 0;
2029
+ this.renderer?.destroy();
2030
+ this.renderer = void 0;
2031
+ this.canvas?.remove();
2032
+ this.canvas = void 0;
2033
+ }
2034
+ };
2035
+
2036
+ // src/animations/particles.ts
2037
+ var DEFAULT_COLORS8 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
2038
+ var FADE_IN_END = 0.15;
2039
+ var FADE_OUT_START = 0.6;
2040
+ function rand01(seed, index, salt) {
2041
+ let h = (seed ^ Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
2042
+ h = Math.imul(h ^ h >>> 16, 73244475);
2043
+ h = Math.imul(h ^ h >>> 16, 73244475);
2044
+ h ^= h >>> 16;
2045
+ return (h >>> 0) / 4294967296;
2046
+ }
2047
+ function smoothstep(edge0, edge1, value) {
2048
+ const x = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
2049
+ return x * x * (3 - 2 * x);
2050
+ }
2051
+ function positiveFinite(value, name) {
2052
+ if (!Number.isFinite(value) || value <= 0) {
2053
+ throw new RangeError(`3d-spinner: ${name} must be a finite number greater than zero.`);
2054
+ }
2055
+ return value;
2056
+ }
2057
+ function emitBasis(direction) {
2058
+ const d = normalize(direction);
2059
+ const helper = Math.abs(d.y) < 0.99 ? { x: 0, y: 1, z: 0 } : { x: 1, y: 0, z: 0 };
2060
+ const right = normalize(cross(helper, d));
2061
+ return { d, right, up: cross(d, right) };
2062
+ }
2063
+ function particleField(options = {}) {
2064
+ const rate = positiveFinite(options.rate ?? 20, "rate");
2065
+ const lifeMs = positiveFinite(options.lifeMs ?? 1800, "lifeMs");
2066
+ const size = options.size ?? 0.16;
2067
+ const speed = options.speed ?? 0.6;
2068
+ const gravity = options.gravity;
2069
+ const spread = options.spread ?? 0.5;
2070
+ const peak = Math.max(0, Math.min(1, options.opacity ?? 0.9));
2071
+ const spin = options.spin ?? 2e-3;
2072
+ const alignToMotion = options.alignToMotion ?? false;
2073
+ const seed = options.seed ?? 1;
2074
+ const basis = options.direction && emitBasis(options.direction);
2075
+ const spawnGapMs = 1e3 / rate;
2076
+ const directionOf = (index) => {
2077
+ const u = rand01(seed, index, 0);
2078
+ const phi = 2 * Math.PI * rand01(seed, index, 1);
2079
+ if (!basis) {
2080
+ const z = 2 * u - 1;
2081
+ const r = Math.sqrt(Math.max(0, 1 - z * z));
2082
+ return { x: r * Math.cos(phi), y: r * Math.sin(phi), z };
2083
+ }
2084
+ const cos = 1 - u * (1 - Math.cos(spread));
2085
+ const sin = Math.sqrt(Math.max(0, 1 - cos * cos));
2086
+ const { d, right, up } = basis;
2087
+ return {
2088
+ x: d.x * cos + (right.x * Math.cos(phi) + up.x * Math.sin(phi)) * sin,
2089
+ y: d.y * cos + (right.y * Math.cos(phi) + up.y * Math.sin(phi)) * sin,
2090
+ z: d.z * cos + (right.z * Math.cos(phi) + up.z * Math.sin(phi)) * sin
2091
+ };
2092
+ };
2093
+ return {
2094
+ maxLive: Math.ceil(lifeMs * rate / 1e3) + 1,
2095
+ spawnGapMs,
2096
+ lifeMs,
2097
+ sample(index, t) {
2098
+ if (index < 0) return void 0;
2099
+ const age = t - index * spawnGapMs;
2100
+ if (age < 0 || age >= lifeMs) return void 0;
2101
+ const seconds = age / 1e3;
2102
+ const dir = directionOf(index);
2103
+ const particleSpeed = speed * (0.6 + 0.8 * rand01(seed, index, 2));
2104
+ const travel = particleSpeed * seconds;
2105
+ const pull = gravity ? 0.5 * seconds * seconds : 0;
2106
+ const life = age / lifeMs;
2107
+ const roll = alignToMotion ? Math.atan2(
2108
+ dir.y * particleSpeed + (gravity?.y ?? 0) * seconds,
2109
+ dir.x * particleSpeed + (gravity?.x ?? 0) * seconds
2110
+ ) : 2 * Math.PI * rand01(seed, index, 3) + (2 * rand01(seed, index, 4) - 1) * spin * age;
2111
+ return {
2112
+ position: {
2113
+ x: dir.x * travel + (gravity ? gravity.x * pull : 0),
2114
+ y: dir.y * travel + (gravity ? gravity.y * pull : 0),
2115
+ z: dir.z * travel + (gravity ? gravity.z * pull : 0)
2116
+ },
2117
+ roll,
2118
+ size: size * (0.7 + 0.6 * rand01(seed, index, 5)),
2119
+ opacity: peak * smoothstep(0, FADE_IN_END, life) * (1 - smoothstep(FADE_OUT_START, 1, life))
2120
+ };
2121
+ }
2122
+ };
2123
+ }
2124
+ var ParticlesAnimation = class {
2125
+ constructor(options = {}) {
2126
+ this.handles = [];
2127
+ this.fades = [];
2128
+ this.enterAt = Infinity;
2129
+ this.exitAt = Infinity;
2130
+ this.finished = false;
2131
+ this.field = particleField(options);
2132
+ this.colors = options.colors ?? DEFAULT_COLORS8;
2133
+ this.backend = options.backend;
2134
+ this.texture = options.texture;
2135
+ this.labelContent = options.label;
2136
+ this.fadeLabel = options.fadeLabel ?? true;
2137
+ this.emitter = options.emitter;
2138
+ this.outroMs = Math.max(0, options.outroMs ?? 0);
2139
+ }
2140
+ mount(target) {
2141
+ if (!target.style.position) target.style.position = "relative";
2142
+ const meshes = this.colors.map((color) => quad(1, [color]));
2143
+ const texture = this.texture;
2144
+ const backend = texture ? async (rendererOptions) => {
2145
+ 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);
2146
+ for (const mesh of meshes) renderer.setTexture(mesh, texture);
2147
+ return renderer;
2148
+ } : this.backend;
2149
+ const engine = new Little3dEngine({
2150
+ backend,
2151
+ camera: { position: { x: 0, y: 0, z: 3 } },
2152
+ light: { intensity: 0, ambient: 1 }
2153
+ });
2154
+ for (let slot = 0; slot < this.field.maxLive; slot++) {
2155
+ const fade = { mode: "one-sided", opacity: 0 };
2156
+ this.fades.push(fade);
2157
+ this.handles.push(engine.add(meshes[slot % meshes.length], { scale: 0, transparency: fade }));
2158
+ }
2159
+ this.engine = engine;
2160
+ engine.mount(target).catch((error) => {
2161
+ target.textContent = error instanceof Error ? error.message : String(error);
2162
+ });
2163
+ this.label = mountAnimationLabel(target, this.labelContent);
2164
+ if (this.fadeLabel) this.label.setOpacity(0);
2165
+ }
2166
+ enter(now) {
2167
+ if (this.enterAt === Infinity) this.enterAt = now;
2168
+ }
2169
+ exit(now) {
2170
+ if (this.exitAt === Infinity) this.exitAt = now;
2171
+ }
2172
+ isFinished() {
2173
+ return this.finished;
2174
+ }
2175
+ render(now, frame) {
2176
+ if (!this.engine || !this.label) return;
2177
+ if (this.exitAt !== Infinity && now >= this.exitAt + this.outroMs + this.field.lifeMs) this.finished = true;
2178
+ for (const handle of this.handles) handle.transform.scale = 0;
2179
+ if (this.enterAt !== Infinity) {
2180
+ const t = now - this.enterAt;
2181
+ const gap = this.field.spawnGapMs;
2182
+ let first = Math.max(0, Math.ceil((t - this.field.lifeMs) / gap));
2183
+ let last = Math.floor(t / gap);
2184
+ if (this.exitAt !== Infinity) {
2185
+ last = Math.min(last, Math.floor((this.exitAt - this.enterAt + this.outroMs) / gap));
2186
+ }
2187
+ first = Math.max(first, last - this.field.maxLive + 1);
2188
+ for (let index = first; index <= last; index++) {
2189
+ const sample = this.field.sample(index, t);
2190
+ if (!sample) continue;
2191
+ const slot = index % this.handles.length;
2192
+ const transform2 = this.handles[slot].transform;
2193
+ const origin = this.emitter?.positionAt(this.enterAt + index * gap);
2194
+ transform2.position.x = sample.position.x + (origin?.x ?? 0);
2195
+ transform2.position.y = sample.position.y + (origin?.y ?? 0);
2196
+ transform2.position.z = sample.position.z + (origin?.z ?? 0);
2197
+ transform2.rotation.z = sample.roll;
2198
+ transform2.scale = sample.size;
2199
+ this.fades[slot].opacity = sample.opacity;
2200
+ }
2201
+ }
2202
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
2203
+ if (this.fadeLabel) {
2204
+ this.label.setOpacity(animationLabelOpacity(
2205
+ now,
2206
+ this.enterAt,
2207
+ this.field.lifeMs * FADE_IN_END,
2208
+ this.exitAt,
2209
+ this.field.lifeMs
2210
+ ));
2211
+ }
2212
+ this.engine.render();
2213
+ }
2214
+ destroy() {
2215
+ this.label?.container.remove();
2216
+ this.label = void 0;
2217
+ this.engine?.destroy();
2218
+ this.engine = void 0;
2219
+ this.handles.length = 0;
2220
+ this.fades.length = 0;
2221
+ }
2222
+ };
2223
+
2224
+ // src/engines/little-tween-engine/core/tweens.ts
2225
+ function input(value, overextend) {
2226
+ if (Number.isNaN(value)) return 0;
2227
+ if (overextend) return value;
2228
+ return Math.min(1, Math.max(0, value));
2229
+ }
2230
+ function easeInQuad(value, overextend = false) {
2231
+ const x = input(value, overextend);
2232
+ return x * x;
2233
+ }
2234
+ function easeOutQuad(value, overextend = false) {
2235
+ const x = input(value, overextend);
2236
+ return 1 - (1 - x) * (1 - x);
2237
+ }
2238
+ function easeInCubic(value, overextend = false) {
2239
+ const x = input(value, overextend);
2240
+ return x * x * x;
2241
+ }
2242
+ function easeOutCubic(value, overextend = false) {
2243
+ const x = input(value, overextend);
2244
+ return 1 - Math.pow(1 - x, 3);
2245
+ }
2246
+ function easeInOutCubic(value, overextend = false) {
2247
+ const x = input(value, overextend);
2248
+ return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
2249
+ }
2250
+ function easeOutBack(value, overextend = false) {
2251
+ const x = input(value, overextend);
2252
+ const c1 = 1.70158;
2253
+ const c3 = c1 + 1;
2254
+ return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
2255
+ }
2256
+
2257
+ // src/animations/charged-orb.ts
2258
+ var MINIS = 10;
2259
+ var CAMERA_Z = 3;
2260
+ var CENTER_SCALE = 0.76;
2261
+ var MINI_SCALE = 0.36;
2262
+ var MINI_TRANSPARENCY = { mode: "two-sided", frontOpacity: 0.68, backOpacity: 0.87 };
2263
+ var ORBIT_RADIUS = 1.2;
2264
+ var TILT = 0.8;
2265
+ var TWO_PI = Math.PI * 2;
2266
+ var CENTER_POP_MS = 500;
2267
+ var LAUNCH_MS = 550;
2268
+ var EXIT_HURRY = 2.5;
2269
+ var SPREAD_TAU_MS = 250;
2270
+ var EXTRA_PAUSE_MS = 250;
2271
+ var EXTRA_SPIN_MS = 1300;
2272
+ var REENTER_MS = 600;
2273
+ var REENTER_STAGGER_MS = 45;
2274
+ var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
2275
+ var CENTER_POP_OUT_MS = 420;
2276
+ var PARKED = { x: 0, y: 0, z: 50 };
2277
+ var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
2278
+ var MINI_COLORS = [
2279
+ ["#e0f2fe", "#bae6fd", "#7dd3fc"],
2280
+ ["#c7d2fe", "#a5b4fc", "#818cf8"],
2281
+ ["#a5f3fc", "#67e8f9", "#22d3ee"]
2282
+ ];
2283
+ function clamp012(value) {
2284
+ return Math.max(0, Math.min(1, value));
2285
+ }
2286
+ var ChargedOrbAnimation = class {
2287
+ constructor(options = {}) {
2288
+ this.minis = [];
2289
+ this.blends = new Array(MINIS).fill(0);
2290
+ this.offsets = new Array(MINIS).fill(0);
2291
+ this.enterAt = Infinity;
2292
+ this.exitAt = Infinity;
2293
+ this.allOutAt = Infinity;
2294
+ this.lastNow = 0;
2295
+ this.finished = false;
2296
+ this.orbitPeriodMs = options.orbitPeriodMs ?? 6e3;
2297
+ this.backend = options.backend;
2298
+ }
2299
+ mount(target) {
2300
+ if (!target.style.position) target.style.position = "relative";
2301
+ const engine = new Little3dEngine({
2302
+ backend: this.backend,
2303
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
2304
+ });
2305
+ this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
2306
+ for (let i = 0; i < MINIS; i++) {
2307
+ const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
2308
+ this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
2309
+ }
2310
+ this.engine = engine;
2311
+ engine.mount(target).catch((error) => {
2312
+ target.textContent = error instanceof Error ? error.message : String(error);
2313
+ });
2314
+ }
2315
+ enter(now) {
2316
+ if (this.enterAt === Infinity) this.enterAt = now;
2317
+ }
2318
+ exit(now) {
2319
+ if (this.exitAt === Infinity) this.exitAt = now;
2320
+ }
2321
+ isFinished() {
2322
+ return this.finished;
2323
+ }
2324
+ /** Milliseconds after {@link exit} during which the satellites are still flying. */
2325
+ get outroEmitMs() {
2326
+ return EXTRA_PAUSE_MS + EXTRA_SPIN_MS + CENTER_POP_OUT_AT;
2327
+ }
2328
+ /**
2329
+ * A {@link MotionController} that cycles across the live satellites, one
2330
+ * spawn slot per orb, so a particle layer emits one stream per satellite.
2331
+ * `spawnGapMs` must match the particle layer's emission gap (`1000 / rate`).
2332
+ */
2333
+ satelliteEmitter(spawnGapMs) {
2334
+ return {
2335
+ positionAt: (t) => {
2336
+ const live = [];
2337
+ for (let i = 0; i < MINIS; i++) {
2338
+ const sample = this.miniSample(i, t);
2339
+ if (sample) live.push(sample.position);
2340
+ }
2341
+ if (live.length === 0) return PARKED;
2342
+ const slot = Math.abs(Math.floor(t / spawnGapMs)) % live.length;
2343
+ return live[slot];
2344
+ }
2345
+ };
2346
+ }
2347
+ render(now, frame) {
2348
+ if (!this.engine || !this.center) return;
2349
+ if (this.enterAt === Infinity) {
2350
+ this.center.transform.scale = 0;
2351
+ for (const mini of this.minis) mini.transform.scale = 0;
2352
+ this.engine.render();
2353
+ return;
2354
+ }
2355
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
2356
+ this.lastNow = now;
2357
+ this.updateBlends(dt, frame.progress, now);
2358
+ this.updateSpread(dt);
2359
+ const t = now - this.enterAt;
2360
+ this.center.transform.scale = this.centerScale(now, t);
2361
+ this.center.transform.rotation.x = t * 2e-4;
2362
+ this.center.transform.rotation.y = t * 5e-4;
2363
+ for (let i = 0; i < MINIS; i++) {
2364
+ const transform2 = this.minis[i].transform;
2365
+ const sample = this.miniSample(i, now);
2366
+ if (!sample) {
2367
+ transform2.scale = 0;
2368
+ continue;
2369
+ }
2370
+ transform2.position.x = sample.position.x;
2371
+ transform2.position.y = sample.position.y;
2372
+ transform2.position.z = sample.position.z;
2373
+ transform2.scale = sample.scale;
2374
+ transform2.rotation.y = t * 12e-4;
2375
+ }
2376
+ this.engine.render();
2377
+ }
2378
+ destroy() {
2379
+ this.engine?.destroy();
2380
+ this.engine = void 0;
2381
+ this.center = void 0;
2382
+ this.minis.length = 0;
2383
+ }
2384
+ updateBlends(dt, progress, now) {
2385
+ const exiting = this.exitAt !== Infinity;
2386
+ const want = exiting ? MINIS : Math.min(MINIS, Math.floor(progress * MINIS + 1e-9));
2387
+ const rate = dt / LAUNCH_MS * (exiting ? EXIT_HURRY : 1);
2388
+ for (let i = 0; i < MINIS; i++) {
2389
+ const target = i < want ? 1 : 0;
2390
+ const blend = this.blends[i];
2391
+ if (target > blend && (i === 0 || this.blends[i - 1] >= 0.6)) {
2392
+ this.blends[i] = Math.min(1, blend + rate);
2393
+ if (blend === 0) this.offsets[i] = this.slotAngle(i);
2394
+ } else if (target < blend && (i === MINIS - 1 || this.blends[i + 1] <= 0.4)) {
2395
+ this.blends[i] = Math.max(0, blend - rate);
2396
+ }
2397
+ }
2398
+ if (exiting && this.allOutAt === Infinity && this.blends.every((blend) => blend >= 1)) {
2399
+ this.allOutAt = now;
2400
+ }
2401
+ }
2402
+ updateSpread(dt) {
2403
+ const ease = 1 - Math.exp(-dt / SPREAD_TAU_MS);
2404
+ for (let i = 0; i < MINIS; i++) {
2405
+ if (this.blends[i] <= 0) continue;
2406
+ this.offsets[i] += (this.slotAngle(i) - this.offsets[i]) * ease;
2407
+ }
2408
+ }
2409
+ slotAngle(index) {
2410
+ const launched = Math.max(1, this.blends.filter((blend) => blend > 0).length);
2411
+ return TWO_PI * Math.min(index, launched - 1) / launched;
2412
+ }
2413
+ baseAngleAt(t) {
2414
+ let angle = -TWO_PI * (t - this.enterAt) / this.orbitPeriodMs;
2415
+ if (this.allOutAt !== Infinity) {
2416
+ const u = clamp012((t - this.allOutAt - EXTRA_PAUSE_MS) / EXTRA_SPIN_MS);
2417
+ angle -= TWO_PI * easeInOutCubic(u);
2418
+ }
2419
+ return angle;
2420
+ }
2421
+ reenterStart() {
2422
+ return this.allOutAt + EXTRA_PAUSE_MS + EXTRA_SPIN_MS;
2423
+ }
2424
+ miniSample(index, t) {
2425
+ const blend = this.blends[index];
2426
+ if (blend <= 0) return void 0;
2427
+ let radial = easeOutCubic(blend);
2428
+ let scale2 = MINI_SCALE * easeOutBack(blend);
2429
+ if (this.allOutAt !== Infinity) {
2430
+ const start = this.reenterStart() + index * REENTER_STAGGER_MS;
2431
+ const pull = easeInCubic(clamp012((t - start) / REENTER_MS));
2432
+ if (pull >= 1) return void 0;
2433
+ radial *= 1 - pull;
2434
+ scale2 *= 1 - pull;
2435
+ }
2436
+ const angle = this.baseAngleAt(t) + this.offsets[index];
2437
+ const flat = Math.sin(angle) * ORBIT_RADIUS * radial;
2438
+ return {
2439
+ position: {
2440
+ x: Math.cos(angle) * ORBIT_RADIUS * radial,
2441
+ y: flat * Math.cos(TILT),
2442
+ z: flat * Math.sin(TILT)
2443
+ },
2444
+ scale: scale2
2445
+ };
2446
+ }
2447
+ centerScale(now, t) {
2448
+ if (this.allOutAt !== Infinity) {
2449
+ const w = clamp012((now - this.reenterStart() - CENTER_POP_OUT_AT) / CENTER_POP_OUT_MS);
2450
+ if (w >= 1) {
2451
+ this.finished = true;
2452
+ return 0;
2453
+ }
2454
+ if (w > 0) {
2455
+ return CENTER_SCALE * (w < 0.35 ? 1 + 0.18 * easeOutQuad(w / 0.35) : 1.18 * (1 - easeInQuad((w - 0.35) / 0.65)));
2456
+ }
2457
+ }
2458
+ return CENTER_SCALE * easeOutBack(clamp012(t / CENTER_POP_MS));
2459
+ }
2460
+ };
2461
+
2462
+ // src/prefabs/spinner.ts
2463
+ function spinner(animation, options) {
2464
+ return {
2465
+ type: "indeterminate",
2466
+ animation,
2467
+ loop: options.loop,
2468
+ periodMs: options.periodMs
2469
+ };
2470
+ }
2471
+ function progressSpinner(animation, options) {
2472
+ return {
2473
+ type: "progress",
2474
+ animation,
2475
+ progress: options.progress ?? 1e-3,
2476
+ timeout: options.timeout,
2477
+ until: options.until
2478
+ };
2479
+ }
2480
+
2481
+ // src/prefabs/charged-orb.ts
2482
+ function chargedOrb(options = {}) {
2483
+ const particles = options.particles ?? {};
2484
+ const rate = particles.rate ?? 60;
2485
+ const orb = new ChargedOrbAnimation({
2486
+ backend: options.backend,
2487
+ ...options.orb
2488
+ });
2489
+ const streams = new ParticlesAnimation({
2490
+ rate,
2491
+ lifeMs: 1200,
2492
+ size: 0.12,
2493
+ speed: 0.05,
2494
+ colors: ["#ffffff", "#a5f3fc", "#818cf8"],
2495
+ texture: particles.texture ?? shineTexture(),
2496
+ emitter: orb.satelliteEmitter(1e3 / rate),
2497
+ outroMs: orb.outroEmitMs,
2498
+ seed: 5,
2499
+ backend: options.backend,
2500
+ ...particles,
2501
+ label: options.label ?? particles.label,
2502
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
2503
+ });
2504
+ return progressSpinner(new CompositeAnimation([orb, streams]), options);
2505
+ }
2506
+
2507
+ // src/animations/object-motion.ts
2508
+ init_math();
2509
+
2510
+ // src/motion/transitions.ts
2511
+ var DEFAULT_DISTANCE = 3.5;
2512
+ function add(a, b) {
2513
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
2514
+ }
2515
+ function scaleVector(v, factor) {
2516
+ return { x: v.x * factor, y: v.y * factor, z: v.z * factor };
2517
+ }
2518
+ function vectorLength(v) {
2519
+ return Math.hypot(v.x, v.y, v.z);
2520
+ }
2521
+ function normalizeVector(v) {
2522
+ const length = vectorLength(v);
2523
+ if (length < 1e-6) return { x: 1, y: 0, z: 0 };
2524
+ return scaleVector(v, 1 / length);
2525
+ }
2526
+ function resolveDirection(input2, fallback) {
2527
+ return normalizeVector(fallback ?? input2.direction ?? input2.velocity ?? { x: 1, y: 0, z: 0 });
2528
+ }
2529
+ function joinVelocity(input2, options, durationMs) {
2530
+ const inputSpeed = input2.velocity ? vectorLength(input2.velocity) : 0;
2531
+ if (input2.velocity && inputSpeed > 1e-6 && !options.direction) {
2532
+ return input2.velocity;
2533
+ }
2534
+ const distance = options.distance ?? DEFAULT_DISTANCE;
2535
+ return scaleVector(resolveDirection(input2, options.direction), distance / durationMs);
2536
+ }
2537
+ function enterFromObjectDirection(options = {}) {
2538
+ return (input2) => {
2539
+ const durationMs = Math.max(1, input2.durationMs);
2540
+ const velocity = joinVelocity(input2, options, durationMs);
2541
+ const remaining = durationMs - input2.elapsedMs;
2542
+ return { position: add(input2.position, scaleVector(velocity, -remaining)) };
2543
+ };
2544
+ }
2545
+ function leaveInObjectDirection(options = {}) {
2546
+ return (input2) => {
2547
+ const durationMs = Math.max(1, input2.durationMs);
2548
+ const velocity = joinVelocity(input2, options, durationMs);
2549
+ return { position: add(input2.position, scaleVector(velocity, input2.elapsedMs)) };
2550
+ };
2551
+ }
2552
+
2553
+ // src/animations/object-motion.ts
2554
+ var WORLD_UP = { x: 0, y: 1, z: 0 };
2555
+ var DEFAULT_INTRO_MS = 2100;
2556
+ var DEFAULT_OUTRO_MS = 2100;
2557
+ var BANK_GAIN = 26;
2558
+ var BANK_LIMIT = 0.7;
2559
+ var BANK_SMOOTH = 0.12;
2560
+ var SAMPLE_MS = 8;
2561
+ var FACE_FORWARD = {
2562
+ "+x": (v) => v,
2563
+ "-x": (v) => ({ x: -v.x, y: v.y, z: -v.z }),
2564
+ "+z": (v) => ({ x: v.z, y: v.y, z: -v.x }),
2565
+ "-z": (v) => ({ x: -v.z, y: v.y, z: v.x }),
2566
+ "+y": (v) => ({ x: v.y, y: -v.x, z: v.z }),
2567
+ "-y": (v) => ({ x: -v.y, y: v.x, z: v.z })
2568
+ };
2569
+ function add2(a, b) {
2570
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
2571
+ }
2572
+ function resolveMesh(mesh) {
2573
+ return typeof mesh === "function" ? mesh() : mesh;
2574
+ }
2575
+ function applyColor(mesh, color) {
2576
+ if (color === void 0) return mesh;
2577
+ return { vertices: mesh.vertices, faces: mesh.faces.map((face) => ({ ...face, color })) };
2578
+ }
2579
+ function faceForward(mesh, facing) {
2580
+ if (facing === "+x") return mesh;
2581
+ const turn = FACE_FORWARD[facing];
2582
+ return { vertices: mesh.vertices.map(turn), faces: mesh.faces };
2583
+ }
2584
+ function centerAndScaleMesh(mesh, targetSize) {
2585
+ let minX = Infinity;
2586
+ let minY = Infinity;
2587
+ let minZ = Infinity;
2588
+ let maxX = -Infinity;
2589
+ let maxY = -Infinity;
2590
+ let maxZ = -Infinity;
2591
+ for (const vertex of mesh.vertices) {
2592
+ minX = Math.min(minX, vertex.x);
2593
+ minY = Math.min(minY, vertex.y);
2594
+ minZ = Math.min(minZ, vertex.z);
2595
+ maxX = Math.max(maxX, vertex.x);
2596
+ maxY = Math.max(maxY, vertex.y);
2597
+ maxZ = Math.max(maxZ, vertex.z);
2598
+ }
2599
+ const centerX = (minX + maxX) / 2;
2600
+ const centerY = (minY + maxY) / 2;
2601
+ const centerZ = (minZ + maxZ) / 2;
2602
+ const extent = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1;
2603
+ const factor = targetSize / extent;
2604
+ return {
2605
+ vertices: mesh.vertices.map((vertex) => ({
2606
+ x: (vertex.x - centerX) * factor,
2607
+ y: (vertex.y - centerY) * factor,
2608
+ z: (vertex.z - centerZ) * factor
2609
+ })),
2610
+ faces: mesh.faces
2611
+ };
2612
+ }
2613
+ function orientationFor(forward, bank) {
2614
+ const fwd = normalize(forward);
2615
+ let right = cross(fwd, WORLD_UP);
2616
+ if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
2617
+ right = normalize(right);
2618
+ const levelUp = cross(right, fwd);
2619
+ const up = add2(scale(levelUp, Math.cos(bank)), scale(right, Math.sin(bank)));
2620
+ const w = normalize(cross(fwd, up));
2621
+ return {
2622
+ x: Math.atan2(cross(w, fwd).z, w.z),
2623
+ y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
2624
+ z: Math.atan2(fwd.y, fwd.x)
2625
+ };
2626
+ }
2627
+ function rotationMatrix(x, y, z) {
2628
+ return multiply(rotationZ(z), multiply(rotationY(y), rotationX(x)));
2629
+ }
2630
+ function eulerFromRotationMatrix(matrix) {
2631
+ const sy = Math.hypot(matrix[0], matrix[1]);
2632
+ if (sy > 1e-6) {
2633
+ return {
2634
+ x: Math.atan2(matrix[9], matrix[10]),
2635
+ y: Math.asin(Math.max(-1, Math.min(1, -matrix[8]))),
2636
+ z: Math.atan2(matrix[4], matrix[0])
2637
+ };
2638
+ }
2639
+ return {
2640
+ x: Math.atan2(-matrix[6], matrix[5]),
2641
+ y: Math.asin(Math.max(-1, Math.min(1, -matrix[8]))),
2642
+ z: 0
2643
+ };
2644
+ }
2645
+ function combineLocalRotation(path, extra) {
2646
+ return eulerFromRotationMatrix(
2647
+ multiply(rotationMatrix(path.x, path.y, path.z), rotationMatrix(extra.x, extra.y, extra.z))
2648
+ );
2649
+ }
2650
+ function clamp013(value) {
2651
+ return Math.max(0, Math.min(1, value));
2652
+ }
2653
+ function motionVectorAt(motion, t) {
2654
+ return scale(subtract(motion.positionAt(t + 1), motion.positionAt(t - 1)), 0.5);
2655
+ }
2656
+ function resolveDirection2(velocity, fallback) {
2657
+ return Math.hypot(velocity.x, velocity.y, velocity.z) > 1e-6 ? normalize(velocity) : fallback;
2658
+ }
2659
+ function resolveTransition(config, fallback, durationMs) {
2660
+ if (!config) return { transition: fallback, durationMs };
2661
+ if (typeof config === "function") return { transition: config, durationMs };
2662
+ return { transition: config.transition, durationMs: Math.max(0, config.durationMs ?? durationMs) };
2663
+ }
2664
+ var ObjectMotionAnimation = class {
2665
+ constructor(options) {
2666
+ this.handles = [];
2667
+ this.banks = [];
2668
+ this.headings = [];
2669
+ this.started = false;
2670
+ this.finished = false;
2671
+ this.introStart = 0;
2672
+ this.outroStart = Infinity;
2673
+ this.outroPosition = { x: 0, y: 0, z: 0 };
2674
+ this.outroVelocity = { x: 0, y: 0, z: 0 };
2675
+ this.outroDirection = { x: 1, y: 0, z: 0 };
2676
+ const centered = centerAndScaleMesh(resolveMesh(options.mesh), options.size ?? 1);
2677
+ const facing = faceForward(centered, options.facing ?? "+x");
2678
+ this.mesh = applyColor(facing, options.color);
2679
+ this.motion = options.motion;
2680
+ this.backend = options.backend;
2681
+ this.transparency = options.transparency;
2682
+ this.labelContent = options.label;
2683
+ this.fadeLabel = options.fadeLabel ?? true;
2684
+ this.tailCount = Math.max(0, Math.floor(options.tail?.count ?? 0));
2685
+ this.tailGap = Math.max(0, options.tail?.gapMs ?? 0);
2686
+ this.intro = resolveTransition(options.intro, enterFromObjectDirection(), DEFAULT_INTRO_MS);
2687
+ this.outro = resolveTransition(options.outro, leaveInObjectDirection(), DEFAULT_OUTRO_MS);
2688
+ const rotation = options.rotation;
2689
+ this.rotationOffset = { x: rotation?.x ?? 0, y: rotation?.y ?? 0, z: rotation?.z ?? 0 };
2690
+ this.rotationSpin = {
2691
+ x: rotation?.spinX ?? 0,
2692
+ y: rotation?.spinY ?? 0,
2693
+ z: rotation?.spinZ ?? 0
2694
+ };
2695
+ 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;
2696
+ }
2697
+ mount(target) {
2698
+ if (!target.style.position) target.style.position = "relative";
2699
+ const engine = new Little3dEngine({
2700
+ backend: this.backend,
2701
+ camera: { position: { x: 0, y: 0, z: 3 } }
2702
+ });
2703
+ for (let i = 0; i <= this.tailCount; i++) {
2704
+ this.handles.push(engine.add(this.mesh, { transparency: this.transparency }));
2705
+ this.banks.push(0);
2706
+ this.headings.push({ x: 1, y: 0, z: 0 });
2707
+ }
2708
+ this.engine = engine;
2709
+ engine.mount(target).catch((error) => {
2710
+ target.textContent = error instanceof Error ? error.message : String(error);
2711
+ });
2712
+ this.label = mountAnimationLabel(target, this.labelContent);
2713
+ if (this.fadeLabel) this.label.setOpacity(0);
2714
+ }
2715
+ enter(now) {
2716
+ if (this.started) return;
2717
+ this.started = true;
2718
+ this.introStart = now;
2719
+ }
2720
+ exit(now) {
2721
+ if (!this.started || this.outroStart !== Infinity) return;
2722
+ this.outroPosition = this.motion.positionAt(now);
2723
+ this.outroVelocity = motionVectorAt(this.motion, now);
2724
+ this.outroDirection = resolveDirection2(this.outroVelocity, this.headings[0]);
2725
+ this.outroStart = now;
2726
+ }
2727
+ isFinished() {
2728
+ return this.finished;
2729
+ }
2730
+ /** Milliseconds the fly-out takes; used to align a following particle trail's outro. */
2731
+ get outroDurationMs() {
2732
+ return this.outro.durationMs;
2733
+ }
2734
+ /**
2735
+ * A {@link MotionController} that follows the object's *actual* position, including
2736
+ * the intro fly-in and outro fly-out (it falls back to the raw motion path before
2737
+ * {@link enter} and once idle). Feed it to a particle layer's `emitter` so the
2738
+ * particles trail the object through its transitions instead of the bare path.
2739
+ */
2740
+ trailEmitter() {
2741
+ return { positionAt: (t) => this.sampleAt(t)?.position ?? this.motion.positionAt(t) };
2742
+ }
2743
+ render(now, frame) {
2744
+ if (!this.engine || !this.label) return;
2745
+ if (this.outroStart !== Infinity && now >= this.outroStart + this.outro.durationMs + this.tailCount * this.tailGap) {
2746
+ this.finished = true;
2747
+ }
2748
+ for (let k = 0; k < this.handles.length; k++) {
2749
+ const transform2 = this.handles[k].transform;
2750
+ const t = now - k * this.tailGap;
2751
+ const sample = this.sampleAt(t);
2752
+ if (!sample) {
2753
+ transform2.scale = 0;
2754
+ continue;
2755
+ }
2756
+ transform2.scale = sample.size;
2757
+ let euler = sample.orientation;
2758
+ if (!euler) {
2759
+ const heading = subtract(this.positionAt(t + SAMPLE_MS) ?? sample.position, sample.position);
2760
+ if (Math.hypot(heading.x, heading.y, heading.z) > 1e-5) {
2761
+ this.headings[k] = normalize(heading);
2762
+ }
2763
+ const ahead = this.aheadAt(t) ?? this.headings[k];
2764
+ const targetBank = Math.max(
2765
+ -BANK_LIMIT,
2766
+ Math.min(BANK_LIMIT, cross(this.headings[k], ahead).y * BANK_GAIN)
2767
+ );
2768
+ this.banks[k] += (targetBank - this.banks[k]) * BANK_SMOOTH;
2769
+ euler = orientationFor(this.headings[k], this.banks[k]);
2770
+ }
2771
+ if (this.hasExtraRotation) {
2772
+ euler = combineLocalRotation(euler, {
2773
+ x: this.rotationOffset.x + this.rotationSpin.x * t,
2774
+ y: this.rotationOffset.y + this.rotationSpin.y * t,
2775
+ z: this.rotationOffset.z + this.rotationSpin.z * t
2776
+ });
2777
+ }
2778
+ transform2.position.x = sample.position.x;
2779
+ transform2.position.y = sample.position.y;
2780
+ transform2.position.z = sample.position.z;
2781
+ transform2.rotation.x = euler.x;
2782
+ transform2.rotation.y = euler.y;
2783
+ transform2.rotation.z = euler.z;
2784
+ }
2785
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
2786
+ if (this.fadeLabel) {
2787
+ this.label.setOpacity(animationLabelOpacity(
2788
+ now,
2789
+ this.started ? this.introStart : Infinity,
2790
+ this.intro.durationMs,
2791
+ this.outroStart,
2792
+ this.outro.durationMs
2793
+ ));
2794
+ }
2795
+ this.engine.render();
2796
+ }
2797
+ destroy() {
2798
+ this.label?.container.remove();
2799
+ this.label = void 0;
2800
+ this.engine?.destroy();
2801
+ this.engine = void 0;
2802
+ this.handles.length = 0;
2803
+ }
2804
+ aheadAt(t) {
2805
+ const next = this.positionAt(t + SAMPLE_MS);
2806
+ const afterNext = this.positionAt(t + 2 * SAMPLE_MS);
2807
+ if (!next || !afterNext) return void 0;
2808
+ const ahead = subtract(afterNext, next);
2809
+ if (Math.hypot(ahead.x, ahead.y, ahead.z) <= 1e-5) return void 0;
2810
+ return normalize(ahead);
2811
+ }
2812
+ positionAt(t) {
2813
+ return this.sampleAt(t)?.position;
2814
+ }
2815
+ sampleAt(t) {
2816
+ if (!this.started || t < this.introStart) return void 0;
2817
+ if (t < this.introStart + this.intro.durationMs) {
2818
+ return this.transitionSample("intro", t, this.intro, this.introStart);
2819
+ }
2820
+ if (this.outroStart !== Infinity) {
2821
+ if (t > this.outroStart + this.outro.durationMs) return void 0;
2822
+ if (t >= this.outroStart) return this.transitionSample("outro", t, this.outro, this.outroStart);
2823
+ }
2824
+ return { position: this.motion.positionAt(t), size: 1 };
2825
+ }
2826
+ transitionSample(phase, t, transition, start) {
2827
+ const elapsedMs = Math.max(0, t - start);
2828
+ const delta = transition.durationMs === 0 ? 1 : clamp013(elapsedMs / transition.durationMs);
2829
+ const input2 = this.transitionInput(phase, delta, elapsedMs, transition.durationMs, start);
2830
+ const output = transition.transition(input2);
2831
+ return this.applyTransitionOutput(input2, output, phase, t);
2832
+ }
2833
+ transitionInput(phase, delta, elapsedMs, durationMs, start) {
2834
+ if (phase === "intro") {
2835
+ const handoff = start + durationMs;
2836
+ const velocity = motionVectorAt(this.motion, handoff);
2837
+ return {
2838
+ delta,
2839
+ position: this.motion.positionAt(handoff),
2840
+ direction: resolveDirection2(velocity, { x: 1, y: 0, z: 0 }),
2841
+ velocity,
2842
+ size: 1,
2843
+ durationMs,
2844
+ elapsedMs,
2845
+ phase
2846
+ };
2847
+ }
2848
+ return {
2849
+ delta,
2850
+ position: this.outroPosition,
2851
+ direction: this.outroDirection,
2852
+ velocity: this.outroVelocity,
2853
+ size: 1,
2854
+ durationMs,
2855
+ elapsedMs,
2856
+ phase
2857
+ };
2858
+ }
2859
+ applyTransitionOutput(input2, output, phase, t) {
2860
+ return {
2861
+ position: output.position ?? (phase === "intro" ? this.motion.positionAt(t) : input2.position),
2862
+ size: output.size ?? input2.size ?? 1,
2863
+ orientation: output.orientation
2864
+ };
2865
+ }
2866
+ };
2867
+
2868
+ // src/motion/figure-eight.ts
2869
+ var LOOP_X = 1.5;
2870
+ var LOOP_Y = 1;
2871
+ var LOOP_Z = 1.05;
2872
+ function figureEightMotion(options = {}) {
2873
+ const size = options.size ?? 1;
2874
+ const periodMs = options.periodMs ?? 3600;
2875
+ return {
2876
+ positionAt(t) {
2877
+ const a = t / periodMs * Math.PI * 2;
2878
+ return {
2879
+ x: size * LOOP_X * Math.sin(a),
2880
+ y: size * LOOP_Y * Math.sin(a) * Math.cos(a),
2881
+ z: size * LOOP_Z * Math.cos(a)
2882
+ };
2883
+ }
2884
+ };
2885
+ }
2886
+
2887
+ // src/prefabs/crystal-comet.ts
2888
+ function crystalComet(options = {}) {
2889
+ const motion = options.object?.motion ?? figureEightMotion({ size: 0.66, periodMs: 7200 });
2890
+ const particles = options.particles ?? {};
2891
+ const object = new ObjectMotionAnimation({
2892
+ mesh: () => tetrahedron(1, ["#f0f9ff", "#7dd3fc", "#818cf8", "#e879f9"]),
2893
+ motion,
2894
+ size: 0.42,
2895
+ rotation: { spinX: 2e-3, spinY: 3e-3 },
2896
+ backend: options.backend,
2897
+ ...options.object,
2898
+ label: options.object?.label
2899
+ });
2900
+ const animation = new CompositeAnimation([
2901
+ new ParticlesAnimation({
2902
+ rate: 44,
2903
+ lifeMs: 2300,
2904
+ size: 0.25,
2905
+ speed: 0.08,
2906
+ colors: ["#ffffff", "#bae6fd", "#818cf8"],
2907
+ texture: particles.texture ?? shineTexture(),
2908
+ emitter: object.trailEmitter(),
2909
+ outroMs: object.outroDurationMs,
2910
+ seed: 28,
2911
+ backend: options.backend,
2912
+ ...particles,
2913
+ label: options.label ?? particles.label ?? "Polishing pixels",
2914
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
2915
+ }),
2916
+ object
2917
+ ]);
2918
+ return spinner(animation, options);
2919
+ }
2920
+
2921
+ // src/motion/square.ts
2922
+ function squareMotion(options = {}) {
2923
+ const half = (options.size ?? 2.4) / 2;
2924
+ const periodMs = options.periodMs ?? 4e3;
2925
+ const tilt = options.tilt ?? 0.45;
2926
+ const direction = options.direction ?? 1;
2927
+ const cosTilt = Math.cos(tilt);
2928
+ const sinTilt = Math.sin(tilt);
2929
+ return {
2930
+ positionAt(t) {
2931
+ const lap = direction * t / periodMs;
2932
+ const s = (lap - Math.floor(lap)) * 4;
2933
+ const edge = Math.floor(s);
2934
+ const f = s - edge;
2935
+ let flatX;
2936
+ let flatY;
2937
+ if (edge === 0) {
2938
+ flatX = -half + 2 * half * f;
2939
+ flatY = -half;
2940
+ } else if (edge === 1) {
2941
+ flatX = half;
2942
+ flatY = -half + 2 * half * f;
2943
+ } else if (edge === 2) {
2944
+ flatX = half - 2 * half * f;
2945
+ flatY = half;
2946
+ } else {
2947
+ flatX = -half;
2948
+ flatY = half - 2 * half * f;
2949
+ }
2950
+ return { x: flatX, y: flatY * cosTilt, z: flatY * sinTilt };
2951
+ }
2952
+ };
2953
+ }
2954
+
2955
+ // src/prefabs/ghost-train.ts
2956
+ function ghostTrain(options = {}) {
2957
+ const motion = options.object?.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
2958
+ const particles = options.particles ?? {};
2959
+ const object = new ObjectMotionAnimation({
2960
+ mesh: () => cube(1, ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"]),
2961
+ motion,
2962
+ size: 0.3,
2963
+ transparency: { mode: "two-sided", opacity: 0.55 },
2964
+ tail: { count: 4, gapMs: 240 },
2965
+ backend: options.backend,
2966
+ ...options.object,
2967
+ label: options.object?.label
2968
+ });
2969
+ const trail = new ParticlesAnimation({
2970
+ rate: 30,
2971
+ lifeMs: 1700,
2972
+ size: 0.13,
2973
+ speed: 0.07,
2974
+ colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
2975
+ texture: particles.texture ?? starTexture(),
2976
+ emitter: object.trailEmitter(),
2977
+ outroMs: object.outroDurationMs,
2978
+ seed: 17,
2979
+ backend: options.backend,
2980
+ ...particles,
2981
+ label: options.label ?? particles.label,
2982
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
2983
+ });
2984
+ return progressSpinner(new CompositeAnimation([trail, object]), options);
2985
+ }
2986
+
2987
+ // src/animations/grid-assembly.ts
2988
+ var GRID = 5;
2989
+ var COUNT = GRID * GRID;
2990
+ var CAMERA_Z2 = 4;
2991
+ var FOV = 55 * Math.PI / 180;
2992
+ var TWO_PI2 = Math.PI * 2;
2993
+ var INTRO_MS = 900;
2994
+ var INTRO_STAGGER_MS = 60;
2995
+ var GATE_DOCK = 0.35;
2996
+ var GATE_UNDOCK = 0.65;
2997
+ var EXIT_HURRY2 = 2.5;
2998
+ var SPIN_EVERY_MS = 2e3;
2999
+ var SPIN_MS = 380;
3000
+ var SPIN_STAGGER_MS = 80;
3001
+ var HOLD_MS = 1e3;
3002
+ var COLLAPSE_MS = 700;
3003
+ var POP_MS = 170;
3004
+ var LABEL_FADE_MS = 600;
3005
+ var DEFAULT_MESHES = [
3006
+ () => cube(1, ["#60a5fa", "#3b82f6", "#2563eb", "#38bdf8", "#0ea5e9", "#1d4ed8"]),
3007
+ () => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
3008
+ () => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
3009
+ () => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
3010
+ () => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
3011
+ ];
3012
+ function clamp014(value) {
3013
+ return Math.max(0, Math.min(1, value));
3014
+ }
3015
+ function smooth01(value) {
3016
+ const x = clamp014(value);
3017
+ return x * x * (3 - 2 * x);
3018
+ }
3019
+ function resolveMesh2(mesh) {
3020
+ return typeof mesh === "function" ? mesh() : mesh;
3021
+ }
3022
+ function wrapAngle(angle) {
3023
+ const wrapped = angle - TWO_PI2 * Math.floor(angle / TWO_PI2);
3024
+ return wrapped > Math.PI ? wrapped - TWO_PI2 : wrapped;
3025
+ }
3026
+ function hash01(index, salt) {
3027
+ let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
3028
+ h = Math.imul(h ^ h >>> 16, 73244475);
3029
+ h ^= h >>> 16;
3030
+ return (h >>> 0) / 4294967296;
3031
+ }
3032
+ var GridAssemblyAnimation = class {
3033
+ constructor(options = {}) {
3034
+ this.handles = [];
3035
+ this.blends = new Array(COUNT).fill(0);
3036
+ this.dockedAt = new Array(COUNT).fill(Infinity);
3037
+ this.tumbleX = [];
3038
+ this.tumbleY = [];
3039
+ this.fades = [];
3040
+ this.slots = [];
3041
+ this.aspect = 16 / 9;
3042
+ this.enterAt = Infinity;
3043
+ this.exitAt = Infinity;
3044
+ this.allDockedAt = Infinity;
3045
+ this.collapseAt = Infinity;
3046
+ this.lastNow = 0;
3047
+ this.popFading = false;
3048
+ this.finished = false;
3049
+ const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
3050
+ this.meshes = sources.map(resolveMesh2);
3051
+ this.size = options.size ?? 0.34;
3052
+ this.orbitPeriodMs = options.orbitPeriodMs ?? 9e3;
3053
+ this.dockMs = options.dockMs ?? 800;
3054
+ this.backend = options.backend;
3055
+ this.labelContent = options.label;
3056
+ this.fadeLabel = options.fadeLabel ?? true;
3057
+ const spacing = this.size + (options.gap ?? 0.12);
3058
+ for (let i = 0; i < COUNT; i++) {
3059
+ const row = Math.floor(i / GRID);
3060
+ const col = i % GRID;
3061
+ this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
3062
+ this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
3063
+ this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
3064
+ }
3065
+ }
3066
+ mount(target) {
3067
+ if (!target.style.position) target.style.position = "relative";
3068
+ const engine = new Little3dEngine({
3069
+ backend: this.backend,
3070
+ camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
3071
+ });
3072
+ for (let i = 0; i < COUNT; i++) {
3073
+ this.fades.push({ mode: "one-sided", opacity: 1 });
3074
+ this.handles.push(engine.add(this.meshes[i % this.meshes.length], { scale: 0 }));
3075
+ }
3076
+ this.engine = engine;
3077
+ engine.mount(target).catch((error) => {
3078
+ target.textContent = error instanceof Error ? error.message : String(error);
3079
+ });
3080
+ const measure = () => {
3081
+ if (target.clientWidth > 0 && target.clientHeight > 0) {
3082
+ this.aspect = target.clientWidth / target.clientHeight;
3083
+ }
3084
+ };
3085
+ measure();
3086
+ this.observer = new ResizeObserver(measure);
3087
+ this.observer.observe(target);
3088
+ this.label = mountAnimationLabel(target, this.labelContent);
3089
+ if (this.fadeLabel) this.label.setOpacity(0);
3090
+ }
3091
+ enter(now) {
3092
+ if (this.enterAt === Infinity) this.enterAt = now;
3093
+ }
3094
+ exit(now) {
3095
+ if (this.exitAt === Infinity) this.exitAt = now;
3096
+ }
3097
+ isFinished() {
3098
+ return this.finished;
3099
+ }
3100
+ render(now, frame) {
3101
+ if (!this.engine || !this.label) return;
3102
+ if (this.enterAt === Infinity) {
3103
+ for (const handle of this.handles) handle.transform.scale = 0;
3104
+ this.engine.render();
3105
+ return;
3106
+ }
3107
+ const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
3108
+ this.lastNow = now;
3109
+ this.updateBlends(dt, frame.progress, now);
3110
+ if (this.exitAt !== Infinity && this.allDockedAt !== Infinity && this.collapseAt === Infinity) {
3111
+ this.collapseAt = Math.max(this.exitAt, this.allDockedAt) + HOLD_MS;
3112
+ }
3113
+ if (now >= this.collapseAt) this.renderCollapse(now);
3114
+ else this.renderStory(now, dt);
3115
+ this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
3116
+ if (this.fadeLabel) {
3117
+ this.label.setOpacity(animationLabelOpacity(
3118
+ now,
3119
+ this.enterAt,
3120
+ LABEL_FADE_MS,
3121
+ this.collapseAt,
3122
+ COLLAPSE_MS
3123
+ ));
3124
+ }
3125
+ if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS + POP_MS) {
3126
+ this.finished = true;
3127
+ }
3128
+ this.engine.render();
3129
+ }
3130
+ destroy() {
3131
+ this.observer?.disconnect();
3132
+ this.observer = void 0;
3133
+ this.label?.container.remove();
3134
+ this.label = void 0;
3135
+ this.engine?.destroy();
3136
+ this.engine = void 0;
3137
+ this.handles.length = 0;
3138
+ this.fades.length = 0;
3139
+ }
3140
+ updateBlends(dt, progress, now) {
3141
+ const exiting = this.exitAt !== Infinity;
3142
+ const want = exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
3143
+ const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
3144
+ for (let i = 0; i < COUNT; i++) {
3145
+ const target = i < want ? 1 : 0;
3146
+ const blend = this.blends[i];
3147
+ if (target > blend && (i === 0 || this.blends[i - 1] >= GATE_DOCK)) {
3148
+ this.blends[i] = Math.min(1, blend + rate);
3149
+ } else if (target < blend && (i === COUNT - 1 || this.blends[i + 1] <= GATE_UNDOCK)) {
3150
+ this.blends[i] = Math.max(0, blend - rate);
3151
+ }
3152
+ if (this.blends[i] >= 1) {
3153
+ if (this.dockedAt[i] === Infinity) this.dockedAt[i] = now;
3154
+ } else {
3155
+ this.dockedAt[i] = Infinity;
3156
+ }
3157
+ }
3158
+ const allDocked = want === COUNT && this.blends.every((blend) => blend >= 1);
3159
+ if (allDocked) {
3160
+ if (this.allDockedAt === Infinity) this.allDockedAt = now;
3161
+ } else if (!exiting) {
3162
+ this.allDockedAt = Infinity;
3163
+ }
3164
+ }
3165
+ renderStory(now, dt) {
3166
+ const t = now - this.enterAt;
3167
+ const halfHeight = Math.tan(FOV / 2) * CAMERA_Z2;
3168
+ const halfWidth = halfHeight * this.aspect;
3169
+ const radius = Math.max(0.6, Math.min(halfWidth, halfHeight) - this.size * 0.55);
3170
+ const spawnFactor = (Math.max(halfWidth, halfHeight) * 1.25 + this.size * 2) / radius;
3171
+ for (let i = 0; i < COUNT; i++) {
3172
+ const transform2 = this.handles[i].transform;
3173
+ const eased = smooth01(this.blends[i]);
3174
+ const introT = clamp014((t - i * INTRO_STAGGER_MS) / INTRO_MS);
3175
+ const reach = 1 + (spawnFactor - 1) * (1 - easeOutCubic(introT));
3176
+ const angle = Math.PI / 2 - i / COUNT * TWO_PI2 - t / this.orbitPeriodMs * TWO_PI2;
3177
+ const orbitX = Math.cos(angle) * radius * reach;
3178
+ const orbitY = Math.sin(angle) * radius * reach;
3179
+ const slot = this.slots[i];
3180
+ transform2.position.x = orbitX + (slot.x - orbitX) * eased;
3181
+ transform2.position.y = orbitY + (slot.y - orbitY) * eased;
3182
+ transform2.position.z = 0;
3183
+ transform2.scale = this.size;
3184
+ if (this.blends[i] === 0) {
3185
+ this.tumbleX[i] = wrapAngle(this.tumbleX[i] + (4e-4 + 8e-4 * hash01(i, 1)) * dt);
3186
+ this.tumbleY[i] = wrapAngle(this.tumbleY[i] + (6e-4 + 1e-3 * hash01(i, 3)) * dt);
3187
+ }
3188
+ transform2.rotation.z = 0;
3189
+ if (this.blends[i] >= 1) {
3190
+ const phase = (t + i * SPIN_STAGGER_MS) % SPIN_EVERY_MS;
3191
+ const spinning = phase < SPIN_MS && now - phase >= this.dockedAt[i];
3192
+ transform2.rotation.x = 0;
3193
+ transform2.rotation.y = spinning ? TWO_PI2 * easeInOutCubic(phase / SPIN_MS) : 0;
3194
+ } else {
3195
+ const free = 1 - eased;
3196
+ transform2.rotation.x = this.tumbleX[i] * free;
3197
+ transform2.rotation.y = this.tumbleY[i] * free;
3198
+ }
3199
+ }
3200
+ }
3201
+ renderCollapse(now) {
3202
+ if (!this.captured) {
3203
+ this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
3204
+ }
3205
+ const u = clamp014((now - this.collapseAt) / COLLAPSE_MS);
3206
+ const pull = easeInCubic(u);
3207
+ for (let i = 0; i < COUNT; i++) {
3208
+ const transform2 = this.handles[i].transform;
3209
+ const from = this.captured[i];
3210
+ transform2.position.x = from.x * (1 - pull);
3211
+ transform2.position.y = from.y * (1 - pull);
3212
+ transform2.position.z = from.z * (1 - pull);
3213
+ transform2.scale = this.size * (1 - 0.99 * pull);
3214
+ }
3215
+ if (u >= 1) {
3216
+ if (!this.popFading) {
3217
+ this.popFading = true;
3218
+ for (let i = 0; i < COUNT; i++) this.handles[i].transparency = this.fades[i];
3219
+ }
3220
+ const v = clamp014((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
3221
+ for (let i = 0; i < COUNT; i++) {
3222
+ this.fades[i].opacity = 1 - v;
3223
+ this.handles[i].transform.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
3224
+ }
3225
+ }
3226
+ }
3227
+ };
3228
+
3229
+ // src/prefabs/grid-assembly.ts
3230
+ function gridAssembly(options = {}) {
3231
+ return progressSpinner(
3232
+ new GridAssemblyAnimation({
3233
+ backend: options.backend,
3234
+ label: options.label,
3235
+ fadeLabel: options.fadeLabel,
3236
+ ...options.assembly
3237
+ }),
3238
+ options
3239
+ );
3240
+ }
3241
+
3242
+ // src/prefabs/monochrome-streak.ts
3243
+ function monochromeStreak(options = {}) {
3244
+ const particles = options.particles ?? {};
3245
+ return spinner(new ParticlesAnimation({
3246
+ rate: 70,
3247
+ lifeMs: 2800,
3248
+ size: 0.38,
3249
+ speed: 1.35,
3250
+ direction: { x: 0, y: 1, z: 0 },
3251
+ spread: 0.62,
3252
+ gravity: { x: 0, y: -1.45, z: 0 },
3253
+ colors: ["#fff", "#000"],
3254
+ texture: particles.texture ?? streakTexture(),
3255
+ spin: 0,
3256
+ alignToMotion: true,
3257
+ seed: 37,
3258
+ backend: options.backend,
3259
+ ...particles,
3260
+ label: options.label ?? particles.label ?? "Loading...",
3261
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3262
+ }), options);
3263
+ }
3264
+
3265
+ // src/prefabs/plane-star-trail.ts
3266
+ function planeStarTrail(options = {}) {
3267
+ const motion = options.object?.motion ?? figureEightMotion({ size: 0.72, periodMs: 6200 });
3268
+ const particles = options.particles ?? {};
3269
+ const object = new ObjectMotionAnimation({
3270
+ mesh: planeMesh,
3271
+ motion,
3272
+ size: 0.48,
3273
+ backend: options.backend,
3274
+ ...options.object,
3275
+ label: options.object?.label
3276
+ });
3277
+ const animation = new CompositeAnimation([
3278
+ new ParticlesAnimation({
3279
+ rate: 34,
3280
+ lifeMs: 1900,
3281
+ size: 0.16,
3282
+ speed: 0.11,
3283
+ colors: ["#fde047", "#f472b6", "#7dd3fc"],
3284
+ texture: particles.texture ?? starTexture(),
3285
+ emitter: object.trailEmitter(),
3286
+ outroMs: object.outroDurationMs,
3287
+ seed: 11,
3288
+ backend: options.backend,
3289
+ ...particles,
3290
+ label: options.label ?? particles.label ?? "Flying in...",
3291
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3292
+ }),
3293
+ object
3294
+ ]);
3295
+ return spinner(animation, options);
3296
+ }
3297
+
3298
+ // src/prefabs/pulsing-starfield.ts
3299
+ function pulsingLabel() {
3300
+ const label = document.createElement("div");
3301
+ label.innerHTML = `<style>
3302
+ @keyframes spinner-prefab-pulse { 0%,100% { color:#fff; transform:scale(1); } 50% { color:#93c5fd; transform:scale(1.06); } }
3303
+ </style><div style="animation:spinner-prefab-pulse 2.4s ease-in-out infinite;font-size:2rem">Loading the good stuff</div>`;
3304
+ return label;
3305
+ }
3306
+ function pulsingStarfield(options = {}) {
3307
+ const particles = options.particles ?? {};
3308
+ return spinner(new ParticlesAnimation({
3309
+ rate: 48,
3310
+ lifeMs: 4200,
3311
+ size: 0.3,
3312
+ speed: 0.34,
3313
+ colors: ["#ffffff", "#dbeafe", "#93c5fd", "#c4b5fd"],
3314
+ texture: particles.texture ?? shineTexture(),
3315
+ seed: 71,
3316
+ backend: options.backend,
3317
+ ...particles,
3318
+ label: options.label ?? particles.label ?? pulsingLabel(),
3319
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3320
+ }), options);
3321
+ }
3322
+
3323
+ // src/motion/circle.ts
3324
+ function circleMotion(options = {}) {
3325
+ const radius = options.radius ?? 1.3;
3326
+ const periodMs = options.periodMs ?? 3e3;
3327
+ const tilt = options.tilt ?? 0.5;
3328
+ const direction = options.direction ?? 1;
3329
+ const cosTilt = Math.cos(tilt);
3330
+ const sinTilt = Math.sin(tilt);
3331
+ return {
3332
+ positionAt(t) {
3333
+ const angle = direction * t / periodMs * Math.PI * 2;
3334
+ const x = radius * Math.cos(angle);
3335
+ const flatY = radius * Math.sin(angle);
3336
+ return { x, y: flatY * cosTilt, z: flatY * sinTilt };
3337
+ }
3338
+ };
3339
+ }
3340
+
3341
+ // src/prefabs/rocket-launch.ts
3342
+ var launchUp = ({ delta, position, velocity, durationMs }) => {
3343
+ const coast = durationMs * delta * (1 - 0.5 * delta);
3344
+ const climb = 5.5 * easeInCubic(delta);
3345
+ return {
3346
+ position: {
3347
+ x: position.x + (velocity?.x ?? 0) * coast,
3348
+ y: position.y + (velocity?.y ?? 0) * coast + climb,
3349
+ z: position.z + (velocity?.z ?? 0) * coast
3350
+ }
3351
+ };
3352
+ };
3353
+ function rocketLaunch(options = {}) {
3354
+ const motion = options.object?.motion ?? circleMotion({ radius: 0.55, periodMs: 7e3, tilt: 0.15 });
3355
+ const particles = options.particles ?? {};
3356
+ const object = new ObjectMotionAnimation({
3357
+ mesh: () => pyramid(1, ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"]),
3358
+ motion,
3359
+ size: 0.44,
3360
+ facing: "+y",
3361
+ outro: { transition: launchUp, durationMs: 1300 },
3362
+ backend: options.backend,
3363
+ ...options.object,
3364
+ label: options.object?.label
3365
+ });
3366
+ const exhaust = new ParticlesAnimation({
3367
+ rate: 46,
3368
+ lifeMs: 1100,
3369
+ size: 0.18,
3370
+ speed: 0.5,
3371
+ direction: { x: 0, y: -1, z: 0 },
3372
+ spread: 0.3,
3373
+ gravity: { x: 0, y: -0.6, z: 0 },
3374
+ colors: ["#fde047", "#fb923c", "#ef4444", "#fef3c7"],
3375
+ texture: particles.texture ?? shineTexture(),
3376
+ emitter: object.trailEmitter(),
3377
+ outroMs: object.outroDurationMs,
3378
+ seed: 9,
3379
+ backend: options.backend,
3380
+ ...particles,
3381
+ label: options.label ?? particles.label,
3382
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3383
+ });
3384
+ return progressSpinner(new CompositeAnimation([exhaust, object]), options);
3385
+ }
3386
+
3387
+ // src/motion/wander.ts
3388
+ function mulberry32(seed) {
3389
+ let a = seed >>> 0;
3390
+ return () => {
3391
+ a = a + 1831565813 | 0;
3392
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
3393
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
3394
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
3395
+ };
3396
+ }
3397
+ function axisDrift(rnd, omega, bound) {
3398
+ const components = [0, 1, 2].map(() => ({
3399
+ freq: 0.5 + rnd() * 1.4,
3400
+ phase: rnd() * Math.PI * 2,
3401
+ weight: 0.5 + rnd()
3402
+ }));
3403
+ const total = components.reduce((sum, c) => sum + c.weight, 0);
3404
+ return (t) => {
3405
+ let value = 0;
3406
+ for (const c of components) value += c.weight * Math.sin(omega * c.freq * t + c.phase);
3407
+ return value / total * bound;
3408
+ };
3409
+ }
3410
+ function wanderMotion(options = {}) {
3411
+ const boundX = options.bounds?.x ?? 1.4;
3412
+ const boundY = options.bounds?.y ?? 1;
3413
+ const boundZ = options.bounds?.z ?? 0.6;
3414
+ const periodMs = options.periodMs ?? 9e3;
3415
+ const seed = options.seed ?? Math.random() * 1e9 | 0;
3416
+ const rnd = mulberry32(seed);
3417
+ const omega = 2 * Math.PI / periodMs;
3418
+ const driftX = axisDrift(rnd, omega, boundX);
3419
+ const driftY = axisDrift(rnd, omega, boundY);
3420
+ const driftZ = axisDrift(rnd, omega, boundZ);
3421
+ return {
3422
+ positionAt(t) {
3423
+ return { x: driftX(t), y: driftY(t), z: driftZ(t) };
3424
+ }
3425
+ };
3426
+ }
3427
+
3428
+ // src/prefabs/star-swarm.ts
3429
+ function starSwarm(options = {}) {
3430
+ const particles = options.particles ?? {};
3431
+ const emitter = particles.emitter ?? wanderMotion({
3432
+ bounds: { x: 1.1, y: 0.72, z: 0.35 },
3433
+ periodMs: 7200,
3434
+ seed: 19
3435
+ });
3436
+ return spinner(new ParticlesAnimation({
3437
+ rate: 38,
3438
+ lifeMs: 2600,
3439
+ size: 0.15,
3440
+ speed: 0.17,
3441
+ colors: ["#fef08a", "#f9a8d4", "#a5f3fc"],
3442
+ texture: particles.texture ?? starTexture(),
3443
+ emitter,
3444
+ seed: 91,
3445
+ backend: options.backend,
3446
+ ...particles,
3447
+ label: options.label ?? particles.label ?? "Loading...",
3448
+ fadeLabel: options.fadeLabel ?? particles.fadeLabel
3449
+ }), options);
3450
+ }