3d-spinner 0.9.1 → 0.9.2

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.
@@ -0,0 +1,2408 @@
1
+ var Spinner3D = (() => {
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 vec3(x, y, z) {
30
+ return { x, y, z };
31
+ }
32
+ function subtract(a, b) {
33
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
34
+ }
35
+ function cross(a, b) {
36
+ return {
37
+ x: a.y * b.z - a.z * b.y,
38
+ y: a.z * b.x - a.x * b.z,
39
+ z: a.x * b.y - a.y * b.x
40
+ };
41
+ }
42
+ function dot(a, b) {
43
+ return a.x * b.x + a.y * b.y + a.z * b.z;
44
+ }
45
+ function scale(v, s) {
46
+ return { x: v.x * s, y: v.y * s, z: v.z * s };
47
+ }
48
+ function normalize(v) {
49
+ const length = Math.hypot(v.x, v.y, v.z);
50
+ if (length === 0) return { x: 0, y: 0, z: 0 };
51
+ return { x: v.x / length, y: v.y / length, z: v.z / length };
52
+ }
53
+ function multiply(a, b) {
54
+ const out = new Array(16);
55
+ for (let col = 0; col < 4; col++) {
56
+ for (let row = 0; row < 4; row++) {
57
+ let sum = 0;
58
+ for (let k = 0; k < 4; k++) {
59
+ sum += a[k * 4 + row] * b[col * 4 + k];
60
+ }
61
+ out[col * 4 + row] = sum;
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+ function translation(x, y, z) {
67
+ return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
68
+ }
69
+ function scaleMatrix(s) {
70
+ return [s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1];
71
+ }
72
+ function rotationX(rad) {
73
+ const c = Math.cos(rad);
74
+ const s = Math.sin(rad);
75
+ return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
76
+ }
77
+ function rotationY(rad) {
78
+ const c = Math.cos(rad);
79
+ const s = Math.sin(rad);
80
+ return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
81
+ }
82
+ function rotationZ(rad) {
83
+ const c = Math.cos(rad);
84
+ const s = Math.sin(rad);
85
+ return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
86
+ }
87
+ function perspective(fovY, aspect, near, far) {
88
+ const f = 1 / Math.tan(fovY / 2);
89
+ const nf = 1 / (near - far);
90
+ return [
91
+ f / aspect,
92
+ 0,
93
+ 0,
94
+ 0,
95
+ 0,
96
+ f,
97
+ 0,
98
+ 0,
99
+ 0,
100
+ 0,
101
+ (far + near) * nf,
102
+ -1,
103
+ 0,
104
+ 0,
105
+ 2 * far * near * nf,
106
+ 0
107
+ ];
108
+ }
109
+ function transformPoint(m, p) {
110
+ const x = m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12];
111
+ const y = m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13];
112
+ const z = m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14];
113
+ const w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15] || 1;
114
+ return { x: x / w, y: y / w, z: z / w };
115
+ }
116
+ function transformAffine(m, p) {
117
+ return {
118
+ x: m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
119
+ y: m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
120
+ z: m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]
121
+ };
122
+ }
123
+ var init_math = __esm({
124
+ "src/engines/little-3d-engine/core/math.ts"() {
125
+ "use strict";
126
+ }
127
+ });
128
+
129
+ // src/engines/little-3d-engine/core/geometry.ts
130
+ function parseColor(color) {
131
+ const hex = color.trim().replace("#", "");
132
+ const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
133
+ const n = parseInt(full, 16);
134
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
135
+ }
136
+ function expandToTriangles(mesh) {
137
+ let triangles = 0;
138
+ for (const face of mesh.faces) triangles += Math.max(0, face.indices.length - 2);
139
+ const positions = new Float32Array(triangles * 9);
140
+ const normals = new Float32Array(triangles * 9);
141
+ const colors = new Float32Array(triangles * 9);
142
+ let o = 0;
143
+ for (const face of mesh.faces) {
144
+ const v0 = mesh.vertices[face.indices[0]];
145
+ const v1 = mesh.vertices[face.indices[1]];
146
+ const v2 = mesh.vertices[face.indices[2]];
147
+ const normal = normalize(cross(subtract(v1, v0), subtract(v2, v0)));
148
+ const [r, g, b] = parseColor(face.color);
149
+ const cr = r / 255;
150
+ const cg = g / 255;
151
+ const cb = b / 255;
152
+ for (let k = 1; k < face.indices.length - 1; k++) {
153
+ const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
154
+ for (const index of tri) {
155
+ const v = mesh.vertices[index];
156
+ positions[o] = v.x;
157
+ positions[o + 1] = v.y;
158
+ positions[o + 2] = v.z;
159
+ normals[o] = normal.x;
160
+ normals[o + 1] = normal.y;
161
+ normals[o + 2] = normal.z;
162
+ colors[o] = cr;
163
+ colors[o + 1] = cg;
164
+ colors[o + 2] = cb;
165
+ o += 3;
166
+ }
167
+ }
168
+ }
169
+ return { positions, normals, colors, count: positions.length / 3 };
170
+ }
171
+ function midpoint(a, b) {
172
+ return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
173
+ }
174
+ function sphereFromTriangles(seedVertices, seedFaces, size, detail, colors) {
175
+ const radius = size / 2;
176
+ let triangles = seedFaces.map((f) => f.map((i) => normalize(seedVertices[i])));
177
+ const levels = Math.max(0, Math.floor(detail) - 1);
178
+ for (let level = 0; level < levels; level++) {
179
+ const next = [];
180
+ for (const [a, b, c] of triangles) {
181
+ const ab = normalize(midpoint(a, b));
182
+ const bc = normalize(midpoint(b, c));
183
+ const ca = normalize(midpoint(c, a));
184
+ next.push([a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]);
185
+ }
186
+ triangles = next;
187
+ }
188
+ const vertices = [];
189
+ const faces = triangles.map((tri, i) => {
190
+ const base = vertices.length;
191
+ vertices.push(scale(tri[0], radius), scale(tri[1], radius), scale(tri[2], radius));
192
+ return { indices: [base, base + 1, base + 2], color: colors[i % colors.length] };
193
+ });
194
+ return { vertices, faces };
195
+ }
196
+ var init_geometry = __esm({
197
+ "src/engines/little-3d-engine/core/geometry.ts"() {
198
+ "use strict";
199
+ init_math();
200
+ }
201
+ });
202
+
203
+ // src/engines/little-3d-engine/core/light.ts
204
+ function clamp012(value) {
205
+ return Math.min(1, Math.max(0, value));
206
+ }
207
+ function shadeColor(normal, color, light) {
208
+ const lambert = Math.max(0, dot(normal, light.toLight));
209
+ const brightness = clamp012(light.ambient + light.intensity * lambert);
210
+ const [r, g, b] = parseColor(color);
211
+ return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
212
+ b * brightness
213
+ )})`;
214
+ }
215
+ var DEFAULTS2, Light;
216
+ var init_light = __esm({
217
+ "src/engines/little-3d-engine/core/light.ts"() {
218
+ "use strict";
219
+ init_math();
220
+ init_geometry();
221
+ DEFAULTS2 = {
222
+ direction: { x: -0.4, y: -0.7, z: -0.6 },
223
+ intensity: 0.85,
224
+ ambient: 0.25
225
+ };
226
+ Light = class {
227
+ constructor(options) {
228
+ this.options = { ...DEFAULTS2, ...options };
229
+ this.params = {
230
+ toLight: normalize(scale(this.options.direction, -1)),
231
+ intensity: this.options.intensity,
232
+ ambient: this.options.ambient
233
+ };
234
+ }
235
+ /** Convenience wrapper around {@link shadeColor} using this light. */
236
+ shade(normal, color) {
237
+ return shadeColor(normal, color, this.params);
238
+ }
239
+ };
240
+ }
241
+ });
242
+
243
+ // src/engines/little-3d-engine/renderers/webgl.ts
244
+ var webgl_exports = {};
245
+ __export(webgl_exports, {
246
+ WebGLRenderer: () => WebGLRenderer
247
+ });
248
+ function compile(gl, type, source) {
249
+ const shader = gl.createShader(type);
250
+ gl.shaderSource(shader, source);
251
+ gl.compileShader(shader);
252
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
253
+ throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
254
+ }
255
+ return shader;
256
+ }
257
+ function link(gl) {
258
+ const program = gl.createProgram();
259
+ gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER));
260
+ gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER));
261
+ gl.linkProgram(program);
262
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
263
+ throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
264
+ }
265
+ return program;
266
+ }
267
+ var VERTEX_SHADER, FRAGMENT_SHADER, WebGLRenderer;
268
+ var init_webgl = __esm({
269
+ "src/engines/little-3d-engine/renderers/webgl.ts"() {
270
+ "use strict";
271
+ init_geometry();
272
+ init_renderer();
273
+ VERTEX_SHADER = `#version 300 es
274
+ in vec3 aPos;
275
+ in vec3 aNormal;
276
+ in vec3 aColor;
277
+ uniform mat4 uViewProj;
278
+ uniform mat4 uModel;
279
+ out vec3 vNormal;
280
+ out vec3 vColor;
281
+ void main() {
282
+ vNormal = mat3(uModel) * aNormal;
283
+ vColor = aColor;
284
+ gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
285
+ }`;
286
+ FRAGMENT_SHADER = `#version 300 es
287
+ precision mediump float;
288
+ in vec3 vNormal;
289
+ in vec3 vColor;
290
+ uniform vec3 uToLight;
291
+ uniform float uIntensity;
292
+ uniform float uAmbient;
293
+ uniform float uOpacity;
294
+ out vec4 fragColor;
295
+ void main() {
296
+ float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
297
+ float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
298
+ fragColor = vec4(vColor * brightness, uOpacity);
299
+ }`;
300
+ WebGLRenderer = class {
301
+ constructor(options = {}) {
302
+ this.cache = /* @__PURE__ */ new Map();
303
+ if (options.background) {
304
+ const [r, g, b] = parseColor(options.background);
305
+ this.clearColor = [r / 255, g / 255, b / 255, 1];
306
+ } else {
307
+ this.clearColor = [0, 0, 0, 0];
308
+ }
309
+ }
310
+ init(canvas) {
311
+ const gl = canvas.getContext("webgl2");
312
+ if (!gl) throw new Error("3d-spinner: WebGL2 is not supported in this browser.");
313
+ this.gl = gl;
314
+ this.program = link(gl);
315
+ this.locations = {
316
+ aPos: gl.getAttribLocation(this.program, "aPos"),
317
+ aNormal: gl.getAttribLocation(this.program, "aNormal"),
318
+ aColor: gl.getAttribLocation(this.program, "aColor"),
319
+ uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
320
+ uModel: gl.getUniformLocation(this.program, "uModel"),
321
+ uToLight: gl.getUniformLocation(this.program, "uToLight"),
322
+ uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
323
+ uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
324
+ uOpacity: gl.getUniformLocation(this.program, "uOpacity")
325
+ };
326
+ gl.enable(gl.DEPTH_TEST);
327
+ gl.enable(gl.CULL_FACE);
328
+ gl.cullFace(gl.BACK);
329
+ gl.frontFace(gl.CCW);
330
+ }
331
+ resize() {
332
+ const gl = this.gl;
333
+ if (!gl) return;
334
+ const canvas = gl.canvas;
335
+ gl.viewport(0, 0, canvas.width, canvas.height);
336
+ }
337
+ buffers(mesh) {
338
+ const cached = this.cache.get(mesh);
339
+ if (cached) return cached;
340
+ const gl = this.gl;
341
+ const loc = this.locations;
342
+ const data = expandToTriangles(mesh);
343
+ const vao = gl.createVertexArray();
344
+ gl.bindVertexArray(vao);
345
+ const attribute = (location, array) => {
346
+ if (location < 0) return;
347
+ const buffer = gl.createBuffer();
348
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
349
+ gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
350
+ gl.enableVertexAttribArray(location);
351
+ gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
352
+ };
353
+ attribute(loc.aPos, data.positions);
354
+ attribute(loc.aNormal, data.normals);
355
+ attribute(loc.aColor, data.colors);
356
+ gl.bindVertexArray(null);
357
+ const result = { vao, count: data.count };
358
+ this.cache.set(mesh, result);
359
+ return result;
360
+ }
361
+ render(frame) {
362
+ const gl = this.gl;
363
+ const loc = this.locations;
364
+ if (!gl || !this.program || !loc) return;
365
+ gl.clearColor(...this.clearColor);
366
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
367
+ gl.useProgram(this.program);
368
+ gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
369
+ gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
370
+ gl.uniform1f(loc.uIntensity, frame.light.intensity);
371
+ gl.uniform1f(loc.uAmbient, frame.light.ambient);
372
+ gl.disable(gl.BLEND);
373
+ gl.depthMask(true);
374
+ gl.cullFace(gl.BACK);
375
+ for (const item of frame.items) {
376
+ if (item.transparency) continue;
377
+ const mesh = this.buffers(item.mesh);
378
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
379
+ gl.uniform1f(loc.uOpacity, 1);
380
+ gl.bindVertexArray(mesh.vao);
381
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
382
+ }
383
+ gl.enable(gl.BLEND);
384
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
385
+ gl.depthMask(false);
386
+ for (const item of frame.items) {
387
+ const transparency = item.transparency;
388
+ if (!transparency) continue;
389
+ const mesh = this.buffers(item.mesh);
390
+ gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
391
+ gl.bindVertexArray(mesh.vao);
392
+ if (transparency.mode === "two-sided") {
393
+ const resolved = resolveTwoSidedOpacity(transparency);
394
+ gl.cullFace(gl.FRONT);
395
+ gl.uniform1f(loc.uOpacity, resolved.back);
396
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
397
+ gl.cullFace(gl.BACK);
398
+ gl.uniform1f(loc.uOpacity, resolved.front);
399
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
400
+ } else {
401
+ gl.cullFace(gl.BACK);
402
+ gl.uniform1f(loc.uOpacity, opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY));
403
+ gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
404
+ }
405
+ }
406
+ gl.depthMask(true);
407
+ gl.disable(gl.BLEND);
408
+ gl.cullFace(gl.BACK);
409
+ gl.bindVertexArray(null);
410
+ }
411
+ destroy() {
412
+ const gl = this.gl;
413
+ if (gl) {
414
+ for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
415
+ if (this.program) gl.deleteProgram(this.program);
416
+ }
417
+ this.cache.clear();
418
+ this.gl = void 0;
419
+ this.program = void 0;
420
+ this.locations = void 0;
421
+ }
422
+ };
423
+ }
424
+ });
425
+
426
+ // src/engines/little-3d-engine/renderers/webgpu.ts
427
+ var webgpu_exports = {};
428
+ __export(webgpu_exports, {
429
+ WebGPURenderer: () => WebGPURenderer
430
+ });
431
+ var WGSL, CLIP_Z_FIX, UNIFORM_STRIDE, WebGPURenderer;
432
+ var init_webgpu = __esm({
433
+ "src/engines/little-3d-engine/renderers/webgpu.ts"() {
434
+ "use strict";
435
+ init_geometry();
436
+ init_math();
437
+ init_renderer();
438
+ WGSL = `
439
+ struct Uniforms {
440
+ viewProj: mat4x4<f32>,
441
+ model: mat4x4<f32>,
442
+ toLight: vec4<f32>,
443
+ params: vec4<f32>,
444
+ };
445
+ @group(0) @binding(0) var<uniform> u: Uniforms;
446
+
447
+ struct VSOut {
448
+ @builtin(position) position: vec4<f32>,
449
+ @location(0) normal: vec3<f32>,
450
+ @location(1) color: vec3<f32>,
451
+ };
452
+
453
+ @vertex
454
+ fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
455
+ var out: VSOut;
456
+ let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
457
+ out.normal = m * normal;
458
+ out.color = color;
459
+ out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
460
+ return out;
461
+ }
462
+
463
+ @fragment
464
+ fn fs(in: VSOut) -> @location(0) vec4<f32> {
465
+ let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
466
+ let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
467
+ return vec4<f32>(in.color * brightness, u.params.z);
468
+ }
469
+ `;
470
+ CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
471
+ UNIFORM_STRIDE = 256;
472
+ WebGPURenderer = class {
473
+ constructor(options = {}) {
474
+ this.uniformCapacity = 0;
475
+ this.depthSize = "";
476
+ this.destroyed = false;
477
+ this.cache = /* @__PURE__ */ new Map();
478
+ if (options.background) {
479
+ const [r, g, b] = parseColor(options.background);
480
+ this.clearValue = { r: r / 255, g: g / 255, b: b / 255, a: 1 };
481
+ this.alphaMode = "opaque";
482
+ } else {
483
+ this.clearValue = { r: 0, g: 0, b: 0, a: 0 };
484
+ this.alphaMode = "premultiplied";
485
+ }
486
+ }
487
+ async init(canvas) {
488
+ const gpu = navigator.gpu;
489
+ if (!gpu) throw new Error("3d-spinner: WebGPU is not supported in this browser.");
490
+ const adapter = await gpu.requestAdapter();
491
+ if (!adapter) throw new Error("3d-spinner: no WebGPU adapter is available.");
492
+ const device = await adapter.requestDevice();
493
+ if (this.destroyed) {
494
+ device.destroy?.();
495
+ return;
496
+ }
497
+ const context = canvas.getContext("webgpu");
498
+ if (!context) throw new Error("3d-spinner: could not get a WebGPU canvas context.");
499
+ const format = gpu.getPreferredCanvasFormat();
500
+ context.configure({ device, format, alphaMode: this.alphaMode });
501
+ const module = device.createShaderModule({ code: WGSL });
502
+ const stage = globalThis.GPUShaderStage;
503
+ const layout = device.createBindGroupLayout({
504
+ entries: [
505
+ {
506
+ binding: 0,
507
+ visibility: stage.VERTEX | stage.FRAGMENT,
508
+ buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
509
+ }
510
+ ]
511
+ });
512
+ const vertexBuffer = (location) => ({
513
+ arrayStride: 12,
514
+ attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
515
+ });
516
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
517
+ const blend = {
518
+ color: {
519
+ srcFactor: "src-alpha",
520
+ dstFactor: "one-minus-src-alpha",
521
+ operation: "add"
522
+ },
523
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
524
+ };
525
+ const pipeline = (cullMode, transparent) => device.createRenderPipeline({
526
+ layout: pipelineLayout,
527
+ vertex: {
528
+ module,
529
+ entryPoint: "vs",
530
+ buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
531
+ },
532
+ fragment: {
533
+ module,
534
+ entryPoint: "fs",
535
+ targets: [{ format, ...transparent ? { blend } : {} }]
536
+ },
537
+ primitive: { topology: "triangle-list", cullMode, frontFace: "ccw" },
538
+ depthStencil: {
539
+ format: "depth24plus",
540
+ depthWriteEnabled: !transparent,
541
+ depthCompare: "less"
542
+ }
543
+ });
544
+ this.pipeline = pipeline("back", false);
545
+ this.transparentBackPipeline = pipeline("front", true);
546
+ this.transparentFrontPipeline = pipeline("back", true);
547
+ this.canvas = canvas;
548
+ this.device = device;
549
+ this.context = context;
550
+ }
551
+ resize() {
552
+ this.ensureDepth();
553
+ }
554
+ ensureDepth() {
555
+ const canvas = this.canvas;
556
+ if (!this.device || !canvas) return;
557
+ const width = Math.max(1, canvas.width);
558
+ const height = Math.max(1, canvas.height);
559
+ const key = `${width}x${height}`;
560
+ if (key === this.depthSize && this.depthTexture) return;
561
+ this.depthTexture?.destroy?.();
562
+ this.depthTexture = this.device.createTexture({
563
+ size: { width, height },
564
+ format: "depth24plus",
565
+ usage: globalThis.GPUTextureUsage.RENDER_ATTACHMENT
566
+ });
567
+ this.depthSize = key;
568
+ }
569
+ buffers(mesh) {
570
+ const cached = this.cache.get(mesh);
571
+ if (cached) return cached;
572
+ const data = expandToTriangles(mesh);
573
+ const usage = globalThis.GPUBufferUsage.VERTEX | globalThis.GPUBufferUsage.COPY_DST;
574
+ const upload = (array) => {
575
+ const buffer = this.device.createBuffer({ size: array.byteLength, usage });
576
+ this.device.queue.writeBuffer(buffer, 0, array);
577
+ return buffer;
578
+ };
579
+ const result = {
580
+ position: upload(data.positions),
581
+ normal: upload(data.normals),
582
+ color: upload(data.colors),
583
+ count: data.count
584
+ };
585
+ this.cache.set(mesh, result);
586
+ return result;
587
+ }
588
+ ensureUniformCapacity(draws) {
589
+ if (draws <= this.uniformCapacity && this.uniformBuffer) return;
590
+ this.uniformBuffer?.destroy?.();
591
+ this.uniformBuffer = this.device.createBuffer({
592
+ size: Math.max(1, draws) * UNIFORM_STRIDE,
593
+ usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST
594
+ });
595
+ this.uniformCapacity = draws;
596
+ }
597
+ render(frame) {
598
+ if (this.destroyed || !this.device || !this.context || !this.pipeline) return;
599
+ if (frame.width === 0 || frame.height === 0 || frame.items.length === 0) return;
600
+ this.ensureDepth();
601
+ const draws = [];
602
+ for (const item of frame.items) {
603
+ if (!item.transparency) draws.push({ item, opacity: 1, pipeline: this.pipeline });
604
+ }
605
+ for (const item of frame.items) {
606
+ const transparency = item.transparency;
607
+ if (!transparency) continue;
608
+ if (transparency.mode === "two-sided") {
609
+ const resolved = resolveTwoSidedOpacity(transparency);
610
+ draws.push({
611
+ item,
612
+ opacity: resolved.back,
613
+ pipeline: this.transparentBackPipeline
614
+ });
615
+ draws.push({
616
+ item,
617
+ opacity: resolved.front,
618
+ pipeline: this.transparentFrontPipeline
619
+ });
620
+ } else {
621
+ draws.push({
622
+ item,
623
+ opacity: opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY),
624
+ pipeline: this.transparentFrontPipeline
625
+ });
626
+ }
627
+ }
628
+ this.ensureUniformCapacity(draws.length);
629
+ const viewProj = multiply(CLIP_Z_FIX, frame.viewProjection);
630
+ const layout = this.pipeline.getBindGroupLayout(0);
631
+ const bindGroup = this.device.createBindGroup({
632
+ layout,
633
+ entries: [
634
+ { binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
635
+ ]
636
+ });
637
+ draws.forEach((draw, i) => {
638
+ const data = new Float32Array(UNIFORM_STRIDE / 4);
639
+ data.set(viewProj, 0);
640
+ data.set(draw.item.model, 16);
641
+ data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
642
+ data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
643
+ this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
644
+ });
645
+ const encoder = this.device.createCommandEncoder();
646
+ const pass = encoder.beginRenderPass({
647
+ colorAttachments: [
648
+ {
649
+ view: this.context.getCurrentTexture().createView(),
650
+ clearValue: this.clearValue,
651
+ loadOp: "clear",
652
+ storeOp: "store"
653
+ }
654
+ ],
655
+ depthStencilAttachment: {
656
+ view: this.depthTexture.createView(),
657
+ depthClearValue: 1,
658
+ depthLoadOp: "clear",
659
+ depthStoreOp: "store"
660
+ }
661
+ });
662
+ draws.forEach((draw, i) => {
663
+ const mesh = this.buffers(draw.item.mesh);
664
+ pass.setPipeline(draw.pipeline);
665
+ pass.setBindGroup(0, bindGroup, [i * UNIFORM_STRIDE]);
666
+ pass.setVertexBuffer(0, mesh.position);
667
+ pass.setVertexBuffer(1, mesh.normal);
668
+ pass.setVertexBuffer(2, mesh.color);
669
+ pass.draw(mesh.count);
670
+ });
671
+ pass.end();
672
+ this.device.queue.submit([encoder.finish()]);
673
+ }
674
+ destroy() {
675
+ this.destroyed = true;
676
+ for (const mesh of this.cache.values()) {
677
+ mesh.position.destroy?.();
678
+ mesh.normal.destroy?.();
679
+ mesh.color.destroy?.();
680
+ }
681
+ this.cache.clear();
682
+ this.uniformBuffer?.destroy?.();
683
+ this.depthTexture?.destroy?.();
684
+ this.device?.destroy?.();
685
+ this.device = void 0;
686
+ this.context = void 0;
687
+ this.pipeline = void 0;
688
+ this.transparentBackPipeline = void 0;
689
+ this.transparentFrontPipeline = void 0;
690
+ this.uniformBuffer = void 0;
691
+ this.depthTexture = void 0;
692
+ this.canvas = void 0;
693
+ }
694
+ };
695
+ }
696
+ });
697
+
698
+ // src/engines/little-3d-engine/renderers/canvas2d.ts
699
+ var canvas2d_exports = {};
700
+ __export(canvas2d_exports, {
701
+ Canvas2DRenderer: () => Canvas2DRenderer
702
+ });
703
+ var Canvas2DRenderer;
704
+ var init_canvas2d = __esm({
705
+ "src/engines/little-3d-engine/renderers/canvas2d.ts"() {
706
+ "use strict";
707
+ init_light();
708
+ init_math();
709
+ init_renderer();
710
+ Canvas2DRenderer = class {
711
+ constructor(options = {}) {
712
+ this.options = options;
713
+ this.dpr = 1;
714
+ }
715
+ init(canvas) {
716
+ this.ctx = canvas.getContext("2d") ?? void 0;
717
+ }
718
+ resize(_cssWidth, _cssHeight, dpr) {
719
+ this.dpr = dpr;
720
+ this.ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
721
+ }
722
+ render(frame) {
723
+ const ctx = this.ctx;
724
+ if (!ctx) return;
725
+ if (this.options.background) {
726
+ ctx.fillStyle = this.options.background;
727
+ ctx.fillRect(0, 0, frame.width, frame.height);
728
+ } else {
729
+ ctx.clearRect(0, 0, frame.width, frame.height);
730
+ }
731
+ const polygons = [];
732
+ for (const item of frame.items) {
733
+ const world = item.mesh.vertices.map((v) => transformAffine(item.model, v));
734
+ const twoSidedOpacity = item.transparency?.mode === "two-sided" ? resolveTwoSidedOpacity(item.transparency) : void 0;
735
+ for (const face of item.mesh.faces) {
736
+ const a = world[face.indices[0]];
737
+ const b = world[face.indices[1]];
738
+ const c = world[face.indices[2]];
739
+ const normal = normalize(cross(subtract(b, a), subtract(c, a)));
740
+ const frontFacing = dot(normal, subtract(frame.eye, a)) > 0;
741
+ const transparency = item.transparency;
742
+ if (!frontFacing && transparency?.mode !== "two-sided") continue;
743
+ let faceOpacity = 1;
744
+ if (transparency?.mode === "one-sided") {
745
+ faceOpacity = opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
746
+ } else if (twoSidedOpacity) {
747
+ faceOpacity = frontFacing ? twoSidedOpacity.front : twoSidedOpacity.back;
748
+ }
749
+ const points = face.indices.map((i) => {
750
+ const ndc = transformPoint(frame.viewProjection, world[i]);
751
+ return {
752
+ x: (ndc.x * 0.5 + 0.5) * frame.width,
753
+ y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height
754
+ };
755
+ });
756
+ let depth = 0;
757
+ for (const i of face.indices) {
758
+ const d = subtract(world[i], frame.eye);
759
+ depth += dot(d, d);
760
+ }
761
+ depth /= face.indices.length;
762
+ polygons.push({
763
+ points,
764
+ color: shadeColor(normal, face.color, frame.light),
765
+ depth,
766
+ opacity: faceOpacity
767
+ });
768
+ }
769
+ }
770
+ polygons.sort((p, q) => q.depth - p.depth);
771
+ for (const poly of polygons) {
772
+ if (poly.points.length < 3) continue;
773
+ ctx.beginPath();
774
+ ctx.moveTo(poly.points[0].x, poly.points[0].y);
775
+ for (let i = 1; i < poly.points.length; i++) {
776
+ ctx.lineTo(poly.points[i].x, poly.points[i].y);
777
+ }
778
+ ctx.closePath();
779
+ ctx.fillStyle = poly.color;
780
+ ctx.strokeStyle = poly.color;
781
+ ctx.lineWidth = 1;
782
+ ctx.globalAlpha = poly.opacity;
783
+ ctx.fill();
784
+ ctx.stroke();
785
+ }
786
+ ctx.globalAlpha = 1;
787
+ }
788
+ destroy() {
789
+ this.ctx = void 0;
790
+ }
791
+ };
792
+ }
793
+ });
794
+
795
+ // src/engines/little-3d-engine/renderer.ts
796
+ function opacity(value, fallback) {
797
+ return Math.max(0, Math.min(1, value ?? fallback));
798
+ }
799
+ function resolveTwoSidedOpacity(transparency) {
800
+ const front = opacity(
801
+ transparency.frontOpacity ?? transparency.opacity,
802
+ DEFAULT_FRONT_OPACITY
803
+ );
804
+ const backFallback = transparency.opacity === void 0 ? DEFAULT_BACK_OPACITY : front * (2 / 3);
805
+ return {
806
+ front,
807
+ back: opacity(transparency.backOpacity, backFallback)
808
+ };
809
+ }
810
+ function orderRenderItems(items, eye) {
811
+ const opaque = [];
812
+ const transparent = [];
813
+ for (const item of items) {
814
+ (item.transparency ? transparent : opaque).push(item);
815
+ }
816
+ transparent.sort((a, b) => {
817
+ const ax = a.model[12] - eye.x;
818
+ const ay = a.model[13] - eye.y;
819
+ const az = a.model[14] - eye.z;
820
+ const bx = b.model[12] - eye.x;
821
+ const by = b.model[13] - eye.y;
822
+ const bz = b.model[14] - eye.z;
823
+ return bx * bx + by * by + bz * bz - (ax * ax + ay * ay + az * az);
824
+ });
825
+ return opaque.concat(transparent);
826
+ }
827
+ async function createRenderer(backend, options = {}) {
828
+ switch (backend) {
829
+ case "webgl":
830
+ return new (await Promise.resolve().then(() => (init_webgl(), webgl_exports))).WebGLRenderer(options);
831
+ case "webgpu":
832
+ return new (await Promise.resolve().then(() => (init_webgpu(), webgpu_exports))).WebGPURenderer(options);
833
+ case "canvas2d":
834
+ default:
835
+ return new (await Promise.resolve().then(() => (init_canvas2d(), canvas2d_exports))).Canvas2DRenderer(options);
836
+ }
837
+ }
838
+ var DEFAULT_ONE_SIDED_OPACITY, DEFAULT_BACK_OPACITY, DEFAULT_FRONT_OPACITY;
839
+ var init_renderer = __esm({
840
+ "src/engines/little-3d-engine/renderer.ts"() {
841
+ "use strict";
842
+ DEFAULT_ONE_SIDED_OPACITY = 0.35;
843
+ DEFAULT_BACK_OPACITY = 0.84;
844
+ DEFAULT_FRONT_OPACITY = 0.56;
845
+ }
846
+ });
847
+
848
+ // <stdin>
849
+ var stdin_exports = {};
850
+ __export(stdin_exports, {
851
+ Camera: () => Camera,
852
+ Light: () => Light,
853
+ Little3dEngine: () => Little3dEngine,
854
+ LittleTweenEngine: () => LittleTweenEngine,
855
+ ObjectMotionAnimation: () => ObjectMotionAnimation,
856
+ SpinAnimation: () => SpinAnimation,
857
+ centerAndScaleMesh: () => centerAndScaleMesh,
858
+ circleMotion: () => circleMotion,
859
+ createSpinner: () => createSpinner,
860
+ cross: () => cross,
861
+ cube: () => cube,
862
+ cubeSphere: () => cubeSphere,
863
+ cubic: () => cubic,
864
+ dot: () => dot,
865
+ ease: () => ease,
866
+ easeInBack: () => easeInBack,
867
+ easeInBounce: () => easeInBounce,
868
+ easeInCirc: () => easeInCirc,
869
+ easeInCubic: () => easeInCubic,
870
+ easeInElastic: () => easeInElastic,
871
+ easeInExpo: () => easeInExpo,
872
+ easeInOutBack: () => easeInOutBack,
873
+ easeInOutBounce: () => easeInOutBounce,
874
+ easeInOutCirc: () => easeInOutCirc,
875
+ easeInOutCubic: () => easeInOutCubic,
876
+ easeInOutElastic: () => easeInOutElastic,
877
+ easeInOutExpo: () => easeInOutExpo,
878
+ easeInOutQuad: () => easeInOutQuad,
879
+ easeInOutQuart: () => easeInOutQuart,
880
+ easeInOutQuint: () => easeInOutQuint,
881
+ easeInOutSine: () => easeInOutSine,
882
+ easeInQuad: () => easeInQuad,
883
+ easeInQuart: () => easeInQuart,
884
+ easeInQuint: () => easeInQuint,
885
+ easeInSine: () => easeInSine,
886
+ easeOutBack: () => easeOutBack,
887
+ easeOutBounce: () => easeOutBounce,
888
+ easeOutCirc: () => easeOutCirc,
889
+ easeOutCubic: () => easeOutCubic,
890
+ easeOutElastic: () => easeOutElastic,
891
+ easeOutExpo: () => easeOutExpo,
892
+ easeOutQuad: () => easeOutQuad,
893
+ easeOutQuart: () => easeOutQuart,
894
+ easeOutQuint: () => easeOutQuint,
895
+ easeOutSine: () => easeOutSine,
896
+ easeTypes: () => easeTypes,
897
+ enterFromObjectDirection: () => enterFromObjectDirection,
898
+ expandToTriangles: () => expandToTriangles,
899
+ figureEightMotion: () => figureEightMotion,
900
+ grow: () => grow,
901
+ icosphere: () => icosphere,
902
+ leaveInObjectDirection: () => leaveInObjectDirection,
903
+ linear: () => linear,
904
+ normalize: () => normalize,
905
+ octaSphere: () => octaSphere,
906
+ octahedron: () => octahedron,
907
+ orderRenderItems: () => orderRenderItems,
908
+ parseObj: () => parseObj,
909
+ pyramid: () => pyramid,
910
+ quadratic: () => quadratic,
911
+ quartic: () => quartic,
912
+ quintic: () => quintic,
913
+ scale: () => scale,
914
+ shrink: () => shrink,
915
+ squareMotion: () => squareMotion,
916
+ subtract: () => subtract,
917
+ tetrahedron: () => tetrahedron,
918
+ transform: () => transform,
919
+ uvSphere: () => uvSphere,
920
+ vec3: () => vec3,
921
+ wanderMotion: () => wanderMotion
922
+ });
923
+
924
+ // src/index.ts
925
+ function clamp01(value) {
926
+ if (Number.isNaN(value)) return 0;
927
+ return Math.min(1, Math.max(0, value));
928
+ }
929
+ function lerp(from, to, t) {
930
+ return from + (to - from) * t;
931
+ }
932
+ function createSpinner(target, options) {
933
+ if (!(target instanceof HTMLElement)) {
934
+ throw new Error("3d-spinner: createSpinner requires a target HTMLElement.");
935
+ }
936
+ const { animation } = options;
937
+ const indeterminate = options.type === "indeterminate";
938
+ if (indeterminate && options.periodMs !== void 0 && (!Number.isFinite(options.periodMs) || options.periodMs <= 0)) {
939
+ throw new RangeError("3d-spinner: periodMs must be a finite number greater than zero.");
940
+ }
941
+ animation.mount(target);
942
+ const start = performance.now();
943
+ let rafId = 0;
944
+ let stopped = false;
945
+ let destroyed = false;
946
+ let entered = false;
947
+ let exiting = false;
948
+ let current = 0;
949
+ let targetProgress = 0;
950
+ let deadline = Infinity;
951
+ if (!indeterminate) {
952
+ const opts = options;
953
+ if (typeof opts.progress === "number") {
954
+ current = clamp01(opts.progress);
955
+ targetProgress = current;
956
+ }
957
+ if (typeof opts.timeout === "number") deadline = Math.min(deadline, start + opts.timeout);
958
+ if (opts.until instanceof Date) deadline = Math.min(deadline, opts.until.getTime());
959
+ }
960
+ function computeProgress(now) {
961
+ if (!indeterminate) {
962
+ if (now >= deadline) targetProgress = 1;
963
+ current = lerp(current, targetProgress, 0.12);
964
+ if (Math.abs(targetProgress - current) < 5e-4) current = targetProgress;
965
+ return current;
966
+ }
967
+ const opts = options;
968
+ const period = opts.periodMs ?? 2e3;
969
+ const t = (now - start) / period;
970
+ if ((opts.loop ?? "bounce") === "restart") return t - Math.floor(t);
971
+ const phase = t - 2 * Math.floor(t / 2);
972
+ return phase <= 1 ? phase : 2 - phase;
973
+ }
974
+ function frame(now) {
975
+ if (stopped) return;
976
+ const progress = computeProgress(now);
977
+ if (!entered && (indeterminate || progress > 0)) {
978
+ animation.enter(now);
979
+ entered = true;
980
+ }
981
+ if (!exiting && entered && !indeterminate && progress >= 1 && targetProgress >= 1) {
982
+ animation.exit(now);
983
+ exiting = true;
984
+ }
985
+ const target2 = indeterminate ? progress : targetProgress;
986
+ animation.render(now, { progress, targetProgress: target2, indeterminate });
987
+ if (exiting && animation.isFinished()) {
988
+ halt();
989
+ return;
990
+ }
991
+ rafId = requestAnimationFrame(frame);
992
+ }
993
+ function halt() {
994
+ if (stopped) return;
995
+ stopped = true;
996
+ if (rafId) cancelAnimationFrame(rafId);
997
+ rafId = 0;
998
+ }
999
+ function setProgress(value) {
1000
+ if (!indeterminate) targetProgress = clamp01(value);
1001
+ }
1002
+ function stop() {
1003
+ if (stopped || exiting) return;
1004
+ if (!entered) {
1005
+ halt();
1006
+ return;
1007
+ }
1008
+ animation.exit(performance.now());
1009
+ exiting = true;
1010
+ }
1011
+ function destroy() {
1012
+ if (destroyed) return;
1013
+ destroyed = true;
1014
+ halt();
1015
+ animation.destroy();
1016
+ }
1017
+ rafId = requestAnimationFrame(frame);
1018
+ return { setProgress, stop, destroy };
1019
+ }
1020
+
1021
+ // src/engines/little-3d-engine/core/camera.ts
1022
+ init_math();
1023
+ var DEFAULTS = {
1024
+ position: { x: 0, y: 0, z: 4 },
1025
+ fov: 55 * Math.PI / 180,
1026
+ near: 0.1,
1027
+ far: 100
1028
+ };
1029
+ var Camera = class {
1030
+ constructor(options) {
1031
+ this.options = { ...DEFAULTS, ...options };
1032
+ }
1033
+ /** Transform a world-space point into view (camera) space. */
1034
+ toView(p) {
1035
+ const { position } = this.options;
1036
+ return transformAffine(translation(-position.x, -position.y, -position.z), p);
1037
+ }
1038
+ /** Combined view-projection matrix for the given viewport aspect ratio. */
1039
+ viewProjection(aspect) {
1040
+ const { position, fov, near, far } = this.options;
1041
+ const view = translation(-position.x, -position.y, -position.z);
1042
+ const projection = perspective(fov, aspect, near, far);
1043
+ return multiply(projection, view);
1044
+ }
1045
+ /** Convert a normalized device coordinate (-1..1) to a pixel position. */
1046
+ toScreen(ndc, width, height) {
1047
+ return {
1048
+ x: (ndc.x * 0.5 + 0.5) * width,
1049
+ y: (1 - (ndc.y * 0.5 + 0.5)) * height
1050
+ };
1051
+ }
1052
+ };
1053
+
1054
+ // src/engines/little-3d-engine/little-3d-engine.ts
1055
+ init_light();
1056
+ init_math();
1057
+
1058
+ // src/engines/little-3d-engine/core/mesh.ts
1059
+ function transform(init) {
1060
+ return {
1061
+ position: init?.position ?? { x: 0, y: 0, z: 0 },
1062
+ rotation: init?.rotation ?? { x: 0, y: 0, z: 0 },
1063
+ scale: init?.scale ?? 1
1064
+ };
1065
+ }
1066
+
1067
+ // src/engines/little-3d-engine/little-3d-engine.ts
1068
+ init_renderer();
1069
+ init_light();
1070
+
1071
+ // src/engines/little-3d-engine/shapes/cube.ts
1072
+ var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1073
+ function cube(size = 1, colors = DEFAULT_COLORS) {
1074
+ const h = size / 2;
1075
+ const vertices = [
1076
+ { x: -h, y: -h, z: h },
1077
+ { x: h, y: -h, z: h },
1078
+ { x: h, y: h, z: h },
1079
+ { x: -h, y: h, z: h },
1080
+ { x: -h, y: -h, z: -h },
1081
+ { x: h, y: -h, z: -h },
1082
+ { x: h, y: h, z: -h },
1083
+ { x: -h, y: h, z: -h }
1084
+ ];
1085
+ const faces = [
1086
+ { indices: [0, 1, 2, 3], color: colors[0 % colors.length] },
1087
+ { indices: [5, 4, 7, 6], color: colors[1 % colors.length] },
1088
+ { indices: [3, 2, 6, 7], color: colors[2 % colors.length] },
1089
+ { indices: [4, 5, 1, 0], color: colors[3 % colors.length] },
1090
+ { indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
1091
+ { indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
1092
+ ];
1093
+ return { vertices, faces };
1094
+ }
1095
+
1096
+ // src/engines/little-3d-engine/shapes/tetrahedron.ts
1097
+ var DEFAULT_COLORS2 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
1098
+ function tetrahedron(size = 1, colors = DEFAULT_COLORS2) {
1099
+ const s = size / 2;
1100
+ const vertices = [
1101
+ { x: s, y: s, z: s },
1102
+ { x: s, y: -s, z: -s },
1103
+ { x: -s, y: s, z: -s },
1104
+ { x: -s, y: -s, z: s }
1105
+ ];
1106
+ const faces = [
1107
+ { indices: [0, 1, 2], color: colors[0 % colors.length] },
1108
+ { indices: [0, 3, 1], color: colors[1 % colors.length] },
1109
+ { indices: [0, 2, 3], color: colors[2 % colors.length] },
1110
+ { indices: [1, 3, 2], color: colors[3 % colors.length] }
1111
+ ];
1112
+ return { vertices, faces };
1113
+ }
1114
+
1115
+ // src/engines/little-3d-engine/shapes/octahedron.ts
1116
+ var DEFAULT_COLORS3 = [
1117
+ "#3b82f6",
1118
+ "#8b5cf6",
1119
+ "#ec4899",
1120
+ "#f59e0b",
1121
+ "#10b981",
1122
+ "#ef4444",
1123
+ "#06b6d4",
1124
+ "#eab308"
1125
+ ];
1126
+ function octahedron(size = 1, colors = DEFAULT_COLORS3) {
1127
+ const r = size / 2;
1128
+ const vertices = [
1129
+ { x: r, y: 0, z: 0 },
1130
+ { x: -r, y: 0, z: 0 },
1131
+ { x: 0, y: r, z: 0 },
1132
+ { x: 0, y: -r, z: 0 },
1133
+ { x: 0, y: 0, z: r },
1134
+ { x: 0, y: 0, z: -r }
1135
+ ];
1136
+ const faces = [
1137
+ { indices: [4, 0, 2], color: colors[0 % colors.length] },
1138
+ { indices: [4, 2, 1], color: colors[1 % colors.length] },
1139
+ { indices: [4, 1, 3], color: colors[2 % colors.length] },
1140
+ { indices: [4, 3, 0], color: colors[3 % colors.length] },
1141
+ { indices: [5, 2, 0], color: colors[4 % colors.length] },
1142
+ { indices: [5, 1, 2], color: colors[5 % colors.length] },
1143
+ { indices: [5, 3, 1], color: colors[6 % colors.length] },
1144
+ { indices: [5, 0, 3], color: colors[7 % colors.length] }
1145
+ ];
1146
+ return { vertices, faces };
1147
+ }
1148
+
1149
+ // src/engines/little-3d-engine/shapes/pyramid.ts
1150
+ var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1151
+ function pyramid(size = 1, colors = DEFAULT_COLORS4) {
1152
+ const h = size / 2;
1153
+ const vertices = [
1154
+ { x: -h, y: -h, z: h },
1155
+ { x: h, y: -h, z: h },
1156
+ { x: h, y: -h, z: -h },
1157
+ { x: -h, y: -h, z: -h },
1158
+ { x: 0, y: h, z: 0 }
1159
+ ];
1160
+ const faces = [
1161
+ { indices: [0, 3, 2, 1], color: colors[0 % colors.length] },
1162
+ { indices: [4, 0, 1], color: colors[1 % colors.length] },
1163
+ { indices: [4, 1, 2], color: colors[2 % colors.length] },
1164
+ { indices: [4, 2, 3], color: colors[3 % colors.length] },
1165
+ { indices: [4, 3, 0], color: colors[4 % colors.length] }
1166
+ ];
1167
+ return { vertices, faces };
1168
+ }
1169
+
1170
+ // src/engines/little-3d-engine/shapes/uv-sphere.ts
1171
+ var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1172
+ function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS5) {
1173
+ const r = size / 2;
1174
+ const d = Math.max(1, Math.floor(detail));
1175
+ const slices = Math.max(4, d * 4);
1176
+ const stacks = Math.max(2, d * 2);
1177
+ const vertices = [{ x: 0, y: r, z: 0 }];
1178
+ const ring = (i, j) => 1 + (i - 1) * slices + j;
1179
+ for (let i = 1; i < stacks; i++) {
1180
+ const phi = Math.PI * i / stacks;
1181
+ const y = r * Math.cos(phi);
1182
+ const rr = r * Math.sin(phi);
1183
+ for (let j = 0; j < slices; j++) {
1184
+ const theta = 2 * Math.PI * j / slices;
1185
+ vertices.push({ x: rr * Math.cos(theta), y, z: rr * Math.sin(theta) });
1186
+ }
1187
+ }
1188
+ const bottom = vertices.length;
1189
+ vertices.push({ x: 0, y: -r, z: 0 });
1190
+ const faces = [];
1191
+ let ci = 0;
1192
+ const color = () => colors[ci++ % colors.length];
1193
+ for (let j = 0; j < slices; j++) {
1194
+ faces.push({ indices: [0, ring(1, (j + 1) % slices), ring(1, j)], color: color() });
1195
+ }
1196
+ for (let i = 1; i < stacks - 1; i++) {
1197
+ for (let j = 0; j < slices; j++) {
1198
+ const j1 = (j + 1) % slices;
1199
+ faces.push({
1200
+ indices: [ring(i, j), ring(i, j1), ring(i + 1, j1), ring(i + 1, j)],
1201
+ color: color()
1202
+ });
1203
+ }
1204
+ }
1205
+ for (let j = 0; j < slices; j++) {
1206
+ faces.push({
1207
+ indices: [bottom, ring(stacks - 1, j), ring(stacks - 1, (j + 1) % slices)],
1208
+ color: color()
1209
+ });
1210
+ }
1211
+ return { vertices, faces };
1212
+ }
1213
+
1214
+ // src/engines/little-3d-engine/shapes/icosphere.ts
1215
+ init_geometry();
1216
+ var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1217
+ var T = (1 + Math.sqrt(5)) / 2;
1218
+ var SEED_VERTICES = [
1219
+ { x: -1, y: T, z: 0 },
1220
+ { x: 1, y: T, z: 0 },
1221
+ { x: -1, y: -T, z: 0 },
1222
+ { x: 1, y: -T, z: 0 },
1223
+ { x: 0, y: -1, z: T },
1224
+ { x: 0, y: 1, z: T },
1225
+ { x: 0, y: -1, z: -T },
1226
+ { x: 0, y: 1, z: -T },
1227
+ { x: T, y: 0, z: -1 },
1228
+ { x: T, y: 0, z: 1 },
1229
+ { x: -T, y: 0, z: -1 },
1230
+ { x: -T, y: 0, z: 1 }
1231
+ ];
1232
+ var SEED_FACES = [
1233
+ [0, 11, 5],
1234
+ [0, 5, 1],
1235
+ [0, 1, 7],
1236
+ [0, 7, 10],
1237
+ [0, 10, 11],
1238
+ [1, 5, 9],
1239
+ [5, 11, 4],
1240
+ [11, 10, 2],
1241
+ [10, 7, 6],
1242
+ [7, 1, 8],
1243
+ [3, 9, 4],
1244
+ [3, 4, 2],
1245
+ [3, 2, 6],
1246
+ [3, 6, 8],
1247
+ [3, 8, 9],
1248
+ [4, 9, 5],
1249
+ [2, 4, 11],
1250
+ [6, 2, 10],
1251
+ [8, 6, 7],
1252
+ [9, 8, 1]
1253
+ ];
1254
+ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
1255
+ return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
1256
+ }
1257
+
1258
+ // src/engines/little-3d-engine/shapes/octa-sphere.ts
1259
+ init_geometry();
1260
+ var DEFAULT_COLORS7 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1261
+ var SEED_VERTICES2 = [
1262
+ { x: 1, y: 0, z: 0 },
1263
+ { x: -1, y: 0, z: 0 },
1264
+ { x: 0, y: 1, z: 0 },
1265
+ { x: 0, y: -1, z: 0 },
1266
+ { x: 0, y: 0, z: 1 },
1267
+ { x: 0, y: 0, z: -1 }
1268
+ ];
1269
+ var SEED_FACES2 = [
1270
+ [4, 0, 2],
1271
+ [4, 2, 1],
1272
+ [4, 1, 3],
1273
+ [4, 3, 0],
1274
+ [5, 2, 0],
1275
+ [5, 1, 2],
1276
+ [5, 3, 1],
1277
+ [5, 0, 3]
1278
+ ];
1279
+ function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
1280
+ return sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors);
1281
+ }
1282
+
1283
+ // src/engines/little-3d-engine/shapes/cube-sphere.ts
1284
+ var DEFAULT_COLORS8 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
1285
+ var CUBE_FACES = [
1286
+ { normal: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
1287
+ { normal: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] },
1288
+ { normal: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
1289
+ { normal: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
1290
+ { normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
1291
+ { normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
1292
+ ];
1293
+ function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
1294
+ const r = size / 2;
1295
+ const n = Math.max(1, Math.floor(detail));
1296
+ const vertices = [];
1297
+ const faces = [];
1298
+ let ci = 0;
1299
+ for (const face of CUBE_FACES) {
1300
+ const base = vertices.length;
1301
+ for (let i = 0; i <= n; i++) {
1302
+ for (let j = 0; j <= n; j++) {
1303
+ const u = -1 + 2 * i / n;
1304
+ const v = -1 + 2 * j / n;
1305
+ const x = face.normal[0] + u * face.right[0] + v * face.up[0];
1306
+ const y = face.normal[1] + u * face.right[1] + v * face.up[1];
1307
+ const z = face.normal[2] + u * face.right[2] + v * face.up[2];
1308
+ const len = Math.hypot(x, y, z);
1309
+ vertices.push({ x: x / len * r, y: y / len * r, z: z / len * r });
1310
+ }
1311
+ }
1312
+ const idx = (i, j) => base + i * (n + 1) + j;
1313
+ for (let i = 0; i < n; i++) {
1314
+ for (let j = 0; j < n; j++) {
1315
+ faces.push({
1316
+ indices: [idx(i, j), idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1)],
1317
+ color: colors[ci++ % colors.length]
1318
+ });
1319
+ }
1320
+ }
1321
+ }
1322
+ return { vertices, faces };
1323
+ }
1324
+
1325
+ // src/engines/little-3d-engine/little-3d-engine.ts
1326
+ init_geometry();
1327
+ init_renderer();
1328
+ init_math();
1329
+ function modelMatrix(t) {
1330
+ const rotation = multiply(
1331
+ rotationZ(t.rotation.z),
1332
+ multiply(rotationY(t.rotation.y), rotationX(t.rotation.x))
1333
+ );
1334
+ return multiply(
1335
+ translation(t.position.x, t.position.y, t.position.z),
1336
+ multiply(rotation, scaleMatrix(t.scale))
1337
+ );
1338
+ }
1339
+ var Little3dEngine = class {
1340
+ constructor(options = {}) {
1341
+ this.scene = [];
1342
+ this.cssWidth = 0;
1343
+ this.cssHeight = 0;
1344
+ this.ready = false;
1345
+ this.generation = 0;
1346
+ this.rafId = 0;
1347
+ this.running = false;
1348
+ this.camera = new Camera(options.camera);
1349
+ this.light = new Light(options.light);
1350
+ this.backend = options.backend ?? "canvas2d";
1351
+ this.background = options.background;
1352
+ }
1353
+ /**
1354
+ * Create the canvas inside `target`, load the selected backend, and start
1355
+ * tracking size. Resolves once the renderer is ready; rejects if the backend
1356
+ * is unavailable. Drawing is a no-op until it resolves.
1357
+ */
1358
+ async mount(target) {
1359
+ const canvas = document.createElement("canvas");
1360
+ canvas.style.display = "block";
1361
+ canvas.style.width = "100%";
1362
+ canvas.style.height = "100%";
1363
+ target.appendChild(canvas);
1364
+ this.canvas = canvas;
1365
+ this.observer = new ResizeObserver(() => this.resize());
1366
+ this.observer.observe(canvas);
1367
+ this.resize();
1368
+ const generation = this.generation;
1369
+ const dropCanvas = () => {
1370
+ if (this.canvas !== canvas) return;
1371
+ this.observer?.disconnect();
1372
+ this.observer = void 0;
1373
+ canvas.remove();
1374
+ this.canvas = void 0;
1375
+ };
1376
+ try {
1377
+ const renderer = await createRenderer(this.backend, { background: this.background });
1378
+ if (generation !== this.generation) {
1379
+ renderer.destroy();
1380
+ dropCanvas();
1381
+ return;
1382
+ }
1383
+ await renderer.init(canvas);
1384
+ if (generation !== this.generation) {
1385
+ renderer.destroy();
1386
+ dropCanvas();
1387
+ return;
1388
+ }
1389
+ this.renderer = renderer;
1390
+ this.resize();
1391
+ this.ready = true;
1392
+ } catch (error) {
1393
+ dropCanvas();
1394
+ throw error;
1395
+ }
1396
+ }
1397
+ /** Add a mesh to the scene and return a handle for animating it. */
1398
+ add(mesh, init) {
1399
+ const entry = {
1400
+ mesh,
1401
+ transform: transform(init),
1402
+ transparency: init?.transparency,
1403
+ remove: () => {
1404
+ const i = this.scene.indexOf(entry);
1405
+ if (i >= 0) this.scene.splice(i, 1);
1406
+ }
1407
+ };
1408
+ this.scene.push(entry);
1409
+ return entry;
1410
+ }
1411
+ resize() {
1412
+ const canvas = this.canvas;
1413
+ if (!canvas) return;
1414
+ const dpr = window.devicePixelRatio || 1;
1415
+ this.cssWidth = canvas.clientWidth || canvas.parentElement?.clientWidth || 0;
1416
+ this.cssHeight = canvas.clientHeight || canvas.parentElement?.clientHeight || 0;
1417
+ canvas.width = Math.max(1, Math.round(this.cssWidth * dpr));
1418
+ canvas.height = Math.max(1, Math.round(this.cssHeight * dpr));
1419
+ this.renderer?.resize(this.cssWidth, this.cssHeight, dpr);
1420
+ }
1421
+ /** Draw a single frame from the current scene state. */
1422
+ render() {
1423
+ if (!this.ready || !this.renderer) return;
1424
+ const width = this.cssWidth;
1425
+ const height = this.cssHeight;
1426
+ if (width === 0 || height === 0) return;
1427
+ const items = this.scene.map((entry) => ({
1428
+ mesh: entry.mesh,
1429
+ model: modelMatrix(entry.transform),
1430
+ transparency: entry.transparency
1431
+ }));
1432
+ const eye = this.camera.options.position;
1433
+ this.renderer.render({
1434
+ items: orderRenderItems(items, eye),
1435
+ viewProjection: this.camera.viewProjection(width / height),
1436
+ eye,
1437
+ light: this.light.params,
1438
+ width,
1439
+ height
1440
+ });
1441
+ }
1442
+ /** Start an internal animation loop that calls {@link render} each frame. */
1443
+ start() {
1444
+ if (this.running) return;
1445
+ this.running = true;
1446
+ const loop = () => {
1447
+ if (!this.running) return;
1448
+ this.render();
1449
+ this.rafId = requestAnimationFrame(loop);
1450
+ };
1451
+ this.rafId = requestAnimationFrame(loop);
1452
+ }
1453
+ /** Stop the internal animation loop started by {@link start}. */
1454
+ stop() {
1455
+ this.running = false;
1456
+ if (this.rafId) cancelAnimationFrame(this.rafId);
1457
+ this.rafId = 0;
1458
+ }
1459
+ /** Stop animating, release the renderer, and remove the canvas. */
1460
+ destroy() {
1461
+ this.generation++;
1462
+ this.ready = false;
1463
+ this.stop();
1464
+ this.observer?.disconnect();
1465
+ this.observer = void 0;
1466
+ this.renderer?.destroy();
1467
+ this.renderer = void 0;
1468
+ this.canvas?.remove();
1469
+ this.canvas = void 0;
1470
+ }
1471
+ };
1472
+
1473
+ // src/engines/little-tween-engine/core/tweens.ts
1474
+ function input(value, overextend) {
1475
+ if (Number.isNaN(value)) return 0;
1476
+ if (overextend) return value;
1477
+ return Math.min(1, Math.max(0, value));
1478
+ }
1479
+ function linear(value, overextend = false) {
1480
+ return input(value, overextend);
1481
+ }
1482
+ function quadratic(value, overextend = false) {
1483
+ return easeInQuad(value, overextend);
1484
+ }
1485
+ function cubic(value, overextend = false) {
1486
+ return easeInCubic(value, overextend);
1487
+ }
1488
+ function quartic(value, overextend = false) {
1489
+ return easeInQuart(value, overextend);
1490
+ }
1491
+ function quintic(value, overextend = false) {
1492
+ return easeInQuint(value, overextend);
1493
+ }
1494
+ function easeInSine(value, overextend = false) {
1495
+ const x = input(value, overextend);
1496
+ return 1 - Math.cos(x * Math.PI / 2);
1497
+ }
1498
+ function easeOutSine(value, overextend = false) {
1499
+ const x = input(value, overextend);
1500
+ return Math.sin(x * Math.PI / 2);
1501
+ }
1502
+ function easeInOutSine(value, overextend = false) {
1503
+ const x = input(value, overextend);
1504
+ return -(Math.cos(Math.PI * x) - 1) / 2;
1505
+ }
1506
+ function easeInQuad(value, overextend = false) {
1507
+ const x = input(value, overextend);
1508
+ return x * x;
1509
+ }
1510
+ function easeOutQuad(value, overextend = false) {
1511
+ const x = input(value, overextend);
1512
+ return 1 - (1 - x) * (1 - x);
1513
+ }
1514
+ function easeInOutQuad(value, overextend = false) {
1515
+ const x = input(value, overextend);
1516
+ return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
1517
+ }
1518
+ function easeInCubic(value, overextend = false) {
1519
+ const x = input(value, overextend);
1520
+ return x * x * x;
1521
+ }
1522
+ function easeOutCubic(value, overextend = false) {
1523
+ const x = input(value, overextend);
1524
+ return 1 - Math.pow(1 - x, 3);
1525
+ }
1526
+ function easeInOutCubic(value, overextend = false) {
1527
+ const x = input(value, overextend);
1528
+ return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
1529
+ }
1530
+ function easeInQuart(value, overextend = false) {
1531
+ const x = input(value, overextend);
1532
+ return x * x * x * x;
1533
+ }
1534
+ function easeOutQuart(value, overextend = false) {
1535
+ const x = input(value, overextend);
1536
+ return 1 - Math.pow(1 - x, 4);
1537
+ }
1538
+ function easeInOutQuart(value, overextend = false) {
1539
+ const x = input(value, overextend);
1540
+ return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
1541
+ }
1542
+ function easeInQuint(value, overextend = false) {
1543
+ const x = input(value, overextend);
1544
+ return x * x * x * x * x;
1545
+ }
1546
+ function easeOutQuint(value, overextend = false) {
1547
+ const x = input(value, overextend);
1548
+ return 1 - Math.pow(1 - x, 5);
1549
+ }
1550
+ function easeInOutQuint(value, overextend = false) {
1551
+ const x = input(value, overextend);
1552
+ return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
1553
+ }
1554
+ function easeInExpo(value, overextend = false) {
1555
+ const x = input(value, overextend);
1556
+ return x === 0 ? 0 : Math.pow(2, 10 * x - 10);
1557
+ }
1558
+ function easeOutExpo(value, overextend = false) {
1559
+ const x = input(value, overextend);
1560
+ return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
1561
+ }
1562
+ function easeInOutExpo(value, overextend = false) {
1563
+ const x = input(value, overextend);
1564
+ return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2;
1565
+ }
1566
+ function easeInCirc(value, overextend = false) {
1567
+ const x = input(value, overextend);
1568
+ return 1 - Math.sqrt(1 - Math.pow(x, 2));
1569
+ }
1570
+ function easeOutCirc(value, overextend = false) {
1571
+ const x = input(value, overextend);
1572
+ return Math.sqrt(1 - Math.pow(x - 1, 2));
1573
+ }
1574
+ function easeInOutCirc(value, overextend = false) {
1575
+ const x = input(value, overextend);
1576
+ return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2;
1577
+ }
1578
+ function easeInBack(value, overextend = false) {
1579
+ const x = input(value, overextend);
1580
+ const c1 = 1.70158;
1581
+ const c3 = c1 + 1;
1582
+ return c3 * x * x * x - c1 * x * x;
1583
+ }
1584
+ function easeOutBack(value, overextend = false) {
1585
+ const x = input(value, overextend);
1586
+ const c1 = 1.70158;
1587
+ const c3 = c1 + 1;
1588
+ return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
1589
+ }
1590
+ function easeInOutBack(value, overextend = false) {
1591
+ const x = input(value, overextend);
1592
+ const c1 = 1.70158;
1593
+ const c2 = c1 * 1.525;
1594
+ return x < 0.5 ? Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2) / 2 : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
1595
+ }
1596
+ function easeInElastic(value, overextend = false) {
1597
+ const x = input(value, overextend);
1598
+ const c4 = 2 * Math.PI / 3;
1599
+ return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4);
1600
+ }
1601
+ function easeOutElastic(value, overextend = false) {
1602
+ const x = input(value, overextend);
1603
+ const c4 = 2 * Math.PI / 3;
1604
+ return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
1605
+ }
1606
+ function easeInOutElastic(value, overextend = false) {
1607
+ const x = input(value, overextend);
1608
+ const c5 = 2 * Math.PI / 4.5;
1609
+ return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 : Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5) / 2 + 1;
1610
+ }
1611
+ function easeInBounce(value, overextend = false) {
1612
+ const x = input(value, overextend);
1613
+ return 1 - easeOutBounce(1 - x, true);
1614
+ }
1615
+ function easeOutBounce(value, overextend = false) {
1616
+ let x = input(value, overextend);
1617
+ const n1 = 7.5625;
1618
+ const d1 = 2.75;
1619
+ if (x < 1 / d1) {
1620
+ return n1 * x * x;
1621
+ }
1622
+ if (x < 2 / d1) {
1623
+ x -= 1.5 / d1;
1624
+ return n1 * x * x + 0.75;
1625
+ }
1626
+ if (x < 2.5 / d1) {
1627
+ x -= 2.25 / d1;
1628
+ return n1 * x * x + 0.9375;
1629
+ }
1630
+ x -= 2.625 / d1;
1631
+ return n1 * x * x + 0.984375;
1632
+ }
1633
+ function easeInOutBounce(value, overextend = false) {
1634
+ const x = input(value, overextend);
1635
+ return x < 0.5 ? (1 - easeOutBounce(1 - 2 * x, true)) / 2 : (1 + easeOutBounce(2 * x - 1, true)) / 2;
1636
+ }
1637
+ var easeTypes = {
1638
+ linear,
1639
+ quadratic,
1640
+ cubic,
1641
+ quartic,
1642
+ quintic,
1643
+ easeInSine,
1644
+ easeOutSine,
1645
+ easeInOutSine,
1646
+ easeInQuad,
1647
+ easeOutQuad,
1648
+ easeInOutQuad,
1649
+ easeInCubic,
1650
+ easeOutCubic,
1651
+ easeInOutCubic,
1652
+ easeInQuart,
1653
+ easeOutQuart,
1654
+ easeInOutQuart,
1655
+ easeInQuint,
1656
+ easeOutQuint,
1657
+ easeInOutQuint,
1658
+ easeInExpo,
1659
+ easeOutExpo,
1660
+ easeInOutExpo,
1661
+ easeInCirc,
1662
+ easeOutCirc,
1663
+ easeInOutCirc,
1664
+ easeInBack,
1665
+ easeOutBack,
1666
+ easeInOutBack,
1667
+ easeInElastic,
1668
+ easeOutElastic,
1669
+ easeInOutElastic,
1670
+ easeInBounce,
1671
+ easeOutBounce,
1672
+ easeInOutBounce
1673
+ };
1674
+ function ease(type, value, overextend = false) {
1675
+ return easeTypes[type](value, overextend);
1676
+ }
1677
+
1678
+ // src/progress-animation.ts
1679
+ function resolveOptions(options = {}) {
1680
+ return {
1681
+ popDurationMs: options.popDurationMs ?? 500,
1682
+ overextend: options.overextend ?? 0.2,
1683
+ startSnapRatio: options.startSnapRatio ?? 0.2,
1684
+ loadingText: options.loadingText === void 0 ? "loading" : options.loadingText,
1685
+ doneText: options.doneText ?? "done",
1686
+ doneFadeDurationMs: options.doneFadeDurationMs ?? 2e3,
1687
+ removeOnComplete: options.removeOnComplete ?? false
1688
+ };
1689
+ }
1690
+ function popPhaseT(now, phaseStart, durationMs) {
1691
+ if (durationMs <= 0) return 1;
1692
+ return Math.min(1, Math.max(0, (now - phaseStart) / durationMs));
1693
+ }
1694
+ var ProgressAnimation = class {
1695
+ constructor(options = {}) {
1696
+ this.phase = "idle";
1697
+ this.phaseStart = 0;
1698
+ this.activeProgress = 0;
1699
+ this.popTarget = 0;
1700
+ this.doneFadeStart = 0;
1701
+ this.options = resolveOptions(options);
1702
+ }
1703
+ /** Begin the intro pop. Ignored unless idle. */
1704
+ enter(now) {
1705
+ if (this.phase !== "idle") return;
1706
+ this.phase = "startPop";
1707
+ this.phaseStart = now;
1708
+ this.activeProgress = 0;
1709
+ this.popTarget = 0;
1710
+ }
1711
+ /** Begin the outro pop. Ignored unless mid-intro or active. */
1712
+ exit(now) {
1713
+ if (this.phase !== "startPop" && this.phase !== "active") return;
1714
+ this.phase = "endPop";
1715
+ this.phaseStart = now;
1716
+ }
1717
+ isFinished() {
1718
+ return this.phase === "finished";
1719
+ }
1720
+ update(now, progress, targetProgress) {
1721
+ const {
1722
+ popDurationMs,
1723
+ overextend,
1724
+ startSnapRatio,
1725
+ loadingText,
1726
+ doneText,
1727
+ doneFadeDurationMs,
1728
+ removeOnComplete
1729
+ } = this.options;
1730
+ const goal = targetProgress ?? progress;
1731
+ if (this.phase === "startPop" || this.phase === "active") {
1732
+ this.activeProgress = progress;
1733
+ if (this.phase === "startPop") this.popTarget = Math.max(this.popTarget, goal, progress);
1734
+ }
1735
+ let scale2 = 0;
1736
+ let text = null;
1737
+ let textOpacity = 0;
1738
+ let hidden = false;
1739
+ if (this.phase === "startPop") {
1740
+ const t = popPhaseT(now, this.phaseStart, popDurationMs);
1741
+ const peak = this.popTarget * (1 + overextend);
1742
+ if (t < startSnapRatio) {
1743
+ const snapT = startSnapRatio > 0 ? t / startSnapRatio : 1;
1744
+ scale2 = peak * easeOutExpo(snapT);
1745
+ } else {
1746
+ const settleT = startSnapRatio < 1 ? (t - startSnapRatio) / (1 - startSnapRatio) : 1;
1747
+ scale2 = peak + (this.activeProgress - peak) * easeOutCubic(settleT);
1748
+ }
1749
+ if (t >= 1) this.phase = "active";
1750
+ } else if (this.phase === "active") {
1751
+ scale2 = this.activeProgress;
1752
+ } else if (this.phase === "endPop") {
1753
+ const t = popPhaseT(now, this.phaseStart, popDurationMs);
1754
+ const peak = 1 + overextend;
1755
+ if (t < 0.5) {
1756
+ scale2 = 1 + (peak - 1) * easeOutQuad(t * 2);
1757
+ } else {
1758
+ scale2 = peak * (1 - easeInQuad((t - 0.5) * 2));
1759
+ }
1760
+ if (t >= 1) {
1761
+ this.phase = "done";
1762
+ this.doneFadeStart = now;
1763
+ scale2 = 0;
1764
+ }
1765
+ }
1766
+ if (this.phase === "startPop" || this.phase === "active") {
1767
+ if (loadingText !== false) {
1768
+ text = loadingText;
1769
+ textOpacity = 0.65;
1770
+ }
1771
+ } else if (this.phase === "endPop") {
1772
+ text = doneText;
1773
+ textOpacity = 0.65;
1774
+ } else if (this.phase === "done") {
1775
+ const fadeT = popPhaseT(now, this.doneFadeStart, doneFadeDurationMs);
1776
+ if (fadeT >= 1) {
1777
+ if (removeOnComplete) hidden = true;
1778
+ this.phase = "finished";
1779
+ } else {
1780
+ text = doneText;
1781
+ textOpacity = 0.65 * (1 - fadeT);
1782
+ }
1783
+ }
1784
+ return { scale: scale2, text, textOpacity, hidden };
1785
+ }
1786
+ };
1787
+
1788
+ // src/animations/spin.ts
1789
+ var LABEL_STYLE = [
1790
+ "position:absolute",
1791
+ "inset:0",
1792
+ "display:flex",
1793
+ "align-items:center",
1794
+ "justify-content:center",
1795
+ "pointer-events:none",
1796
+ "font:600 1.1rem/1.2 system-ui,sans-serif",
1797
+ "letter-spacing:0.06em",
1798
+ "text-transform:lowercase",
1799
+ "color:rgba(255,255,255,0.65)",
1800
+ "z-index:1"
1801
+ ].join(";");
1802
+ function resolveMesh(shape) {
1803
+ if (!shape) return cube();
1804
+ return typeof shape === "function" ? shape() : shape;
1805
+ }
1806
+ function applyColor(mesh, color) {
1807
+ if (color === void 0 || Array.isArray(color) && color.length === 0) return mesh;
1808
+ const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
1809
+ return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
1810
+ }
1811
+ var SpinAnimation = class {
1812
+ constructor(options = {}) {
1813
+ this.exited = false;
1814
+ this.mesh = applyColor(resolveMesh(options.shape), options.color);
1815
+ this.spinX = options.spinX ?? 7e-4;
1816
+ this.spinY = options.spinY ?? 11e-4;
1817
+ this.backend = options.backend;
1818
+ this.transparency = options.transparency;
1819
+ this.progress = options.progressAnimation ? new ProgressAnimation(options.progressAnimation) : void 0;
1820
+ }
1821
+ mount(target) {
1822
+ target.style.position = "relative";
1823
+ const engine = new Little3dEngine({
1824
+ backend: this.backend,
1825
+ camera: { position: { x: 0, y: 0, z: 2.8 } }
1826
+ });
1827
+ this.handle = engine.add(this.mesh, { transparency: this.transparency });
1828
+ this.engine = engine;
1829
+ engine.mount(target).catch((error) => {
1830
+ target.textContent = error instanceof Error ? error.message : String(error);
1831
+ });
1832
+ if (this.progress) {
1833
+ const label = document.createElement("div");
1834
+ label.style.cssText = LABEL_STYLE;
1835
+ label.setAttribute("aria-hidden", "true");
1836
+ label.hidden = true;
1837
+ target.appendChild(label);
1838
+ this.label = label;
1839
+ }
1840
+ }
1841
+ enter(now) {
1842
+ this.progress?.enter(now);
1843
+ }
1844
+ exit(now) {
1845
+ this.exited = true;
1846
+ this.progress?.exit(now);
1847
+ }
1848
+ isFinished() {
1849
+ return this.progress ? this.progress.isFinished() : this.exited;
1850
+ }
1851
+ render(now, frame) {
1852
+ if (!this.engine || !this.handle) return;
1853
+ const rotation = this.handle.transform.rotation;
1854
+ rotation.x = now * this.spinX;
1855
+ rotation.y = now * this.spinY;
1856
+ if (this.progress) {
1857
+ const visual = this.progress.update(now, frame.progress, frame.targetProgress);
1858
+ this.handle.transform.scale = visual.hidden ? 0 : visual.scale;
1859
+ this.applyLabel(visual);
1860
+ } else {
1861
+ this.handle.transform.scale = 1;
1862
+ }
1863
+ this.engine.render();
1864
+ }
1865
+ destroy() {
1866
+ this.label?.remove();
1867
+ this.label = void 0;
1868
+ this.engine?.destroy();
1869
+ this.engine = void 0;
1870
+ this.handle = void 0;
1871
+ }
1872
+ applyLabel(visual) {
1873
+ if (!this.label) return;
1874
+ if (visual.hidden || visual.text == null) {
1875
+ this.label.hidden = true;
1876
+ this.label.textContent = "";
1877
+ return;
1878
+ }
1879
+ this.label.hidden = false;
1880
+ this.label.textContent = visual.text;
1881
+ this.label.style.opacity = String(visual.textOpacity);
1882
+ }
1883
+ };
1884
+
1885
+ // src/animations/object-motion.ts
1886
+ init_math();
1887
+
1888
+ // src/motion/transitions.ts
1889
+ var DEFAULT_DISTANCE = 3.5;
1890
+ function add(a, b) {
1891
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
1892
+ }
1893
+ function scaleVector(v, factor) {
1894
+ return { x: v.x * factor, y: v.y * factor, z: v.z * factor };
1895
+ }
1896
+ function vectorLength(v) {
1897
+ return Math.hypot(v.x, v.y, v.z);
1898
+ }
1899
+ function normalizeVector(v) {
1900
+ const length = vectorLength(v);
1901
+ if (length < 1e-6) return { x: 1, y: 0, z: 0 };
1902
+ return scaleVector(v, 1 / length);
1903
+ }
1904
+ function resolveDirection(input2, fallback) {
1905
+ return normalizeVector(fallback ?? input2.direction ?? input2.velocity ?? { x: 1, y: 0, z: 0 });
1906
+ }
1907
+ function easeOutBack2(delta) {
1908
+ const c = 1.70158;
1909
+ const u = delta - 1;
1910
+ return 1 + (c + 1) * u * u * u + c * u * u;
1911
+ }
1912
+ function joinVelocity(input2, options, durationMs) {
1913
+ const inputSpeed = input2.velocity ? vectorLength(input2.velocity) : 0;
1914
+ if (input2.velocity && inputSpeed > 1e-6 && !options.direction) {
1915
+ return input2.velocity;
1916
+ }
1917
+ const distance = options.distance ?? DEFAULT_DISTANCE;
1918
+ return scaleVector(resolveDirection(input2, options.direction), distance / durationMs);
1919
+ }
1920
+ function enterFromObjectDirection(options = {}) {
1921
+ return (input2) => {
1922
+ const durationMs = Math.max(1, input2.durationMs);
1923
+ const velocity = joinVelocity(input2, options, durationMs);
1924
+ const remaining = durationMs - input2.elapsedMs;
1925
+ return { position: add(input2.position, scaleVector(velocity, -remaining)) };
1926
+ };
1927
+ }
1928
+ function leaveInObjectDirection(options = {}) {
1929
+ return (input2) => {
1930
+ const durationMs = Math.max(1, input2.durationMs);
1931
+ const velocity = joinVelocity(input2, options, durationMs);
1932
+ return { position: add(input2.position, scaleVector(velocity, input2.elapsedMs)) };
1933
+ };
1934
+ }
1935
+ function grow() {
1936
+ return (input2) => ({ size: (input2.size ?? 1) * easeOutBack2(input2.delta) });
1937
+ }
1938
+ function shrink() {
1939
+ return (input2) => ({ size: (input2.size ?? 1) * (1 - input2.delta * input2.delta) });
1940
+ }
1941
+
1942
+ // src/animations/object-motion.ts
1943
+ var LABEL_STYLE2 = [
1944
+ "position:absolute",
1945
+ "inset:0",
1946
+ "display:flex",
1947
+ "align-items:center",
1948
+ "justify-content:center",
1949
+ "pointer-events:none",
1950
+ "font:700 1.6rem/1 system-ui,sans-serif",
1951
+ "letter-spacing:0.02em",
1952
+ "color:rgba(255,255,255,0.9)",
1953
+ "text-shadow:0 1px 10px rgba(0,0,0,0.6)",
1954
+ "z-index:1"
1955
+ ].join(";");
1956
+ var WORLD_UP = { x: 0, y: 1, z: 0 };
1957
+ var DEFAULT_INTRO_MS = 2100;
1958
+ var DEFAULT_OUTRO_MS = 2100;
1959
+ var BANK_GAIN = 26;
1960
+ var BANK_LIMIT = 0.7;
1961
+ var BANK_SMOOTH = 0.12;
1962
+ var SAMPLE_MS = 8;
1963
+ var FACE_FORWARD = {
1964
+ "+x": (v) => v,
1965
+ "-x": (v) => ({ x: -v.x, y: v.y, z: -v.z }),
1966
+ "+z": (v) => ({ x: v.z, y: v.y, z: -v.x }),
1967
+ "-z": (v) => ({ x: -v.z, y: v.y, z: v.x }),
1968
+ "+y": (v) => ({ x: v.y, y: -v.x, z: v.z }),
1969
+ "-y": (v) => ({ x: -v.y, y: v.x, z: v.z })
1970
+ };
1971
+ function add2(a, b) {
1972
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
1973
+ }
1974
+ function resolveMesh2(mesh) {
1975
+ return typeof mesh === "function" ? mesh() : mesh;
1976
+ }
1977
+ function applyColor2(mesh, color) {
1978
+ return { vertices: mesh.vertices, faces: mesh.faces.map((face) => ({ ...face, color })) };
1979
+ }
1980
+ function faceForward(mesh, facing) {
1981
+ if (facing === "+x") return mesh;
1982
+ const turn = FACE_FORWARD[facing];
1983
+ return { vertices: mesh.vertices.map(turn), faces: mesh.faces };
1984
+ }
1985
+ function centerAndScaleMesh(mesh, targetSize) {
1986
+ let minX = Infinity;
1987
+ let minY = Infinity;
1988
+ let minZ = Infinity;
1989
+ let maxX = -Infinity;
1990
+ let maxY = -Infinity;
1991
+ let maxZ = -Infinity;
1992
+ for (const vertex of mesh.vertices) {
1993
+ minX = Math.min(minX, vertex.x);
1994
+ minY = Math.min(minY, vertex.y);
1995
+ minZ = Math.min(minZ, vertex.z);
1996
+ maxX = Math.max(maxX, vertex.x);
1997
+ maxY = Math.max(maxY, vertex.y);
1998
+ maxZ = Math.max(maxZ, vertex.z);
1999
+ }
2000
+ const centerX = (minX + maxX) / 2;
2001
+ const centerY = (minY + maxY) / 2;
2002
+ const centerZ = (minZ + maxZ) / 2;
2003
+ const extent = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1;
2004
+ const factor = targetSize / extent;
2005
+ return {
2006
+ vertices: mesh.vertices.map((vertex) => ({
2007
+ x: (vertex.x - centerX) * factor,
2008
+ y: (vertex.y - centerY) * factor,
2009
+ z: (vertex.z - centerZ) * factor
2010
+ })),
2011
+ faces: mesh.faces
2012
+ };
2013
+ }
2014
+ function orientationFor(forward, bank) {
2015
+ const fwd = normalize(forward);
2016
+ let right = cross(fwd, WORLD_UP);
2017
+ if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
2018
+ right = normalize(right);
2019
+ const levelUp = cross(right, fwd);
2020
+ const up = add2(scale(levelUp, Math.cos(bank)), scale(right, Math.sin(bank)));
2021
+ const w = normalize(cross(fwd, up));
2022
+ return {
2023
+ x: Math.atan2(cross(w, fwd).z, w.z),
2024
+ y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
2025
+ z: Math.atan2(fwd.y, fwd.x)
2026
+ };
2027
+ }
2028
+ function rotationMatrix(x, y, z) {
2029
+ return multiply(rotationZ(z), multiply(rotationY(y), rotationX(x)));
2030
+ }
2031
+ function eulerFromRotationMatrix(matrix) {
2032
+ const sy = Math.hypot(matrix[0], matrix[1]);
2033
+ if (sy > 1e-6) {
2034
+ return {
2035
+ x: Math.atan2(matrix[9], matrix[10]),
2036
+ y: Math.asin(Math.max(-1, Math.min(1, -matrix[8]))),
2037
+ z: Math.atan2(matrix[4], matrix[0])
2038
+ };
2039
+ }
2040
+ return {
2041
+ x: Math.atan2(-matrix[6], matrix[5]),
2042
+ y: Math.asin(Math.max(-1, Math.min(1, -matrix[8]))),
2043
+ z: 0
2044
+ };
2045
+ }
2046
+ function combineLocalRotation(path, extra) {
2047
+ return eulerFromRotationMatrix(
2048
+ multiply(rotationMatrix(path.x, path.y, path.z), rotationMatrix(extra.x, extra.y, extra.z))
2049
+ );
2050
+ }
2051
+ function clamp013(value) {
2052
+ return Math.max(0, Math.min(1, value));
2053
+ }
2054
+ function motionVectorAt(motion, t) {
2055
+ return scale(subtract(motion.positionAt(t + 1), motion.positionAt(t - 1)), 0.5);
2056
+ }
2057
+ function resolveDirection2(velocity, fallback) {
2058
+ return Math.hypot(velocity.x, velocity.y, velocity.z) > 1e-6 ? normalize(velocity) : fallback;
2059
+ }
2060
+ function resolveTransition(config, fallback, durationMs) {
2061
+ if (!config) return { transition: fallback, durationMs };
2062
+ if (typeof config === "function") return { transition: config, durationMs };
2063
+ return { transition: config.transition, durationMs: Math.max(0, config.durationMs ?? durationMs) };
2064
+ }
2065
+ var ObjectMotionAnimation = class {
2066
+ constructor(options) {
2067
+ this.handles = [];
2068
+ this.banks = [];
2069
+ this.headings = [];
2070
+ this.started = false;
2071
+ this.finished = false;
2072
+ this.introStart = 0;
2073
+ this.outroStart = Infinity;
2074
+ this.outroPosition = { x: 0, y: 0, z: 0 };
2075
+ this.outroVelocity = { x: 0, y: 0, z: 0 };
2076
+ this.outroDirection = { x: 1, y: 0, z: 0 };
2077
+ const centered = centerAndScaleMesh(resolveMesh2(options.mesh), options.size ?? 1);
2078
+ const facing = faceForward(centered, options.facing ?? "+x");
2079
+ this.mesh = applyColor2(facing, options.color ?? "#cbd5e1");
2080
+ this.motion = options.motion;
2081
+ this.backend = options.backend;
2082
+ this.transparency = options.transparency;
2083
+ this.labelText = options.label;
2084
+ this.tailCount = Math.max(0, Math.floor(options.tail?.count ?? 0));
2085
+ this.tailGap = Math.max(0, options.tail?.gapMs ?? 0);
2086
+ this.intro = resolveTransition(options.intro, grow(), DEFAULT_INTRO_MS);
2087
+ this.outro = resolveTransition(options.outro, shrink(), DEFAULT_OUTRO_MS);
2088
+ const rotation = options.rotation;
2089
+ this.rotationOffset = { x: rotation?.x ?? 0, y: rotation?.y ?? 0, z: rotation?.z ?? 0 };
2090
+ this.rotationSpin = {
2091
+ x: rotation?.spinX ?? 0,
2092
+ y: rotation?.spinY ?? 0,
2093
+ z: rotation?.spinZ ?? 0
2094
+ };
2095
+ this.hasExtraRotation = this.rotationOffset.x !== 0 || this.rotationOffset.y !== 0 || this.rotationOffset.z !== 0 || this.rotationSpin.x !== 0 || this.rotationSpin.y !== 0 || this.rotationSpin.z !== 0;
2096
+ }
2097
+ mount(target) {
2098
+ target.style.position = "relative";
2099
+ const engine = new Little3dEngine({
2100
+ backend: this.backend,
2101
+ camera: { position: { x: 0, y: 0, z: 3 } }
2102
+ });
2103
+ for (let i = 0; i <= this.tailCount; i++) {
2104
+ this.handles.push(engine.add(this.mesh, { transparency: this.transparency }));
2105
+ this.banks.push(0);
2106
+ this.headings.push({ x: 1, y: 0, z: 0 });
2107
+ }
2108
+ this.engine = engine;
2109
+ engine.mount(target).catch((error) => {
2110
+ target.textContent = error instanceof Error ? error.message : String(error);
2111
+ });
2112
+ const label = document.createElement("div");
2113
+ label.style.cssText = LABEL_STYLE2;
2114
+ label.setAttribute("role", "status");
2115
+ target.appendChild(label);
2116
+ this.label = label;
2117
+ }
2118
+ enter(now) {
2119
+ if (this.started) return;
2120
+ this.started = true;
2121
+ this.introStart = now;
2122
+ }
2123
+ exit(now) {
2124
+ if (!this.started || this.outroStart !== Infinity) return;
2125
+ this.outroPosition = this.motion.positionAt(now);
2126
+ this.outroVelocity = motionVectorAt(this.motion, now);
2127
+ this.outroDirection = resolveDirection2(this.outroVelocity, this.headings[0]);
2128
+ this.outroStart = now;
2129
+ }
2130
+ isFinished() {
2131
+ return this.finished;
2132
+ }
2133
+ render(now, frame) {
2134
+ if (!this.engine || !this.label) return;
2135
+ if (this.outroStart !== Infinity && now >= this.outroStart + this.outro.durationMs + this.tailCount * this.tailGap) {
2136
+ this.finished = true;
2137
+ }
2138
+ for (let k = 0; k < this.handles.length; k++) {
2139
+ const transform2 = this.handles[k].transform;
2140
+ const t = now - k * this.tailGap;
2141
+ const sample = this.sampleAt(t);
2142
+ if (!sample) {
2143
+ transform2.scale = 0;
2144
+ continue;
2145
+ }
2146
+ transform2.scale = sample.size;
2147
+ let euler = sample.orientation;
2148
+ if (!euler) {
2149
+ const heading = subtract(this.positionAt(t + SAMPLE_MS) ?? sample.position, sample.position);
2150
+ if (Math.hypot(heading.x, heading.y, heading.z) > 1e-5) {
2151
+ this.headings[k] = normalize(heading);
2152
+ }
2153
+ const ahead = this.aheadAt(t) ?? this.headings[k];
2154
+ const targetBank = Math.max(
2155
+ -BANK_LIMIT,
2156
+ Math.min(BANK_LIMIT, cross(this.headings[k], ahead).y * BANK_GAIN)
2157
+ );
2158
+ this.banks[k] += (targetBank - this.banks[k]) * BANK_SMOOTH;
2159
+ euler = orientationFor(this.headings[k], this.banks[k]);
2160
+ }
2161
+ if (this.hasExtraRotation) {
2162
+ euler = combineLocalRotation(euler, {
2163
+ x: this.rotationOffset.x + this.rotationSpin.x * t,
2164
+ y: this.rotationOffset.y + this.rotationSpin.y * t,
2165
+ z: this.rotationOffset.z + this.rotationSpin.z * t
2166
+ });
2167
+ }
2168
+ transform2.position.x = sample.position.x;
2169
+ transform2.position.y = sample.position.y;
2170
+ transform2.position.z = sample.position.z;
2171
+ transform2.rotation.x = euler.x;
2172
+ transform2.rotation.y = euler.y;
2173
+ transform2.rotation.z = euler.z;
2174
+ }
2175
+ this.label.textContent = frame.indeterminate ? this.labelText ?? "" : `${Math.round(frame.progress * 100)}%`;
2176
+ this.engine.render();
2177
+ }
2178
+ destroy() {
2179
+ this.label?.remove();
2180
+ this.label = void 0;
2181
+ this.engine?.destroy();
2182
+ this.engine = void 0;
2183
+ this.handles.length = 0;
2184
+ }
2185
+ aheadAt(t) {
2186
+ const next = this.positionAt(t + SAMPLE_MS);
2187
+ const afterNext = this.positionAt(t + 2 * SAMPLE_MS);
2188
+ if (!next || !afterNext) return void 0;
2189
+ const ahead = subtract(afterNext, next);
2190
+ if (Math.hypot(ahead.x, ahead.y, ahead.z) <= 1e-5) return void 0;
2191
+ return normalize(ahead);
2192
+ }
2193
+ positionAt(t) {
2194
+ return this.sampleAt(t)?.position;
2195
+ }
2196
+ sampleAt(t) {
2197
+ if (!this.started || t < this.introStart) return void 0;
2198
+ if (t < this.introStart + this.intro.durationMs) {
2199
+ return this.transitionSample("intro", t, this.intro, this.introStart);
2200
+ }
2201
+ if (this.outroStart !== Infinity) {
2202
+ if (t > this.outroStart + this.outro.durationMs) return void 0;
2203
+ if (t >= this.outroStart) return this.transitionSample("outro", t, this.outro, this.outroStart);
2204
+ }
2205
+ return { position: this.motion.positionAt(t), size: 1 };
2206
+ }
2207
+ transitionSample(phase, t, transition, start) {
2208
+ const elapsedMs = Math.max(0, t - start);
2209
+ const delta = transition.durationMs === 0 ? 1 : clamp013(elapsedMs / transition.durationMs);
2210
+ const input2 = this.transitionInput(phase, delta, elapsedMs, transition.durationMs, start);
2211
+ const output = transition.transition(input2);
2212
+ return this.applyTransitionOutput(input2, output);
2213
+ }
2214
+ transitionInput(phase, delta, elapsedMs, durationMs, start) {
2215
+ if (phase === "intro") {
2216
+ const handoff = start + durationMs;
2217
+ const velocity = motionVectorAt(this.motion, handoff);
2218
+ return {
2219
+ delta,
2220
+ position: this.motion.positionAt(handoff),
2221
+ direction: resolveDirection2(velocity, { x: 1, y: 0, z: 0 }),
2222
+ velocity,
2223
+ size: 1,
2224
+ durationMs,
2225
+ elapsedMs,
2226
+ phase
2227
+ };
2228
+ }
2229
+ return {
2230
+ delta,
2231
+ position: this.outroPosition,
2232
+ direction: this.outroDirection,
2233
+ velocity: this.outroVelocity,
2234
+ size: 1,
2235
+ durationMs,
2236
+ elapsedMs,
2237
+ phase
2238
+ };
2239
+ }
2240
+ applyTransitionOutput(input2, output) {
2241
+ return {
2242
+ position: output.position ?? input2.position,
2243
+ size: output.size ?? input2.size ?? 1,
2244
+ orientation: output.orientation
2245
+ };
2246
+ }
2247
+ };
2248
+
2249
+ // src/motion/circle.ts
2250
+ function circleMotion(options = {}) {
2251
+ const radius = options.radius ?? 1.3;
2252
+ const periodMs = options.periodMs ?? 3e3;
2253
+ const tilt = options.tilt ?? 0.5;
2254
+ const direction = options.direction ?? 1;
2255
+ const cosTilt = Math.cos(tilt);
2256
+ const sinTilt = Math.sin(tilt);
2257
+ return {
2258
+ positionAt(t) {
2259
+ const angle = direction * t / periodMs * Math.PI * 2;
2260
+ const x = radius * Math.cos(angle);
2261
+ const flatY = radius * Math.sin(angle);
2262
+ return { x, y: flatY * cosTilt, z: flatY * sinTilt };
2263
+ }
2264
+ };
2265
+ }
2266
+
2267
+ // src/motion/square.ts
2268
+ function squareMotion(options = {}) {
2269
+ const half = (options.size ?? 2.4) / 2;
2270
+ const periodMs = options.periodMs ?? 4e3;
2271
+ const tilt = options.tilt ?? 0.45;
2272
+ const direction = options.direction ?? 1;
2273
+ const cosTilt = Math.cos(tilt);
2274
+ const sinTilt = Math.sin(tilt);
2275
+ return {
2276
+ positionAt(t) {
2277
+ const lap = direction * t / periodMs;
2278
+ const s = (lap - Math.floor(lap)) * 4;
2279
+ const edge = Math.floor(s);
2280
+ const f = s - edge;
2281
+ let flatX;
2282
+ let flatY;
2283
+ if (edge === 0) {
2284
+ flatX = -half + 2 * half * f;
2285
+ flatY = -half;
2286
+ } else if (edge === 1) {
2287
+ flatX = half;
2288
+ flatY = -half + 2 * half * f;
2289
+ } else if (edge === 2) {
2290
+ flatX = half - 2 * half * f;
2291
+ flatY = half;
2292
+ } else {
2293
+ flatX = -half;
2294
+ flatY = half - 2 * half * f;
2295
+ }
2296
+ return { x: flatX, y: flatY * cosTilt, z: flatY * sinTilt };
2297
+ }
2298
+ };
2299
+ }
2300
+
2301
+ // src/motion/figure-eight.ts
2302
+ var LOOP_X = 1.5;
2303
+ var LOOP_Y = 1;
2304
+ var LOOP_Z = 1.05;
2305
+ function figureEightMotion(options = {}) {
2306
+ const size = options.size ?? 1;
2307
+ const periodMs = options.periodMs ?? 3600;
2308
+ return {
2309
+ positionAt(t) {
2310
+ const a = t / periodMs * Math.PI * 2;
2311
+ return {
2312
+ x: size * LOOP_X * Math.sin(a),
2313
+ y: size * LOOP_Y * Math.sin(a) * Math.cos(a),
2314
+ z: size * LOOP_Z * Math.cos(a)
2315
+ };
2316
+ }
2317
+ };
2318
+ }
2319
+
2320
+ // src/motion/wander.ts
2321
+ function mulberry32(seed) {
2322
+ let a = seed >>> 0;
2323
+ return () => {
2324
+ a = a + 1831565813 | 0;
2325
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
2326
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
2327
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
2328
+ };
2329
+ }
2330
+ function axisDrift(rnd, omega, bound) {
2331
+ const components = [0, 1, 2].map(() => ({
2332
+ freq: 0.5 + rnd() * 1.4,
2333
+ phase: rnd() * Math.PI * 2,
2334
+ weight: 0.5 + rnd()
2335
+ }));
2336
+ const total = components.reduce((sum, c) => sum + c.weight, 0);
2337
+ return (t) => {
2338
+ let value = 0;
2339
+ for (const c of components) value += c.weight * Math.sin(omega * c.freq * t + c.phase);
2340
+ return value / total * bound;
2341
+ };
2342
+ }
2343
+ function wanderMotion(options = {}) {
2344
+ const boundX = options.bounds?.x ?? 1.4;
2345
+ const boundY = options.bounds?.y ?? 1;
2346
+ const boundZ = options.bounds?.z ?? 0.6;
2347
+ const periodMs = options.periodMs ?? 9e3;
2348
+ const seed = options.seed ?? Math.random() * 1e9 | 0;
2349
+ const rnd = mulberry32(seed);
2350
+ const omega = 2 * Math.PI / periodMs;
2351
+ const driftX = axisDrift(rnd, omega, boundX);
2352
+ const driftY = axisDrift(rnd, omega, boundY);
2353
+ const driftZ = axisDrift(rnd, omega, boundZ);
2354
+ return {
2355
+ positionAt(t) {
2356
+ return { x: driftX(t), y: driftY(t), z: driftZ(t) };
2357
+ }
2358
+ };
2359
+ }
2360
+
2361
+ // src/engines/little-3d-engine/loaders/obj.ts
2362
+ var DEFAULT_COLORS9 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
2363
+ function resolveIndex(token, vertexCount) {
2364
+ const n = parseInt(token, 10);
2365
+ return n < 0 ? vertexCount + n : n - 1;
2366
+ }
2367
+ function parseObj(text, options = {}) {
2368
+ const colors = options.colors ?? DEFAULT_COLORS9;
2369
+ const vertices = [];
2370
+ const faces = [];
2371
+ for (const line of text.split("\n")) {
2372
+ const trimmed = line.trim();
2373
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
2374
+ const parts = trimmed.split(/\s+/);
2375
+ const keyword = parts[0];
2376
+ if (keyword === "v") {
2377
+ vertices.push({
2378
+ x: parseFloat(parts[1]),
2379
+ y: parseFloat(parts[2]),
2380
+ z: parseFloat(parts[3])
2381
+ });
2382
+ } else if (keyword === "f") {
2383
+ const indices = [];
2384
+ for (let i = 1; i < parts.length; i++) {
2385
+ const vertexToken = parts[i].split("/")[0];
2386
+ indices.push(resolveIndex(vertexToken, vertices.length));
2387
+ }
2388
+ if (indices.length >= 3) {
2389
+ faces.push({ indices, color: colors[faces.length % colors.length] });
2390
+ }
2391
+ }
2392
+ }
2393
+ return { vertices, faces };
2394
+ }
2395
+
2396
+ // src/engines/little-tween-engine/little-tween-engine.ts
2397
+ var LittleTweenEngine = class {
2398
+ constructor(options = {}) {
2399
+ this.type = options.type ?? "linear";
2400
+ this.overextend = options.overextend ?? false;
2401
+ }
2402
+ /** Map `value` through the selected ease type. */
2403
+ value(value, type = this.type, overextend = this.overextend) {
2404
+ return ease(type, value, overextend);
2405
+ }
2406
+ };
2407
+ return __toCommonJS(stdin_exports);
2408
+ })();