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