3d-spinner 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animations/charged-orb.js +4 -2
- package/dist/animations/ghost-train.d.ts +72 -0
- package/dist/animations/ghost-train.js +249 -0
- package/dist/animations/grid-assembly.d.ts +11 -8
- package/dist/animations/grid-assembly.js +44 -30
- package/dist/animations/rocket-launch.d.ts +58 -0
- package/dist/animations/rocket-launch.js +375 -0
- package/dist/animations/spin.d.ts +3 -1
- package/dist/animations/spin.js +6 -1
- package/dist/cjs/animations/charged-orb.cjs +170 -28
- package/dist/cjs/animations/grid-assembly.cjs +192 -172
- package/dist/cjs/animations/object-motion.cjs +156 -24
- package/dist/cjs/animations/particles.cjs +167 -26
- package/dist/cjs/animations/spin.cjs +169 -27
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +215 -55
- package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +51 -13
- package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +56 -6
- package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +55 -6
- package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +64 -11
- package/dist/cjs/prefabs/prefabs.cjs +817 -205
- package/dist/engines/little-3d-engine/core/geometry.d.ts +15 -2
- package/dist/engines/little-3d-engine/core/geometry.js +25 -3
- package/dist/engines/little-3d-engine/core/light.d.ts +28 -4
- package/dist/engines/little-3d-engine/core/light.js +48 -7
- package/dist/engines/little-3d-engine/core/mesh.d.ts +33 -0
- package/dist/engines/little-3d-engine/core/mesh.js +12 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +2 -2
- package/dist/engines/little-3d-engine/little-3d-engine.js +1 -1
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +8 -7
- package/dist/engines/little-3d-engine/loaders/obj.js +74 -20
- package/dist/engines/little-3d-engine/renderers/canvas2d.js +23 -1
- package/dist/engines/little-3d-engine/renderers/webgl.js +34 -5
- package/dist/engines/little-3d-engine/renderers/webgpu.js +43 -10
- package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +8 -3
- package/dist/engines/little-3d-engine/shapes/complex/plane.js +10 -4
- package/dist/engines/little-3d-engine/shapes/primitives/cube.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/cube.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/octahedron.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/octahedron.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/pyramid.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/pyramid.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/quad.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/icosphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.js +4 -2
- package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.d.ts +3 -2
- package/dist/engines/little-3d-engine/shapes/primitives/tetrahedron.js +4 -2
- package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +14 -2
- package/dist/engines/little-3d-engine/textures/dynamic/star.js +26 -11
- package/dist/prefabs/ghost-train.d.ts +5 -4
- package/dist/prefabs/ghost-train.js +17 -24
- package/dist/prefabs/rocket-launch.d.ts +7 -5
- package/dist/prefabs/rocket-launch.js +10 -53
- package/dist/umd/spinner.global.js +877 -167
- package/dist/umd/spinner.global.min.js +58 -17
- package/package.json +2 -2
|
@@ -139,7 +139,10 @@ var Spinner3D = (() => {
|
|
|
139
139
|
const positions = new Float32Array(triangles * 9);
|
|
140
140
|
const normals = new Float32Array(triangles * 9);
|
|
141
141
|
const colors = new Float32Array(triangles * 9);
|
|
142
|
+
const emissives = new Float32Array(triangles * 9);
|
|
143
|
+
const speculars = new Float32Array(triangles * 12);
|
|
142
144
|
let o = 0;
|
|
145
|
+
let so = 0;
|
|
143
146
|
for (const face of mesh.faces) {
|
|
144
147
|
const v0 = mesh.vertices[face.indices[0]];
|
|
145
148
|
const v1 = mesh.vertices[face.indices[1]];
|
|
@@ -149,6 +152,15 @@ var Spinner3D = (() => {
|
|
|
149
152
|
const cr = r / 255;
|
|
150
153
|
const cg = g / 255;
|
|
151
154
|
const cb = b / 255;
|
|
155
|
+
const emissive = face.material?.emissive;
|
|
156
|
+
const er = emissive ? emissive[0] : 0;
|
|
157
|
+
const eg = emissive ? emissive[1] : 0;
|
|
158
|
+
const eb = emissive ? emissive[2] : 0;
|
|
159
|
+
const specular = face.material?.specular;
|
|
160
|
+
const sr = specular ? specular[0] : 0;
|
|
161
|
+
const sg = specular ? specular[1] : 0;
|
|
162
|
+
const sb = specular ? specular[2] : 0;
|
|
163
|
+
const sn = specular ? face.material?.shininess ?? 32 : 1;
|
|
152
164
|
for (let k = 1; k < face.indices.length - 1; k++) {
|
|
153
165
|
const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
|
|
154
166
|
for (const index of tri) {
|
|
@@ -162,11 +174,19 @@ var Spinner3D = (() => {
|
|
|
162
174
|
colors[o] = cr;
|
|
163
175
|
colors[o + 1] = cg;
|
|
164
176
|
colors[o + 2] = cb;
|
|
177
|
+
emissives[o] = er;
|
|
178
|
+
emissives[o + 1] = eg;
|
|
179
|
+
emissives[o + 2] = eb;
|
|
180
|
+
speculars[so] = sr;
|
|
181
|
+
speculars[so + 1] = sg;
|
|
182
|
+
speculars[so + 2] = sb;
|
|
183
|
+
speculars[so + 3] = sn;
|
|
165
184
|
o += 3;
|
|
185
|
+
so += 4;
|
|
166
186
|
}
|
|
167
187
|
}
|
|
168
188
|
}
|
|
169
|
-
return { positions, normals, colors, count: positions.length / 3 };
|
|
189
|
+
return { positions, normals, colors, emissives, speculars, count: positions.length / 3 };
|
|
170
190
|
}
|
|
171
191
|
function midpoint(a, b) {
|
|
172
192
|
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
|
|
@@ -204,13 +224,42 @@ var Spinner3D = (() => {
|
|
|
204
224
|
function clamp012(value) {
|
|
205
225
|
return Math.min(1, Math.max(0, value));
|
|
206
226
|
}
|
|
207
|
-
function
|
|
227
|
+
function clamp255(value) {
|
|
228
|
+
return Math.round(Math.min(255, Math.max(0, value)));
|
|
229
|
+
}
|
|
230
|
+
function shade(normal, color, light, surface) {
|
|
208
231
|
const lambert = Math.max(0, dot(normal, light.toLight));
|
|
209
232
|
const brightness = clamp012(light.ambient + light.intensity * lambert);
|
|
210
|
-
const [
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
233
|
+
const [baseR, baseG, baseB] = parseColor(color);
|
|
234
|
+
let r = baseR * brightness;
|
|
235
|
+
let g = baseG * brightness;
|
|
236
|
+
let b = baseB * brightness;
|
|
237
|
+
const material = surface?.material;
|
|
238
|
+
const specular = material?.specular;
|
|
239
|
+
const viewDir = surface?.viewDir;
|
|
240
|
+
if (specular && viewDir && lambert > 0) {
|
|
241
|
+
const half = normalize({
|
|
242
|
+
x: light.toLight.x + viewDir.x,
|
|
243
|
+
y: light.toLight.y + viewDir.y,
|
|
244
|
+
z: light.toLight.z + viewDir.z
|
|
245
|
+
});
|
|
246
|
+
const shininess = material?.shininess ?? 32;
|
|
247
|
+
const highlight = Math.pow(Math.max(0, dot(normal, half)), shininess) * light.intensity * 255;
|
|
248
|
+
r += highlight * specular[0];
|
|
249
|
+
g += highlight * specular[1];
|
|
250
|
+
b += highlight * specular[2];
|
|
251
|
+
}
|
|
252
|
+
const emissive = material?.emissive;
|
|
253
|
+
if (emissive) {
|
|
254
|
+
r += emissive[0] * 255;
|
|
255
|
+
g += emissive[1] * 255;
|
|
256
|
+
b += emissive[2] * 255;
|
|
257
|
+
}
|
|
258
|
+
return [clamp255(r), clamp255(g), clamp255(b)];
|
|
259
|
+
}
|
|
260
|
+
function shadeColor(normal, color, light, surface) {
|
|
261
|
+
const [r, g, b] = shade(normal, color, light, surface);
|
|
262
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
214
263
|
}
|
|
215
264
|
var DEFAULTS2, Light;
|
|
216
265
|
var init_light = __esm({
|
|
@@ -233,8 +282,8 @@ var Spinner3D = (() => {
|
|
|
233
282
|
};
|
|
234
283
|
}
|
|
235
284
|
/** Convenience wrapper around {@link shadeColor} using this light. */
|
|
236
|
-
shade(normal, color) {
|
|
237
|
-
return shadeColor(normal, color, this.params);
|
|
285
|
+
shade(normal, color, surface) {
|
|
286
|
+
return shadeColor(normal, color, this.params, surface);
|
|
238
287
|
}
|
|
239
288
|
};
|
|
240
289
|
}
|
|
@@ -274,28 +323,51 @@ var Spinner3D = (() => {
|
|
|
274
323
|
in vec3 aPos;
|
|
275
324
|
in vec3 aNormal;
|
|
276
325
|
in vec3 aColor;
|
|
326
|
+
in vec3 aEmissive;
|
|
327
|
+
in vec4 aSpecular;
|
|
277
328
|
uniform mat4 uViewProj;
|
|
278
329
|
uniform mat4 uModel;
|
|
279
330
|
out vec3 vNormal;
|
|
280
331
|
out vec3 vColor;
|
|
332
|
+
out vec3 vEmissive;
|
|
333
|
+
out vec4 vSpecular;
|
|
334
|
+
out vec3 vWorldPos;
|
|
281
335
|
void main() {
|
|
282
336
|
vNormal = mat3(uModel) * aNormal;
|
|
283
337
|
vColor = aColor;
|
|
284
|
-
|
|
338
|
+
vEmissive = aEmissive;
|
|
339
|
+
vSpecular = aSpecular;
|
|
340
|
+
vec4 world = uModel * vec4(aPos, 1.0);
|
|
341
|
+
vWorldPos = world.xyz;
|
|
342
|
+
gl_Position = uViewProj * world;
|
|
285
343
|
}`;
|
|
286
344
|
FRAGMENT_SHADER = `#version 300 es
|
|
287
345
|
precision mediump float;
|
|
288
346
|
in vec3 vNormal;
|
|
289
347
|
in vec3 vColor;
|
|
348
|
+
in vec3 vEmissive;
|
|
349
|
+
in vec4 vSpecular;
|
|
350
|
+
in vec3 vWorldPos;
|
|
290
351
|
uniform vec3 uToLight;
|
|
352
|
+
uniform vec3 uEye;
|
|
291
353
|
uniform float uIntensity;
|
|
292
354
|
uniform float uAmbient;
|
|
293
355
|
uniform float uOpacity;
|
|
294
356
|
out vec4 fragColor;
|
|
295
357
|
void main() {
|
|
296
|
-
|
|
358
|
+
vec3 normal = normalize(vNormal);
|
|
359
|
+
vec3 toLight = normalize(uToLight);
|
|
360
|
+
float lambert = max(dot(normal, toLight), 0.0);
|
|
297
361
|
float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
|
|
298
|
-
|
|
362
|
+
vec3 lit = vColor * brightness;
|
|
363
|
+
if (lambert > 0.0) {
|
|
364
|
+
vec3 viewDir = normalize(uEye - vWorldPos);
|
|
365
|
+
vec3 halfVec = normalize(toLight + viewDir);
|
|
366
|
+
float highlight = pow(max(dot(normal, halfVec), 0.0), vSpecular.w) * uIntensity;
|
|
367
|
+
lit += highlight * vSpecular.xyz;
|
|
368
|
+
}
|
|
369
|
+
lit += vEmissive;
|
|
370
|
+
fragColor = vec4(lit, uOpacity);
|
|
299
371
|
}`;
|
|
300
372
|
WebGLRenderer = class {
|
|
301
373
|
constructor(options = {}) {
|
|
@@ -316,9 +388,12 @@ void main() {
|
|
|
316
388
|
aPos: gl.getAttribLocation(this.program, "aPos"),
|
|
317
389
|
aNormal: gl.getAttribLocation(this.program, "aNormal"),
|
|
318
390
|
aColor: gl.getAttribLocation(this.program, "aColor"),
|
|
391
|
+
aEmissive: gl.getAttribLocation(this.program, "aEmissive"),
|
|
392
|
+
aSpecular: gl.getAttribLocation(this.program, "aSpecular"),
|
|
319
393
|
uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
|
|
320
394
|
uModel: gl.getUniformLocation(this.program, "uModel"),
|
|
321
395
|
uToLight: gl.getUniformLocation(this.program, "uToLight"),
|
|
396
|
+
uEye: gl.getUniformLocation(this.program, "uEye"),
|
|
322
397
|
uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
|
|
323
398
|
uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
|
|
324
399
|
uOpacity: gl.getUniformLocation(this.program, "uOpacity")
|
|
@@ -342,17 +417,19 @@ void main() {
|
|
|
342
417
|
const data = expandToTriangles(mesh);
|
|
343
418
|
const vao = gl.createVertexArray();
|
|
344
419
|
gl.bindVertexArray(vao);
|
|
345
|
-
const attribute = (location, array) => {
|
|
420
|
+
const attribute = (location, array, size = 3) => {
|
|
346
421
|
if (location < 0) return;
|
|
347
422
|
const buffer = gl.createBuffer();
|
|
348
423
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
349
424
|
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
|
|
350
425
|
gl.enableVertexAttribArray(location);
|
|
351
|
-
gl.vertexAttribPointer(location,
|
|
426
|
+
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
|
|
352
427
|
};
|
|
353
428
|
attribute(loc.aPos, data.positions);
|
|
354
429
|
attribute(loc.aNormal, data.normals);
|
|
355
430
|
attribute(loc.aColor, data.colors);
|
|
431
|
+
attribute(loc.aEmissive, data.emissives);
|
|
432
|
+
attribute(loc.aSpecular, data.speculars, 4);
|
|
356
433
|
gl.bindVertexArray(null);
|
|
357
434
|
const result = { vao, count: data.count };
|
|
358
435
|
this.cache.set(mesh, result);
|
|
@@ -367,6 +444,7 @@ void main() {
|
|
|
367
444
|
gl.useProgram(this.program);
|
|
368
445
|
gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
|
|
369
446
|
gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
|
|
447
|
+
gl.uniform3f(loc.uEye, frame.eye.x, frame.eye.y, frame.eye.z);
|
|
370
448
|
gl.uniform1f(loc.uIntensity, frame.light.intensity);
|
|
371
449
|
gl.uniform1f(loc.uAmbient, frame.light.ambient);
|
|
372
450
|
gl.disable(gl.BLEND);
|
|
@@ -441,6 +519,7 @@ struct Uniforms {
|
|
|
441
519
|
model: mat4x4<f32>,
|
|
442
520
|
toLight: vec4<f32>,
|
|
443
521
|
params: vec4<f32>,
|
|
522
|
+
eye: vec4<f32>,
|
|
444
523
|
};
|
|
445
524
|
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
446
525
|
|
|
@@ -448,23 +527,40 @@ struct VSOut {
|
|
|
448
527
|
@builtin(position) position: vec4<f32>,
|
|
449
528
|
@location(0) normal: vec3<f32>,
|
|
450
529
|
@location(1) color: vec3<f32>,
|
|
530
|
+
@location(2) emissive: vec3<f32>,
|
|
531
|
+
@location(3) specular: vec4<f32>,
|
|
532
|
+
@location(4) worldPos: vec3<f32>,
|
|
451
533
|
};
|
|
452
534
|
|
|
453
535
|
@vertex
|
|
454
|
-
fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
|
|
536
|
+
fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>, @location(3) emissive: vec3<f32>, @location(4) specular: vec4<f32>) -> VSOut {
|
|
455
537
|
var out: VSOut;
|
|
456
538
|
let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
|
|
457
539
|
out.normal = m * normal;
|
|
458
540
|
out.color = color;
|
|
459
|
-
out.
|
|
541
|
+
out.emissive = emissive;
|
|
542
|
+
out.specular = specular;
|
|
543
|
+
let world = u.model * vec4<f32>(pos, 1.0);
|
|
544
|
+
out.worldPos = world.xyz;
|
|
545
|
+
out.position = u.viewProj * world;
|
|
460
546
|
return out;
|
|
461
547
|
}
|
|
462
548
|
|
|
463
549
|
@fragment
|
|
464
550
|
fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
465
|
-
let
|
|
551
|
+
let normal = normalize(in.normal);
|
|
552
|
+
let toLight = normalize(u.toLight.xyz);
|
|
553
|
+
let lambert = max(dot(normal, toLight), 0.0);
|
|
466
554
|
let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
|
|
467
|
-
|
|
555
|
+
var lit = in.color * brightness;
|
|
556
|
+
if (lambert > 0.0) {
|
|
557
|
+
let viewDir = normalize(u.eye.xyz - in.worldPos);
|
|
558
|
+
let halfVec = normalize(toLight + viewDir);
|
|
559
|
+
let highlight = pow(max(dot(normal, halfVec), 0.0), in.specular.w) * u.params.x;
|
|
560
|
+
lit = lit + highlight * in.specular.xyz;
|
|
561
|
+
}
|
|
562
|
+
lit = lit + in.emissive;
|
|
563
|
+
return vec4<f32>(lit, u.params.z);
|
|
468
564
|
}
|
|
469
565
|
`;
|
|
470
566
|
CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
|
|
@@ -505,13 +601,15 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
505
601
|
{
|
|
506
602
|
binding: 0,
|
|
507
603
|
visibility: stage.VERTEX | stage.FRAGMENT,
|
|
508
|
-
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize:
|
|
604
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 176 }
|
|
509
605
|
}
|
|
510
606
|
]
|
|
511
607
|
});
|
|
512
|
-
const vertexBuffer = (location) => ({
|
|
513
|
-
arrayStride:
|
|
514
|
-
attributes: [
|
|
608
|
+
const vertexBuffer = (location, components = 3) => ({
|
|
609
|
+
arrayStride: components * 4,
|
|
610
|
+
attributes: [
|
|
611
|
+
{ shaderLocation: location, offset: 0, format: `float32x${components}` }
|
|
612
|
+
]
|
|
515
613
|
});
|
|
516
614
|
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
|
|
517
615
|
const blend = {
|
|
@@ -527,7 +625,13 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
527
625
|
vertex: {
|
|
528
626
|
module,
|
|
529
627
|
entryPoint: "vs",
|
|
530
|
-
buffers: [
|
|
628
|
+
buffers: [
|
|
629
|
+
vertexBuffer(0),
|
|
630
|
+
vertexBuffer(1),
|
|
631
|
+
vertexBuffer(2),
|
|
632
|
+
vertexBuffer(3),
|
|
633
|
+
vertexBuffer(4, 4)
|
|
634
|
+
]
|
|
531
635
|
},
|
|
532
636
|
fragment: {
|
|
533
637
|
module,
|
|
@@ -580,6 +684,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
580
684
|
position: upload(data.positions),
|
|
581
685
|
normal: upload(data.normals),
|
|
582
686
|
color: upload(data.colors),
|
|
687
|
+
emissive: upload(data.emissives),
|
|
688
|
+
specular: upload(data.speculars),
|
|
583
689
|
count: data.count
|
|
584
690
|
};
|
|
585
691
|
this.cache.set(mesh, result);
|
|
@@ -631,7 +737,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
631
737
|
const bindGroup = this.device.createBindGroup({
|
|
632
738
|
layout,
|
|
633
739
|
entries: [
|
|
634
|
-
{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size:
|
|
740
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 176 } }
|
|
635
741
|
]
|
|
636
742
|
});
|
|
637
743
|
draws.forEach((draw, i) => {
|
|
@@ -640,6 +746,7 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
640
746
|
data.set(draw.item.model, 16);
|
|
641
747
|
data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
|
|
642
748
|
data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
|
|
749
|
+
data.set([frame.eye.x, frame.eye.y, frame.eye.z, 0], 40);
|
|
643
750
|
this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
|
|
644
751
|
});
|
|
645
752
|
const encoder = this.device.createCommandEncoder();
|
|
@@ -666,6 +773,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
666
773
|
pass.setVertexBuffer(0, mesh.position);
|
|
667
774
|
pass.setVertexBuffer(1, mesh.normal);
|
|
668
775
|
pass.setVertexBuffer(2, mesh.color);
|
|
776
|
+
pass.setVertexBuffer(3, mesh.emissive);
|
|
777
|
+
pass.setVertexBuffer(4, mesh.specular);
|
|
669
778
|
pass.draw(mesh.count);
|
|
670
779
|
});
|
|
671
780
|
pass.end();
|
|
@@ -677,6 +786,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
677
786
|
mesh.position.destroy?.();
|
|
678
787
|
mesh.normal.destroy?.();
|
|
679
788
|
mesh.color.destroy?.();
|
|
789
|
+
mesh.emissive.destroy?.();
|
|
790
|
+
mesh.specular.destroy?.();
|
|
680
791
|
}
|
|
681
792
|
this.cache.clear();
|
|
682
793
|
this.uniformBuffer?.destroy?.();
|
|
@@ -759,9 +870,30 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
759
870
|
depth += dot(d, d);
|
|
760
871
|
}
|
|
761
872
|
depth /= face.indices.length;
|
|
873
|
+
let surface;
|
|
874
|
+
const material = face.material;
|
|
875
|
+
if (material) {
|
|
876
|
+
if (material.specular) {
|
|
877
|
+
let cx = 0;
|
|
878
|
+
let cy = 0;
|
|
879
|
+
let cz = 0;
|
|
880
|
+
for (const i of face.indices) {
|
|
881
|
+
cx += world[i].x;
|
|
882
|
+
cy += world[i].y;
|
|
883
|
+
cz += world[i].z;
|
|
884
|
+
}
|
|
885
|
+
const inv = 1 / face.indices.length;
|
|
886
|
+
const viewDir = normalize(
|
|
887
|
+
subtract(frame.eye, { x: cx * inv, y: cy * inv, z: cz * inv })
|
|
888
|
+
);
|
|
889
|
+
surface = { material, viewDir };
|
|
890
|
+
} else {
|
|
891
|
+
surface = { material };
|
|
892
|
+
}
|
|
893
|
+
}
|
|
762
894
|
polygons.push({
|
|
763
895
|
points,
|
|
764
|
-
color: shadeColor(normal, face.color, frame.light),
|
|
896
|
+
color: shadeColor(normal, face.color, frame.light, surface),
|
|
765
897
|
depth,
|
|
766
898
|
opacity: faceOpacity
|
|
767
899
|
});
|
|
@@ -1512,6 +1644,7 @@ void main() {
|
|
|
1512
1644
|
SpinAnimation: () => SpinAnimation,
|
|
1513
1645
|
WebGLTexturedRenderer: () => WebGLTexturedRenderer,
|
|
1514
1646
|
WebGPUTexturedRenderer: () => WebGPUTexturedRenderer,
|
|
1647
|
+
attachMaterial: () => attachMaterial,
|
|
1515
1648
|
centerAndScaleMesh: () => centerAndScaleMesh,
|
|
1516
1649
|
chargedOrb: () => chargedOrb,
|
|
1517
1650
|
circleMotion: () => circleMotion,
|
|
@@ -1729,6 +1862,12 @@ void main() {
|
|
|
1729
1862
|
init_math();
|
|
1730
1863
|
|
|
1731
1864
|
// src/engines/little-3d-engine/core/mesh.ts
|
|
1865
|
+
function attachMaterial(mesh, material) {
|
|
1866
|
+
if (material) {
|
|
1867
|
+
for (const face of mesh.faces) face.material = material;
|
|
1868
|
+
}
|
|
1869
|
+
return mesh;
|
|
1870
|
+
}
|
|
1732
1871
|
function transform(init) {
|
|
1733
1872
|
return {
|
|
1734
1873
|
position: init?.position ?? { x: 0, y: 0, z: 0 },
|
|
@@ -1743,7 +1882,7 @@ void main() {
|
|
|
1743
1882
|
|
|
1744
1883
|
// src/engines/little-3d-engine/shapes/primitives/cube.ts
|
|
1745
1884
|
var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1746
|
-
function cube(size = 1, colors = DEFAULT_COLORS) {
|
|
1885
|
+
function cube(size = 1, colors = DEFAULT_COLORS, material) {
|
|
1747
1886
|
const h = size / 2;
|
|
1748
1887
|
const vertices = [
|
|
1749
1888
|
{ x: -h, y: -h, z: h },
|
|
@@ -1763,12 +1902,12 @@ void main() {
|
|
|
1763
1902
|
{ indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
|
|
1764
1903
|
{ indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
|
|
1765
1904
|
];
|
|
1766
|
-
return { vertices, faces };
|
|
1905
|
+
return attachMaterial({ vertices, faces }, material);
|
|
1767
1906
|
}
|
|
1768
1907
|
|
|
1769
1908
|
// src/engines/little-3d-engine/shapes/primitives/quad.ts
|
|
1770
1909
|
var DEFAULT_COLORS2 = ["#3b82f6"];
|
|
1771
|
-
function quad(size = 1, colors = DEFAULT_COLORS2) {
|
|
1910
|
+
function quad(size = 1, colors = DEFAULT_COLORS2, material) {
|
|
1772
1911
|
const s = size / 2;
|
|
1773
1912
|
const vertices = [
|
|
1774
1913
|
{ x: -s, y: -s, z: 0 },
|
|
@@ -1776,12 +1915,15 @@ void main() {
|
|
|
1776
1915
|
{ x: s, y: s, z: 0 },
|
|
1777
1916
|
{ x: -s, y: s, z: 0 }
|
|
1778
1917
|
];
|
|
1779
|
-
return
|
|
1918
|
+
return attachMaterial(
|
|
1919
|
+
{ vertices, faces: [{ indices: [0, 1, 2, 3], color: colors[0] }] },
|
|
1920
|
+
material
|
|
1921
|
+
);
|
|
1780
1922
|
}
|
|
1781
1923
|
|
|
1782
1924
|
// src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
|
|
1783
1925
|
var DEFAULT_COLORS3 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
|
|
1784
|
-
function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
|
|
1926
|
+
function tetrahedron(size = 1, colors = DEFAULT_COLORS3, material) {
|
|
1785
1927
|
const s = size / 2;
|
|
1786
1928
|
const vertices = [
|
|
1787
1929
|
{ x: s, y: s, z: s },
|
|
@@ -1795,7 +1937,7 @@ void main() {
|
|
|
1795
1937
|
{ indices: [0, 2, 3], color: colors[2 % colors.length] },
|
|
1796
1938
|
{ indices: [1, 3, 2], color: colors[3 % colors.length] }
|
|
1797
1939
|
];
|
|
1798
|
-
return { vertices, faces };
|
|
1940
|
+
return attachMaterial({ vertices, faces }, material);
|
|
1799
1941
|
}
|
|
1800
1942
|
|
|
1801
1943
|
// src/engines/little-3d-engine/shapes/primitives/octahedron.ts
|
|
@@ -1809,7 +1951,7 @@ void main() {
|
|
|
1809
1951
|
"#06b6d4",
|
|
1810
1952
|
"#eab308"
|
|
1811
1953
|
];
|
|
1812
|
-
function octahedron(size = 1, colors = DEFAULT_COLORS4) {
|
|
1954
|
+
function octahedron(size = 1, colors = DEFAULT_COLORS4, material) {
|
|
1813
1955
|
const r = size / 2;
|
|
1814
1956
|
const vertices = [
|
|
1815
1957
|
{ x: r, y: 0, z: 0 },
|
|
@@ -1829,12 +1971,12 @@ void main() {
|
|
|
1829
1971
|
{ indices: [5, 3, 1], color: colors[6 % colors.length] },
|
|
1830
1972
|
{ indices: [5, 0, 3], color: colors[7 % colors.length] }
|
|
1831
1973
|
];
|
|
1832
|
-
return { vertices, faces };
|
|
1974
|
+
return attachMaterial({ vertices, faces }, material);
|
|
1833
1975
|
}
|
|
1834
1976
|
|
|
1835
1977
|
// src/engines/little-3d-engine/shapes/primitives/pyramid.ts
|
|
1836
1978
|
var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
|
|
1837
|
-
function pyramid(size = 1, colors = DEFAULT_COLORS5) {
|
|
1979
|
+
function pyramid(size = 1, colors = DEFAULT_COLORS5, material) {
|
|
1838
1980
|
const h = size / 2;
|
|
1839
1981
|
const vertices = [
|
|
1840
1982
|
{ x: -h, y: -h, z: h },
|
|
@@ -1850,12 +1992,12 @@ void main() {
|
|
|
1850
1992
|
{ indices: [4, 2, 3], color: colors[3 % colors.length] },
|
|
1851
1993
|
{ indices: [4, 3, 0], color: colors[4 % colors.length] }
|
|
1852
1994
|
];
|
|
1853
|
-
return { vertices, faces };
|
|
1995
|
+
return attachMaterial({ vertices, faces }, material);
|
|
1854
1996
|
}
|
|
1855
1997
|
|
|
1856
1998
|
// src/engines/little-3d-engine/shapes/primitives/spheres/uv-sphere.ts
|
|
1857
1999
|
var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1858
|
-
function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
|
|
2000
|
+
function uvSphere(size = 1, detail = 1, colors = DEFAULT_COLORS6, material) {
|
|
1859
2001
|
const r = size / 2;
|
|
1860
2002
|
const d = Math.max(1, Math.floor(detail));
|
|
1861
2003
|
const slices = Math.max(4, d * 4);
|
|
@@ -1894,7 +2036,7 @@ void main() {
|
|
|
1894
2036
|
color: color()
|
|
1895
2037
|
});
|
|
1896
2038
|
}
|
|
1897
|
-
return { vertices, faces };
|
|
2039
|
+
return attachMaterial({ vertices, faces }, material);
|
|
1898
2040
|
}
|
|
1899
2041
|
|
|
1900
2042
|
// src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
|
|
@@ -1937,8 +2079,11 @@ void main() {
|
|
|
1937
2079
|
[8, 6, 7],
|
|
1938
2080
|
[9, 8, 1]
|
|
1939
2081
|
];
|
|
1940
|
-
function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
|
|
1941
|
-
return
|
|
2082
|
+
function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7, material) {
|
|
2083
|
+
return attachMaterial(
|
|
2084
|
+
sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors),
|
|
2085
|
+
material
|
|
2086
|
+
);
|
|
1942
2087
|
}
|
|
1943
2088
|
|
|
1944
2089
|
// src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
|
|
@@ -1962,8 +2107,11 @@ void main() {
|
|
|
1962
2107
|
[5, 3, 1],
|
|
1963
2108
|
[5, 0, 3]
|
|
1964
2109
|
];
|
|
1965
|
-
function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
|
|
1966
|
-
return
|
|
2110
|
+
function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8, material) {
|
|
2111
|
+
return attachMaterial(
|
|
2112
|
+
sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors),
|
|
2113
|
+
material
|
|
2114
|
+
);
|
|
1967
2115
|
}
|
|
1968
2116
|
|
|
1969
2117
|
// src/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.ts
|
|
@@ -1976,7 +2124,7 @@ void main() {
|
|
|
1976
2124
|
{ normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
|
|
1977
2125
|
{ normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
|
|
1978
2126
|
];
|
|
1979
|
-
function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
|
|
2127
|
+
function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9, material) {
|
|
1980
2128
|
const r = size / 2;
|
|
1981
2129
|
const n = Math.max(1, Math.floor(detail));
|
|
1982
2130
|
const vertices = [];
|
|
@@ -2005,13 +2153,13 @@ void main() {
|
|
|
2005
2153
|
}
|
|
2006
2154
|
}
|
|
2007
2155
|
}
|
|
2008
|
-
return { vertices, faces };
|
|
2156
|
+
return attachMaterial({ vertices, faces }, material);
|
|
2009
2157
|
}
|
|
2010
2158
|
|
|
2011
2159
|
// src/engines/little-3d-engine/shapes/complex/plane.ts
|
|
2012
2160
|
var DEFAULT_COLORS10 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
|
|
2013
|
-
function planeMesh(colors = DEFAULT_COLORS10) {
|
|
2014
|
-
return {
|
|
2161
|
+
function planeMesh(colors = DEFAULT_COLORS10, material) {
|
|
2162
|
+
return attachMaterial({
|
|
2015
2163
|
vertices: [
|
|
2016
2164
|
{ x: 0.9, y: 0, z: 0 },
|
|
2017
2165
|
{ x: -0.2, y: 0, z: 0.82 },
|
|
@@ -2039,7 +2187,7 @@ void main() {
|
|
|
2039
2187
|
{ indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS10[0] },
|
|
2040
2188
|
{ indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS10[1] }
|
|
2041
2189
|
]
|
|
2042
|
-
};
|
|
2190
|
+
}, material);
|
|
2043
2191
|
}
|
|
2044
2192
|
|
|
2045
2193
|
// src/engines/little-3d-engine/textures/dynamic/canvas-texture.ts
|
|
@@ -2052,18 +2200,30 @@ void main() {
|
|
|
2052
2200
|
}
|
|
2053
2201
|
|
|
2054
2202
|
// src/engines/little-3d-engine/textures/dynamic/star.ts
|
|
2055
|
-
function
|
|
2203
|
+
function drawStar(ctx) {
|
|
2204
|
+
ctx.save();
|
|
2205
|
+
ctx.translate(48, 48);
|
|
2206
|
+
ctx.fillStyle = "#fff";
|
|
2207
|
+
ctx.beginPath();
|
|
2208
|
+
for (let index = 0; index < 10; index++) {
|
|
2209
|
+
const radius = index % 2 === 0 ? 43 : 16;
|
|
2210
|
+
const angle = index * Math.PI / 5 - Math.PI / 2;
|
|
2211
|
+
ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
|
|
2212
|
+
}
|
|
2213
|
+
ctx.closePath();
|
|
2214
|
+
ctx.fill();
|
|
2215
|
+
ctx.restore();
|
|
2216
|
+
}
|
|
2217
|
+
function starTexture(options = {}) {
|
|
2218
|
+
const glow = Math.max(0, options.glow ?? 0);
|
|
2056
2219
|
return canvasTexture((ctx) => {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
const angle = index * Math.PI / 5 - Math.PI / 2;
|
|
2063
|
-
ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
|
|
2220
|
+
if (glow > 0) {
|
|
2221
|
+
ctx.save();
|
|
2222
|
+
ctx.filter = `blur(${glow}px)`;
|
|
2223
|
+
drawStar(ctx);
|
|
2224
|
+
ctx.restore();
|
|
2064
2225
|
}
|
|
2065
|
-
ctx
|
|
2066
|
-
ctx.fill();
|
|
2226
|
+
drawStar(ctx);
|
|
2067
2227
|
});
|
|
2068
2228
|
}
|
|
2069
2229
|
|
|
@@ -2584,10 +2744,14 @@ void main() {
|
|
|
2584
2744
|
const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
|
|
2585
2745
|
return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
|
|
2586
2746
|
}
|
|
2747
|
+
function applyMaterial(mesh, material) {
|
|
2748
|
+
if (!material) return mesh;
|
|
2749
|
+
return { vertices: mesh.vertices, faces: mesh.faces.map((f) => ({ ...f, material })) };
|
|
2750
|
+
}
|
|
2587
2751
|
var SpinAnimation = class {
|
|
2588
2752
|
constructor(options = {}) {
|
|
2589
2753
|
this.exited = false;
|
|
2590
|
-
this.mesh = applyColor(resolveMesh(options.shape), options.color);
|
|
2754
|
+
this.mesh = applyMaterial(applyColor(resolveMesh(options.shape), options.color), options.material);
|
|
2591
2755
|
this.spinX = options.spinX ?? 7e-4;
|
|
2592
2756
|
this.spinY = options.spinY ?? 11e-4;
|
|
2593
2757
|
this.backend = options.backend;
|
|
@@ -3280,6 +3444,7 @@ void main() {
|
|
|
3280
3444
|
var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
|
|
3281
3445
|
var CENTER_POP_OUT_MS = 420;
|
|
3282
3446
|
var PARKED = { x: 0, y: 0, z: 50 };
|
|
3447
|
+
var ORB_MATERIAL = { specular: [1, 1, 1], shininess: 28 };
|
|
3283
3448
|
var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
|
|
3284
3449
|
var MINI_COLORS = [
|
|
3285
3450
|
["#e0f2fe", "#bae6fd", "#7dd3fc"],
|
|
@@ -3308,9 +3473,9 @@ void main() {
|
|
|
3308
3473
|
backend: this.backend,
|
|
3309
3474
|
camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
|
|
3310
3475
|
});
|
|
3311
|
-
this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
|
|
3476
|
+
this.center = engine.add(icosphere(1, 2, CENTER_COLORS, ORB_MATERIAL), { scale: 0 });
|
|
3312
3477
|
for (let i = 0; i < MINIS; i++) {
|
|
3313
|
-
const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
|
|
3478
|
+
const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length], ORB_MATERIAL);
|
|
3314
3479
|
this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
|
|
3315
3480
|
}
|
|
3316
3481
|
this.engine = engine;
|
|
@@ -3473,6 +3638,7 @@ void main() {
|
|
|
3473
3638
|
var TWO_PI2 = Math.PI * 2;
|
|
3474
3639
|
var INTRO_MS = 900;
|
|
3475
3640
|
var INTRO_STAGGER_MS = 60;
|
|
3641
|
+
var INTRO_DONE_MS = (COUNT - 1) * INTRO_STAGGER_MS + INTRO_MS;
|
|
3476
3642
|
var GATE_DOCK = 0.35;
|
|
3477
3643
|
var GATE_UNDOCK = 0.65;
|
|
3478
3644
|
var EXIT_HURRY2 = 2.5;
|
|
@@ -3481,15 +3647,11 @@ void main() {
|
|
|
3481
3647
|
var SPIN_STAGGER_MS = 80;
|
|
3482
3648
|
var HOLD_MS = 1e3;
|
|
3483
3649
|
var COLLAPSE_MS = 700;
|
|
3650
|
+
var COLLAPSE_SPREAD_MS = 500;
|
|
3484
3651
|
var POP_MS = 170;
|
|
3485
3652
|
var LABEL_FADE_MS = 600;
|
|
3486
|
-
var
|
|
3487
|
-
|
|
3488
|
-
() => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
|
|
3489
|
-
() => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
|
|
3490
|
-
() => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
|
|
3491
|
-
() => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
|
|
3492
|
-
];
|
|
3653
|
+
var CUBE_COLORS = ["#8397c6", "#7186b8", "#6176a8", "#93a6cf", "#556a9c", "#7a8fc0"];
|
|
3654
|
+
var DEFAULT_MESHES = [() => cube(1, CUBE_COLORS)];
|
|
3493
3655
|
function clamp015(value) {
|
|
3494
3656
|
return Math.max(0, Math.min(1, value));
|
|
3495
3657
|
}
|
|
@@ -3517,6 +3679,9 @@ void main() {
|
|
|
3517
3679
|
this.dockedAt = new Array(COUNT).fill(Infinity);
|
|
3518
3680
|
this.tumbleX = [];
|
|
3519
3681
|
this.tumbleY = [];
|
|
3682
|
+
this.collapseDelay = [];
|
|
3683
|
+
this.popStarted = new Array(COUNT).fill(false);
|
|
3684
|
+
this.maxCollapseDelay = 0;
|
|
3520
3685
|
this.fades = [];
|
|
3521
3686
|
this.slots = [];
|
|
3522
3687
|
this.aspect = 16 / 9;
|
|
@@ -3525,7 +3690,6 @@ void main() {
|
|
|
3525
3690
|
this.allDockedAt = Infinity;
|
|
3526
3691
|
this.collapseAt = Infinity;
|
|
3527
3692
|
this.lastNow = 0;
|
|
3528
|
-
this.popFading = false;
|
|
3529
3693
|
this.finished = false;
|
|
3530
3694
|
const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
|
|
3531
3695
|
this.meshes = sources.map(resolveMesh3);
|
|
@@ -3542,7 +3706,9 @@ void main() {
|
|
|
3542
3706
|
this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
|
|
3543
3707
|
this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
|
|
3544
3708
|
this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
|
|
3709
|
+
this.collapseDelay.push(hash01(i, 7) * COLLAPSE_SPREAD_MS);
|
|
3545
3710
|
}
|
|
3711
|
+
this.maxCollapseDelay = Math.max(...this.collapseDelay);
|
|
3546
3712
|
}
|
|
3547
3713
|
mount(target) {
|
|
3548
3714
|
if (!target.style.position) target.style.position = "relative";
|
|
@@ -3603,7 +3769,7 @@ void main() {
|
|
|
3603
3769
|
COLLAPSE_MS
|
|
3604
3770
|
));
|
|
3605
3771
|
}
|
|
3606
|
-
if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS + POP_MS) {
|
|
3772
|
+
if (this.collapseAt !== Infinity && now >= this.collapseAt + this.maxCollapseDelay + COLLAPSE_MS + POP_MS) {
|
|
3607
3773
|
this.finished = true;
|
|
3608
3774
|
}
|
|
3609
3775
|
this.engine.render();
|
|
@@ -3620,7 +3786,8 @@ void main() {
|
|
|
3620
3786
|
}
|
|
3621
3787
|
updateBlends(dt, progress, now) {
|
|
3622
3788
|
const exiting = this.exitAt !== Infinity;
|
|
3623
|
-
const
|
|
3789
|
+
const ringComplete = now - this.enterAt >= INTRO_DONE_MS;
|
|
3790
|
+
const want = !ringComplete ? 0 : exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
|
|
3624
3791
|
const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
|
|
3625
3792
|
for (let i = 0; i < COUNT; i++) {
|
|
3626
3793
|
const target = i < want ? 1 : 0;
|
|
@@ -3683,25 +3850,30 @@ void main() {
|
|
|
3683
3850
|
if (!this.captured) {
|
|
3684
3851
|
this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
|
|
3685
3852
|
}
|
|
3686
|
-
const u = clamp015((now - this.collapseAt) / COLLAPSE_MS);
|
|
3687
|
-
const pull = easeInCubic(u);
|
|
3688
3853
|
for (let i = 0; i < COUNT; i++) {
|
|
3689
3854
|
const transform2 = this.handles[i].transform;
|
|
3690
3855
|
const from = this.captured[i];
|
|
3856
|
+
const local = now - this.collapseAt - this.collapseDelay[i];
|
|
3857
|
+
if (local <= 0) {
|
|
3858
|
+
transform2.position.x = from.x;
|
|
3859
|
+
transform2.position.y = from.y;
|
|
3860
|
+
transform2.position.z = from.z;
|
|
3861
|
+
transform2.scale = this.size;
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
const pull = easeInCubic(clamp015(local / COLLAPSE_MS));
|
|
3691
3865
|
transform2.position.x = from.x * (1 - pull);
|
|
3692
3866
|
transform2.position.y = from.y * (1 - pull);
|
|
3693
3867
|
transform2.position.z = from.z * (1 - pull);
|
|
3694
3868
|
transform2.scale = this.size * (1 - 0.99 * pull);
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
const v = clamp015((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
|
|
3702
|
-
for (let i = 0; i < COUNT; i++) {
|
|
3869
|
+
if (local >= COLLAPSE_MS) {
|
|
3870
|
+
if (!this.popStarted[i]) {
|
|
3871
|
+
this.popStarted[i] = true;
|
|
3872
|
+
this.handles[i].transparency = this.fades[i];
|
|
3873
|
+
}
|
|
3874
|
+
const v = clamp015((local - COLLAPSE_MS) / POP_MS);
|
|
3703
3875
|
this.fades[i].opacity = 1 - v;
|
|
3704
|
-
|
|
3876
|
+
transform2.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
|
|
3705
3877
|
}
|
|
3706
3878
|
}
|
|
3707
3879
|
}
|
|
@@ -3874,36 +4046,239 @@ void main() {
|
|
|
3874
4046
|
};
|
|
3875
4047
|
}
|
|
3876
4048
|
|
|
4049
|
+
// src/animations/ghost-train.ts
|
|
4050
|
+
var MAX_CARS = 50;
|
|
4051
|
+
var CAMERA_Z3 = 3;
|
|
4052
|
+
var FOV2 = 55 * Math.PI / 180;
|
|
4053
|
+
var HALF_HEIGHT = Math.tan(FOV2 / 2) * CAMERA_Z3;
|
|
4054
|
+
var RUN_GAP_MS = 130;
|
|
4055
|
+
var POP_MS2 = 320;
|
|
4056
|
+
var SAMPLE_MS2 = 8;
|
|
4057
|
+
var TURN_RATE = 0.4 * Math.PI / 180;
|
|
4058
|
+
var MAX_OUTRO_MS = 4e3;
|
|
4059
|
+
var WARP_ACCEL = 1e3;
|
|
4060
|
+
var TRAIL_OUTRO_MS = 1200;
|
|
4061
|
+
var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
|
|
4062
|
+
var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
|
|
4063
|
+
var CAR_MATERIAL = { emissive: [0.1, 0.2, 0.3] };
|
|
4064
|
+
var WORLD_UP2 = { x: 0, y: 1, z: 0 };
|
|
4065
|
+
function clamp016(value) {
|
|
4066
|
+
return Math.max(0, Math.min(1, value));
|
|
4067
|
+
}
|
|
4068
|
+
function orientationFor2(forward) {
|
|
4069
|
+
const fwd = normalize(forward);
|
|
4070
|
+
let right = cross(fwd, WORLD_UP2);
|
|
4071
|
+
if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
|
|
4072
|
+
right = normalize(right);
|
|
4073
|
+
const up = cross(right, fwd);
|
|
4074
|
+
const w = normalize(cross(fwd, up));
|
|
4075
|
+
return {
|
|
4076
|
+
x: Math.atan2(cross(w, fwd).z, w.z),
|
|
4077
|
+
y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
|
|
4078
|
+
z: Math.atan2(fwd.y, fwd.x)
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
function rotateToward(from, to, maxRad) {
|
|
4082
|
+
const a = normalize(from);
|
|
4083
|
+
const b = normalize(to);
|
|
4084
|
+
const d = Math.max(-1, Math.min(1, dot(a, b)));
|
|
4085
|
+
const angle = Math.acos(d);
|
|
4086
|
+
if (angle <= maxRad || angle < 1e-4) return b;
|
|
4087
|
+
const sin = Math.sin(angle);
|
|
4088
|
+
if (sin < 1e-4) return b;
|
|
4089
|
+
const t = maxRad / angle;
|
|
4090
|
+
const w1 = Math.sin((1 - t) * angle) / sin;
|
|
4091
|
+
const w2 = Math.sin(t * angle) / sin;
|
|
4092
|
+
return normalize({ x: a.x * w1 + b.x * w2, y: a.y * w1 + b.y * w2, z: a.z * w1 + b.z * w2 });
|
|
4093
|
+
}
|
|
4094
|
+
var GhostTrainAnimation = class {
|
|
4095
|
+
constructor(options = {}) {
|
|
4096
|
+
this.cars = [];
|
|
4097
|
+
this.appear = new Array(MAX_CARS).fill(0);
|
|
4098
|
+
this.headings = new Array(MAX_CARS).fill(void 0);
|
|
4099
|
+
this.aspect = 16 / 9;
|
|
4100
|
+
this.enterAt = Infinity;
|
|
4101
|
+
this.outroAt = Infinity;
|
|
4102
|
+
this.carsAtOutro = 0;
|
|
4103
|
+
this.exitPathTime = 0;
|
|
4104
|
+
// lead car's path-time at blast-off (the escape switch point)
|
|
4105
|
+
this.exitPoint = { x: 0, y: 0, z: 0 };
|
|
4106
|
+
this.exitDir = { x: 1, y: 0, z: 0 };
|
|
4107
|
+
// shared escape direction, outward from the track
|
|
4108
|
+
this.exitSpeed = 1e-3;
|
|
4109
|
+
// path-units per path-millisecond at the switch (keeps speed continuous)
|
|
4110
|
+
this.lastNow = 0;
|
|
4111
|
+
this.finished = false;
|
|
4112
|
+
this.motion = options.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
4113
|
+
this.size = options.size ?? 0.15;
|
|
4114
|
+
this.backend = options.backend;
|
|
4115
|
+
this.labelContent = options.label;
|
|
4116
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
4117
|
+
}
|
|
4118
|
+
mount(target) {
|
|
4119
|
+
if (!target.style.position) target.style.position = "relative";
|
|
4120
|
+
const engine = new Little3dEngine({
|
|
4121
|
+
backend: this.backend,
|
|
4122
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
|
|
4123
|
+
});
|
|
4124
|
+
const mesh = cube(1, CAR_COLORS, CAR_MATERIAL);
|
|
4125
|
+
for (let i = 0; i < MAX_CARS; i++) {
|
|
4126
|
+
this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
|
|
4127
|
+
}
|
|
4128
|
+
this.engine = engine;
|
|
4129
|
+
engine.mount(target).catch((error) => {
|
|
4130
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
4131
|
+
});
|
|
4132
|
+
const measure = () => {
|
|
4133
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
4134
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
4135
|
+
}
|
|
4136
|
+
};
|
|
4137
|
+
measure();
|
|
4138
|
+
this.observer = new ResizeObserver(measure);
|
|
4139
|
+
this.observer.observe(target);
|
|
4140
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
4141
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
4142
|
+
}
|
|
4143
|
+
enter(now) {
|
|
4144
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
4145
|
+
}
|
|
4146
|
+
exit(now) {
|
|
4147
|
+
if (this.outroAt !== Infinity || this.enterAt === Infinity) return;
|
|
4148
|
+
this.outroAt = now;
|
|
4149
|
+
this.carsAtOutro = this.appear.filter((a) => a > 0.5).length;
|
|
4150
|
+
this.exitPathTime = now - this.enterAt;
|
|
4151
|
+
const from = this.motion.positionAt(this.exitPathTime);
|
|
4152
|
+
const velocity = subtract(
|
|
4153
|
+
this.motion.positionAt(this.exitPathTime + 1),
|
|
4154
|
+
this.motion.positionAt(this.exitPathTime - 1)
|
|
4155
|
+
);
|
|
4156
|
+
const speed = Math.hypot(velocity.x, velocity.y, velocity.z);
|
|
4157
|
+
this.exitPoint = from;
|
|
4158
|
+
if (speed > 1e-6) this.exitSpeed = speed / 2;
|
|
4159
|
+
this.exitDir = speed > 1e-6 ? normalize(velocity) : { x: 1, y: 0, z: 0 };
|
|
4160
|
+
}
|
|
4161
|
+
isFinished() {
|
|
4162
|
+
return this.finished;
|
|
4163
|
+
}
|
|
4164
|
+
/** Milliseconds the lead car keeps moving into the outro; feed a trail layer's `outroMs`. */
|
|
4165
|
+
get outroDurationMs() {
|
|
4166
|
+
return TRAIL_OUTRO_MS;
|
|
4167
|
+
}
|
|
4168
|
+
/**
|
|
4169
|
+
* A {@link MotionController} following the lead car's actual position, through
|
|
4170
|
+
* laps and the accelerating escape. Feed it to a particle layer's `emitter`
|
|
4171
|
+
* so the star trail stays behind the train.
|
|
4172
|
+
*/
|
|
4173
|
+
trailEmitter() {
|
|
4174
|
+
return {
|
|
4175
|
+
positionAt: (t) => this.enterAt === Infinity ? this.motion.positionAt(t) : this.pathPosition(t - this.enterAt + this.warp(t))
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4178
|
+
render(now, frame) {
|
|
4179
|
+
if (!this.engine || !this.label) return;
|
|
4180
|
+
for (const car of this.cars) car.transform.scale = 0;
|
|
4181
|
+
if (this.enterAt === Infinity) {
|
|
4182
|
+
this.engine.render();
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
4186
|
+
this.lastNow = now;
|
|
4187
|
+
const want = this.outroAt !== Infinity ? this.carsAtOutro : Math.min(MAX_CARS, Math.round(frame.progress * MAX_CARS));
|
|
4188
|
+
const halfWidth = HALF_HEIGHT * this.aspect;
|
|
4189
|
+
const warp = this.warp(now);
|
|
4190
|
+
let anyOnScreen = false;
|
|
4191
|
+
for (let k = 0; k < MAX_CARS; k++) {
|
|
4192
|
+
const target = k < want ? 1 : 0;
|
|
4193
|
+
this.appear[k] = clamp016(this.appear[k] + Math.sign(target - this.appear[k]) * (dt / POP_MS2));
|
|
4194
|
+
if (this.appear[k] <= 0) {
|
|
4195
|
+
this.headings[k] = void 0;
|
|
4196
|
+
continue;
|
|
4197
|
+
}
|
|
4198
|
+
const p = now - this.enterAt - k * RUN_GAP_MS + warp;
|
|
4199
|
+
const position = this.pathPosition(p);
|
|
4200
|
+
if (Math.abs(position.x) > halfWidth + this.size || Math.abs(position.y) > HALF_HEIGHT + this.size) {
|
|
4201
|
+
continue;
|
|
4202
|
+
}
|
|
4203
|
+
const ahead = subtract(this.pathPosition(p + SAMPLE_MS2), position);
|
|
4204
|
+
const targetDir = Math.hypot(ahead.x, ahead.y, ahead.z) > 1e-5 ? ahead : this.headings[k] ?? { x: 1, y: 0, z: 0 };
|
|
4205
|
+
this.headings[k] = this.headings[k] ? rotateToward(this.headings[k], targetDir, TURN_RATE * dt) : normalize(targetDir);
|
|
4206
|
+
const orientation = orientationFor2(this.headings[k]);
|
|
4207
|
+
const transform2 = this.cars[k].transform;
|
|
4208
|
+
transform2.position.x = position.x;
|
|
4209
|
+
transform2.position.y = position.y;
|
|
4210
|
+
transform2.position.z = position.z;
|
|
4211
|
+
transform2.rotation.x = orientation.x;
|
|
4212
|
+
transform2.rotation.y = orientation.y;
|
|
4213
|
+
transform2.rotation.z = orientation.z;
|
|
4214
|
+
transform2.scale = this.size * easeOutBack(this.appear[k]);
|
|
4215
|
+
anyOnScreen = true;
|
|
4216
|
+
}
|
|
4217
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
4218
|
+
if (this.fadeLabel) {
|
|
4219
|
+
this.label.setOpacity(animationLabelOpacity(now, this.enterAt, POP_MS2, this.outroAt, TRAIL_OUTRO_MS));
|
|
4220
|
+
}
|
|
4221
|
+
if (this.outroAt !== Infinity && now > this.outroAt + 300 && (!anyOnScreen || now >= this.outroAt + MAX_OUTRO_MS)) {
|
|
4222
|
+
this.finished = true;
|
|
4223
|
+
}
|
|
4224
|
+
this.engine.render();
|
|
4225
|
+
}
|
|
4226
|
+
destroy() {
|
|
4227
|
+
this.observer?.disconnect();
|
|
4228
|
+
this.observer = void 0;
|
|
4229
|
+
this.label?.container.remove();
|
|
4230
|
+
this.label = void 0;
|
|
4231
|
+
this.engine?.destroy();
|
|
4232
|
+
this.engine = void 0;
|
|
4233
|
+
this.cars.length = 0;
|
|
4234
|
+
}
|
|
4235
|
+
/** Extra path-time every car has accelerated forward by, `now` ms into the outro. */
|
|
4236
|
+
warp(now) {
|
|
4237
|
+
if (this.outroAt === Infinity) return 0;
|
|
4238
|
+
const seconds = (now - this.outroAt) / 1e3;
|
|
4239
|
+
return 0.5 * WARP_ACCEL * seconds * seconds;
|
|
4240
|
+
}
|
|
4241
|
+
/**
|
|
4242
|
+
* The single trajectory every car rides, sampled at path-time `p`: the track
|
|
4243
|
+
* up to the exit switch point, then a straight escape outward. Because the
|
|
4244
|
+
* switch point and direction are shared, all cars follow the exact same path.
|
|
4245
|
+
*/
|
|
4246
|
+
pathPosition(p) {
|
|
4247
|
+
if (this.outroAt === Infinity || p <= this.exitPathTime) {
|
|
4248
|
+
return this.motion.positionAt(p);
|
|
4249
|
+
}
|
|
4250
|
+
const distance = this.exitSpeed * (p - this.exitPathTime);
|
|
4251
|
+
return {
|
|
4252
|
+
x: this.exitPoint.x + this.exitDir.x * distance,
|
|
4253
|
+
y: this.exitPoint.y + this.exitDir.y * distance,
|
|
4254
|
+
z: this.exitPoint.z + this.exitDir.z * distance
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
};
|
|
4258
|
+
|
|
3877
4259
|
// src/prefabs/ghost-train.ts
|
|
3878
4260
|
function ghostTrain(options = {}) {
|
|
3879
|
-
const motion = options.object?.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
3880
4261
|
const particles = options.particles ?? {};
|
|
3881
|
-
const
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
size: 0.3,
|
|
3885
|
-
transparency: { mode: "two-sided", opacity: 0.55 },
|
|
3886
|
-
tail: { count: 4, gapMs: 240 },
|
|
3887
|
-
backend: options.backend,
|
|
3888
|
-
...options.object,
|
|
3889
|
-
label: options.object?.label
|
|
4262
|
+
const train = new GhostTrainAnimation({
|
|
4263
|
+
motion: options.object?.motion,
|
|
4264
|
+
backend: options.backend
|
|
3890
4265
|
});
|
|
3891
4266
|
const trail = new ParticlesAnimation({
|
|
3892
4267
|
rate: 30,
|
|
3893
4268
|
lifeMs: 1700,
|
|
3894
|
-
size: 0.
|
|
3895
|
-
speed: 0.
|
|
4269
|
+
size: 0.15,
|
|
4270
|
+
speed: 0.11,
|
|
3896
4271
|
colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
|
|
3897
|
-
texture: particles.texture ?? starTexture(),
|
|
3898
|
-
emitter:
|
|
3899
|
-
outroMs:
|
|
4272
|
+
texture: particles.texture ?? starTexture({ glow: 5 }),
|
|
4273
|
+
emitter: train.trailEmitter(),
|
|
4274
|
+
outroMs: train.outroDurationMs,
|
|
3900
4275
|
seed: 17,
|
|
3901
4276
|
backend: options.backend,
|
|
3902
4277
|
...particles,
|
|
3903
4278
|
label: options.label ?? particles.label,
|
|
3904
4279
|
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3905
4280
|
});
|
|
3906
|
-
return progressSpinner(new CompositeAnimation([trail,
|
|
4281
|
+
return progressSpinner(new CompositeAnimation([trail, train]), options);
|
|
3907
4282
|
}
|
|
3908
4283
|
|
|
3909
4284
|
// src/prefabs/grid-assembly.ts
|
|
@@ -4000,68 +4375,347 @@ void main() {
|
|
|
4000
4375
|
}), options);
|
|
4001
4376
|
}
|
|
4002
4377
|
|
|
4003
|
-
// src/
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4378
|
+
// src/animations/rocket-launch.ts
|
|
4379
|
+
var ROCKETS = 20;
|
|
4380
|
+
var CAMERA_Z4 = 3;
|
|
4381
|
+
var FOV3 = 55 * Math.PI / 180;
|
|
4382
|
+
var HALF_HEIGHT2 = Math.tan(FOV3 / 2) * CAMERA_Z4;
|
|
4383
|
+
var SIZE = 0.12;
|
|
4384
|
+
var ROW_Y = -0.5;
|
|
4385
|
+
var PARTICLE_Z = 0.3;
|
|
4386
|
+
var SLIDE_MS = 460;
|
|
4387
|
+
var SLIDE_GATE = 0.45;
|
|
4388
|
+
var EXIT_HURRY3 = 2.5;
|
|
4389
|
+
var LAUNCH_SPREAD_MS = 620;
|
|
4390
|
+
var ASCENT_G = 5.2;
|
|
4391
|
+
var FINISH_PAD_MS = 2e3;
|
|
4392
|
+
var TURNERS = 3;
|
|
4393
|
+
var TURN_MIN_Y = 0.2;
|
|
4394
|
+
var TURN_MAX_Y = 0.8;
|
|
4395
|
+
var TURN_MIN_DEG = 30;
|
|
4396
|
+
var TURN_MAX_DEG = 50;
|
|
4397
|
+
var DEG = Math.PI / 180;
|
|
4398
|
+
var SMOKE_LIFE_MS = 1400;
|
|
4399
|
+
var SMOKE_GAP_MS = 320;
|
|
4400
|
+
var SMOKE_RISE = 0.55;
|
|
4401
|
+
var SMOKE_SIZE = 0.17;
|
|
4402
|
+
var SMOKE_PEAK = 0.16;
|
|
4403
|
+
var SMOKE_POOL = 104;
|
|
4404
|
+
var FIRE_LIFE_MS = 420;
|
|
4405
|
+
var FIRE_GAP_MS = 55;
|
|
4406
|
+
var FIRE_ON_MS = 950;
|
|
4407
|
+
var FIRE_TRAIL = 0.25;
|
|
4408
|
+
var FIRE_SPREAD = 0.09;
|
|
4409
|
+
var FIRE_SIZE = 0.15;
|
|
4410
|
+
var FIRE_PEAK = 0.9;
|
|
4411
|
+
var FIRE_POOL = 140;
|
|
4412
|
+
var SMOKE_COLORS = ["#e2e8f0", "#cbd5e1"];
|
|
4413
|
+
var FIRE_COLORS = ["#fef3c7", "#fde047", "#fb923c", "#ef4444"];
|
|
4414
|
+
var ROCKET_COLORS = ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"];
|
|
4415
|
+
function clamp017(value) {
|
|
4416
|
+
return Math.max(0, Math.min(1, value));
|
|
4019
4417
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4418
|
+
function smoothstep2(edge0, edge1, value) {
|
|
4419
|
+
const x = clamp017((value - edge0) / (edge1 - edge0));
|
|
4420
|
+
return x * x * (3 - 2 * x);
|
|
4421
|
+
}
|
|
4422
|
+
function hash012(index, salt) {
|
|
4423
|
+
let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
|
|
4424
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
4425
|
+
h ^= h >>> 16;
|
|
4426
|
+
return (h >>> 0) / 4294967296;
|
|
4427
|
+
}
|
|
4428
|
+
function puffTexture(coreAlpha, coreStop) {
|
|
4429
|
+
return canvasTexture((ctx) => {
|
|
4430
|
+
const g = ctx.createRadialGradient(48, 48, 1, 48, 48, 47);
|
|
4431
|
+
g.addColorStop(0, `rgba(255,255,255,${coreAlpha})`);
|
|
4432
|
+
g.addColorStop(coreStop, `rgba(255,255,255,${coreAlpha * 0.6})`);
|
|
4433
|
+
g.addColorStop(1, "rgba(255,255,255,0)");
|
|
4434
|
+
ctx.fillStyle = g;
|
|
4435
|
+
ctx.fillRect(0, 0, 96, 96);
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
var RocketLaunchAnimation = class {
|
|
4439
|
+
constructor(options = {}) {
|
|
4440
|
+
this.rockets = [];
|
|
4441
|
+
this.smoke = [];
|
|
4442
|
+
this.fire = [];
|
|
4443
|
+
this.smokeFades = [];
|
|
4444
|
+
this.fireFades = [];
|
|
4445
|
+
this.blends = new Array(ROCKETS).fill(0);
|
|
4446
|
+
this.groundedAt = new Array(ROCKETS).fill(Infinity);
|
|
4447
|
+
// Per-rocket veer parameters (turnS = Infinity for a rocket that climbs straight).
|
|
4448
|
+
this.turnS = new Array(ROCKETS).fill(Infinity);
|
|
4449
|
+
this.turnDir = [];
|
|
4450
|
+
this.turnRoll = new Array(ROCKETS).fill(0);
|
|
4451
|
+
this.stagger = new Array(ROCKETS).fill(0);
|
|
4452
|
+
this.aspect = 16 / 9;
|
|
4453
|
+
this.enterAt = Infinity;
|
|
4454
|
+
this.exitAt = Infinity;
|
|
4455
|
+
this.launchedAt = Infinity;
|
|
4456
|
+
this.lastNow = 0;
|
|
4457
|
+
this.finished = false;
|
|
4458
|
+
this.backend = options.backend;
|
|
4459
|
+
this.labelContent = options.label;
|
|
4460
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
4461
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
4462
|
+
this.turnDir.push({ x: 0, y: 1 });
|
|
4463
|
+
this.stagger[i] = hash012(i, 7) * LAUNCH_SPREAD_MS;
|
|
4030
4464
|
}
|
|
4031
|
-
|
|
4465
|
+
const order = Array.from({ length: ROCKETS }, (_, i) => i).sort(
|
|
4466
|
+
(a, b) => hash012(a, 11) - hash012(b, 11)
|
|
4467
|
+
);
|
|
4468
|
+
for (const i of order.slice(0, TURNERS)) {
|
|
4469
|
+
const height = TURN_MIN_Y + hash012(i, 12) * (TURN_MAX_Y - TURN_MIN_Y);
|
|
4470
|
+
const angle = (TURN_MIN_DEG + hash012(i, 13) * (TURN_MAX_DEG - TURN_MIN_DEG)) * DEG;
|
|
4471
|
+
const sign = hash012(i, 14) < 0.5 ? -1 : 1;
|
|
4472
|
+
this.turnS[i] = height - ROW_Y;
|
|
4473
|
+
this.turnDir[i] = { x: sign * Math.sin(angle), y: Math.cos(angle) };
|
|
4474
|
+
this.turnRoll[i] = -sign * angle;
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
mount(target) {
|
|
4478
|
+
if (!target.style.position) target.style.position = "relative";
|
|
4479
|
+
const smokeMeshes = SMOKE_COLORS.map((color) => quad(1, [color]));
|
|
4480
|
+
const fireMeshes = FIRE_COLORS.map((color) => quad(1, [color]));
|
|
4481
|
+
const smokeTexture = puffTexture(0.85, 0.5);
|
|
4482
|
+
const fireTexture = puffTexture(1, 0.32);
|
|
4483
|
+
const backend = async (rendererOptions) => {
|
|
4484
|
+
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);
|
|
4485
|
+
for (const mesh of smokeMeshes) renderer.setTexture(mesh, smokeTexture);
|
|
4486
|
+
for (const mesh of fireMeshes) renderer.setTexture(mesh, fireTexture);
|
|
4487
|
+
return renderer;
|
|
4488
|
+
};
|
|
4489
|
+
const engine = new Little3dEngine({
|
|
4490
|
+
backend,
|
|
4491
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z4 }, fov: FOV3 }
|
|
4492
|
+
});
|
|
4493
|
+
const rocketMesh = pyramid(1, ROCKET_COLORS);
|
|
4494
|
+
for (let i = 0; i < ROCKETS; i++) this.rockets.push(engine.add(rocketMesh, { scale: 0 }));
|
|
4495
|
+
for (let s = 0; s < SMOKE_POOL; s++) {
|
|
4496
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
4497
|
+
this.smokeFades.push(fade);
|
|
4498
|
+
this.smoke.push(engine.add(smokeMeshes[s % smokeMeshes.length], { scale: 0, transparency: fade }));
|
|
4499
|
+
}
|
|
4500
|
+
for (let f = 0; f < FIRE_POOL; f++) {
|
|
4501
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
4502
|
+
this.fireFades.push(fade);
|
|
4503
|
+
this.fire.push(engine.add(fireMeshes[f % fireMeshes.length], { scale: 0, transparency: fade }));
|
|
4504
|
+
}
|
|
4505
|
+
this.engine = engine;
|
|
4506
|
+
engine.mount(target).catch((error) => {
|
|
4507
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
4508
|
+
});
|
|
4509
|
+
const measure = () => {
|
|
4510
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
4511
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
measure();
|
|
4515
|
+
this.observer = new ResizeObserver(measure);
|
|
4516
|
+
this.observer.observe(target);
|
|
4517
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
4518
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
4519
|
+
}
|
|
4520
|
+
enter(now) {
|
|
4521
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
4522
|
+
}
|
|
4523
|
+
exit(now) {
|
|
4524
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
4525
|
+
}
|
|
4526
|
+
isFinished() {
|
|
4527
|
+
return this.finished;
|
|
4528
|
+
}
|
|
4529
|
+
render(now, frame) {
|
|
4530
|
+
if (!this.engine || !this.label) return;
|
|
4531
|
+
for (const handle of this.rockets) handle.transform.scale = 0;
|
|
4532
|
+
for (const fade of this.smokeFades) fade.opacity = 0;
|
|
4533
|
+
for (const fade of this.fireFades) fade.opacity = 0;
|
|
4534
|
+
for (const handle of this.smoke) handle.transform.scale = 0;
|
|
4535
|
+
for (const handle of this.fire) handle.transform.scale = 0;
|
|
4536
|
+
if (this.enterAt === Infinity) {
|
|
4537
|
+
this.engine.render();
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
4541
|
+
this.lastNow = now;
|
|
4542
|
+
const exiting = this.exitAt !== Infinity;
|
|
4543
|
+
this.updateBlends(dt, frame.progress, now, exiting);
|
|
4544
|
+
if (exiting && this.launchedAt === Infinity && this.blends.every((blend) => blend >= 1)) {
|
|
4545
|
+
this.launchedAt = now;
|
|
4546
|
+
}
|
|
4547
|
+
const launched = this.launchedAt !== Infinity;
|
|
4548
|
+
const halfWidth = HALF_HEIGHT2 * this.aspect;
|
|
4549
|
+
const rowHalf = Math.min(halfWidth * 0.8, 1.18);
|
|
4550
|
+
const spawnX = halfWidth + 0.6;
|
|
4551
|
+
let smokeCursor = 0;
|
|
4552
|
+
let fireCursor = 0;
|
|
4553
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
4554
|
+
const homeX = -rowHalf + 2 * rowHalf * i / (ROCKETS - 1);
|
|
4555
|
+
const launchAt = launched ? this.launchedAt + this.stagger[i] : Infinity;
|
|
4556
|
+
const la = now - launchAt;
|
|
4557
|
+
if (launched && la >= 0) {
|
|
4558
|
+
this.renderAscent(i, homeX, la, halfWidth);
|
|
4559
|
+
fireCursor = this.emitFire(i, homeX, la, fireCursor);
|
|
4560
|
+
continue;
|
|
4561
|
+
}
|
|
4562
|
+
const blend = this.blends[i];
|
|
4563
|
+
if (blend <= 0) continue;
|
|
4564
|
+
const transform2 = this.rockets[i].transform;
|
|
4565
|
+
transform2.position.x = spawnX + (homeX - spawnX) * easeOutBack(blend);
|
|
4566
|
+
transform2.position.y = ROW_Y;
|
|
4567
|
+
transform2.position.z = 0;
|
|
4568
|
+
transform2.rotation.x = 0;
|
|
4569
|
+
transform2.rotation.y = 0;
|
|
4570
|
+
transform2.rotation.z = 0;
|
|
4571
|
+
transform2.scale = SIZE * smoothstep2(0, 0.6, blend);
|
|
4572
|
+
if (this.groundedAt[i] !== Infinity) {
|
|
4573
|
+
smokeCursor = this.emitSmoke(i, homeX, now, launchAt, smokeCursor);
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
4577
|
+
if (this.fadeLabel) {
|
|
4578
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
4579
|
+
now,
|
|
4580
|
+
this.enterAt,
|
|
4581
|
+
SLIDE_MS,
|
|
4582
|
+
this.launchedAt,
|
|
4583
|
+
LAUNCH_SPREAD_MS
|
|
4584
|
+
));
|
|
4585
|
+
}
|
|
4586
|
+
if (launched && now >= this.launchedAt + LAUNCH_SPREAD_MS + FINISH_PAD_MS) {
|
|
4587
|
+
this.finished = true;
|
|
4588
|
+
}
|
|
4589
|
+
this.engine.render();
|
|
4590
|
+
}
|
|
4591
|
+
destroy() {
|
|
4592
|
+
this.observer?.disconnect();
|
|
4593
|
+
this.observer = void 0;
|
|
4594
|
+
this.label?.container.remove();
|
|
4595
|
+
this.label = void 0;
|
|
4596
|
+
this.engine?.destroy();
|
|
4597
|
+
this.engine = void 0;
|
|
4598
|
+
this.rockets.length = 0;
|
|
4599
|
+
this.smoke.length = 0;
|
|
4600
|
+
this.fire.length = 0;
|
|
4601
|
+
this.smokeFades.length = 0;
|
|
4602
|
+
this.fireFades.length = 0;
|
|
4603
|
+
}
|
|
4604
|
+
updateBlends(dt, progress, now, exiting) {
|
|
4605
|
+
const want = exiting ? ROCKETS : Math.min(ROCKETS, Math.round(progress * ROCKETS));
|
|
4606
|
+
const rate = dt / SLIDE_MS * (exiting ? EXIT_HURRY3 : 1);
|
|
4607
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
4608
|
+
const target = i < want ? 1 : 0;
|
|
4609
|
+
const blend = this.blends[i];
|
|
4610
|
+
if (target > blend && (i === 0 || this.blends[i - 1] >= SLIDE_GATE)) {
|
|
4611
|
+
this.blends[i] = Math.min(1, blend + rate);
|
|
4612
|
+
} else if (target < blend && (i === ROCKETS - 1 || this.blends[i + 1] <= 1 - SLIDE_GATE)) {
|
|
4613
|
+
this.blends[i] = Math.max(0, blend - rate);
|
|
4614
|
+
}
|
|
4615
|
+
if (this.blends[i] >= 1) {
|
|
4616
|
+
if (this.groundedAt[i] === Infinity) this.groundedAt[i] = now;
|
|
4617
|
+
} else {
|
|
4618
|
+
this.groundedAt[i] = Infinity;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
/** Along-track distance climbed `la` ms after this rocket's own blast-off. */
|
|
4623
|
+
ascentDistance(la) {
|
|
4624
|
+
const seconds = la / 1e3;
|
|
4625
|
+
return 0.5 * ASCENT_G * seconds * seconds;
|
|
4626
|
+
}
|
|
4627
|
+
/** Rocket center, nose direction, and roll `la` ms into its climb. */
|
|
4628
|
+
ascentPose(i, homeX, la) {
|
|
4629
|
+
const s = this.ascentDistance(la);
|
|
4630
|
+
const turnS = this.turnS[i];
|
|
4631
|
+
if (s <= turnS) {
|
|
4632
|
+
return { pos: { x: homeX, y: ROW_Y + s }, dir: { x: 0, y: 1 }, roll: 0 };
|
|
4633
|
+
}
|
|
4634
|
+
const post = s - turnS;
|
|
4635
|
+
const dir = this.turnDir[i];
|
|
4636
|
+
return {
|
|
4637
|
+
pos: { x: homeX + dir.x * post, y: ROW_Y + turnS + dir.y * post },
|
|
4638
|
+
dir,
|
|
4639
|
+
roll: this.turnRoll[i]
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
renderAscent(i, homeX, la, halfWidth) {
|
|
4643
|
+
const { pos, roll } = this.ascentPose(i, homeX, la);
|
|
4644
|
+
if (pos.y > HALF_HEIGHT2 + 0.4 || Math.abs(pos.x) > halfWidth + 0.4) return;
|
|
4645
|
+
const transform2 = this.rockets[i].transform;
|
|
4646
|
+
transform2.position.x = pos.x;
|
|
4647
|
+
transform2.position.y = pos.y;
|
|
4648
|
+
transform2.position.z = 0;
|
|
4649
|
+
transform2.rotation.x = 0;
|
|
4650
|
+
transform2.rotation.y = 0;
|
|
4651
|
+
transform2.rotation.z = roll;
|
|
4652
|
+
transform2.scale = SIZE;
|
|
4653
|
+
}
|
|
4654
|
+
emitFire(i, homeX, la, cursor) {
|
|
4655
|
+
const gap = FIRE_GAP_MS;
|
|
4656
|
+
const last = Math.min(Math.floor(la / gap), Math.floor(FIRE_ON_MS / gap));
|
|
4657
|
+
const first = Math.max(0, Math.ceil((la - FIRE_LIFE_MS) / gap));
|
|
4658
|
+
for (let n = first; n <= last; n++) {
|
|
4659
|
+
if (cursor >= this.fire.length) return cursor;
|
|
4660
|
+
const emitLa = n * gap;
|
|
4661
|
+
const age = la - emitLa;
|
|
4662
|
+
if (age < 0 || age >= FIRE_LIFE_MS) continue;
|
|
4663
|
+
const life = age / FIRE_LIFE_MS;
|
|
4664
|
+
const seconds = age / 1e3;
|
|
4665
|
+
const pose = this.ascentPose(i, homeX, emitLa);
|
|
4666
|
+
const back = { x: -pose.dir.x, y: -pose.dir.y };
|
|
4667
|
+
const perp = { x: -pose.dir.y, y: pose.dir.x };
|
|
4668
|
+
const lat = (hash012(i * 97 + n, 1) - 0.5) * FIRE_SPREAD;
|
|
4669
|
+
const baseX = pose.pos.x + back.x * SIZE * 0.5;
|
|
4670
|
+
const baseY = pose.pos.y + back.y * SIZE * 0.5;
|
|
4671
|
+
const transform2 = this.fire[cursor].transform;
|
|
4672
|
+
transform2.position.x = baseX + back.x * FIRE_TRAIL * seconds + perp.x * lat;
|
|
4673
|
+
transform2.position.y = baseY + back.y * FIRE_TRAIL * seconds + perp.y * lat - 0.12 * seconds * seconds;
|
|
4674
|
+
transform2.position.z = PARTICLE_Z;
|
|
4675
|
+
transform2.rotation.z = hash012(i * 97 + n, 2) * Math.PI * 2;
|
|
4676
|
+
transform2.scale = FIRE_SIZE * (0.7 + 0.5 * hash012(i * 97 + n, 3)) * (1 - 0.55 * life);
|
|
4677
|
+
this.fireFades[cursor].opacity = FIRE_PEAK * smoothstep2(0, 0.15, life) * (1 - smoothstep2(0.55, 1, life));
|
|
4678
|
+
cursor++;
|
|
4679
|
+
}
|
|
4680
|
+
return cursor;
|
|
4681
|
+
}
|
|
4682
|
+
emitSmoke(i, homeX, now, launchAt, cursor) {
|
|
4683
|
+
const start = this.groundedAt[i];
|
|
4684
|
+
const tr = now - start;
|
|
4685
|
+
const gap = SMOKE_GAP_MS;
|
|
4686
|
+
const emitUntil = Number.isFinite(launchAt) ? launchAt - start : tr;
|
|
4687
|
+
const last = Math.min(Math.floor(tr / gap), Math.floor(emitUntil / gap));
|
|
4688
|
+
const first = Math.max(0, Math.ceil((tr - SMOKE_LIFE_MS) / gap));
|
|
4689
|
+
const baseY = ROW_Y - SIZE * 0.4;
|
|
4690
|
+
for (let n = first; n <= last; n++) {
|
|
4691
|
+
if (cursor >= this.smoke.length) return cursor;
|
|
4692
|
+
const age = tr - n * gap;
|
|
4693
|
+
if (age < 0 || age >= SMOKE_LIFE_MS) continue;
|
|
4694
|
+
const life = age / SMOKE_LIFE_MS;
|
|
4695
|
+
const drift = (hash012(i * 131 + n, 1) - 0.5) * 0.14;
|
|
4696
|
+
const transform2 = this.smoke[cursor].transform;
|
|
4697
|
+
transform2.position.x = homeX + drift * life;
|
|
4698
|
+
transform2.position.y = baseY + SMOKE_RISE * life;
|
|
4699
|
+
transform2.position.z = PARTICLE_Z;
|
|
4700
|
+
transform2.rotation.z = hash012(i * 131 + n, 2) * Math.PI * 2;
|
|
4701
|
+
transform2.scale = SMOKE_SIZE * (0.5 + 0.8 * life) * (0.7 + 0.6 * hash012(i * 131 + n, 3));
|
|
4702
|
+
this.smokeFades[cursor].opacity = SMOKE_PEAK * smoothstep2(0, 0.25, life) * (1 - smoothstep2(0.5, 1, life));
|
|
4703
|
+
cursor++;
|
|
4704
|
+
}
|
|
4705
|
+
return cursor;
|
|
4706
|
+
}
|
|
4032
4707
|
};
|
|
4708
|
+
|
|
4709
|
+
// src/prefabs/rocket-launch.ts
|
|
4033
4710
|
function rocketLaunch(options = {}) {
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
backend: options.backend,
|
|
4043
|
-
...options.object,
|
|
4044
|
-
label: options.object?.label
|
|
4045
|
-
});
|
|
4046
|
-
const exhaust = new ParticlesAnimation({
|
|
4047
|
-
rate: 46,
|
|
4048
|
-
lifeMs: 1100,
|
|
4049
|
-
size: 0.18,
|
|
4050
|
-
speed: 0.5,
|
|
4051
|
-
direction: { x: 0, y: -1, z: 0 },
|
|
4052
|
-
spread: 0.3,
|
|
4053
|
-
gravity: { x: 0, y: -0.6, z: 0 },
|
|
4054
|
-
colors: ["#fde047", "#fb923c", "#ef4444", "#fef3c7"],
|
|
4055
|
-
texture: particles.texture ?? shineTexture(),
|
|
4056
|
-
emitter: object.trailEmitter(),
|
|
4057
|
-
outroMs: object.outroDurationMs,
|
|
4058
|
-
seed: 9,
|
|
4059
|
-
backend: options.backend,
|
|
4060
|
-
...particles,
|
|
4061
|
-
label: options.label ?? particles.label,
|
|
4062
|
-
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
4063
|
-
});
|
|
4064
|
-
return progressSpinner(new CompositeAnimation([exhaust, object]), options);
|
|
4711
|
+
return progressSpinner(
|
|
4712
|
+
new RocketLaunchAnimation({
|
|
4713
|
+
backend: options.backend,
|
|
4714
|
+
label: options.label,
|
|
4715
|
+
fadeLabel: options.fadeLabel
|
|
4716
|
+
}),
|
|
4717
|
+
options
|
|
4718
|
+
);
|
|
4065
4719
|
}
|
|
4066
4720
|
|
|
4067
4721
|
// src/motion/wander.ts
|
|
@@ -4129,34 +4783,88 @@ void main() {
|
|
|
4129
4783
|
}), options);
|
|
4130
4784
|
}
|
|
4131
4785
|
|
|
4786
|
+
// src/motion/circle.ts
|
|
4787
|
+
function circleMotion(options = {}) {
|
|
4788
|
+
const radius = options.radius ?? 1.3;
|
|
4789
|
+
const periodMs = options.periodMs ?? 3e3;
|
|
4790
|
+
const tilt = options.tilt ?? 0.5;
|
|
4791
|
+
const direction = options.direction ?? 1;
|
|
4792
|
+
const cosTilt = Math.cos(tilt);
|
|
4793
|
+
const sinTilt = Math.sin(tilt);
|
|
4794
|
+
return {
|
|
4795
|
+
positionAt(t) {
|
|
4796
|
+
const angle = direction * t / periodMs * Math.PI * 2;
|
|
4797
|
+
const x = radius * Math.cos(angle);
|
|
4798
|
+
const flatY = radius * Math.sin(angle);
|
|
4799
|
+
return { x, y: flatY * cosTilt, z: flatY * sinTilt };
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4132
4804
|
// <stdin>
|
|
4133
4805
|
init_webgl_textured();
|
|
4134
4806
|
init_webgpu_textured();
|
|
4135
4807
|
|
|
4136
4808
|
// src/engines/little-3d-engine/loaders/obj.ts
|
|
4137
4809
|
var DEFAULT_COLORS12 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
4810
|
+
function clamp018(value) {
|
|
4811
|
+
return Math.min(1, Math.max(0, value));
|
|
4812
|
+
}
|
|
4138
4813
|
function channelToHex(value) {
|
|
4139
4814
|
const channel = Number.parseFloat(value);
|
|
4140
4815
|
if (!Number.isFinite(channel)) return void 0;
|
|
4141
|
-
return Math.round(
|
|
4142
|
-
}
|
|
4143
|
-
function
|
|
4144
|
-
const
|
|
4145
|
-
|
|
4816
|
+
return Math.round(clamp018(channel) * 255).toString(16).padStart(2, "0");
|
|
4817
|
+
}
|
|
4818
|
+
function parseRgb(parts) {
|
|
4819
|
+
const channels = parts.slice(1, 4).map(Number.parseFloat);
|
|
4820
|
+
if (channels.length !== 3 || !channels.every(Number.isFinite)) return void 0;
|
|
4821
|
+
return [clamp018(channels[0]), clamp018(channels[1]), clamp018(channels[2])];
|
|
4822
|
+
}
|
|
4823
|
+
function toMaterial(surface) {
|
|
4824
|
+
const material = {};
|
|
4825
|
+
if (surface.specular) material.specular = surface.specular;
|
|
4826
|
+
if (surface.shininess !== void 0) material.shininess = surface.shininess;
|
|
4827
|
+
if (surface.emissive) material.emissive = surface.emissive;
|
|
4828
|
+
return Object.keys(material).length > 0 ? material : void 0;
|
|
4829
|
+
}
|
|
4830
|
+
function parseMtl(text) {
|
|
4831
|
+
const materials = /* @__PURE__ */ new Map();
|
|
4832
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
4833
|
+
let name;
|
|
4146
4834
|
for (const line of text.split("\n")) {
|
|
4147
4835
|
const trimmed = line.trim();
|
|
4148
4836
|
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
4149
4837
|
const parts = trimmed.split(/\s+/);
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4838
|
+
const keyword = parts[0];
|
|
4839
|
+
if (keyword === "newmtl") {
|
|
4840
|
+
name = parts.slice(1).join(" ");
|
|
4841
|
+
if (name && !materials.has(name)) {
|
|
4842
|
+
materials.set(name, {});
|
|
4843
|
+
surfaces.set(name, {});
|
|
4844
|
+
}
|
|
4845
|
+
continue;
|
|
4846
|
+
}
|
|
4847
|
+
if (!name) continue;
|
|
4848
|
+
const entry = materials.get(name);
|
|
4849
|
+
const surface = surfaces.get(name);
|
|
4850
|
+
if (keyword === "Kd") {
|
|
4153
4851
|
const channels = parts.slice(1, 4).map(channelToHex);
|
|
4154
4852
|
if (channels.length === 3 && channels.every((channel) => channel !== void 0)) {
|
|
4155
|
-
|
|
4853
|
+
entry.color = `#${channels.join("")}`;
|
|
4156
4854
|
}
|
|
4855
|
+
} else if (keyword === "Ks") {
|
|
4856
|
+
surface.specular = parseRgb(parts);
|
|
4857
|
+
} else if (keyword === "Ns") {
|
|
4858
|
+
const ns = Number.parseFloat(parts[1]);
|
|
4859
|
+
if (Number.isFinite(ns)) surface.shininess = Math.max(0, ns);
|
|
4860
|
+
} else if (keyword === "Ke") {
|
|
4861
|
+
surface.emissive = parseRgb(parts);
|
|
4157
4862
|
}
|
|
4158
4863
|
}
|
|
4159
|
-
|
|
4864
|
+
for (const [key, surface] of surfaces) {
|
|
4865
|
+
materials.get(key).material = toMaterial(surface);
|
|
4866
|
+
}
|
|
4867
|
+
return materials;
|
|
4160
4868
|
}
|
|
4161
4869
|
function resolveIndex(token, vertexCount) {
|
|
4162
4870
|
const n = parseInt(token, 10);
|
|
@@ -4164,7 +4872,7 @@ void main() {
|
|
|
4164
4872
|
}
|
|
4165
4873
|
function parseObj(text, options = {}) {
|
|
4166
4874
|
const colors = options.colors ?? DEFAULT_COLORS12;
|
|
4167
|
-
const
|
|
4875
|
+
const materials = options.useMtlColors && options.mtl ? parseMtl(options.mtl) : void 0;
|
|
4168
4876
|
const vertices = [];
|
|
4169
4877
|
const faces = [];
|
|
4170
4878
|
let material;
|
|
@@ -4188,9 +4896,11 @@ void main() {
|
|
|
4188
4896
|
indices.push(resolveIndex(vertexToken, vertices.length));
|
|
4189
4897
|
}
|
|
4190
4898
|
if (indices.length >= 3) {
|
|
4191
|
-
const
|
|
4192
|
-
const color =
|
|
4193
|
-
|
|
4899
|
+
const entry = material ? materials?.get(material) : void 0;
|
|
4900
|
+
const color = entry?.color ?? (materials ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
|
|
4901
|
+
const face = { indices, color };
|
|
4902
|
+
if (entry?.material) face.material = entry.material;
|
|
4903
|
+
faces.push(face);
|
|
4194
4904
|
}
|
|
4195
4905
|
}
|
|
4196
4906
|
}
|