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