3d-spinner 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/grid-assembly.d.ts +67 -0
- package/dist/animations/grid-assembly.js +268 -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/cjs/animations/charged-orb.cjs +1334 -0
- package/dist/cjs/animations/grid-assembly.cjs +1498 -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 +131 -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 +3450 -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 +2 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.js +16 -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 +9 -0
- package/dist/prefabs/ghost-train.js +44 -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 +8 -0
- package/dist/prefabs/rocket-launch.js +59 -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 +3055 -1251
- package/dist/umd/spinner.global.min.js +60 -5
- package/package.json +42 -1
- /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,1425 +846,3000 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
845
846
|
}
|
|
846
847
|
});
|
|
847
848
|
|
|
848
|
-
//
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
|
922
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
|
+
|
|
1500
|
+
// <stdin>
|
|
1501
|
+
var stdin_exports = {};
|
|
1502
|
+
__export(stdin_exports, {
|
|
1503
|
+
Camera: () => Camera,
|
|
1504
|
+
ChargedOrbAnimation: () => ChargedOrbAnimation,
|
|
1505
|
+
CompositeAnimation: () => CompositeAnimation,
|
|
1506
|
+
GridAssemblyAnimation: () => GridAssemblyAnimation,
|
|
1507
|
+
Light: () => Light,
|
|
1508
|
+
Little3dEngine: () => Little3dEngine,
|
|
1509
|
+
LittleTweenEngine: () => LittleTweenEngine,
|
|
1510
|
+
ObjectMotionAnimation: () => ObjectMotionAnimation,
|
|
1511
|
+
ParticlesAnimation: () => ParticlesAnimation,
|
|
1512
|
+
SpinAnimation: () => SpinAnimation,
|
|
1513
|
+
WebGLTexturedRenderer: () => WebGLTexturedRenderer,
|
|
1514
|
+
WebGPUTexturedRenderer: () => WebGPUTexturedRenderer,
|
|
1515
|
+
centerAndScaleMesh: () => centerAndScaleMesh,
|
|
1516
|
+
chargedOrb: () => chargedOrb,
|
|
1517
|
+
circleMotion: () => circleMotion,
|
|
1518
|
+
createSpinner: () => createSpinner,
|
|
1519
|
+
cross: () => cross,
|
|
1520
|
+
crystalComet: () => crystalComet,
|
|
1521
|
+
cube: () => cube,
|
|
1522
|
+
cubeSphere: () => cubeSphere,
|
|
1523
|
+
cubic: () => cubic,
|
|
1524
|
+
dot: () => dot,
|
|
1525
|
+
ease: () => ease,
|
|
1526
|
+
easeInBack: () => easeInBack,
|
|
1527
|
+
easeInBounce: () => easeInBounce,
|
|
1528
|
+
easeInCirc: () => easeInCirc,
|
|
1529
|
+
easeInCubic: () => easeInCubic,
|
|
1530
|
+
easeInElastic: () => easeInElastic,
|
|
1531
|
+
easeInExpo: () => easeInExpo,
|
|
1532
|
+
easeInOutBack: () => easeInOutBack,
|
|
1533
|
+
easeInOutBounce: () => easeInOutBounce,
|
|
1534
|
+
easeInOutCirc: () => easeInOutCirc,
|
|
1535
|
+
easeInOutCubic: () => easeInOutCubic,
|
|
1536
|
+
easeInOutElastic: () => easeInOutElastic,
|
|
1537
|
+
easeInOutExpo: () => easeInOutExpo,
|
|
1538
|
+
easeInOutQuad: () => easeInOutQuad,
|
|
1539
|
+
easeInOutQuart: () => easeInOutQuart,
|
|
1540
|
+
easeInOutQuint: () => easeInOutQuint,
|
|
1541
|
+
easeInOutSine: () => easeInOutSine,
|
|
1542
|
+
easeInQuad: () => easeInQuad,
|
|
1543
|
+
easeInQuart: () => easeInQuart,
|
|
1544
|
+
easeInQuint: () => easeInQuint,
|
|
1545
|
+
easeInSine: () => easeInSine,
|
|
1546
|
+
easeOutBack: () => easeOutBack,
|
|
1547
|
+
easeOutBounce: () => easeOutBounce,
|
|
1548
|
+
easeOutCirc: () => easeOutCirc,
|
|
1549
|
+
easeOutCubic: () => easeOutCubic,
|
|
1550
|
+
easeOutElastic: () => easeOutElastic,
|
|
1551
|
+
easeOutExpo: () => easeOutExpo,
|
|
1552
|
+
easeOutQuad: () => easeOutQuad,
|
|
1553
|
+
easeOutQuart: () => easeOutQuart,
|
|
1554
|
+
easeOutQuint: () => easeOutQuint,
|
|
1555
|
+
easeOutSine: () => easeOutSine,
|
|
1556
|
+
easeTypes: () => easeTypes,
|
|
1557
|
+
enterFromObjectDirection: () => enterFromObjectDirection,
|
|
1558
|
+
expandToTriangles: () => expandToTriangles,
|
|
1559
|
+
figureEightMotion: () => figureEightMotion,
|
|
1560
|
+
ghostTrain: () => ghostTrain,
|
|
1561
|
+
gridAssembly: () => gridAssembly,
|
|
1562
|
+
grow: () => grow,
|
|
1563
|
+
icosphere: () => icosphere,
|
|
1564
|
+
leaveInObjectDirection: () => leaveInObjectDirection,
|
|
1565
|
+
linear: () => linear,
|
|
1566
|
+
monochromeStreak: () => monochromeStreak,
|
|
1567
|
+
normalize: () => normalize,
|
|
1568
|
+
octaSphere: () => octaSphere,
|
|
1569
|
+
octahedron: () => octahedron,
|
|
1570
|
+
orderRenderItems: () => orderRenderItems,
|
|
1571
|
+
parseObj: () => parseObj,
|
|
1572
|
+
particleField: () => particleField,
|
|
1573
|
+
planeMesh: () => planeMesh,
|
|
1574
|
+
planeStarTrail: () => planeStarTrail,
|
|
1575
|
+
pulsingStarfield: () => pulsingStarfield,
|
|
1576
|
+
pyramid: () => pyramid,
|
|
1577
|
+
quad: () => quad,
|
|
1578
|
+
quadratic: () => quadratic,
|
|
1579
|
+
quartic: () => quartic,
|
|
1580
|
+
quintic: () => quintic,
|
|
1581
|
+
rocketLaunch: () => rocketLaunch,
|
|
1582
|
+
scale: () => scale,
|
|
1583
|
+
shineTexture: () => shineTexture,
|
|
1584
|
+
shrink: () => shrink,
|
|
1585
|
+
squareMotion: () => squareMotion,
|
|
1586
|
+
starSwarm: () => starSwarm,
|
|
1587
|
+
starTexture: () => starTexture,
|
|
1588
|
+
streakTexture: () => streakTexture,
|
|
1589
|
+
subtract: () => subtract,
|
|
1590
|
+
tetrahedron: () => tetrahedron,
|
|
1591
|
+
transform: () => transform,
|
|
1592
|
+
uvSphere: () => uvSphere,
|
|
1593
|
+
vec3: () => vec3,
|
|
1594
|
+
wanderMotion: () => wanderMotion
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
// src/index.ts
|
|
1598
|
+
function clamp01(value) {
|
|
1599
|
+
if (Number.isNaN(value)) return 0;
|
|
1600
|
+
return Math.min(1, Math.max(0, value));
|
|
1601
|
+
}
|
|
1602
|
+
function lerp(from, to, t) {
|
|
1603
|
+
return from + (to - from) * t;
|
|
1604
|
+
}
|
|
1605
|
+
function createSpinner(target, options) {
|
|
1606
|
+
if (!(target instanceof HTMLElement)) {
|
|
1607
|
+
throw new Error("3d-spinner: createSpinner requires a target HTMLElement.");
|
|
1608
|
+
}
|
|
1609
|
+
const { animation } = options;
|
|
1610
|
+
const indeterminate = options.type === "indeterminate";
|
|
1611
|
+
if (indeterminate && options.periodMs !== void 0 && (!Number.isFinite(options.periodMs) || options.periodMs <= 0)) {
|
|
1612
|
+
throw new RangeError("3d-spinner: periodMs must be a finite number greater than zero.");
|
|
1613
|
+
}
|
|
1614
|
+
animation.mount(target);
|
|
1615
|
+
const start = performance.now();
|
|
1616
|
+
let rafId = 0;
|
|
1617
|
+
let stopped = false;
|
|
1618
|
+
let destroyed = false;
|
|
1619
|
+
let entered = false;
|
|
1620
|
+
let exiting = false;
|
|
1621
|
+
let current = 0;
|
|
1622
|
+
let targetProgress = 0;
|
|
1623
|
+
let deadline = Infinity;
|
|
1624
|
+
if (!indeterminate) {
|
|
1625
|
+
const opts = options;
|
|
1626
|
+
if (typeof opts.progress === "number") {
|
|
1627
|
+
current = clamp01(opts.progress);
|
|
1628
|
+
targetProgress = current;
|
|
1629
|
+
}
|
|
1630
|
+
if (typeof opts.timeout === "number") deadline = Math.min(deadline, start + opts.timeout);
|
|
1631
|
+
if (opts.until instanceof Date) deadline = Math.min(deadline, opts.until.getTime());
|
|
1632
|
+
}
|
|
1633
|
+
function computeProgress(now) {
|
|
1634
|
+
if (!indeterminate) {
|
|
1635
|
+
if (now >= deadline) targetProgress = 1;
|
|
1636
|
+
current = lerp(current, targetProgress, 0.12);
|
|
1637
|
+
if (Math.abs(targetProgress - current) < 5e-4) current = targetProgress;
|
|
1638
|
+
return current;
|
|
1639
|
+
}
|
|
1640
|
+
const opts = options;
|
|
1641
|
+
const period = opts.periodMs ?? 2e3;
|
|
1642
|
+
const t = (now - start) / period;
|
|
1643
|
+
if ((opts.loop ?? "bounce") === "restart") return t - Math.floor(t);
|
|
1644
|
+
const phase = t - 2 * Math.floor(t / 2);
|
|
1645
|
+
return phase <= 1 ? phase : 2 - phase;
|
|
1646
|
+
}
|
|
1647
|
+
function frame(now) {
|
|
1648
|
+
if (stopped) return;
|
|
1649
|
+
const progress = computeProgress(now);
|
|
1650
|
+
if (!entered && (indeterminate || progress > 0)) {
|
|
1651
|
+
animation.enter(now);
|
|
1652
|
+
entered = true;
|
|
1653
|
+
}
|
|
1654
|
+
if (!exiting && entered && !indeterminate && progress >= 1 && targetProgress >= 1) {
|
|
1655
|
+
animation.exit(now);
|
|
1656
|
+
exiting = true;
|
|
1657
|
+
}
|
|
1658
|
+
const target2 = indeterminate ? progress : targetProgress;
|
|
1659
|
+
animation.render(now, { progress, targetProgress: target2, indeterminate });
|
|
1660
|
+
if (exiting && animation.isFinished()) {
|
|
1661
|
+
halt();
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
rafId = requestAnimationFrame(frame);
|
|
1665
|
+
}
|
|
1666
|
+
function halt() {
|
|
1667
|
+
if (stopped) return;
|
|
1668
|
+
stopped = true;
|
|
1669
|
+
if (rafId) cancelAnimationFrame(rafId);
|
|
1670
|
+
rafId = 0;
|
|
1671
|
+
}
|
|
1672
|
+
function setProgress(value) {
|
|
1673
|
+
if (!indeterminate) targetProgress = clamp01(value);
|
|
1674
|
+
}
|
|
1675
|
+
function stop() {
|
|
1676
|
+
if (stopped || exiting) return;
|
|
1677
|
+
if (!entered) {
|
|
1678
|
+
halt();
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
animation.exit(performance.now());
|
|
1682
|
+
exiting = true;
|
|
1683
|
+
}
|
|
1684
|
+
function destroy() {
|
|
1685
|
+
if (destroyed) return;
|
|
1686
|
+
destroyed = true;
|
|
1687
|
+
halt();
|
|
1688
|
+
animation.destroy();
|
|
1689
|
+
}
|
|
1690
|
+
rafId = requestAnimationFrame(frame);
|
|
1691
|
+
return { setProgress, stop, destroy };
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/engines/little-3d-engine/core/camera.ts
|
|
1695
|
+
init_math();
|
|
1696
|
+
var DEFAULTS = {
|
|
1697
|
+
position: { x: 0, y: 0, z: 4 },
|
|
1698
|
+
fov: 55 * Math.PI / 180,
|
|
1699
|
+
near: 0.1,
|
|
1700
|
+
far: 100
|
|
1701
|
+
};
|
|
1702
|
+
var Camera = class {
|
|
1703
|
+
constructor(options) {
|
|
1704
|
+
this.options = { ...DEFAULTS, ...options };
|
|
1705
|
+
}
|
|
1706
|
+
/** Transform a world-space point into view (camera) space. */
|
|
1707
|
+
toView(p) {
|
|
1708
|
+
const { position } = this.options;
|
|
1709
|
+
return transformAffine(translation(-position.x, -position.y, -position.z), p);
|
|
1710
|
+
}
|
|
1711
|
+
/** Combined view-projection matrix for the given viewport aspect ratio. */
|
|
1712
|
+
viewProjection(aspect) {
|
|
1713
|
+
const { position, fov, near, far } = this.options;
|
|
1714
|
+
const view = translation(-position.x, -position.y, -position.z);
|
|
1715
|
+
const projection = perspective(fov, aspect, near, far);
|
|
1716
|
+
return multiply(projection, view);
|
|
1717
|
+
}
|
|
1718
|
+
/** Convert a normalized device coordinate (-1..1) to a pixel position. */
|
|
1719
|
+
toScreen(ndc, width, height) {
|
|
1720
|
+
return {
|
|
1721
|
+
x: (ndc.x * 0.5 + 0.5) * width,
|
|
1722
|
+
y: (1 - (ndc.y * 0.5 + 0.5)) * height
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
// src/engines/little-3d-engine/little-3d-engine.ts
|
|
1728
|
+
init_light();
|
|
1729
|
+
init_math();
|
|
1730
|
+
|
|
1731
|
+
// src/engines/little-3d-engine/core/mesh.ts
|
|
1732
|
+
function transform(init) {
|
|
1733
|
+
return {
|
|
1734
|
+
position: init?.position ?? { x: 0, y: 0, z: 0 },
|
|
1735
|
+
rotation: init?.rotation ?? { x: 0, y: 0, z: 0 },
|
|
1736
|
+
scale: init?.scale ?? 1
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/engines/little-3d-engine/little-3d-engine.ts
|
|
1741
|
+
init_renderer();
|
|
1742
|
+
init_light();
|
|
1743
|
+
|
|
1744
|
+
// src/engines/little-3d-engine/shapes/primitives/cube.ts
|
|
1745
|
+
var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1746
|
+
function cube(size = 1, colors = DEFAULT_COLORS) {
|
|
1747
|
+
const h = size / 2;
|
|
1748
|
+
const vertices = [
|
|
1749
|
+
{ x: -h, y: -h, z: h },
|
|
1750
|
+
{ x: h, y: -h, z: h },
|
|
1751
|
+
{ x: h, y: h, z: h },
|
|
1752
|
+
{ x: -h, y: h, z: h },
|
|
1753
|
+
{ x: -h, y: -h, z: -h },
|
|
1754
|
+
{ x: h, y: -h, z: -h },
|
|
1755
|
+
{ x: h, y: h, z: -h },
|
|
1756
|
+
{ x: -h, y: h, z: -h }
|
|
1757
|
+
];
|
|
1758
|
+
const faces = [
|
|
1759
|
+
{ indices: [0, 1, 2, 3], color: colors[0 % colors.length] },
|
|
1760
|
+
{ indices: [5, 4, 7, 6], color: colors[1 % colors.length] },
|
|
1761
|
+
{ indices: [3, 2, 6, 7], color: colors[2 % colors.length] },
|
|
1762
|
+
{ indices: [4, 5, 1, 0], color: colors[3 % colors.length] },
|
|
1763
|
+
{ indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
|
|
1764
|
+
{ indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
|
|
1765
|
+
];
|
|
1766
|
+
return { vertices, faces };
|
|
1767
|
+
}
|
|
1768
|
+
|
|
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) {
|
|
1785
|
+
const s = size / 2;
|
|
1786
|
+
const vertices = [
|
|
1787
|
+
{ x: s, y: s, z: s },
|
|
1788
|
+
{ x: s, y: -s, z: -s },
|
|
1789
|
+
{ x: -s, y: s, z: -s },
|
|
1790
|
+
{ x: -s, y: -s, z: s }
|
|
1791
|
+
];
|
|
1792
|
+
const faces = [
|
|
1793
|
+
{ indices: [0, 1, 2], color: colors[0 % colors.length] },
|
|
1794
|
+
{ indices: [0, 3, 1], color: colors[1 % colors.length] },
|
|
1795
|
+
{ indices: [0, 2, 3], color: colors[2 % colors.length] },
|
|
1796
|
+
{ indices: [1, 3, 2], color: colors[3 % colors.length] }
|
|
1797
|
+
];
|
|
1798
|
+
return { vertices, faces };
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/engines/little-3d-engine/shapes/primitives/octahedron.ts
|
|
1802
|
+
var DEFAULT_COLORS4 = [
|
|
1803
|
+
"#3b82f6",
|
|
1804
|
+
"#8b5cf6",
|
|
1805
|
+
"#ec4899",
|
|
1806
|
+
"#f59e0b",
|
|
1807
|
+
"#10b981",
|
|
1808
|
+
"#ef4444",
|
|
1809
|
+
"#06b6d4",
|
|
1810
|
+
"#eab308"
|
|
1811
|
+
];
|
|
1812
|
+
function octahedron(size = 1, colors = DEFAULT_COLORS4) {
|
|
1813
|
+
const r = size / 2;
|
|
1814
|
+
const vertices = [
|
|
1815
|
+
{ x: r, y: 0, z: 0 },
|
|
1816
|
+
{ x: -r, y: 0, z: 0 },
|
|
1817
|
+
{ x: 0, y: r, z: 0 },
|
|
1818
|
+
{ x: 0, y: -r, z: 0 },
|
|
1819
|
+
{ x: 0, y: 0, z: r },
|
|
1820
|
+
{ x: 0, y: 0, z: -r }
|
|
1821
|
+
];
|
|
1822
|
+
const faces = [
|
|
1823
|
+
{ indices: [4, 0, 2], color: colors[0 % colors.length] },
|
|
1824
|
+
{ indices: [4, 2, 1], color: colors[1 % colors.length] },
|
|
1825
|
+
{ indices: [4, 1, 3], color: colors[2 % colors.length] },
|
|
1826
|
+
{ indices: [4, 3, 0], color: colors[3 % colors.length] },
|
|
1827
|
+
{ indices: [5, 2, 0], color: colors[4 % colors.length] },
|
|
1828
|
+
{ indices: [5, 1, 2], color: colors[5 % colors.length] },
|
|
1829
|
+
{ indices: [5, 3, 1], color: colors[6 % colors.length] },
|
|
1830
|
+
{ indices: [5, 0, 3], color: colors[7 % colors.length] }
|
|
1831
|
+
];
|
|
1832
|
+
return { vertices, faces };
|
|
1833
|
+
}
|
|
1834
|
+
|
|
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) {
|
|
1838
|
+
const h = size / 2;
|
|
1839
|
+
const vertices = [
|
|
1840
|
+
{ x: -h, y: -h, z: h },
|
|
1841
|
+
{ x: h, y: -h, z: h },
|
|
1842
|
+
{ x: h, y: -h, z: -h },
|
|
1843
|
+
{ x: -h, y: -h, z: -h },
|
|
1844
|
+
{ x: 0, y: h, z: 0 }
|
|
1845
|
+
];
|
|
1846
|
+
const faces = [
|
|
1847
|
+
{ indices: [0, 3, 2, 1], color: colors[0 % colors.length] },
|
|
1848
|
+
{ indices: [4, 0, 1], color: colors[1 % colors.length] },
|
|
1849
|
+
{ indices: [4, 1, 2], color: colors[2 % colors.length] },
|
|
1850
|
+
{ indices: [4, 2, 3], color: colors[3 % colors.length] },
|
|
1851
|
+
{ indices: [4, 3, 0], color: colors[4 % colors.length] }
|
|
1852
|
+
];
|
|
1853
|
+
return { vertices, faces };
|
|
1854
|
+
}
|
|
1855
|
+
|
|
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) {
|
|
1859
|
+
const r = size / 2;
|
|
1860
|
+
const d = Math.max(1, Math.floor(detail));
|
|
1861
|
+
const slices = Math.max(4, d * 4);
|
|
1862
|
+
const stacks = Math.max(2, d * 2);
|
|
1863
|
+
const vertices = [{ x: 0, y: r, z: 0 }];
|
|
1864
|
+
const ring = (i, j) => 1 + (i - 1) * slices + j;
|
|
1865
|
+
for (let i = 1; i < stacks; i++) {
|
|
1866
|
+
const phi = Math.PI * i / stacks;
|
|
1867
|
+
const y = r * Math.cos(phi);
|
|
1868
|
+
const rr = r * Math.sin(phi);
|
|
1869
|
+
for (let j = 0; j < slices; j++) {
|
|
1870
|
+
const theta = 2 * Math.PI * j / slices;
|
|
1871
|
+
vertices.push({ x: rr * Math.cos(theta), y, z: rr * Math.sin(theta) });
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
const bottom = vertices.length;
|
|
1875
|
+
vertices.push({ x: 0, y: -r, z: 0 });
|
|
1876
|
+
const faces = [];
|
|
1877
|
+
let ci = 0;
|
|
1878
|
+
const color = () => colors[ci++ % colors.length];
|
|
1879
|
+
for (let j = 0; j < slices; j++) {
|
|
1880
|
+
faces.push({ indices: [0, ring(1, (j + 1) % slices), ring(1, j)], color: color() });
|
|
1881
|
+
}
|
|
1882
|
+
for (let i = 1; i < stacks - 1; i++) {
|
|
1883
|
+
for (let j = 0; j < slices; j++) {
|
|
1884
|
+
const j1 = (j + 1) % slices;
|
|
1885
|
+
faces.push({
|
|
1886
|
+
indices: [ring(i, j), ring(i, j1), ring(i + 1, j1), ring(i + 1, j)],
|
|
1887
|
+
color: color()
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
for (let j = 0; j < slices; j++) {
|
|
1892
|
+
faces.push({
|
|
1893
|
+
indices: [bottom, ring(stacks - 1, j), ring(stacks - 1, (j + 1) % slices)],
|
|
1894
|
+
color: color()
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
return { vertices, faces };
|
|
1898
|
+
}
|
|
923
1899
|
|
|
924
|
-
// src/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1900
|
+
// src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
|
|
1901
|
+
init_geometry();
|
|
1902
|
+
var DEFAULT_COLORS7 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1903
|
+
var T = (1 + Math.sqrt(5)) / 2;
|
|
1904
|
+
var SEED_VERTICES = [
|
|
1905
|
+
{ x: -1, y: T, z: 0 },
|
|
1906
|
+
{ x: 1, y: T, z: 0 },
|
|
1907
|
+
{ x: -1, y: -T, z: 0 },
|
|
1908
|
+
{ x: 1, y: -T, z: 0 },
|
|
1909
|
+
{ x: 0, y: -1, z: T },
|
|
1910
|
+
{ x: 0, y: 1, z: T },
|
|
1911
|
+
{ x: 0, y: -1, z: -T },
|
|
1912
|
+
{ x: 0, y: 1, z: -T },
|
|
1913
|
+
{ x: T, y: 0, z: -1 },
|
|
1914
|
+
{ x: T, y: 0, z: 1 },
|
|
1915
|
+
{ x: -T, y: 0, z: -1 },
|
|
1916
|
+
{ x: -T, y: 0, z: 1 }
|
|
1917
|
+
];
|
|
1918
|
+
var SEED_FACES = [
|
|
1919
|
+
[0, 11, 5],
|
|
1920
|
+
[0, 5, 1],
|
|
1921
|
+
[0, 1, 7],
|
|
1922
|
+
[0, 7, 10],
|
|
1923
|
+
[0, 10, 11],
|
|
1924
|
+
[1, 5, 9],
|
|
1925
|
+
[5, 11, 4],
|
|
1926
|
+
[11, 10, 2],
|
|
1927
|
+
[10, 7, 6],
|
|
1928
|
+
[7, 1, 8],
|
|
1929
|
+
[3, 9, 4],
|
|
1930
|
+
[3, 4, 2],
|
|
1931
|
+
[3, 2, 6],
|
|
1932
|
+
[3, 6, 8],
|
|
1933
|
+
[3, 8, 9],
|
|
1934
|
+
[4, 9, 5],
|
|
1935
|
+
[2, 4, 11],
|
|
1936
|
+
[6, 2, 10],
|
|
1937
|
+
[8, 6, 7],
|
|
1938
|
+
[9, 8, 1]
|
|
1939
|
+
];
|
|
1940
|
+
function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS7) {
|
|
1941
|
+
return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
|
|
928
1942
|
}
|
|
929
|
-
|
|
930
|
-
|
|
1943
|
+
|
|
1944
|
+
// src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
|
|
1945
|
+
init_geometry();
|
|
1946
|
+
var DEFAULT_COLORS8 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1947
|
+
var SEED_VERTICES2 = [
|
|
1948
|
+
{ x: 1, y: 0, z: 0 },
|
|
1949
|
+
{ x: -1, y: 0, z: 0 },
|
|
1950
|
+
{ x: 0, y: 1, z: 0 },
|
|
1951
|
+
{ x: 0, y: -1, z: 0 },
|
|
1952
|
+
{ x: 0, y: 0, z: 1 },
|
|
1953
|
+
{ x: 0, y: 0, z: -1 }
|
|
1954
|
+
];
|
|
1955
|
+
var SEED_FACES2 = [
|
|
1956
|
+
[4, 0, 2],
|
|
1957
|
+
[4, 2, 1],
|
|
1958
|
+
[4, 1, 3],
|
|
1959
|
+
[4, 3, 0],
|
|
1960
|
+
[5, 2, 0],
|
|
1961
|
+
[5, 1, 2],
|
|
1962
|
+
[5, 3, 1],
|
|
1963
|
+
[5, 0, 3]
|
|
1964
|
+
];
|
|
1965
|
+
function octaSphere(size = 1, detail = 1, colors = DEFAULT_COLORS8) {
|
|
1966
|
+
return sphereFromTriangles(SEED_VERTICES2, SEED_FACES2, size, detail, colors);
|
|
931
1967
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
let
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1968
|
+
|
|
1969
|
+
// src/engines/little-3d-engine/shapes/primitives/spheres/cube-sphere.ts
|
|
1970
|
+
var DEFAULT_COLORS9 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1971
|
+
var CUBE_FACES = [
|
|
1972
|
+
{ normal: [0, 0, 1], right: [1, 0, 0], up: [0, 1, 0] },
|
|
1973
|
+
{ normal: [0, 0, -1], right: [-1, 0, 0], up: [0, 1, 0] },
|
|
1974
|
+
{ normal: [1, 0, 0], right: [0, 0, -1], up: [0, 1, 0] },
|
|
1975
|
+
{ normal: [-1, 0, 0], right: [0, 0, 1], up: [0, 1, 0] },
|
|
1976
|
+
{ normal: [0, 1, 0], right: [1, 0, 0], up: [0, 0, -1] },
|
|
1977
|
+
{ normal: [0, -1, 0], right: [1, 0, 0], up: [0, 0, 1] }
|
|
1978
|
+
];
|
|
1979
|
+
function cubeSphere(size = 1, detail = 1, colors = DEFAULT_COLORS9) {
|
|
1980
|
+
const r = size / 2;
|
|
1981
|
+
const n = Math.max(1, Math.floor(detail));
|
|
1982
|
+
const vertices = [];
|
|
1983
|
+
const faces = [];
|
|
1984
|
+
let ci = 0;
|
|
1985
|
+
for (const face of CUBE_FACES) {
|
|
1986
|
+
const base = vertices.length;
|
|
1987
|
+
for (let i = 0; i <= n; i++) {
|
|
1988
|
+
for (let j = 0; j <= n; j++) {
|
|
1989
|
+
const u = -1 + 2 * i / n;
|
|
1990
|
+
const v = -1 + 2 * j / n;
|
|
1991
|
+
const x = face.normal[0] + u * face.right[0] + v * face.up[0];
|
|
1992
|
+
const y = face.normal[1] + u * face.right[1] + v * face.up[1];
|
|
1993
|
+
const z = face.normal[2] + u * face.right[2] + v * face.up[2];
|
|
1994
|
+
const len = Math.hypot(x, y, z);
|
|
1995
|
+
vertices.push({ x: x / len * r, y: y / len * r, z: z / len * r });
|
|
1996
|
+
}
|
|
956
1997
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
return current;
|
|
1998
|
+
const idx = (i, j) => base + i * (n + 1) + j;
|
|
1999
|
+
for (let i = 0; i < n; i++) {
|
|
2000
|
+
for (let j = 0; j < n; j++) {
|
|
2001
|
+
faces.push({
|
|
2002
|
+
indices: [idx(i, j), idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1)],
|
|
2003
|
+
color: colors[ci++ % colors.length]
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
966
2006
|
}
|
|
967
|
-
const opts = options;
|
|
968
|
-
const period = opts.periodMs ?? 2e3;
|
|
969
|
-
const t = (now - start) / period;
|
|
970
|
-
if ((opts.loop ?? "bounce") === "restart") return t - Math.floor(t);
|
|
971
|
-
const phase = t - 2 * Math.floor(t / 2);
|
|
972
|
-
return phase <= 1 ? phase : 2 - phase;
|
|
973
2007
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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 starTexture() {
|
|
2056
|
+
return canvasTexture((ctx) => {
|
|
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));
|
|
984
2064
|
}
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
2065
|
+
ctx.closePath();
|
|
2066
|
+
ctx.fill();
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/engines/little-3d-engine/textures/dynamic/shine.ts
|
|
2071
|
+
function shineTexture() {
|
|
2072
|
+
return canvasTexture((ctx) => {
|
|
2073
|
+
const halo = ctx.createRadialGradient(48, 48, 1, 48, 48, 46);
|
|
2074
|
+
halo.addColorStop(0, "rgba(255,255,255,1)");
|
|
2075
|
+
halo.addColorStop(0.08, "rgba(255,255,255,1)");
|
|
2076
|
+
halo.addColorStop(0.22, "rgba(210,240,255,0.7)");
|
|
2077
|
+
halo.addColorStop(0.55, "rgba(120,200,255,0.22)");
|
|
2078
|
+
halo.addColorStop(1, "rgba(80,160,255,0)");
|
|
2079
|
+
ctx.fillStyle = halo;
|
|
2080
|
+
ctx.fillRect(0, 0, 96, 96);
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// src/engines/little-3d-engine/textures/dynamic/streak.ts
|
|
2085
|
+
function streakTexture() {
|
|
2086
|
+
return canvasTexture((ctx) => {
|
|
2087
|
+
const gradient = ctx.createLinearGradient(5, 0, 91, 0);
|
|
2088
|
+
gradient.addColorStop(0, "rgba(255,255,255,0)");
|
|
2089
|
+
gradient.addColorStop(0.7, "rgba(255,255,255,0.4)");
|
|
2090
|
+
gradient.addColorStop(1, "rgba(255,255,255,1)");
|
|
2091
|
+
ctx.strokeStyle = gradient;
|
|
2092
|
+
ctx.lineWidth = 3.5;
|
|
2093
|
+
ctx.lineCap = "round";
|
|
2094
|
+
ctx.beginPath();
|
|
2095
|
+
ctx.moveTo(5, 48);
|
|
2096
|
+
ctx.lineTo(91, 48);
|
|
2097
|
+
ctx.stroke();
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// src/engines/little-3d-engine/little-3d-engine.ts
|
|
2102
|
+
init_geometry();
|
|
2103
|
+
init_renderer();
|
|
2104
|
+
init_math();
|
|
2105
|
+
function modelMatrix(t) {
|
|
2106
|
+
const rotation = multiply(
|
|
2107
|
+
rotationZ(t.rotation.z),
|
|
2108
|
+
multiply(rotationY(t.rotation.y), rotationX(t.rotation.x))
|
|
2109
|
+
);
|
|
2110
|
+
return multiply(
|
|
2111
|
+
translation(t.position.x, t.position.y, t.position.z),
|
|
2112
|
+
multiply(rotation, scaleMatrix(t.scale))
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
var Little3dEngine = class {
|
|
2116
|
+
constructor(options = {}) {
|
|
2117
|
+
this.scene = [];
|
|
2118
|
+
this.cssWidth = 0;
|
|
2119
|
+
this.cssHeight = 0;
|
|
2120
|
+
this.ready = false;
|
|
2121
|
+
this.generation = 0;
|
|
2122
|
+
this.rafId = 0;
|
|
2123
|
+
this.running = false;
|
|
2124
|
+
this.camera = new Camera(options.camera);
|
|
2125
|
+
this.light = new Light(options.light);
|
|
2126
|
+
this.backend = options.backend ?? "canvas2d";
|
|
2127
|
+
this.background = options.background;
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* Create the canvas inside `target`, load the selected backend, and start
|
|
2131
|
+
* tracking size. Resolves once the renderer is ready; rejects if the backend
|
|
2132
|
+
* is unavailable. Drawing is a no-op until it resolves.
|
|
2133
|
+
*/
|
|
2134
|
+
async mount(target) {
|
|
2135
|
+
const canvas = document.createElement("canvas");
|
|
2136
|
+
canvas.style.display = "block";
|
|
2137
|
+
canvas.style.width = "100%";
|
|
2138
|
+
canvas.style.height = "100%";
|
|
2139
|
+
target.appendChild(canvas);
|
|
2140
|
+
this.canvas = canvas;
|
|
2141
|
+
this.observer = new ResizeObserver(() => this.resize());
|
|
2142
|
+
this.observer.observe(canvas);
|
|
2143
|
+
this.resize();
|
|
2144
|
+
const generation = this.generation;
|
|
2145
|
+
const dropCanvas = () => {
|
|
2146
|
+
if (this.canvas !== canvas) return;
|
|
2147
|
+
this.observer?.disconnect();
|
|
2148
|
+
this.observer = void 0;
|
|
2149
|
+
canvas.remove();
|
|
2150
|
+
this.canvas = void 0;
|
|
2151
|
+
};
|
|
2152
|
+
try {
|
|
2153
|
+
const renderer = await createRenderer(this.backend, { background: this.background });
|
|
2154
|
+
if (generation !== this.generation) {
|
|
2155
|
+
renderer.destroy();
|
|
2156
|
+
dropCanvas();
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
await renderer.init(canvas);
|
|
2160
|
+
if (generation !== this.generation) {
|
|
2161
|
+
renderer.destroy();
|
|
2162
|
+
dropCanvas();
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
this.renderer = renderer;
|
|
2166
|
+
this.resize();
|
|
2167
|
+
this.ready = true;
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
dropCanvas();
|
|
2170
|
+
throw error;
|
|
990
2171
|
}
|
|
991
|
-
rafId = requestAnimationFrame(frame);
|
|
992
|
-
}
|
|
993
|
-
function halt() {
|
|
994
|
-
if (stopped) return;
|
|
995
|
-
stopped = true;
|
|
996
|
-
if (rafId) cancelAnimationFrame(rafId);
|
|
997
|
-
rafId = 0;
|
|
998
|
-
}
|
|
999
|
-
function setProgress(value) {
|
|
1000
|
-
if (!indeterminate) targetProgress = clamp01(value);
|
|
1001
2172
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
2173
|
+
/** Add a mesh to the scene and return a handle for animating it. */
|
|
2174
|
+
add(mesh, init) {
|
|
2175
|
+
const entry = {
|
|
2176
|
+
mesh,
|
|
2177
|
+
transform: transform(init),
|
|
2178
|
+
transparency: init?.transparency,
|
|
2179
|
+
remove: () => {
|
|
2180
|
+
const i = this.scene.indexOf(entry);
|
|
2181
|
+
if (i >= 0) this.scene.splice(i, 1);
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
this.scene.push(entry);
|
|
2185
|
+
return entry;
|
|
1010
2186
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
2187
|
+
resize() {
|
|
2188
|
+
const canvas = this.canvas;
|
|
2189
|
+
if (!canvas) return;
|
|
2190
|
+
const dpr = window.devicePixelRatio || 1;
|
|
2191
|
+
this.cssWidth = canvas.clientWidth || canvas.parentElement?.clientWidth || 0;
|
|
2192
|
+
this.cssHeight = canvas.clientHeight || canvas.parentElement?.clientHeight || 0;
|
|
2193
|
+
canvas.width = Math.max(1, Math.round(this.cssWidth * dpr));
|
|
2194
|
+
canvas.height = Math.max(1, Math.round(this.cssHeight * dpr));
|
|
2195
|
+
this.renderer?.resize(this.cssWidth, this.cssHeight, dpr);
|
|
1016
2196
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
2197
|
+
/** Draw a single frame from the current scene state. */
|
|
2198
|
+
render() {
|
|
2199
|
+
if (!this.ready || !this.renderer) return;
|
|
2200
|
+
const width = this.cssWidth;
|
|
2201
|
+
const height = this.cssHeight;
|
|
2202
|
+
if (width === 0 || height === 0) return;
|
|
2203
|
+
const items = this.scene.map((entry) => ({
|
|
2204
|
+
mesh: entry.mesh,
|
|
2205
|
+
model: modelMatrix(entry.transform),
|
|
2206
|
+
transparency: entry.transparency
|
|
2207
|
+
}));
|
|
2208
|
+
const eye = this.camera.options.position;
|
|
2209
|
+
this.renderer.render({
|
|
2210
|
+
items: orderRenderItems(items, eye),
|
|
2211
|
+
viewProjection: this.camera.viewProjection(width / height),
|
|
2212
|
+
eye,
|
|
2213
|
+
light: this.light.params,
|
|
2214
|
+
width,
|
|
2215
|
+
height
|
|
2216
|
+
});
|
|
1032
2217
|
}
|
|
1033
|
-
/**
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
2218
|
+
/** Start an internal animation loop that calls {@link render} each frame. */
|
|
2219
|
+
start() {
|
|
2220
|
+
if (this.running) return;
|
|
2221
|
+
this.running = true;
|
|
2222
|
+
const loop = () => {
|
|
2223
|
+
if (!this.running) return;
|
|
2224
|
+
this.render();
|
|
2225
|
+
this.rafId = requestAnimationFrame(loop);
|
|
2226
|
+
};
|
|
2227
|
+
this.rafId = requestAnimationFrame(loop);
|
|
1037
2228
|
}
|
|
1038
|
-
/**
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
return multiply(projection, view);
|
|
2229
|
+
/** Stop the internal animation loop started by {@link start}. */
|
|
2230
|
+
stop() {
|
|
2231
|
+
this.running = false;
|
|
2232
|
+
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
2233
|
+
this.rafId = 0;
|
|
1044
2234
|
}
|
|
1045
|
-
/**
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
2235
|
+
/** Stop animating, release the renderer, and remove the canvas. */
|
|
2236
|
+
destroy() {
|
|
2237
|
+
this.generation++;
|
|
2238
|
+
this.ready = false;
|
|
2239
|
+
this.stop();
|
|
2240
|
+
this.observer?.disconnect();
|
|
2241
|
+
this.observer = void 0;
|
|
2242
|
+
this.renderer?.destroy();
|
|
2243
|
+
this.renderer = void 0;
|
|
2244
|
+
this.canvas?.remove();
|
|
2245
|
+
this.canvas = void 0;
|
|
1051
2246
|
}
|
|
1052
2247
|
};
|
|
1053
2248
|
|
|
1054
|
-
// src/engines/little-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
function transform(init) {
|
|
1060
|
-
return {
|
|
1061
|
-
position: init?.position ?? { x: 0, y: 0, z: 0 },
|
|
1062
|
-
rotation: init?.rotation ?? { x: 0, y: 0, z: 0 },
|
|
1063
|
-
scale: init?.scale ?? 1
|
|
1064
|
-
};
|
|
2249
|
+
// src/engines/little-tween-engine/core/tweens.ts
|
|
2250
|
+
function input(value, overextend) {
|
|
2251
|
+
if (Number.isNaN(value)) return 0;
|
|
2252
|
+
if (overextend) return value;
|
|
2253
|
+
return Math.min(1, Math.max(0, value));
|
|
1065
2254
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
2255
|
+
function linear(value, overextend = false) {
|
|
2256
|
+
return input(value, overextend);
|
|
2257
|
+
}
|
|
2258
|
+
function quadratic(value, overextend = false) {
|
|
2259
|
+
return easeInQuad(value, overextend);
|
|
2260
|
+
}
|
|
2261
|
+
function cubic(value, overextend = false) {
|
|
2262
|
+
return easeInCubic(value, overextend);
|
|
2263
|
+
}
|
|
2264
|
+
function quartic(value, overextend = false) {
|
|
2265
|
+
return easeInQuart(value, overextend);
|
|
2266
|
+
}
|
|
2267
|
+
function quintic(value, overextend = false) {
|
|
2268
|
+
return easeInQuint(value, overextend);
|
|
2269
|
+
}
|
|
2270
|
+
function easeInSine(value, overextend = false) {
|
|
2271
|
+
const x = input(value, overextend);
|
|
2272
|
+
return 1 - Math.cos(x * Math.PI / 2);
|
|
2273
|
+
}
|
|
2274
|
+
function easeOutSine(value, overextend = false) {
|
|
2275
|
+
const x = input(value, overextend);
|
|
2276
|
+
return Math.sin(x * Math.PI / 2);
|
|
2277
|
+
}
|
|
2278
|
+
function easeInOutSine(value, overextend = false) {
|
|
2279
|
+
const x = input(value, overextend);
|
|
2280
|
+
return -(Math.cos(Math.PI * x) - 1) / 2;
|
|
2281
|
+
}
|
|
2282
|
+
function easeInQuad(value, overextend = false) {
|
|
2283
|
+
const x = input(value, overextend);
|
|
2284
|
+
return x * x;
|
|
2285
|
+
}
|
|
2286
|
+
function easeOutQuad(value, overextend = false) {
|
|
2287
|
+
const x = input(value, overextend);
|
|
2288
|
+
return 1 - (1 - x) * (1 - x);
|
|
2289
|
+
}
|
|
2290
|
+
function easeInOutQuad(value, overextend = false) {
|
|
2291
|
+
const x = input(value, overextend);
|
|
2292
|
+
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
|
|
2293
|
+
}
|
|
2294
|
+
function easeInCubic(value, overextend = false) {
|
|
2295
|
+
const x = input(value, overextend);
|
|
2296
|
+
return x * x * x;
|
|
2297
|
+
}
|
|
2298
|
+
function easeOutCubic(value, overextend = false) {
|
|
2299
|
+
const x = input(value, overextend);
|
|
2300
|
+
return 1 - Math.pow(1 - x, 3);
|
|
2301
|
+
}
|
|
2302
|
+
function easeInOutCubic(value, overextend = false) {
|
|
2303
|
+
const x = input(value, overextend);
|
|
2304
|
+
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
2305
|
+
}
|
|
2306
|
+
function easeInQuart(value, overextend = false) {
|
|
2307
|
+
const x = input(value, overextend);
|
|
2308
|
+
return x * x * x * x;
|
|
2309
|
+
}
|
|
2310
|
+
function easeOutQuart(value, overextend = false) {
|
|
2311
|
+
const x = input(value, overextend);
|
|
2312
|
+
return 1 - Math.pow(1 - x, 4);
|
|
2313
|
+
}
|
|
2314
|
+
function easeInOutQuart(value, overextend = false) {
|
|
2315
|
+
const x = input(value, overextend);
|
|
2316
|
+
return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
|
|
2317
|
+
}
|
|
2318
|
+
function easeInQuint(value, overextend = false) {
|
|
2319
|
+
const x = input(value, overextend);
|
|
2320
|
+
return x * x * x * x * x;
|
|
2321
|
+
}
|
|
2322
|
+
function easeOutQuint(value, overextend = false) {
|
|
2323
|
+
const x = input(value, overextend);
|
|
2324
|
+
return 1 - Math.pow(1 - x, 5);
|
|
2325
|
+
}
|
|
2326
|
+
function easeInOutQuint(value, overextend = false) {
|
|
2327
|
+
const x = input(value, overextend);
|
|
2328
|
+
return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
|
|
2329
|
+
}
|
|
2330
|
+
function easeInExpo(value, overextend = false) {
|
|
2331
|
+
const x = input(value, overextend);
|
|
2332
|
+
return x === 0 ? 0 : Math.pow(2, 10 * x - 10);
|
|
2333
|
+
}
|
|
2334
|
+
function easeOutExpo(value, overextend = false) {
|
|
2335
|
+
const x = input(value, overextend);
|
|
2336
|
+
return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
|
|
2337
|
+
}
|
|
2338
|
+
function easeInOutExpo(value, overextend = false) {
|
|
2339
|
+
const x = input(value, overextend);
|
|
2340
|
+
return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2;
|
|
2341
|
+
}
|
|
2342
|
+
function easeInCirc(value, overextend = false) {
|
|
2343
|
+
const x = input(value, overextend);
|
|
2344
|
+
return 1 - Math.sqrt(1 - Math.pow(x, 2));
|
|
2345
|
+
}
|
|
2346
|
+
function easeOutCirc(value, overextend = false) {
|
|
2347
|
+
const x = input(value, overextend);
|
|
2348
|
+
return Math.sqrt(1 - Math.pow(x - 1, 2));
|
|
1094
2349
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
function tetrahedron(size = 1, colors = DEFAULT_COLORS2) {
|
|
1099
|
-
const s = size / 2;
|
|
1100
|
-
const vertices = [
|
|
1101
|
-
{ x: s, y: s, z: s },
|
|
1102
|
-
{ x: s, y: -s, z: -s },
|
|
1103
|
-
{ x: -s, y: s, z: -s },
|
|
1104
|
-
{ x: -s, y: -s, z: s }
|
|
1105
|
-
];
|
|
1106
|
-
const faces = [
|
|
1107
|
-
{ indices: [0, 1, 2], color: colors[0 % colors.length] },
|
|
1108
|
-
{ indices: [0, 3, 1], color: colors[1 % colors.length] },
|
|
1109
|
-
{ indices: [0, 2, 3], color: colors[2 % colors.length] },
|
|
1110
|
-
{ indices: [1, 3, 2], color: colors[3 % colors.length] }
|
|
1111
|
-
];
|
|
1112
|
-
return { vertices, faces };
|
|
2350
|
+
function easeInOutCirc(value, overextend = false) {
|
|
2351
|
+
const x = input(value, overextend);
|
|
2352
|
+
return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2;
|
|
1113
2353
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
"#ec4899",
|
|
1120
|
-
"#f59e0b",
|
|
1121
|
-
"#10b981",
|
|
1122
|
-
"#ef4444",
|
|
1123
|
-
"#06b6d4",
|
|
1124
|
-
"#eab308"
|
|
1125
|
-
];
|
|
1126
|
-
function octahedron(size = 1, colors = DEFAULT_COLORS3) {
|
|
1127
|
-
const r = size / 2;
|
|
1128
|
-
const vertices = [
|
|
1129
|
-
{ x: r, y: 0, z: 0 },
|
|
1130
|
-
{ x: -r, y: 0, z: 0 },
|
|
1131
|
-
{ x: 0, y: r, z: 0 },
|
|
1132
|
-
{ x: 0, y: -r, z: 0 },
|
|
1133
|
-
{ x: 0, y: 0, z: r },
|
|
1134
|
-
{ x: 0, y: 0, z: -r }
|
|
1135
|
-
];
|
|
1136
|
-
const faces = [
|
|
1137
|
-
{ indices: [4, 0, 2], color: colors[0 % colors.length] },
|
|
1138
|
-
{ indices: [4, 2, 1], color: colors[1 % colors.length] },
|
|
1139
|
-
{ indices: [4, 1, 3], color: colors[2 % colors.length] },
|
|
1140
|
-
{ indices: [4, 3, 0], color: colors[3 % colors.length] },
|
|
1141
|
-
{ indices: [5, 2, 0], color: colors[4 % colors.length] },
|
|
1142
|
-
{ indices: [5, 1, 2], color: colors[5 % colors.length] },
|
|
1143
|
-
{ indices: [5, 3, 1], color: colors[6 % colors.length] },
|
|
1144
|
-
{ indices: [5, 0, 3], color: colors[7 % colors.length] }
|
|
1145
|
-
];
|
|
1146
|
-
return { vertices, faces };
|
|
2354
|
+
function easeInBack(value, overextend = false) {
|
|
2355
|
+
const x = input(value, overextend);
|
|
2356
|
+
const c1 = 1.70158;
|
|
2357
|
+
const c3 = c1 + 1;
|
|
2358
|
+
return c3 * x * x * x - c1 * x * x;
|
|
1147
2359
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const vertices = [
|
|
1154
|
-
{ x: -h, y: -h, z: h },
|
|
1155
|
-
{ x: h, y: -h, z: h },
|
|
1156
|
-
{ x: h, y: -h, z: -h },
|
|
1157
|
-
{ x: -h, y: -h, z: -h },
|
|
1158
|
-
{ x: 0, y: h, z: 0 }
|
|
1159
|
-
];
|
|
1160
|
-
const faces = [
|
|
1161
|
-
{ indices: [0, 3, 2, 1], color: colors[0 % colors.length] },
|
|
1162
|
-
{ indices: [4, 0, 1], color: colors[1 % colors.length] },
|
|
1163
|
-
{ indices: [4, 1, 2], color: colors[2 % colors.length] },
|
|
1164
|
-
{ indices: [4, 2, 3], color: colors[3 % colors.length] },
|
|
1165
|
-
{ indices: [4, 3, 0], color: colors[4 % colors.length] }
|
|
1166
|
-
];
|
|
1167
|
-
return { vertices, faces };
|
|
2360
|
+
function easeOutBack(value, overextend = false) {
|
|
2361
|
+
const x = input(value, overextend);
|
|
2362
|
+
const c1 = 1.70158;
|
|
2363
|
+
const c3 = c1 + 1;
|
|
2364
|
+
return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
|
|
1168
2365
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
const
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
2366
|
+
function easeInOutBack(value, overextend = false) {
|
|
2367
|
+
const x = input(value, overextend);
|
|
2368
|
+
const c1 = 1.70158;
|
|
2369
|
+
const c2 = c1 * 1.525;
|
|
2370
|
+
return x < 0.5 ? Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2) / 2 : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
|
|
2371
|
+
}
|
|
2372
|
+
function easeInElastic(value, overextend = false) {
|
|
2373
|
+
const x = input(value, overextend);
|
|
2374
|
+
const c4 = 2 * Math.PI / 3;
|
|
2375
|
+
return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4);
|
|
2376
|
+
}
|
|
2377
|
+
function easeOutElastic(value, overextend = false) {
|
|
2378
|
+
const x = input(value, overextend);
|
|
2379
|
+
const c4 = 2 * Math.PI / 3;
|
|
2380
|
+
return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
|
|
2381
|
+
}
|
|
2382
|
+
function easeInOutElastic(value, overextend = false) {
|
|
2383
|
+
const x = input(value, overextend);
|
|
2384
|
+
const c5 = 2 * Math.PI / 4.5;
|
|
2385
|
+
return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 : Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5) / 2 + 1;
|
|
2386
|
+
}
|
|
2387
|
+
function easeInBounce(value, overextend = false) {
|
|
2388
|
+
const x = input(value, overextend);
|
|
2389
|
+
return 1 - easeOutBounce(1 - x, true);
|
|
2390
|
+
}
|
|
2391
|
+
function easeOutBounce(value, overextend = false) {
|
|
2392
|
+
let x = input(value, overextend);
|
|
2393
|
+
const n1 = 7.5625;
|
|
2394
|
+
const d1 = 2.75;
|
|
2395
|
+
if (x < 1 / d1) {
|
|
2396
|
+
return n1 * x * x;
|
|
1195
2397
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
faces.push({
|
|
1200
|
-
indices: [ring(i, j), ring(i, j1), ring(i + 1, j1), ring(i + 1, j)],
|
|
1201
|
-
color: color()
|
|
1202
|
-
});
|
|
1203
|
-
}
|
|
2398
|
+
if (x < 2 / d1) {
|
|
2399
|
+
x -= 1.5 / d1;
|
|
2400
|
+
return n1 * x * x + 0.75;
|
|
1204
2401
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
color: color()
|
|
1209
|
-
});
|
|
2402
|
+
if (x < 2.5 / d1) {
|
|
2403
|
+
x -= 2.25 / d1;
|
|
2404
|
+
return n1 * x * x + 0.9375;
|
|
1210
2405
|
}
|
|
1211
|
-
|
|
2406
|
+
x -= 2.625 / d1;
|
|
2407
|
+
return n1 * x * x + 0.984375;
|
|
1212
2408
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
var DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1217
|
-
var T = (1 + Math.sqrt(5)) / 2;
|
|
1218
|
-
var SEED_VERTICES = [
|
|
1219
|
-
{ x: -1, y: T, z: 0 },
|
|
1220
|
-
{ x: 1, y: T, z: 0 },
|
|
1221
|
-
{ x: -1, y: -T, z: 0 },
|
|
1222
|
-
{ x: 1, y: -T, z: 0 },
|
|
1223
|
-
{ x: 0, y: -1, z: T },
|
|
1224
|
-
{ x: 0, y: 1, z: T },
|
|
1225
|
-
{ x: 0, y: -1, z: -T },
|
|
1226
|
-
{ x: 0, y: 1, z: -T },
|
|
1227
|
-
{ x: T, y: 0, z: -1 },
|
|
1228
|
-
{ x: T, y: 0, z: 1 },
|
|
1229
|
-
{ x: -T, y: 0, z: -1 },
|
|
1230
|
-
{ x: -T, y: 0, z: 1 }
|
|
1231
|
-
];
|
|
1232
|
-
var SEED_FACES = [
|
|
1233
|
-
[0, 11, 5],
|
|
1234
|
-
[0, 5, 1],
|
|
1235
|
-
[0, 1, 7],
|
|
1236
|
-
[0, 7, 10],
|
|
1237
|
-
[0, 10, 11],
|
|
1238
|
-
[1, 5, 9],
|
|
1239
|
-
[5, 11, 4],
|
|
1240
|
-
[11, 10, 2],
|
|
1241
|
-
[10, 7, 6],
|
|
1242
|
-
[7, 1, 8],
|
|
1243
|
-
[3, 9, 4],
|
|
1244
|
-
[3, 4, 2],
|
|
1245
|
-
[3, 2, 6],
|
|
1246
|
-
[3, 6, 8],
|
|
1247
|
-
[3, 8, 9],
|
|
1248
|
-
[4, 9, 5],
|
|
1249
|
-
[2, 4, 11],
|
|
1250
|
-
[6, 2, 10],
|
|
1251
|
-
[8, 6, 7],
|
|
1252
|
-
[9, 8, 1]
|
|
1253
|
-
];
|
|
1254
|
-
function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
|
|
1255
|
-
return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
|
|
2409
|
+
function easeInOutBounce(value, overextend = false) {
|
|
2410
|
+
const x = input(value, overextend);
|
|
2411
|
+
return x < 0.5 ? (1 - easeOutBounce(1 - 2 * x, true)) / 2 : (1 + easeOutBounce(2 * x - 1, true)) / 2;
|
|
1256
2412
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
2413
|
+
var easeTypes = {
|
|
2414
|
+
linear,
|
|
2415
|
+
quadratic,
|
|
2416
|
+
cubic,
|
|
2417
|
+
quartic,
|
|
2418
|
+
quintic,
|
|
2419
|
+
easeInSine,
|
|
2420
|
+
easeOutSine,
|
|
2421
|
+
easeInOutSine,
|
|
2422
|
+
easeInQuad,
|
|
2423
|
+
easeOutQuad,
|
|
2424
|
+
easeInOutQuad,
|
|
2425
|
+
easeInCubic,
|
|
2426
|
+
easeOutCubic,
|
|
2427
|
+
easeInOutCubic,
|
|
2428
|
+
easeInQuart,
|
|
2429
|
+
easeOutQuart,
|
|
2430
|
+
easeInOutQuart,
|
|
2431
|
+
easeInQuint,
|
|
2432
|
+
easeOutQuint,
|
|
2433
|
+
easeInOutQuint,
|
|
2434
|
+
easeInExpo,
|
|
2435
|
+
easeOutExpo,
|
|
2436
|
+
easeInOutExpo,
|
|
2437
|
+
easeInCirc,
|
|
2438
|
+
easeOutCirc,
|
|
2439
|
+
easeInOutCirc,
|
|
2440
|
+
easeInBack,
|
|
2441
|
+
easeOutBack,
|
|
2442
|
+
easeInOutBack,
|
|
2443
|
+
easeInElastic,
|
|
2444
|
+
easeOutElastic,
|
|
2445
|
+
easeInOutElastic,
|
|
2446
|
+
easeInBounce,
|
|
2447
|
+
easeOutBounce,
|
|
2448
|
+
easeInOutBounce
|
|
2449
|
+
};
|
|
2450
|
+
function ease(type, value, overextend = false) {
|
|
2451
|
+
return easeTypes[type](value, overextend);
|
|
1281
2452
|
}
|
|
1282
2453
|
|
|
1283
|
-
// src/
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
2454
|
+
// src/progress-animation.ts
|
|
2455
|
+
function resolveOptions(options = {}) {
|
|
2456
|
+
return {
|
|
2457
|
+
popDurationMs: options.popDurationMs ?? 500,
|
|
2458
|
+
overextend: options.overextend ?? 0.2,
|
|
2459
|
+
startSnapRatio: options.startSnapRatio ?? 0.2,
|
|
2460
|
+
loadingText: options.loadingText === void 0 ? "loading" : options.loadingText,
|
|
2461
|
+
doneText: options.doneText ?? "done",
|
|
2462
|
+
doneFadeDurationMs: options.doneFadeDurationMs ?? 2e3,
|
|
2463
|
+
removeOnComplete: options.removeOnComplete ?? false
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
function popPhaseT(now, phaseStart, durationMs) {
|
|
2467
|
+
if (durationMs <= 0) return 1;
|
|
2468
|
+
return Math.min(1, Math.max(0, (now - phaseStart) / durationMs));
|
|
2469
|
+
}
|
|
2470
|
+
var ProgressAnimation = class {
|
|
2471
|
+
constructor(options = {}) {
|
|
2472
|
+
this.phase = "idle";
|
|
2473
|
+
this.phaseStart = 0;
|
|
2474
|
+
this.activeProgress = 0;
|
|
2475
|
+
this.popTarget = 0;
|
|
2476
|
+
this.doneFadeStart = 0;
|
|
2477
|
+
this.options = resolveOptions(options);
|
|
2478
|
+
}
|
|
2479
|
+
/** Begin the intro pop. Ignored unless idle. */
|
|
2480
|
+
enter(now) {
|
|
2481
|
+
if (this.phase !== "idle") return;
|
|
2482
|
+
this.phase = "startPop";
|
|
2483
|
+
this.phaseStart = now;
|
|
2484
|
+
this.activeProgress = 0;
|
|
2485
|
+
this.popTarget = 0;
|
|
2486
|
+
}
|
|
2487
|
+
/** Begin the outro pop. Ignored unless mid-intro or active. */
|
|
2488
|
+
exit(now) {
|
|
2489
|
+
if (this.phase !== "startPop" && this.phase !== "active") return;
|
|
2490
|
+
this.phase = "endPop";
|
|
2491
|
+
this.phaseStart = now;
|
|
2492
|
+
}
|
|
2493
|
+
isFinished() {
|
|
2494
|
+
return this.phase === "finished";
|
|
2495
|
+
}
|
|
2496
|
+
update(now, progress, targetProgress) {
|
|
2497
|
+
const {
|
|
2498
|
+
popDurationMs,
|
|
2499
|
+
overextend,
|
|
2500
|
+
startSnapRatio,
|
|
2501
|
+
loadingText,
|
|
2502
|
+
doneText,
|
|
2503
|
+
doneFadeDurationMs,
|
|
2504
|
+
removeOnComplete
|
|
2505
|
+
} = this.options;
|
|
2506
|
+
const goal = targetProgress ?? progress;
|
|
2507
|
+
if (this.phase === "startPop" || this.phase === "active") {
|
|
2508
|
+
this.activeProgress = progress;
|
|
2509
|
+
if (this.phase === "startPop") this.popTarget = Math.max(this.popTarget, goal, progress);
|
|
2510
|
+
}
|
|
2511
|
+
let scale2 = 0;
|
|
2512
|
+
let text = null;
|
|
2513
|
+
let textOpacity = 0;
|
|
2514
|
+
let hidden = false;
|
|
2515
|
+
if (this.phase === "startPop") {
|
|
2516
|
+
const t = popPhaseT(now, this.phaseStart, popDurationMs);
|
|
2517
|
+
const peak = this.popTarget * (1 + overextend);
|
|
2518
|
+
if (t < startSnapRatio) {
|
|
2519
|
+
const snapT = startSnapRatio > 0 ? t / startSnapRatio : 1;
|
|
2520
|
+
scale2 = peak * easeOutExpo(snapT);
|
|
2521
|
+
} else {
|
|
2522
|
+
const settleT = startSnapRatio < 1 ? (t - startSnapRatio) / (1 - startSnapRatio) : 1;
|
|
2523
|
+
scale2 = peak + (this.activeProgress - peak) * easeOutCubic(settleT);
|
|
2524
|
+
}
|
|
2525
|
+
if (t >= 1) this.phase = "active";
|
|
2526
|
+
} else if (this.phase === "active") {
|
|
2527
|
+
scale2 = this.activeProgress;
|
|
2528
|
+
} else if (this.phase === "endPop") {
|
|
2529
|
+
const t = popPhaseT(now, this.phaseStart, popDurationMs);
|
|
2530
|
+
const peak = 1 + overextend;
|
|
2531
|
+
if (t < 0.5) {
|
|
2532
|
+
scale2 = 1 + (peak - 1) * easeOutQuad(t * 2);
|
|
2533
|
+
} else {
|
|
2534
|
+
scale2 = peak * (1 - easeInQuad((t - 0.5) * 2));
|
|
2535
|
+
}
|
|
2536
|
+
if (t >= 1) {
|
|
2537
|
+
this.phase = "done";
|
|
2538
|
+
this.doneFadeStart = now;
|
|
2539
|
+
scale2 = 0;
|
|
1310
2540
|
}
|
|
1311
2541
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2542
|
+
if (this.phase === "startPop" || this.phase === "active") {
|
|
2543
|
+
if (loadingText !== false) {
|
|
2544
|
+
text = loadingText;
|
|
2545
|
+
textOpacity = 0.65;
|
|
2546
|
+
}
|
|
2547
|
+
} else if (this.phase === "endPop") {
|
|
2548
|
+
text = doneText;
|
|
2549
|
+
textOpacity = 0.65;
|
|
2550
|
+
} else if (this.phase === "done") {
|
|
2551
|
+
const fadeT = popPhaseT(now, this.doneFadeStart, doneFadeDurationMs);
|
|
2552
|
+
if (fadeT >= 1) {
|
|
2553
|
+
if (removeOnComplete) hidden = true;
|
|
2554
|
+
this.phase = "finished";
|
|
2555
|
+
} else {
|
|
2556
|
+
text = doneText;
|
|
2557
|
+
textOpacity = 0.65 * (1 - fadeT);
|
|
1319
2558
|
}
|
|
1320
2559
|
}
|
|
2560
|
+
return { scale: scale2, text, textOpacity, hidden };
|
|
1321
2561
|
}
|
|
1322
|
-
|
|
1323
|
-
}
|
|
2562
|
+
};
|
|
1324
2563
|
|
|
1325
|
-
// src/
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
2564
|
+
// src/animations/spin.ts
|
|
2565
|
+
var LABEL_STYLE = [
|
|
2566
|
+
"position:absolute",
|
|
2567
|
+
"inset:0",
|
|
2568
|
+
"display:flex",
|
|
2569
|
+
"align-items:center",
|
|
2570
|
+
"justify-content:center",
|
|
2571
|
+
"pointer-events:none",
|
|
2572
|
+
"font:600 1.1rem/1.2 system-ui,sans-serif",
|
|
2573
|
+
"letter-spacing:0.06em",
|
|
2574
|
+
"text-transform:lowercase",
|
|
2575
|
+
"color:rgba(255,255,255,0.65)",
|
|
2576
|
+
"z-index:1"
|
|
2577
|
+
].join(";");
|
|
2578
|
+
function resolveMesh(shape) {
|
|
2579
|
+
if (!shape) return cube();
|
|
2580
|
+
return typeof shape === "function" ? shape() : shape;
|
|
1338
2581
|
}
|
|
1339
|
-
|
|
2582
|
+
function applyColor(mesh, color) {
|
|
2583
|
+
if (color === void 0 || Array.isArray(color) && color.length === 0) return mesh;
|
|
2584
|
+
const pick = Array.isArray(color) ? (i) => color[i % color.length] : () => color;
|
|
2585
|
+
return { vertices: mesh.vertices, faces: mesh.faces.map((f, i) => ({ ...f, color: pick(i) })) };
|
|
2586
|
+
}
|
|
2587
|
+
var SpinAnimation = class {
|
|
1340
2588
|
constructor(options = {}) {
|
|
1341
|
-
this.
|
|
1342
|
-
this.
|
|
1343
|
-
this.
|
|
1344
|
-
this.
|
|
1345
|
-
this.
|
|
1346
|
-
this.
|
|
1347
|
-
this.
|
|
1348
|
-
this.camera = new Camera(options.camera);
|
|
1349
|
-
this.light = new Light(options.light);
|
|
1350
|
-
this.backend = options.backend ?? "canvas2d";
|
|
1351
|
-
this.background = options.background;
|
|
2589
|
+
this.exited = false;
|
|
2590
|
+
this.mesh = applyColor(resolveMesh(options.shape), options.color);
|
|
2591
|
+
this.spinX = options.spinX ?? 7e-4;
|
|
2592
|
+
this.spinY = options.spinY ?? 11e-4;
|
|
2593
|
+
this.backend = options.backend;
|
|
2594
|
+
this.transparency = options.transparency;
|
|
2595
|
+
this.progress = options.progressAnimation ? new ProgressAnimation(options.progressAnimation) : void 0;
|
|
1352
2596
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
this.
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
this.observer?.disconnect();
|
|
1372
|
-
this.observer = void 0;
|
|
1373
|
-
canvas.remove();
|
|
1374
|
-
this.canvas = void 0;
|
|
1375
|
-
};
|
|
1376
|
-
try {
|
|
1377
|
-
const renderer = await createRenderer(this.backend, { background: this.background });
|
|
1378
|
-
if (generation !== this.generation) {
|
|
1379
|
-
renderer.destroy();
|
|
1380
|
-
dropCanvas();
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
|
-
await renderer.init(canvas);
|
|
1384
|
-
if (generation !== this.generation) {
|
|
1385
|
-
renderer.destroy();
|
|
1386
|
-
dropCanvas();
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
this.renderer = renderer;
|
|
1390
|
-
this.resize();
|
|
1391
|
-
this.ready = true;
|
|
1392
|
-
} catch (error) {
|
|
1393
|
-
dropCanvas();
|
|
1394
|
-
throw error;
|
|
2597
|
+
mount(target) {
|
|
2598
|
+
target.style.position = "relative";
|
|
2599
|
+
const engine = new Little3dEngine({
|
|
2600
|
+
backend: this.backend,
|
|
2601
|
+
camera: { position: { x: 0, y: 0, z: 2.8 } }
|
|
2602
|
+
});
|
|
2603
|
+
this.handle = engine.add(this.mesh, { transparency: this.transparency });
|
|
2604
|
+
this.engine = engine;
|
|
2605
|
+
engine.mount(target).catch((error) => {
|
|
2606
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
2607
|
+
});
|
|
2608
|
+
if (this.progress) {
|
|
2609
|
+
const label = document.createElement("div");
|
|
2610
|
+
label.style.cssText = LABEL_STYLE;
|
|
2611
|
+
label.setAttribute("aria-hidden", "true");
|
|
2612
|
+
label.hidden = true;
|
|
2613
|
+
target.appendChild(label);
|
|
2614
|
+
this.label = label;
|
|
1395
2615
|
}
|
|
1396
2616
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const entry = {
|
|
1400
|
-
mesh,
|
|
1401
|
-
transform: transform(init),
|
|
1402
|
-
transparency: init?.transparency,
|
|
1403
|
-
remove: () => {
|
|
1404
|
-
const i = this.scene.indexOf(entry);
|
|
1405
|
-
if (i >= 0) this.scene.splice(i, 1);
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1408
|
-
this.scene.push(entry);
|
|
1409
|
-
return entry;
|
|
1410
|
-
}
|
|
1411
|
-
resize() {
|
|
1412
|
-
const canvas = this.canvas;
|
|
1413
|
-
if (!canvas) return;
|
|
1414
|
-
const dpr = window.devicePixelRatio || 1;
|
|
1415
|
-
this.cssWidth = canvas.clientWidth || canvas.parentElement?.clientWidth || 0;
|
|
1416
|
-
this.cssHeight = canvas.clientHeight || canvas.parentElement?.clientHeight || 0;
|
|
1417
|
-
canvas.width = Math.max(1, Math.round(this.cssWidth * dpr));
|
|
1418
|
-
canvas.height = Math.max(1, Math.round(this.cssHeight * dpr));
|
|
1419
|
-
this.renderer?.resize(this.cssWidth, this.cssHeight, dpr);
|
|
2617
|
+
enter(now) {
|
|
2618
|
+
this.progress?.enter(now);
|
|
1420
2619
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const width = this.cssWidth;
|
|
1425
|
-
const height = this.cssHeight;
|
|
1426
|
-
if (width === 0 || height === 0) return;
|
|
1427
|
-
const items = this.scene.map((entry) => ({
|
|
1428
|
-
mesh: entry.mesh,
|
|
1429
|
-
model: modelMatrix(entry.transform),
|
|
1430
|
-
transparency: entry.transparency
|
|
1431
|
-
}));
|
|
1432
|
-
const eye = this.camera.options.position;
|
|
1433
|
-
this.renderer.render({
|
|
1434
|
-
items: orderRenderItems(items, eye),
|
|
1435
|
-
viewProjection: this.camera.viewProjection(width / height),
|
|
1436
|
-
eye,
|
|
1437
|
-
light: this.light.params,
|
|
1438
|
-
width,
|
|
1439
|
-
height
|
|
1440
|
-
});
|
|
2620
|
+
exit(now) {
|
|
2621
|
+
this.exited = true;
|
|
2622
|
+
this.progress?.exit(now);
|
|
1441
2623
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (this.running) return;
|
|
1445
|
-
this.running = true;
|
|
1446
|
-
const loop = () => {
|
|
1447
|
-
if (!this.running) return;
|
|
1448
|
-
this.render();
|
|
1449
|
-
this.rafId = requestAnimationFrame(loop);
|
|
1450
|
-
};
|
|
1451
|
-
this.rafId = requestAnimationFrame(loop);
|
|
2624
|
+
isFinished() {
|
|
2625
|
+
return this.progress ? this.progress.isFinished() : this.exited;
|
|
1452
2626
|
}
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
2627
|
+
render(now, frame) {
|
|
2628
|
+
if (!this.engine || !this.handle) return;
|
|
2629
|
+
const rotation = this.handle.transform.rotation;
|
|
2630
|
+
rotation.x = now * this.spinX;
|
|
2631
|
+
rotation.y = now * this.spinY;
|
|
2632
|
+
if (this.progress) {
|
|
2633
|
+
const visual = this.progress.update(now, frame.progress, frame.targetProgress);
|
|
2634
|
+
this.handle.transform.scale = visual.hidden ? 0 : visual.scale;
|
|
2635
|
+
this.applyLabel(visual);
|
|
2636
|
+
} else {
|
|
2637
|
+
this.handle.transform.scale = 1;
|
|
2638
|
+
}
|
|
2639
|
+
this.engine.render();
|
|
1458
2640
|
}
|
|
1459
|
-
/** Stop animating, release the renderer, and remove the canvas. */
|
|
1460
2641
|
destroy() {
|
|
1461
|
-
this.
|
|
1462
|
-
this.
|
|
1463
|
-
this.
|
|
1464
|
-
this.
|
|
1465
|
-
this.
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
this.
|
|
1469
|
-
|
|
2642
|
+
this.label?.remove();
|
|
2643
|
+
this.label = void 0;
|
|
2644
|
+
this.engine?.destroy();
|
|
2645
|
+
this.engine = void 0;
|
|
2646
|
+
this.handle = void 0;
|
|
2647
|
+
}
|
|
2648
|
+
applyLabel(visual) {
|
|
2649
|
+
if (!this.label) return;
|
|
2650
|
+
if (visual.hidden || visual.text == null) {
|
|
2651
|
+
this.label.hidden = true;
|
|
2652
|
+
this.label.textContent = "";
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
this.label.hidden = false;
|
|
2656
|
+
this.label.textContent = visual.text;
|
|
2657
|
+
this.label.style.opacity = String(visual.textOpacity);
|
|
1470
2658
|
}
|
|
1471
2659
|
};
|
|
1472
2660
|
|
|
1473
|
-
// src/
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
return easeInQuint(value, overextend);
|
|
1493
|
-
}
|
|
1494
|
-
function easeInSine(value, overextend = false) {
|
|
1495
|
-
const x = input(value, overextend);
|
|
1496
|
-
return 1 - Math.cos(x * Math.PI / 2);
|
|
1497
|
-
}
|
|
1498
|
-
function easeOutSine(value, overextend = false) {
|
|
1499
|
-
const x = input(value, overextend);
|
|
1500
|
-
return Math.sin(x * Math.PI / 2);
|
|
1501
|
-
}
|
|
1502
|
-
function easeInOutSine(value, overextend = false) {
|
|
1503
|
-
const x = input(value, overextend);
|
|
1504
|
-
return -(Math.cos(Math.PI * x) - 1) / 2;
|
|
2661
|
+
// src/animation-label.ts
|
|
2662
|
+
var LABEL_STYLE2 = [
|
|
2663
|
+
"position:absolute",
|
|
2664
|
+
"inset:0",
|
|
2665
|
+
"display:flex",
|
|
2666
|
+
"align-items:center",
|
|
2667
|
+
"justify-content:center",
|
|
2668
|
+
"pointer-events:none",
|
|
2669
|
+
"font:700 1.6rem/1 system-ui,sans-serif",
|
|
2670
|
+
"letter-spacing:0.02em",
|
|
2671
|
+
"color:rgba(255,255,255,0.9)",
|
|
2672
|
+
"text-shadow:0 1px 10px rgba(0,0,0,0.6)",
|
|
2673
|
+
"z-index:1"
|
|
2674
|
+
].join(";");
|
|
2675
|
+
function animationLabelOpacity(now, enterAt, introDurationMs, exitAt, outroDurationMs) {
|
|
2676
|
+
if (enterAt === Infinity) return 0;
|
|
2677
|
+
const intro = introDurationMs <= 0 ? 1 : Math.max(0, Math.min(1, (now - enterAt) / introDurationMs));
|
|
2678
|
+
const outro = exitAt === Infinity ? 1 : outroDurationMs <= 0 ? 0 : Math.max(0, Math.min(1, 1 - (now - exitAt) / outroDurationMs));
|
|
2679
|
+
return Math.min(intro, outro);
|
|
1505
2680
|
}
|
|
1506
|
-
function
|
|
1507
|
-
|
|
1508
|
-
|
|
2681
|
+
function mountAnimationLabel(target, content) {
|
|
2682
|
+
var _a;
|
|
2683
|
+
const container = document.createElement("div");
|
|
2684
|
+
container.style.cssText = LABEL_STYLE2;
|
|
2685
|
+
container.setAttribute("role", "status");
|
|
2686
|
+
if (typeof content === "string") container.textContent = content;
|
|
2687
|
+
else if (content) {
|
|
2688
|
+
(_a = content.style).pointerEvents || (_a.pointerEvents = "auto");
|
|
2689
|
+
container.appendChild(content);
|
|
2690
|
+
}
|
|
2691
|
+
target.appendChild(container);
|
|
2692
|
+
return {
|
|
2693
|
+
container,
|
|
2694
|
+
setText(value) {
|
|
2695
|
+
if (typeof content !== "object") container.textContent = value;
|
|
2696
|
+
},
|
|
2697
|
+
setOpacity(value) {
|
|
2698
|
+
container.style.opacity = String(value);
|
|
2699
|
+
}
|
|
2700
|
+
};
|
|
1509
2701
|
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2702
|
+
|
|
2703
|
+
// src/animations/object-motion.ts
|
|
2704
|
+
init_math();
|
|
2705
|
+
|
|
2706
|
+
// src/motion/transitions.ts
|
|
2707
|
+
var DEFAULT_DISTANCE = 3.5;
|
|
2708
|
+
function add(a, b) {
|
|
2709
|
+
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
|
1513
2710
|
}
|
|
1514
|
-
function
|
|
1515
|
-
|
|
1516
|
-
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
|
|
2711
|
+
function scaleVector(v, factor) {
|
|
2712
|
+
return { x: v.x * factor, y: v.y * factor, z: v.z * factor };
|
|
1517
2713
|
}
|
|
1518
|
-
function
|
|
1519
|
-
|
|
1520
|
-
return x * x * x;
|
|
2714
|
+
function vectorLength(v) {
|
|
2715
|
+
return Math.hypot(v.x, v.y, v.z);
|
|
1521
2716
|
}
|
|
1522
|
-
function
|
|
1523
|
-
const
|
|
1524
|
-
|
|
2717
|
+
function normalizeVector(v) {
|
|
2718
|
+
const length = vectorLength(v);
|
|
2719
|
+
if (length < 1e-6) return { x: 1, y: 0, z: 0 };
|
|
2720
|
+
return scaleVector(v, 1 / length);
|
|
1525
2721
|
}
|
|
1526
|
-
function
|
|
1527
|
-
|
|
1528
|
-
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
2722
|
+
function resolveDirection(input2, fallback) {
|
|
2723
|
+
return normalizeVector(fallback ?? input2.direction ?? input2.velocity ?? { x: 1, y: 0, z: 0 });
|
|
1529
2724
|
}
|
|
1530
|
-
function
|
|
1531
|
-
const
|
|
1532
|
-
|
|
2725
|
+
function easeOutBack2(delta) {
|
|
2726
|
+
const c = 1.70158;
|
|
2727
|
+
const u = delta - 1;
|
|
2728
|
+
return 1 + (c + 1) * u * u * u + c * u * u;
|
|
1533
2729
|
}
|
|
1534
|
-
function
|
|
1535
|
-
const
|
|
1536
|
-
|
|
2730
|
+
function joinVelocity(input2, options, durationMs) {
|
|
2731
|
+
const inputSpeed = input2.velocity ? vectorLength(input2.velocity) : 0;
|
|
2732
|
+
if (input2.velocity && inputSpeed > 1e-6 && !options.direction) {
|
|
2733
|
+
return input2.velocity;
|
|
2734
|
+
}
|
|
2735
|
+
const distance = options.distance ?? DEFAULT_DISTANCE;
|
|
2736
|
+
return scaleVector(resolveDirection(input2, options.direction), distance / durationMs);
|
|
1537
2737
|
}
|
|
1538
|
-
function
|
|
1539
|
-
|
|
1540
|
-
|
|
2738
|
+
function enterFromObjectDirection(options = {}) {
|
|
2739
|
+
return (input2) => {
|
|
2740
|
+
const durationMs = Math.max(1, input2.durationMs);
|
|
2741
|
+
const velocity = joinVelocity(input2, options, durationMs);
|
|
2742
|
+
const remaining = durationMs - input2.elapsedMs;
|
|
2743
|
+
return { position: add(input2.position, scaleVector(velocity, -remaining)) };
|
|
2744
|
+
};
|
|
1541
2745
|
}
|
|
1542
|
-
function
|
|
1543
|
-
|
|
1544
|
-
|
|
2746
|
+
function leaveInObjectDirection(options = {}) {
|
|
2747
|
+
return (input2) => {
|
|
2748
|
+
const durationMs = Math.max(1, input2.durationMs);
|
|
2749
|
+
const velocity = joinVelocity(input2, options, durationMs);
|
|
2750
|
+
return { position: add(input2.position, scaleVector(velocity, input2.elapsedMs)) };
|
|
2751
|
+
};
|
|
1545
2752
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
return 1 - Math.pow(1 - x, 5);
|
|
2753
|
+
function grow() {
|
|
2754
|
+
return (input2) => ({ size: (input2.size ?? 1) * easeOutBack2(input2.delta) });
|
|
1549
2755
|
}
|
|
1550
|
-
function
|
|
1551
|
-
|
|
1552
|
-
return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
|
|
2756
|
+
function shrink() {
|
|
2757
|
+
return (input2) => ({ size: (input2.size ?? 1) * (1 - input2.delta * input2.delta) });
|
|
1553
2758
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
2759
|
+
|
|
2760
|
+
// src/animations/object-motion.ts
|
|
2761
|
+
var WORLD_UP = { x: 0, y: 1, z: 0 };
|
|
2762
|
+
var DEFAULT_INTRO_MS = 2100;
|
|
2763
|
+
var DEFAULT_OUTRO_MS = 2100;
|
|
2764
|
+
var BANK_GAIN = 26;
|
|
2765
|
+
var BANK_LIMIT = 0.7;
|
|
2766
|
+
var BANK_SMOOTH = 0.12;
|
|
2767
|
+
var SAMPLE_MS = 8;
|
|
2768
|
+
var FACE_FORWARD = {
|
|
2769
|
+
"+x": (v) => v,
|
|
2770
|
+
"-x": (v) => ({ x: -v.x, y: v.y, z: -v.z }),
|
|
2771
|
+
"+z": (v) => ({ x: v.z, y: v.y, z: -v.x }),
|
|
2772
|
+
"-z": (v) => ({ x: -v.z, y: v.y, z: v.x }),
|
|
2773
|
+
"+y": (v) => ({ x: v.y, y: -v.x, z: v.z }),
|
|
2774
|
+
"-y": (v) => ({ x: -v.y, y: v.x, z: v.z })
|
|
2775
|
+
};
|
|
2776
|
+
function add2(a, b) {
|
|
2777
|
+
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
|
1557
2778
|
}
|
|
1558
|
-
function
|
|
1559
|
-
|
|
1560
|
-
return x === 1 ? 1 : 1 - Math.pow(2, -10 * x);
|
|
2779
|
+
function resolveMesh2(mesh) {
|
|
2780
|
+
return typeof mesh === "function" ? mesh() : mesh;
|
|
1561
2781
|
}
|
|
1562
|
-
function
|
|
1563
|
-
|
|
1564
|
-
return
|
|
2782
|
+
function applyColor2(mesh, color) {
|
|
2783
|
+
if (color === void 0) return mesh;
|
|
2784
|
+
return { vertices: mesh.vertices, faces: mesh.faces.map((face) => ({ ...face, color })) };
|
|
1565
2785
|
}
|
|
1566
|
-
function
|
|
1567
|
-
|
|
1568
|
-
|
|
2786
|
+
function faceForward(mesh, facing) {
|
|
2787
|
+
if (facing === "+x") return mesh;
|
|
2788
|
+
const turn = FACE_FORWARD[facing];
|
|
2789
|
+
return { vertices: mesh.vertices.map(turn), faces: mesh.faces };
|
|
1569
2790
|
}
|
|
1570
|
-
function
|
|
1571
|
-
|
|
1572
|
-
|
|
2791
|
+
function centerAndScaleMesh(mesh, targetSize) {
|
|
2792
|
+
let minX = Infinity;
|
|
2793
|
+
let minY = Infinity;
|
|
2794
|
+
let minZ = Infinity;
|
|
2795
|
+
let maxX = -Infinity;
|
|
2796
|
+
let maxY = -Infinity;
|
|
2797
|
+
let maxZ = -Infinity;
|
|
2798
|
+
for (const vertex of mesh.vertices) {
|
|
2799
|
+
minX = Math.min(minX, vertex.x);
|
|
2800
|
+
minY = Math.min(minY, vertex.y);
|
|
2801
|
+
minZ = Math.min(minZ, vertex.z);
|
|
2802
|
+
maxX = Math.max(maxX, vertex.x);
|
|
2803
|
+
maxY = Math.max(maxY, vertex.y);
|
|
2804
|
+
maxZ = Math.max(maxZ, vertex.z);
|
|
2805
|
+
}
|
|
2806
|
+
const centerX = (minX + maxX) / 2;
|
|
2807
|
+
const centerY = (minY + maxY) / 2;
|
|
2808
|
+
const centerZ = (minZ + maxZ) / 2;
|
|
2809
|
+
const extent = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1;
|
|
2810
|
+
const factor = targetSize / extent;
|
|
2811
|
+
return {
|
|
2812
|
+
vertices: mesh.vertices.map((vertex) => ({
|
|
2813
|
+
x: (vertex.x - centerX) * factor,
|
|
2814
|
+
y: (vertex.y - centerY) * factor,
|
|
2815
|
+
z: (vertex.z - centerZ) * factor
|
|
2816
|
+
})),
|
|
2817
|
+
faces: mesh.faces
|
|
2818
|
+
};
|
|
1573
2819
|
}
|
|
1574
|
-
function
|
|
1575
|
-
const
|
|
1576
|
-
|
|
2820
|
+
function orientationFor(forward, bank) {
|
|
2821
|
+
const fwd = normalize(forward);
|
|
2822
|
+
let right = cross(fwd, WORLD_UP);
|
|
2823
|
+
if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
|
|
2824
|
+
right = normalize(right);
|
|
2825
|
+
const levelUp = cross(right, fwd);
|
|
2826
|
+
const up = add2(scale(levelUp, Math.cos(bank)), scale(right, Math.sin(bank)));
|
|
2827
|
+
const w = normalize(cross(fwd, up));
|
|
2828
|
+
return {
|
|
2829
|
+
x: Math.atan2(cross(w, fwd).z, w.z),
|
|
2830
|
+
y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
|
|
2831
|
+
z: Math.atan2(fwd.y, fwd.x)
|
|
2832
|
+
};
|
|
1577
2833
|
}
|
|
1578
|
-
function
|
|
1579
|
-
|
|
1580
|
-
const c1 = 1.70158;
|
|
1581
|
-
const c3 = c1 + 1;
|
|
1582
|
-
return c3 * x * x * x - c1 * x * x;
|
|
2834
|
+
function rotationMatrix(x, y, z) {
|
|
2835
|
+
return multiply(rotationZ(z), multiply(rotationY(y), rotationX(x)));
|
|
1583
2836
|
}
|
|
1584
|
-
function
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2837
|
+
function eulerFromRotationMatrix(matrix) {
|
|
2838
|
+
const sy = Math.hypot(matrix[0], matrix[1]);
|
|
2839
|
+
if (sy > 1e-6) {
|
|
2840
|
+
return {
|
|
2841
|
+
x: Math.atan2(matrix[9], matrix[10]),
|
|
2842
|
+
y: Math.asin(Math.max(-1, Math.min(1, -matrix[8]))),
|
|
2843
|
+
z: Math.atan2(matrix[4], matrix[0])
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
return {
|
|
2847
|
+
x: Math.atan2(-matrix[6], matrix[5]),
|
|
2848
|
+
y: Math.asin(Math.max(-1, Math.min(1, -matrix[8]))),
|
|
2849
|
+
z: 0
|
|
2850
|
+
};
|
|
1589
2851
|
}
|
|
1590
|
-
function
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
return x < 0.5 ? Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2) / 2 : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2;
|
|
2852
|
+
function combineLocalRotation(path, extra) {
|
|
2853
|
+
return eulerFromRotationMatrix(
|
|
2854
|
+
multiply(rotationMatrix(path.x, path.y, path.z), rotationMatrix(extra.x, extra.y, extra.z))
|
|
2855
|
+
);
|
|
1595
2856
|
}
|
|
1596
|
-
function
|
|
1597
|
-
|
|
1598
|
-
const c4 = 2 * Math.PI / 3;
|
|
1599
|
-
return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * c4);
|
|
2857
|
+
function clamp013(value) {
|
|
2858
|
+
return Math.max(0, Math.min(1, value));
|
|
1600
2859
|
}
|
|
1601
|
-
function
|
|
1602
|
-
|
|
1603
|
-
const c4 = 2 * Math.PI / 3;
|
|
1604
|
-
return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
|
|
2860
|
+
function motionVectorAt(motion, t) {
|
|
2861
|
+
return scale(subtract(motion.positionAt(t + 1), motion.positionAt(t - 1)), 0.5);
|
|
1605
2862
|
}
|
|
1606
|
-
function
|
|
1607
|
-
|
|
1608
|
-
const c5 = 2 * Math.PI / 4.5;
|
|
1609
|
-
return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 : Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5) / 2 + 1;
|
|
2863
|
+
function resolveDirection2(velocity, fallback) {
|
|
2864
|
+
return Math.hypot(velocity.x, velocity.y, velocity.z) > 1e-6 ? normalize(velocity) : fallback;
|
|
1610
2865
|
}
|
|
1611
|
-
function
|
|
1612
|
-
|
|
1613
|
-
|
|
2866
|
+
function resolveTransition(config, fallback, durationMs) {
|
|
2867
|
+
if (!config) return { transition: fallback, durationMs };
|
|
2868
|
+
if (typeof config === "function") return { transition: config, durationMs };
|
|
2869
|
+
return { transition: config.transition, durationMs: Math.max(0, config.durationMs ?? durationMs) };
|
|
1614
2870
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2871
|
+
var ObjectMotionAnimation = class {
|
|
2872
|
+
constructor(options) {
|
|
2873
|
+
this.handles = [];
|
|
2874
|
+
this.banks = [];
|
|
2875
|
+
this.headings = [];
|
|
2876
|
+
this.started = false;
|
|
2877
|
+
this.finished = false;
|
|
2878
|
+
this.introStart = 0;
|
|
2879
|
+
this.outroStart = Infinity;
|
|
2880
|
+
this.outroPosition = { x: 0, y: 0, z: 0 };
|
|
2881
|
+
this.outroVelocity = { x: 0, y: 0, z: 0 };
|
|
2882
|
+
this.outroDirection = { x: 1, y: 0, z: 0 };
|
|
2883
|
+
const centered = centerAndScaleMesh(resolveMesh2(options.mesh), options.size ?? 1);
|
|
2884
|
+
const facing = faceForward(centered, options.facing ?? "+x");
|
|
2885
|
+
this.mesh = applyColor2(facing, options.color);
|
|
2886
|
+
this.motion = options.motion;
|
|
2887
|
+
this.backend = options.backend;
|
|
2888
|
+
this.transparency = options.transparency;
|
|
2889
|
+
this.labelContent = options.label;
|
|
2890
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
2891
|
+
this.tailCount = Math.max(0, Math.floor(options.tail?.count ?? 0));
|
|
2892
|
+
this.tailGap = Math.max(0, options.tail?.gapMs ?? 0);
|
|
2893
|
+
this.intro = resolveTransition(options.intro, enterFromObjectDirection(), DEFAULT_INTRO_MS);
|
|
2894
|
+
this.outro = resolveTransition(options.outro, leaveInObjectDirection(), DEFAULT_OUTRO_MS);
|
|
2895
|
+
const rotation = options.rotation;
|
|
2896
|
+
this.rotationOffset = { x: rotation?.x ?? 0, y: rotation?.y ?? 0, z: rotation?.z ?? 0 };
|
|
2897
|
+
this.rotationSpin = {
|
|
2898
|
+
x: rotation?.spinX ?? 0,
|
|
2899
|
+
y: rotation?.spinY ?? 0,
|
|
2900
|
+
z: rotation?.spinZ ?? 0
|
|
2901
|
+
};
|
|
2902
|
+
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;
|
|
2903
|
+
}
|
|
2904
|
+
mount(target) {
|
|
2905
|
+
if (!target.style.position) target.style.position = "relative";
|
|
2906
|
+
const engine = new Little3dEngine({
|
|
2907
|
+
backend: this.backend,
|
|
2908
|
+
camera: { position: { x: 0, y: 0, z: 3 } }
|
|
2909
|
+
});
|
|
2910
|
+
for (let i = 0; i <= this.tailCount; i++) {
|
|
2911
|
+
this.handles.push(engine.add(this.mesh, { transparency: this.transparency }));
|
|
2912
|
+
this.banks.push(0);
|
|
2913
|
+
this.headings.push({ x: 1, y: 0, z: 0 });
|
|
2914
|
+
}
|
|
2915
|
+
this.engine = engine;
|
|
2916
|
+
engine.mount(target).catch((error) => {
|
|
2917
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
2918
|
+
});
|
|
2919
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
2920
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
2921
|
+
}
|
|
2922
|
+
enter(now) {
|
|
2923
|
+
if (this.started) return;
|
|
2924
|
+
this.started = true;
|
|
2925
|
+
this.introStart = now;
|
|
2926
|
+
}
|
|
2927
|
+
exit(now) {
|
|
2928
|
+
if (!this.started || this.outroStart !== Infinity) return;
|
|
2929
|
+
this.outroPosition = this.motion.positionAt(now);
|
|
2930
|
+
this.outroVelocity = motionVectorAt(this.motion, now);
|
|
2931
|
+
this.outroDirection = resolveDirection2(this.outroVelocity, this.headings[0]);
|
|
2932
|
+
this.outroStart = now;
|
|
2933
|
+
}
|
|
2934
|
+
isFinished() {
|
|
2935
|
+
return this.finished;
|
|
2936
|
+
}
|
|
2937
|
+
/** Milliseconds the fly-out takes; used to align a following particle trail's outro. */
|
|
2938
|
+
get outroDurationMs() {
|
|
2939
|
+
return this.outro.durationMs;
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* A {@link MotionController} that follows the object's *actual* position, including
|
|
2943
|
+
* the intro fly-in and outro fly-out (it falls back to the raw motion path before
|
|
2944
|
+
* {@link enter} and once idle). Feed it to a particle layer's `emitter` so the
|
|
2945
|
+
* particles trail the object through its transitions instead of the bare path.
|
|
2946
|
+
*/
|
|
2947
|
+
trailEmitter() {
|
|
2948
|
+
return { positionAt: (t) => this.sampleAt(t)?.position ?? this.motion.positionAt(t) };
|
|
2949
|
+
}
|
|
2950
|
+
render(now, frame) {
|
|
2951
|
+
if (!this.engine || !this.label) return;
|
|
2952
|
+
if (this.outroStart !== Infinity && now >= this.outroStart + this.outro.durationMs + this.tailCount * this.tailGap) {
|
|
2953
|
+
this.finished = true;
|
|
2954
|
+
}
|
|
2955
|
+
for (let k = 0; k < this.handles.length; k++) {
|
|
2956
|
+
const transform2 = this.handles[k].transform;
|
|
2957
|
+
const t = now - k * this.tailGap;
|
|
2958
|
+
const sample = this.sampleAt(t);
|
|
2959
|
+
if (!sample) {
|
|
2960
|
+
transform2.scale = 0;
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
transform2.scale = sample.size;
|
|
2964
|
+
let euler = sample.orientation;
|
|
2965
|
+
if (!euler) {
|
|
2966
|
+
const heading = subtract(this.positionAt(t + SAMPLE_MS) ?? sample.position, sample.position);
|
|
2967
|
+
if (Math.hypot(heading.x, heading.y, heading.z) > 1e-5) {
|
|
2968
|
+
this.headings[k] = normalize(heading);
|
|
2969
|
+
}
|
|
2970
|
+
const ahead = this.aheadAt(t) ?? this.headings[k];
|
|
2971
|
+
const targetBank = Math.max(
|
|
2972
|
+
-BANK_LIMIT,
|
|
2973
|
+
Math.min(BANK_LIMIT, cross(this.headings[k], ahead).y * BANK_GAIN)
|
|
2974
|
+
);
|
|
2975
|
+
this.banks[k] += (targetBank - this.banks[k]) * BANK_SMOOTH;
|
|
2976
|
+
euler = orientationFor(this.headings[k], this.banks[k]);
|
|
2977
|
+
}
|
|
2978
|
+
if (this.hasExtraRotation) {
|
|
2979
|
+
euler = combineLocalRotation(euler, {
|
|
2980
|
+
x: this.rotationOffset.x + this.rotationSpin.x * t,
|
|
2981
|
+
y: this.rotationOffset.y + this.rotationSpin.y * t,
|
|
2982
|
+
z: this.rotationOffset.z + this.rotationSpin.z * t
|
|
2983
|
+
});
|
|
2984
|
+
}
|
|
2985
|
+
transform2.position.x = sample.position.x;
|
|
2986
|
+
transform2.position.y = sample.position.y;
|
|
2987
|
+
transform2.position.z = sample.position.z;
|
|
2988
|
+
transform2.rotation.x = euler.x;
|
|
2989
|
+
transform2.rotation.y = euler.y;
|
|
2990
|
+
transform2.rotation.z = euler.z;
|
|
2991
|
+
}
|
|
2992
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
2993
|
+
if (this.fadeLabel) {
|
|
2994
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
2995
|
+
now,
|
|
2996
|
+
this.started ? this.introStart : Infinity,
|
|
2997
|
+
this.intro.durationMs,
|
|
2998
|
+
this.outroStart,
|
|
2999
|
+
this.outro.durationMs
|
|
3000
|
+
));
|
|
3001
|
+
}
|
|
3002
|
+
this.engine.render();
|
|
3003
|
+
}
|
|
3004
|
+
destroy() {
|
|
3005
|
+
this.label?.container.remove();
|
|
3006
|
+
this.label = void 0;
|
|
3007
|
+
this.engine?.destroy();
|
|
3008
|
+
this.engine = void 0;
|
|
3009
|
+
this.handles.length = 0;
|
|
3010
|
+
}
|
|
3011
|
+
aheadAt(t) {
|
|
3012
|
+
const next = this.positionAt(t + SAMPLE_MS);
|
|
3013
|
+
const afterNext = this.positionAt(t + 2 * SAMPLE_MS);
|
|
3014
|
+
if (!next || !afterNext) return void 0;
|
|
3015
|
+
const ahead = subtract(afterNext, next);
|
|
3016
|
+
if (Math.hypot(ahead.x, ahead.y, ahead.z) <= 1e-5) return void 0;
|
|
3017
|
+
return normalize(ahead);
|
|
3018
|
+
}
|
|
3019
|
+
positionAt(t) {
|
|
3020
|
+
return this.sampleAt(t)?.position;
|
|
3021
|
+
}
|
|
3022
|
+
sampleAt(t) {
|
|
3023
|
+
if (!this.started || t < this.introStart) return void 0;
|
|
3024
|
+
if (t < this.introStart + this.intro.durationMs) {
|
|
3025
|
+
return this.transitionSample("intro", t, this.intro, this.introStart);
|
|
3026
|
+
}
|
|
3027
|
+
if (this.outroStart !== Infinity) {
|
|
3028
|
+
if (t > this.outroStart + this.outro.durationMs) return void 0;
|
|
3029
|
+
if (t >= this.outroStart) return this.transitionSample("outro", t, this.outro, this.outroStart);
|
|
3030
|
+
}
|
|
3031
|
+
return { position: this.motion.positionAt(t), size: 1 };
|
|
1621
3032
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
3033
|
+
transitionSample(phase, t, transition, start) {
|
|
3034
|
+
const elapsedMs = Math.max(0, t - start);
|
|
3035
|
+
const delta = transition.durationMs === 0 ? 1 : clamp013(elapsedMs / transition.durationMs);
|
|
3036
|
+
const input2 = this.transitionInput(phase, delta, elapsedMs, transition.durationMs, start);
|
|
3037
|
+
const output = transition.transition(input2);
|
|
3038
|
+
return this.applyTransitionOutput(input2, output, phase, t);
|
|
1625
3039
|
}
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
3040
|
+
transitionInput(phase, delta, elapsedMs, durationMs, start) {
|
|
3041
|
+
if (phase === "intro") {
|
|
3042
|
+
const handoff = start + durationMs;
|
|
3043
|
+
const velocity = motionVectorAt(this.motion, handoff);
|
|
3044
|
+
return {
|
|
3045
|
+
delta,
|
|
3046
|
+
position: this.motion.positionAt(handoff),
|
|
3047
|
+
direction: resolveDirection2(velocity, { x: 1, y: 0, z: 0 }),
|
|
3048
|
+
velocity,
|
|
3049
|
+
size: 1,
|
|
3050
|
+
durationMs,
|
|
3051
|
+
elapsedMs,
|
|
3052
|
+
phase
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
return {
|
|
3056
|
+
delta,
|
|
3057
|
+
position: this.outroPosition,
|
|
3058
|
+
direction: this.outroDirection,
|
|
3059
|
+
velocity: this.outroVelocity,
|
|
3060
|
+
size: 1,
|
|
3061
|
+
durationMs,
|
|
3062
|
+
elapsedMs,
|
|
3063
|
+
phase
|
|
3064
|
+
};
|
|
1629
3065
|
}
|
|
1630
|
-
|
|
1631
|
-
|
|
3066
|
+
applyTransitionOutput(input2, output, phase, t) {
|
|
3067
|
+
return {
|
|
3068
|
+
position: output.position ?? (phase === "intro" ? this.motion.positionAt(t) : input2.position),
|
|
3069
|
+
size: output.size ?? input2.size ?? 1,
|
|
3070
|
+
orientation: output.orientation
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3073
|
+
};
|
|
3074
|
+
|
|
3075
|
+
// src/animations/particles.ts
|
|
3076
|
+
var DEFAULT_COLORS11 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
|
|
3077
|
+
var FADE_IN_END = 0.15;
|
|
3078
|
+
var FADE_OUT_START = 0.6;
|
|
3079
|
+
function rand01(seed, index, salt) {
|
|
3080
|
+
let h = (seed ^ Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
|
|
3081
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
3082
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
3083
|
+
h ^= h >>> 16;
|
|
3084
|
+
return (h >>> 0) / 4294967296;
|
|
1632
3085
|
}
|
|
1633
|
-
function
|
|
1634
|
-
const x =
|
|
1635
|
-
return x
|
|
3086
|
+
function smoothstep(edge0, edge1, value) {
|
|
3087
|
+
const x = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
|
|
3088
|
+
return x * x * (3 - 2 * x);
|
|
1636
3089
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
quintic,
|
|
1643
|
-
easeInSine,
|
|
1644
|
-
easeOutSine,
|
|
1645
|
-
easeInOutSine,
|
|
1646
|
-
easeInQuad,
|
|
1647
|
-
easeOutQuad,
|
|
1648
|
-
easeInOutQuad,
|
|
1649
|
-
easeInCubic,
|
|
1650
|
-
easeOutCubic,
|
|
1651
|
-
easeInOutCubic,
|
|
1652
|
-
easeInQuart,
|
|
1653
|
-
easeOutQuart,
|
|
1654
|
-
easeInOutQuart,
|
|
1655
|
-
easeInQuint,
|
|
1656
|
-
easeOutQuint,
|
|
1657
|
-
easeInOutQuint,
|
|
1658
|
-
easeInExpo,
|
|
1659
|
-
easeOutExpo,
|
|
1660
|
-
easeInOutExpo,
|
|
1661
|
-
easeInCirc,
|
|
1662
|
-
easeOutCirc,
|
|
1663
|
-
easeInOutCirc,
|
|
1664
|
-
easeInBack,
|
|
1665
|
-
easeOutBack,
|
|
1666
|
-
easeInOutBack,
|
|
1667
|
-
easeInElastic,
|
|
1668
|
-
easeOutElastic,
|
|
1669
|
-
easeInOutElastic,
|
|
1670
|
-
easeInBounce,
|
|
1671
|
-
easeOutBounce,
|
|
1672
|
-
easeInOutBounce
|
|
1673
|
-
};
|
|
1674
|
-
function ease(type, value, overextend = false) {
|
|
1675
|
-
return easeTypes[type](value, overextend);
|
|
3090
|
+
function positiveFinite(value, name) {
|
|
3091
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
3092
|
+
throw new RangeError(`3d-spinner: ${name} must be a finite number greater than zero.`);
|
|
3093
|
+
}
|
|
3094
|
+
return value;
|
|
1676
3095
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
3096
|
+
function emitBasis(direction) {
|
|
3097
|
+
const d = normalize(direction);
|
|
3098
|
+
const helper = Math.abs(d.y) < 0.99 ? { x: 0, y: 1, z: 0 } : { x: 1, y: 0, z: 0 };
|
|
3099
|
+
const right = normalize(cross(helper, d));
|
|
3100
|
+
return { d, right, up: cross(d, right) };
|
|
3101
|
+
}
|
|
3102
|
+
function particleField(options = {}) {
|
|
3103
|
+
const rate = positiveFinite(options.rate ?? 20, "rate");
|
|
3104
|
+
const lifeMs = positiveFinite(options.lifeMs ?? 1800, "lifeMs");
|
|
3105
|
+
const size = options.size ?? 0.16;
|
|
3106
|
+
const speed = options.speed ?? 0.6;
|
|
3107
|
+
const gravity = options.gravity;
|
|
3108
|
+
const spread = options.spread ?? 0.5;
|
|
3109
|
+
const peak = Math.max(0, Math.min(1, options.opacity ?? 0.9));
|
|
3110
|
+
const spin = options.spin ?? 2e-3;
|
|
3111
|
+
const alignToMotion = options.alignToMotion ?? false;
|
|
3112
|
+
const seed = options.seed ?? 1;
|
|
3113
|
+
const basis = options.direction && emitBasis(options.direction);
|
|
3114
|
+
const spawnGapMs = 1e3 / rate;
|
|
3115
|
+
const directionOf = (index) => {
|
|
3116
|
+
const u = rand01(seed, index, 0);
|
|
3117
|
+
const phi = 2 * Math.PI * rand01(seed, index, 1);
|
|
3118
|
+
if (!basis) {
|
|
3119
|
+
const z = 2 * u - 1;
|
|
3120
|
+
const r = Math.sqrt(Math.max(0, 1 - z * z));
|
|
3121
|
+
return { x: r * Math.cos(phi), y: r * Math.sin(phi), z };
|
|
3122
|
+
}
|
|
3123
|
+
const cos = 1 - u * (1 - Math.cos(spread));
|
|
3124
|
+
const sin = Math.sqrt(Math.max(0, 1 - cos * cos));
|
|
3125
|
+
const { d, right, up } = basis;
|
|
3126
|
+
return {
|
|
3127
|
+
x: d.x * cos + (right.x * Math.cos(phi) + up.x * Math.sin(phi)) * sin,
|
|
3128
|
+
y: d.y * cos + (right.y * Math.cos(phi) + up.y * Math.sin(phi)) * sin,
|
|
3129
|
+
z: d.z * cos + (right.z * Math.cos(phi) + up.z * Math.sin(phi)) * sin
|
|
3130
|
+
};
|
|
3131
|
+
};
|
|
1680
3132
|
return {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
3133
|
+
maxLive: Math.ceil(lifeMs * rate / 1e3) + 1,
|
|
3134
|
+
spawnGapMs,
|
|
3135
|
+
lifeMs,
|
|
3136
|
+
sample(index, t) {
|
|
3137
|
+
if (index < 0) return void 0;
|
|
3138
|
+
const age = t - index * spawnGapMs;
|
|
3139
|
+
if (age < 0 || age >= lifeMs) return void 0;
|
|
3140
|
+
const seconds = age / 1e3;
|
|
3141
|
+
const dir = directionOf(index);
|
|
3142
|
+
const particleSpeed = speed * (0.6 + 0.8 * rand01(seed, index, 2));
|
|
3143
|
+
const travel = particleSpeed * seconds;
|
|
3144
|
+
const pull = gravity ? 0.5 * seconds * seconds : 0;
|
|
3145
|
+
const life = age / lifeMs;
|
|
3146
|
+
const roll = alignToMotion ? Math.atan2(
|
|
3147
|
+
dir.y * particleSpeed + (gravity?.y ?? 0) * seconds,
|
|
3148
|
+
dir.x * particleSpeed + (gravity?.x ?? 0) * seconds
|
|
3149
|
+
) : 2 * Math.PI * rand01(seed, index, 3) + (2 * rand01(seed, index, 4) - 1) * spin * age;
|
|
3150
|
+
return {
|
|
3151
|
+
position: {
|
|
3152
|
+
x: dir.x * travel + (gravity ? gravity.x * pull : 0),
|
|
3153
|
+
y: dir.y * travel + (gravity ? gravity.y * pull : 0),
|
|
3154
|
+
z: dir.z * travel + (gravity ? gravity.z * pull : 0)
|
|
3155
|
+
},
|
|
3156
|
+
roll,
|
|
3157
|
+
size: size * (0.7 + 0.6 * rand01(seed, index, 5)),
|
|
3158
|
+
opacity: peak * smoothstep(0, FADE_IN_END, life) * (1 - smoothstep(FADE_OUT_START, 1, life))
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
1688
3161
|
};
|
|
1689
3162
|
}
|
|
1690
|
-
|
|
1691
|
-
if (durationMs <= 0) return 1;
|
|
1692
|
-
return Math.min(1, Math.max(0, (now - phaseStart) / durationMs));
|
|
1693
|
-
}
|
|
1694
|
-
var ProgressAnimation = class {
|
|
3163
|
+
var ParticlesAnimation = class {
|
|
1695
3164
|
constructor(options = {}) {
|
|
1696
|
-
this.
|
|
1697
|
-
this.
|
|
1698
|
-
this.
|
|
1699
|
-
this.
|
|
1700
|
-
this.
|
|
1701
|
-
this.
|
|
3165
|
+
this.handles = [];
|
|
3166
|
+
this.fades = [];
|
|
3167
|
+
this.enterAt = Infinity;
|
|
3168
|
+
this.exitAt = Infinity;
|
|
3169
|
+
this.finished = false;
|
|
3170
|
+
this.field = particleField(options);
|
|
3171
|
+
this.colors = options.colors ?? DEFAULT_COLORS11;
|
|
3172
|
+
this.backend = options.backend;
|
|
3173
|
+
this.texture = options.texture;
|
|
3174
|
+
this.labelContent = options.label;
|
|
3175
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
3176
|
+
this.emitter = options.emitter;
|
|
3177
|
+
this.outroMs = Math.max(0, options.outroMs ?? 0);
|
|
3178
|
+
}
|
|
3179
|
+
mount(target) {
|
|
3180
|
+
if (!target.style.position) target.style.position = "relative";
|
|
3181
|
+
const meshes = this.colors.map((color) => quad(1, [color]));
|
|
3182
|
+
const texture = this.texture;
|
|
3183
|
+
const backend = texture ? async (rendererOptions) => {
|
|
3184
|
+
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);
|
|
3185
|
+
for (const mesh of meshes) renderer.setTexture(mesh, texture);
|
|
3186
|
+
return renderer;
|
|
3187
|
+
} : this.backend;
|
|
3188
|
+
const engine = new Little3dEngine({
|
|
3189
|
+
backend,
|
|
3190
|
+
camera: { position: { x: 0, y: 0, z: 3 } },
|
|
3191
|
+
light: { intensity: 0, ambient: 1 }
|
|
3192
|
+
});
|
|
3193
|
+
for (let slot = 0; slot < this.field.maxLive; slot++) {
|
|
3194
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
3195
|
+
this.fades.push(fade);
|
|
3196
|
+
this.handles.push(engine.add(meshes[slot % meshes.length], { scale: 0, transparency: fade }));
|
|
3197
|
+
}
|
|
3198
|
+
this.engine = engine;
|
|
3199
|
+
engine.mount(target).catch((error) => {
|
|
3200
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
3201
|
+
});
|
|
3202
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
3203
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
1702
3204
|
}
|
|
1703
|
-
/** Begin the intro pop. Ignored unless idle. */
|
|
1704
3205
|
enter(now) {
|
|
1705
|
-
if (this.
|
|
1706
|
-
this.phase = "startPop";
|
|
1707
|
-
this.phaseStart = now;
|
|
1708
|
-
this.activeProgress = 0;
|
|
1709
|
-
this.popTarget = 0;
|
|
3206
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
1710
3207
|
}
|
|
1711
|
-
/** Begin the outro pop. Ignored unless mid-intro or active. */
|
|
1712
3208
|
exit(now) {
|
|
1713
|
-
if (this.
|
|
1714
|
-
this.phase = "endPop";
|
|
1715
|
-
this.phaseStart = now;
|
|
3209
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
1716
3210
|
}
|
|
1717
3211
|
isFinished() {
|
|
1718
|
-
return this.
|
|
3212
|
+
return this.finished;
|
|
1719
3213
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
if (this.phase === "startPop" || this.phase === "active") {
|
|
1732
|
-
this.activeProgress = progress;
|
|
1733
|
-
if (this.phase === "startPop") this.popTarget = Math.max(this.popTarget, goal, progress);
|
|
1734
|
-
}
|
|
1735
|
-
let scale2 = 0;
|
|
1736
|
-
let text = null;
|
|
1737
|
-
let textOpacity = 0;
|
|
1738
|
-
let hidden = false;
|
|
1739
|
-
if (this.phase === "startPop") {
|
|
1740
|
-
const t = popPhaseT(now, this.phaseStart, popDurationMs);
|
|
1741
|
-
const peak = this.popTarget * (1 + overextend);
|
|
1742
|
-
if (t < startSnapRatio) {
|
|
1743
|
-
const snapT = startSnapRatio > 0 ? t / startSnapRatio : 1;
|
|
1744
|
-
scale2 = peak * easeOutExpo(snapT);
|
|
1745
|
-
} else {
|
|
1746
|
-
const settleT = startSnapRatio < 1 ? (t - startSnapRatio) / (1 - startSnapRatio) : 1;
|
|
1747
|
-
scale2 = peak + (this.activeProgress - peak) * easeOutCubic(settleT);
|
|
1748
|
-
}
|
|
1749
|
-
if (t >= 1) this.phase = "active";
|
|
1750
|
-
} else if (this.phase === "active") {
|
|
1751
|
-
scale2 = this.activeProgress;
|
|
1752
|
-
} else if (this.phase === "endPop") {
|
|
1753
|
-
const t = popPhaseT(now, this.phaseStart, popDurationMs);
|
|
1754
|
-
const peak = 1 + overextend;
|
|
1755
|
-
if (t < 0.5) {
|
|
1756
|
-
scale2 = 1 + (peak - 1) * easeOutQuad(t * 2);
|
|
1757
|
-
} else {
|
|
1758
|
-
scale2 = peak * (1 - easeInQuad((t - 0.5) * 2));
|
|
3214
|
+
render(now, frame) {
|
|
3215
|
+
if (!this.engine || !this.label) return;
|
|
3216
|
+
if (this.exitAt !== Infinity && now >= this.exitAt + this.outroMs + this.field.lifeMs) this.finished = true;
|
|
3217
|
+
for (const handle of this.handles) handle.transform.scale = 0;
|
|
3218
|
+
if (this.enterAt !== Infinity) {
|
|
3219
|
+
const t = now - this.enterAt;
|
|
3220
|
+
const gap = this.field.spawnGapMs;
|
|
3221
|
+
let first = Math.max(0, Math.ceil((t - this.field.lifeMs) / gap));
|
|
3222
|
+
let last = Math.floor(t / gap);
|
|
3223
|
+
if (this.exitAt !== Infinity) {
|
|
3224
|
+
last = Math.min(last, Math.floor((this.exitAt - this.enterAt + this.outroMs) / gap));
|
|
1759
3225
|
}
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
this.
|
|
1763
|
-
|
|
3226
|
+
first = Math.max(first, last - this.field.maxLive + 1);
|
|
3227
|
+
for (let index = first; index <= last; index++) {
|
|
3228
|
+
const sample = this.field.sample(index, t);
|
|
3229
|
+
if (!sample) continue;
|
|
3230
|
+
const slot = index % this.handles.length;
|
|
3231
|
+
const transform2 = this.handles[slot].transform;
|
|
3232
|
+
const origin = this.emitter?.positionAt(this.enterAt + index * gap);
|
|
3233
|
+
transform2.position.x = sample.position.x + (origin?.x ?? 0);
|
|
3234
|
+
transform2.position.y = sample.position.y + (origin?.y ?? 0);
|
|
3235
|
+
transform2.position.z = sample.position.z + (origin?.z ?? 0);
|
|
3236
|
+
transform2.rotation.z = sample.roll;
|
|
3237
|
+
transform2.scale = sample.size;
|
|
3238
|
+
this.fades[slot].opacity = sample.opacity;
|
|
1764
3239
|
}
|
|
1765
3240
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
const fadeT = popPhaseT(now, this.doneFadeStart, doneFadeDurationMs);
|
|
1776
|
-
if (fadeT >= 1) {
|
|
1777
|
-
if (removeOnComplete) hidden = true;
|
|
1778
|
-
this.phase = "finished";
|
|
1779
|
-
} else {
|
|
1780
|
-
text = doneText;
|
|
1781
|
-
textOpacity = 0.65 * (1 - fadeT);
|
|
1782
|
-
}
|
|
3241
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
3242
|
+
if (this.fadeLabel) {
|
|
3243
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
3244
|
+
now,
|
|
3245
|
+
this.enterAt,
|
|
3246
|
+
this.field.lifeMs * FADE_IN_END,
|
|
3247
|
+
this.exitAt,
|
|
3248
|
+
this.field.lifeMs
|
|
3249
|
+
));
|
|
1783
3250
|
}
|
|
1784
|
-
|
|
3251
|
+
this.engine.render();
|
|
3252
|
+
}
|
|
3253
|
+
destroy() {
|
|
3254
|
+
this.label?.container.remove();
|
|
3255
|
+
this.label = void 0;
|
|
3256
|
+
this.engine?.destroy();
|
|
3257
|
+
this.engine = void 0;
|
|
3258
|
+
this.handles.length = 0;
|
|
3259
|
+
this.fades.length = 0;
|
|
1785
3260
|
}
|
|
1786
3261
|
};
|
|
1787
3262
|
|
|
1788
|
-
// src/animations/
|
|
1789
|
-
var
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
3263
|
+
// src/animations/charged-orb.ts
|
|
3264
|
+
var MINIS = 10;
|
|
3265
|
+
var CAMERA_Z = 3;
|
|
3266
|
+
var CENTER_SCALE = 0.76;
|
|
3267
|
+
var MINI_SCALE = 0.36;
|
|
3268
|
+
var MINI_TRANSPARENCY = { mode: "two-sided", frontOpacity: 0.68, backOpacity: 0.87 };
|
|
3269
|
+
var ORBIT_RADIUS = 1.2;
|
|
3270
|
+
var TILT = 0.8;
|
|
3271
|
+
var TWO_PI = Math.PI * 2;
|
|
3272
|
+
var CENTER_POP_MS = 500;
|
|
3273
|
+
var LAUNCH_MS = 550;
|
|
3274
|
+
var EXIT_HURRY = 2.5;
|
|
3275
|
+
var SPREAD_TAU_MS = 250;
|
|
3276
|
+
var EXTRA_PAUSE_MS = 250;
|
|
3277
|
+
var EXTRA_SPIN_MS = 1300;
|
|
3278
|
+
var REENTER_MS = 600;
|
|
3279
|
+
var REENTER_STAGGER_MS = 45;
|
|
3280
|
+
var CENTER_POP_OUT_AT = REENTER_MS + (MINIS - 1) * REENTER_STAGGER_MS + 150;
|
|
3281
|
+
var CENTER_POP_OUT_MS = 420;
|
|
3282
|
+
var PARKED = { x: 0, y: 0, z: 50 };
|
|
3283
|
+
var CENTER_COLORS = ["#67e8f9", "#22d3ee", "#0ea5e9", "#38bdf8", "#7dd3fc"];
|
|
3284
|
+
var MINI_COLORS = [
|
|
3285
|
+
["#e0f2fe", "#bae6fd", "#7dd3fc"],
|
|
3286
|
+
["#c7d2fe", "#a5b4fc", "#818cf8"],
|
|
3287
|
+
["#a5f3fc", "#67e8f9", "#22d3ee"]
|
|
3288
|
+
];
|
|
3289
|
+
function clamp014(value) {
|
|
3290
|
+
return Math.max(0, Math.min(1, value));
|
|
1810
3291
|
}
|
|
1811
|
-
var
|
|
3292
|
+
var ChargedOrbAnimation = class {
|
|
1812
3293
|
constructor(options = {}) {
|
|
1813
|
-
this.
|
|
1814
|
-
this.
|
|
1815
|
-
this.
|
|
1816
|
-
this.
|
|
3294
|
+
this.minis = [];
|
|
3295
|
+
this.blends = new Array(MINIS).fill(0);
|
|
3296
|
+
this.offsets = new Array(MINIS).fill(0);
|
|
3297
|
+
this.enterAt = Infinity;
|
|
3298
|
+
this.exitAt = Infinity;
|
|
3299
|
+
this.allOutAt = Infinity;
|
|
3300
|
+
this.lastNow = 0;
|
|
3301
|
+
this.finished = false;
|
|
3302
|
+
this.orbitPeriodMs = options.orbitPeriodMs ?? 6e3;
|
|
1817
3303
|
this.backend = options.backend;
|
|
1818
|
-
this.transparency = options.transparency;
|
|
1819
|
-
this.progress = options.progressAnimation ? new ProgressAnimation(options.progressAnimation) : void 0;
|
|
1820
3304
|
}
|
|
1821
3305
|
mount(target) {
|
|
1822
|
-
target.style.position = "relative";
|
|
3306
|
+
if (!target.style.position) target.style.position = "relative";
|
|
1823
3307
|
const engine = new Little3dEngine({
|
|
1824
3308
|
backend: this.backend,
|
|
1825
|
-
camera: { position: { x: 0, y: 0, z:
|
|
3309
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z } }
|
|
1826
3310
|
});
|
|
1827
|
-
this.
|
|
3311
|
+
this.center = engine.add(icosphere(1, 2, CENTER_COLORS), { scale: 0 });
|
|
3312
|
+
for (let i = 0; i < MINIS; i++) {
|
|
3313
|
+
const mesh = icosphere(1, 1, MINI_COLORS[i % MINI_COLORS.length]);
|
|
3314
|
+
this.minis.push(engine.add(mesh, { scale: 0, transparency: { ...MINI_TRANSPARENCY } }));
|
|
3315
|
+
}
|
|
1828
3316
|
this.engine = engine;
|
|
1829
3317
|
engine.mount(target).catch((error) => {
|
|
1830
3318
|
target.textContent = error instanceof Error ? error.message : String(error);
|
|
1831
3319
|
});
|
|
1832
|
-
if (this.progress) {
|
|
1833
|
-
const label = document.createElement("div");
|
|
1834
|
-
label.style.cssText = LABEL_STYLE;
|
|
1835
|
-
label.setAttribute("aria-hidden", "true");
|
|
1836
|
-
label.hidden = true;
|
|
1837
|
-
target.appendChild(label);
|
|
1838
|
-
this.label = label;
|
|
1839
|
-
}
|
|
1840
3320
|
}
|
|
1841
3321
|
enter(now) {
|
|
1842
|
-
this.
|
|
3322
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
1843
3323
|
}
|
|
1844
3324
|
exit(now) {
|
|
1845
|
-
this.
|
|
1846
|
-
this.progress?.exit(now);
|
|
3325
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
1847
3326
|
}
|
|
1848
3327
|
isFinished() {
|
|
1849
|
-
return this.
|
|
3328
|
+
return this.finished;
|
|
3329
|
+
}
|
|
3330
|
+
/** Milliseconds after {@link exit} during which the satellites are still flying. */
|
|
3331
|
+
get outroEmitMs() {
|
|
3332
|
+
return EXTRA_PAUSE_MS + EXTRA_SPIN_MS + CENTER_POP_OUT_AT;
|
|
3333
|
+
}
|
|
3334
|
+
/**
|
|
3335
|
+
* A {@link MotionController} that cycles across the live satellites, one
|
|
3336
|
+
* spawn slot per orb, so a particle layer emits one stream per satellite.
|
|
3337
|
+
* `spawnGapMs` must match the particle layer's emission gap (`1000 / rate`).
|
|
3338
|
+
*/
|
|
3339
|
+
satelliteEmitter(spawnGapMs) {
|
|
3340
|
+
return {
|
|
3341
|
+
positionAt: (t) => {
|
|
3342
|
+
const live = [];
|
|
3343
|
+
for (let i = 0; i < MINIS; i++) {
|
|
3344
|
+
const sample = this.miniSample(i, t);
|
|
3345
|
+
if (sample) live.push(sample.position);
|
|
3346
|
+
}
|
|
3347
|
+
if (live.length === 0) return PARKED;
|
|
3348
|
+
const slot = Math.abs(Math.floor(t / spawnGapMs)) % live.length;
|
|
3349
|
+
return live[slot];
|
|
3350
|
+
}
|
|
3351
|
+
};
|
|
1850
3352
|
}
|
|
1851
3353
|
render(now, frame) {
|
|
1852
|
-
if (!this.engine || !this.
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
3354
|
+
if (!this.engine || !this.center) return;
|
|
3355
|
+
if (this.enterAt === Infinity) {
|
|
3356
|
+
this.center.transform.scale = 0;
|
|
3357
|
+
for (const mini of this.minis) mini.transform.scale = 0;
|
|
3358
|
+
this.engine.render();
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
3362
|
+
this.lastNow = now;
|
|
3363
|
+
this.updateBlends(dt, frame.progress, now);
|
|
3364
|
+
this.updateSpread(dt);
|
|
3365
|
+
const t = now - this.enterAt;
|
|
3366
|
+
this.center.transform.scale = this.centerScale(now, t);
|
|
3367
|
+
this.center.transform.rotation.x = t * 2e-4;
|
|
3368
|
+
this.center.transform.rotation.y = t * 5e-4;
|
|
3369
|
+
for (let i = 0; i < MINIS; i++) {
|
|
3370
|
+
const transform2 = this.minis[i].transform;
|
|
3371
|
+
const sample = this.miniSample(i, now);
|
|
3372
|
+
if (!sample) {
|
|
3373
|
+
transform2.scale = 0;
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
transform2.position.x = sample.position.x;
|
|
3377
|
+
transform2.position.y = sample.position.y;
|
|
3378
|
+
transform2.position.z = sample.position.z;
|
|
3379
|
+
transform2.scale = sample.scale;
|
|
3380
|
+
transform2.rotation.y = t * 12e-4;
|
|
1862
3381
|
}
|
|
1863
3382
|
this.engine.render();
|
|
1864
3383
|
}
|
|
1865
3384
|
destroy() {
|
|
1866
|
-
this.label?.remove();
|
|
1867
|
-
this.label = void 0;
|
|
1868
3385
|
this.engine?.destroy();
|
|
1869
3386
|
this.engine = void 0;
|
|
1870
|
-
this.
|
|
3387
|
+
this.center = void 0;
|
|
3388
|
+
this.minis.length = 0;
|
|
1871
3389
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
3390
|
+
updateBlends(dt, progress, now) {
|
|
3391
|
+
const exiting = this.exitAt !== Infinity;
|
|
3392
|
+
const want = exiting ? MINIS : Math.min(MINIS, Math.floor(progress * MINIS + 1e-9));
|
|
3393
|
+
const rate = dt / LAUNCH_MS * (exiting ? EXIT_HURRY : 1);
|
|
3394
|
+
for (let i = 0; i < MINIS; i++) {
|
|
3395
|
+
const target = i < want ? 1 : 0;
|
|
3396
|
+
const blend = this.blends[i];
|
|
3397
|
+
if (target > blend && (i === 0 || this.blends[i - 1] >= 0.6)) {
|
|
3398
|
+
this.blends[i] = Math.min(1, blend + rate);
|
|
3399
|
+
if (blend === 0) this.offsets[i] = this.slotAngle(i);
|
|
3400
|
+
} else if (target < blend && (i === MINIS - 1 || this.blends[i + 1] <= 0.4)) {
|
|
3401
|
+
this.blends[i] = Math.max(0, blend - rate);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
if (exiting && this.allOutAt === Infinity && this.blends.every((blend) => blend >= 1)) {
|
|
3405
|
+
this.allOutAt = now;
|
|
1878
3406
|
}
|
|
1879
|
-
this.label.hidden = false;
|
|
1880
|
-
this.label.textContent = visual.text;
|
|
1881
|
-
this.label.style.opacity = String(visual.textOpacity);
|
|
1882
3407
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
var DEFAULT_DISTANCE = 3.5;
|
|
1890
|
-
function add(a, b) {
|
|
1891
|
-
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
|
1892
|
-
}
|
|
1893
|
-
function scaleVector(v, factor) {
|
|
1894
|
-
return { x: v.x * factor, y: v.y * factor, z: v.z * factor };
|
|
1895
|
-
}
|
|
1896
|
-
function vectorLength(v) {
|
|
1897
|
-
return Math.hypot(v.x, v.y, v.z);
|
|
1898
|
-
}
|
|
1899
|
-
function normalizeVector(v) {
|
|
1900
|
-
const length = vectorLength(v);
|
|
1901
|
-
if (length < 1e-6) return { x: 1, y: 0, z: 0 };
|
|
1902
|
-
return scaleVector(v, 1 / length);
|
|
1903
|
-
}
|
|
1904
|
-
function resolveDirection(input2, fallback) {
|
|
1905
|
-
return normalizeVector(fallback ?? input2.direction ?? input2.velocity ?? { x: 1, y: 0, z: 0 });
|
|
1906
|
-
}
|
|
1907
|
-
function easeOutBack2(delta) {
|
|
1908
|
-
const c = 1.70158;
|
|
1909
|
-
const u = delta - 1;
|
|
1910
|
-
return 1 + (c + 1) * u * u * u + c * u * u;
|
|
1911
|
-
}
|
|
1912
|
-
function joinVelocity(input2, options, durationMs) {
|
|
1913
|
-
const inputSpeed = input2.velocity ? vectorLength(input2.velocity) : 0;
|
|
1914
|
-
if (input2.velocity && inputSpeed > 1e-6 && !options.direction) {
|
|
1915
|
-
return input2.velocity;
|
|
3408
|
+
updateSpread(dt) {
|
|
3409
|
+
const ease2 = 1 - Math.exp(-dt / SPREAD_TAU_MS);
|
|
3410
|
+
for (let i = 0; i < MINIS; i++) {
|
|
3411
|
+
if (this.blends[i] <= 0) continue;
|
|
3412
|
+
this.offsets[i] += (this.slotAngle(i) - this.offsets[i]) * ease2;
|
|
3413
|
+
}
|
|
1916
3414
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
function enterFromObjectDirection(options = {}) {
|
|
1921
|
-
return (input2) => {
|
|
1922
|
-
const durationMs = Math.max(1, input2.durationMs);
|
|
1923
|
-
const velocity = joinVelocity(input2, options, durationMs);
|
|
1924
|
-
const remaining = durationMs - input2.elapsedMs;
|
|
1925
|
-
return { position: add(input2.position, scaleVector(velocity, -remaining)) };
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
function leaveInObjectDirection(options = {}) {
|
|
1929
|
-
return (input2) => {
|
|
1930
|
-
const durationMs = Math.max(1, input2.durationMs);
|
|
1931
|
-
const velocity = joinVelocity(input2, options, durationMs);
|
|
1932
|
-
return { position: add(input2.position, scaleVector(velocity, input2.elapsedMs)) };
|
|
1933
|
-
};
|
|
1934
|
-
}
|
|
1935
|
-
function grow() {
|
|
1936
|
-
return (input2) => ({ size: (input2.size ?? 1) * easeOutBack2(input2.delta) });
|
|
1937
|
-
}
|
|
1938
|
-
function shrink() {
|
|
1939
|
-
return (input2) => ({ size: (input2.size ?? 1) * (1 - input2.delta * input2.delta) });
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
// src/animations/object-motion.ts
|
|
1943
|
-
var LABEL_STYLE2 = [
|
|
1944
|
-
"position:absolute",
|
|
1945
|
-
"inset:0",
|
|
1946
|
-
"display:flex",
|
|
1947
|
-
"align-items:center",
|
|
1948
|
-
"justify-content:center",
|
|
1949
|
-
"pointer-events:none",
|
|
1950
|
-
"font:700 1.6rem/1 system-ui,sans-serif",
|
|
1951
|
-
"letter-spacing:0.02em",
|
|
1952
|
-
"color:rgba(255,255,255,0.9)",
|
|
1953
|
-
"text-shadow:0 1px 10px rgba(0,0,0,0.6)",
|
|
1954
|
-
"z-index:1"
|
|
1955
|
-
].join(";");
|
|
1956
|
-
var WORLD_UP = { x: 0, y: 1, z: 0 };
|
|
1957
|
-
var DEFAULT_INTRO_MS = 2100;
|
|
1958
|
-
var DEFAULT_OUTRO_MS = 2100;
|
|
1959
|
-
var BANK_GAIN = 26;
|
|
1960
|
-
var BANK_LIMIT = 0.7;
|
|
1961
|
-
var BANK_SMOOTH = 0.12;
|
|
1962
|
-
var SAMPLE_MS = 8;
|
|
1963
|
-
var FACE_FORWARD = {
|
|
1964
|
-
"+x": (v) => v,
|
|
1965
|
-
"-x": (v) => ({ x: -v.x, y: v.y, z: -v.z }),
|
|
1966
|
-
"+z": (v) => ({ x: v.z, y: v.y, z: -v.x }),
|
|
1967
|
-
"-z": (v) => ({ x: -v.z, y: v.y, z: v.x }),
|
|
1968
|
-
"+y": (v) => ({ x: v.y, y: -v.x, z: v.z }),
|
|
1969
|
-
"-y": (v) => ({ x: -v.y, y: v.x, z: v.z })
|
|
1970
|
-
};
|
|
1971
|
-
function add2(a, b) {
|
|
1972
|
-
return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
|
|
1973
|
-
}
|
|
1974
|
-
function resolveMesh2(mesh) {
|
|
1975
|
-
return typeof mesh === "function" ? mesh() : mesh;
|
|
1976
|
-
}
|
|
1977
|
-
function applyColor2(mesh, color) {
|
|
1978
|
-
return { vertices: mesh.vertices, faces: mesh.faces.map((face) => ({ ...face, color })) };
|
|
1979
|
-
}
|
|
1980
|
-
function faceForward(mesh, facing) {
|
|
1981
|
-
if (facing === "+x") return mesh;
|
|
1982
|
-
const turn = FACE_FORWARD[facing];
|
|
1983
|
-
return { vertices: mesh.vertices.map(turn), faces: mesh.faces };
|
|
1984
|
-
}
|
|
1985
|
-
function centerAndScaleMesh(mesh, targetSize) {
|
|
1986
|
-
let minX = Infinity;
|
|
1987
|
-
let minY = Infinity;
|
|
1988
|
-
let minZ = Infinity;
|
|
1989
|
-
let maxX = -Infinity;
|
|
1990
|
-
let maxY = -Infinity;
|
|
1991
|
-
let maxZ = -Infinity;
|
|
1992
|
-
for (const vertex of mesh.vertices) {
|
|
1993
|
-
minX = Math.min(minX, vertex.x);
|
|
1994
|
-
minY = Math.min(minY, vertex.y);
|
|
1995
|
-
minZ = Math.min(minZ, vertex.z);
|
|
1996
|
-
maxX = Math.max(maxX, vertex.x);
|
|
1997
|
-
maxY = Math.max(maxY, vertex.y);
|
|
1998
|
-
maxZ = Math.max(maxZ, vertex.z);
|
|
3415
|
+
slotAngle(index) {
|
|
3416
|
+
const launched = Math.max(1, this.blends.filter((blend) => blend > 0).length);
|
|
3417
|
+
return TWO_PI * Math.min(index, launched - 1) / launched;
|
|
1999
3418
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
z: Math.atan2(fwd.y, fwd.x)
|
|
2026
|
-
};
|
|
2027
|
-
}
|
|
2028
|
-
function rotationMatrix(x, y, z) {
|
|
2029
|
-
return multiply(rotationZ(z), multiply(rotationY(y), rotationX(x)));
|
|
2030
|
-
}
|
|
2031
|
-
function eulerFromRotationMatrix(matrix) {
|
|
2032
|
-
const sy = Math.hypot(matrix[0], matrix[1]);
|
|
2033
|
-
if (sy > 1e-6) {
|
|
3419
|
+
baseAngleAt(t) {
|
|
3420
|
+
let angle = -TWO_PI * (t - this.enterAt) / this.orbitPeriodMs;
|
|
3421
|
+
if (this.allOutAt !== Infinity) {
|
|
3422
|
+
const u = clamp014((t - this.allOutAt - EXTRA_PAUSE_MS) / EXTRA_SPIN_MS);
|
|
3423
|
+
angle -= TWO_PI * easeInOutCubic(u);
|
|
3424
|
+
}
|
|
3425
|
+
return angle;
|
|
3426
|
+
}
|
|
3427
|
+
reenterStart() {
|
|
3428
|
+
return this.allOutAt + EXTRA_PAUSE_MS + EXTRA_SPIN_MS;
|
|
3429
|
+
}
|
|
3430
|
+
miniSample(index, t) {
|
|
3431
|
+
const blend = this.blends[index];
|
|
3432
|
+
if (blend <= 0) return void 0;
|
|
3433
|
+
let radial = easeOutCubic(blend);
|
|
3434
|
+
let scale2 = MINI_SCALE * easeOutBack(blend);
|
|
3435
|
+
if (this.allOutAt !== Infinity) {
|
|
3436
|
+
const start = this.reenterStart() + index * REENTER_STAGGER_MS;
|
|
3437
|
+
const pull = easeInCubic(clamp014((t - start) / REENTER_MS));
|
|
3438
|
+
if (pull >= 1) return void 0;
|
|
3439
|
+
radial *= 1 - pull;
|
|
3440
|
+
scale2 *= 1 - pull;
|
|
3441
|
+
}
|
|
3442
|
+
const angle = this.baseAngleAt(t) + this.offsets[index];
|
|
3443
|
+
const flat = Math.sin(angle) * ORBIT_RADIUS * radial;
|
|
2034
3444
|
return {
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
3445
|
+
position: {
|
|
3446
|
+
x: Math.cos(angle) * ORBIT_RADIUS * radial,
|
|
3447
|
+
y: flat * Math.cos(TILT),
|
|
3448
|
+
z: flat * Math.sin(TILT)
|
|
3449
|
+
},
|
|
3450
|
+
scale: scale2
|
|
2038
3451
|
};
|
|
2039
3452
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
3453
|
+
centerScale(now, t) {
|
|
3454
|
+
if (this.allOutAt !== Infinity) {
|
|
3455
|
+
const w = clamp014((now - this.reenterStart() - CENTER_POP_OUT_AT) / CENTER_POP_OUT_MS);
|
|
3456
|
+
if (w >= 1) {
|
|
3457
|
+
this.finished = true;
|
|
3458
|
+
return 0;
|
|
3459
|
+
}
|
|
3460
|
+
if (w > 0) {
|
|
3461
|
+
return CENTER_SCALE * (w < 0.35 ? 1 + 0.18 * easeOutQuad(w / 0.35) : 1.18 * (1 - easeInQuad((w - 0.35) / 0.65)));
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return CENTER_SCALE * easeOutBack(clamp014(t / CENTER_POP_MS));
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
|
|
3468
|
+
// src/animations/grid-assembly.ts
|
|
3469
|
+
var GRID = 5;
|
|
3470
|
+
var COUNT = GRID * GRID;
|
|
3471
|
+
var CAMERA_Z2 = 4;
|
|
3472
|
+
var FOV = 55 * Math.PI / 180;
|
|
3473
|
+
var TWO_PI2 = Math.PI * 2;
|
|
3474
|
+
var INTRO_MS = 900;
|
|
3475
|
+
var INTRO_STAGGER_MS = 60;
|
|
3476
|
+
var GATE_DOCK = 0.35;
|
|
3477
|
+
var GATE_UNDOCK = 0.65;
|
|
3478
|
+
var EXIT_HURRY2 = 2.5;
|
|
3479
|
+
var SPIN_EVERY_MS = 2e3;
|
|
3480
|
+
var SPIN_MS = 380;
|
|
3481
|
+
var SPIN_STAGGER_MS = 80;
|
|
3482
|
+
var HOLD_MS = 1e3;
|
|
3483
|
+
var COLLAPSE_MS = 700;
|
|
3484
|
+
var POP_MS = 170;
|
|
3485
|
+
var LABEL_FADE_MS = 600;
|
|
3486
|
+
var DEFAULT_MESHES = [
|
|
3487
|
+
() => cube(1, ["#60a5fa", "#3b82f6", "#2563eb", "#38bdf8", "#0ea5e9", "#1d4ed8"]),
|
|
3488
|
+
() => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
|
|
3489
|
+
() => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
|
|
3490
|
+
() => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
|
|
3491
|
+
() => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
|
|
3492
|
+
];
|
|
3493
|
+
function clamp015(value) {
|
|
2052
3494
|
return Math.max(0, Math.min(1, value));
|
|
2053
3495
|
}
|
|
2054
|
-
function
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
function resolveDirection2(velocity, fallback) {
|
|
2058
|
-
return Math.hypot(velocity.x, velocity.y, velocity.z) > 1e-6 ? normalize(velocity) : fallback;
|
|
2059
|
-
}
|
|
2060
|
-
function resolveTransition(config, fallback, durationMs) {
|
|
2061
|
-
if (!config) return { transition: fallback, durationMs };
|
|
2062
|
-
if (typeof config === "function") return { transition: config, durationMs };
|
|
2063
|
-
return { transition: config.transition, durationMs: Math.max(0, config.durationMs ?? durationMs) };
|
|
3496
|
+
function smooth01(value) {
|
|
3497
|
+
const x = clamp015(value);
|
|
3498
|
+
return x * x * (3 - 2 * x);
|
|
2064
3499
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
this.
|
|
3500
|
+
function resolveMesh3(mesh) {
|
|
3501
|
+
return typeof mesh === "function" ? mesh() : mesh;
|
|
3502
|
+
}
|
|
3503
|
+
function wrapAngle(angle) {
|
|
3504
|
+
const wrapped = angle - TWO_PI2 * Math.floor(angle / TWO_PI2);
|
|
3505
|
+
return wrapped > Math.PI ? wrapped - TWO_PI2 : wrapped;
|
|
3506
|
+
}
|
|
3507
|
+
function hash01(index, salt) {
|
|
3508
|
+
let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
|
|
3509
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
3510
|
+
h ^= h >>> 16;
|
|
3511
|
+
return (h >>> 0) / 4294967296;
|
|
3512
|
+
}
|
|
3513
|
+
var GridAssemblyAnimation = class {
|
|
3514
|
+
constructor(options = {}) {
|
|
3515
|
+
this.handles = [];
|
|
3516
|
+
this.blends = new Array(COUNT).fill(0);
|
|
3517
|
+
this.dockedAt = new Array(COUNT).fill(Infinity);
|
|
3518
|
+
this.tumbleX = [];
|
|
3519
|
+
this.tumbleY = [];
|
|
3520
|
+
this.fades = [];
|
|
3521
|
+
this.slots = [];
|
|
3522
|
+
this.aspect = 16 / 9;
|
|
3523
|
+
this.enterAt = Infinity;
|
|
3524
|
+
this.exitAt = Infinity;
|
|
3525
|
+
this.allDockedAt = Infinity;
|
|
3526
|
+
this.collapseAt = Infinity;
|
|
3527
|
+
this.lastNow = 0;
|
|
3528
|
+
this.popFading = false;
|
|
3529
|
+
this.finished = false;
|
|
3530
|
+
const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
|
|
3531
|
+
this.meshes = sources.map(resolveMesh3);
|
|
3532
|
+
this.size = options.size ?? 0.34;
|
|
3533
|
+
this.orbitPeriodMs = options.orbitPeriodMs ?? 9e3;
|
|
3534
|
+
this.dockMs = options.dockMs ?? 800;
|
|
2081
3535
|
this.backend = options.backend;
|
|
2082
|
-
this.
|
|
2083
|
-
this.
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
y: rotation?.spinY ?? 0,
|
|
2093
|
-
z: rotation?.spinZ ?? 0
|
|
2094
|
-
};
|
|
2095
|
-
this.hasExtraRotation = this.rotationOffset.x !== 0 || this.rotationOffset.y !== 0 || this.rotationOffset.z !== 0 || this.rotationSpin.x !== 0 || this.rotationSpin.y !== 0 || this.rotationSpin.z !== 0;
|
|
3536
|
+
this.labelContent = options.label;
|
|
3537
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
3538
|
+
const spacing = this.size + (options.gap ?? 0.12);
|
|
3539
|
+
for (let i = 0; i < COUNT; i++) {
|
|
3540
|
+
const row = Math.floor(i / GRID);
|
|
3541
|
+
const col = i % GRID;
|
|
3542
|
+
this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
|
|
3543
|
+
this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
|
|
3544
|
+
this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
|
|
3545
|
+
}
|
|
2096
3546
|
}
|
|
2097
3547
|
mount(target) {
|
|
2098
|
-
target.style.position = "relative";
|
|
3548
|
+
if (!target.style.position) target.style.position = "relative";
|
|
2099
3549
|
const engine = new Little3dEngine({
|
|
2100
3550
|
backend: this.backend,
|
|
2101
|
-
camera: { position: { x: 0, y: 0, z:
|
|
3551
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
|
|
2102
3552
|
});
|
|
2103
|
-
for (let i = 0; i
|
|
2104
|
-
this.
|
|
2105
|
-
this.
|
|
2106
|
-
this.headings.push({ x: 1, y: 0, z: 0 });
|
|
3553
|
+
for (let i = 0; i < COUNT; i++) {
|
|
3554
|
+
this.fades.push({ mode: "one-sided", opacity: 1 });
|
|
3555
|
+
this.handles.push(engine.add(this.meshes[i % this.meshes.length], { scale: 0 }));
|
|
2107
3556
|
}
|
|
2108
3557
|
this.engine = engine;
|
|
2109
3558
|
engine.mount(target).catch((error) => {
|
|
2110
3559
|
target.textContent = error instanceof Error ? error.message : String(error);
|
|
2111
3560
|
});
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
3561
|
+
const measure = () => {
|
|
3562
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
3563
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
measure();
|
|
3567
|
+
this.observer = new ResizeObserver(measure);
|
|
3568
|
+
this.observer.observe(target);
|
|
3569
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
3570
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
2117
3571
|
}
|
|
2118
3572
|
enter(now) {
|
|
2119
|
-
if (this.
|
|
2120
|
-
this.started = true;
|
|
2121
|
-
this.introStart = now;
|
|
3573
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
2122
3574
|
}
|
|
2123
3575
|
exit(now) {
|
|
2124
|
-
if (
|
|
2125
|
-
this.outroPosition = this.motion.positionAt(now);
|
|
2126
|
-
this.outroVelocity = motionVectorAt(this.motion, now);
|
|
2127
|
-
this.outroDirection = resolveDirection2(this.outroVelocity, this.headings[0]);
|
|
2128
|
-
this.outroStart = now;
|
|
3576
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
2129
3577
|
}
|
|
2130
3578
|
isFinished() {
|
|
2131
3579
|
return this.finished;
|
|
2132
3580
|
}
|
|
2133
3581
|
render(now, frame) {
|
|
2134
3582
|
if (!this.engine || !this.label) return;
|
|
2135
|
-
if (this.
|
|
2136
|
-
this.
|
|
3583
|
+
if (this.enterAt === Infinity) {
|
|
3584
|
+
for (const handle of this.handles) handle.transform.scale = 0;
|
|
3585
|
+
this.engine.render();
|
|
3586
|
+
return;
|
|
2137
3587
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
this.banks[k] += (targetBank - this.banks[k]) * BANK_SMOOTH;
|
|
2159
|
-
euler = orientationFor(this.headings[k], this.banks[k]);
|
|
2160
|
-
}
|
|
2161
|
-
if (this.hasExtraRotation) {
|
|
2162
|
-
euler = combineLocalRotation(euler, {
|
|
2163
|
-
x: this.rotationOffset.x + this.rotationSpin.x * t,
|
|
2164
|
-
y: this.rotationOffset.y + this.rotationSpin.y * t,
|
|
2165
|
-
z: this.rotationOffset.z + this.rotationSpin.z * t
|
|
2166
|
-
});
|
|
2167
|
-
}
|
|
2168
|
-
transform2.position.x = sample.position.x;
|
|
2169
|
-
transform2.position.y = sample.position.y;
|
|
2170
|
-
transform2.position.z = sample.position.z;
|
|
2171
|
-
transform2.rotation.x = euler.x;
|
|
2172
|
-
transform2.rotation.y = euler.y;
|
|
2173
|
-
transform2.rotation.z = euler.z;
|
|
3588
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
3589
|
+
this.lastNow = now;
|
|
3590
|
+
this.updateBlends(dt, frame.progress, now);
|
|
3591
|
+
if (this.exitAt !== Infinity && this.allDockedAt !== Infinity && this.collapseAt === Infinity) {
|
|
3592
|
+
this.collapseAt = Math.max(this.exitAt, this.allDockedAt) + HOLD_MS;
|
|
3593
|
+
}
|
|
3594
|
+
if (now >= this.collapseAt) this.renderCollapse(now);
|
|
3595
|
+
else this.renderStory(now, dt);
|
|
3596
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
3597
|
+
if (this.fadeLabel) {
|
|
3598
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
3599
|
+
now,
|
|
3600
|
+
this.enterAt,
|
|
3601
|
+
LABEL_FADE_MS,
|
|
3602
|
+
this.collapseAt,
|
|
3603
|
+
COLLAPSE_MS
|
|
3604
|
+
));
|
|
3605
|
+
}
|
|
3606
|
+
if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS + POP_MS) {
|
|
3607
|
+
this.finished = true;
|
|
2174
3608
|
}
|
|
2175
|
-
this.label.textContent = frame.indeterminate ? this.labelText ?? "" : `${Math.round(frame.progress * 100)}%`;
|
|
2176
3609
|
this.engine.render();
|
|
2177
3610
|
}
|
|
2178
3611
|
destroy() {
|
|
2179
|
-
this.
|
|
3612
|
+
this.observer?.disconnect();
|
|
3613
|
+
this.observer = void 0;
|
|
3614
|
+
this.label?.container.remove();
|
|
2180
3615
|
this.label = void 0;
|
|
2181
3616
|
this.engine?.destroy();
|
|
2182
3617
|
this.engine = void 0;
|
|
2183
3618
|
this.handles.length = 0;
|
|
3619
|
+
this.fades.length = 0;
|
|
2184
3620
|
}
|
|
2185
|
-
|
|
2186
|
-
const
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
3621
|
+
updateBlends(dt, progress, now) {
|
|
3622
|
+
const exiting = this.exitAt !== Infinity;
|
|
3623
|
+
const want = exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
|
|
3624
|
+
const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
|
|
3625
|
+
for (let i = 0; i < COUNT; i++) {
|
|
3626
|
+
const target = i < want ? 1 : 0;
|
|
3627
|
+
const blend = this.blends[i];
|
|
3628
|
+
if (target > blend && (i === 0 || this.blends[i - 1] >= GATE_DOCK)) {
|
|
3629
|
+
this.blends[i] = Math.min(1, blend + rate);
|
|
3630
|
+
} else if (target < blend && (i === COUNT - 1 || this.blends[i + 1] <= GATE_UNDOCK)) {
|
|
3631
|
+
this.blends[i] = Math.max(0, blend - rate);
|
|
3632
|
+
}
|
|
3633
|
+
if (this.blends[i] >= 1) {
|
|
3634
|
+
if (this.dockedAt[i] === Infinity) this.dockedAt[i] = now;
|
|
3635
|
+
} else {
|
|
3636
|
+
this.dockedAt[i] = Infinity;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
const allDocked = want === COUNT && this.blends.every((blend) => blend >= 1);
|
|
3640
|
+
if (allDocked) {
|
|
3641
|
+
if (this.allDockedAt === Infinity) this.allDockedAt = now;
|
|
3642
|
+
} else if (!exiting) {
|
|
3643
|
+
this.allDockedAt = Infinity;
|
|
3644
|
+
}
|
|
2192
3645
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
3646
|
+
renderStory(now, dt) {
|
|
3647
|
+
const t = now - this.enterAt;
|
|
3648
|
+
const halfHeight = Math.tan(FOV / 2) * CAMERA_Z2;
|
|
3649
|
+
const halfWidth = halfHeight * this.aspect;
|
|
3650
|
+
const radius = Math.max(0.6, Math.min(halfWidth, halfHeight) - this.size * 0.55);
|
|
3651
|
+
const spawnFactor = (Math.max(halfWidth, halfHeight) * 1.25 + this.size * 2) / radius;
|
|
3652
|
+
for (let i = 0; i < COUNT; i++) {
|
|
3653
|
+
const transform2 = this.handles[i].transform;
|
|
3654
|
+
const eased = smooth01(this.blends[i]);
|
|
3655
|
+
const introT = clamp015((t - i * INTRO_STAGGER_MS) / INTRO_MS);
|
|
3656
|
+
const reach = 1 + (spawnFactor - 1) * (1 - easeOutCubic(introT));
|
|
3657
|
+
const angle = Math.PI / 2 - i / COUNT * TWO_PI2 - t / this.orbitPeriodMs * TWO_PI2;
|
|
3658
|
+
const orbitX = Math.cos(angle) * radius * reach;
|
|
3659
|
+
const orbitY = Math.sin(angle) * radius * reach;
|
|
3660
|
+
const slot = this.slots[i];
|
|
3661
|
+
transform2.position.x = orbitX + (slot.x - orbitX) * eased;
|
|
3662
|
+
transform2.position.y = orbitY + (slot.y - orbitY) * eased;
|
|
3663
|
+
transform2.position.z = 0;
|
|
3664
|
+
transform2.scale = this.size;
|
|
3665
|
+
if (this.blends[i] === 0) {
|
|
3666
|
+
this.tumbleX[i] = wrapAngle(this.tumbleX[i] + (4e-4 + 8e-4 * hash01(i, 1)) * dt);
|
|
3667
|
+
this.tumbleY[i] = wrapAngle(this.tumbleY[i] + (6e-4 + 1e-3 * hash01(i, 3)) * dt);
|
|
3668
|
+
}
|
|
3669
|
+
transform2.rotation.z = 0;
|
|
3670
|
+
if (this.blends[i] >= 1) {
|
|
3671
|
+
const phase = (t + i * SPIN_STAGGER_MS) % SPIN_EVERY_MS;
|
|
3672
|
+
const spinning = phase < SPIN_MS && now - phase >= this.dockedAt[i];
|
|
3673
|
+
transform2.rotation.x = 0;
|
|
3674
|
+
transform2.rotation.y = spinning ? TWO_PI2 * easeInOutCubic(phase / SPIN_MS) : 0;
|
|
3675
|
+
} else {
|
|
3676
|
+
const free = 1 - eased;
|
|
3677
|
+
transform2.rotation.x = this.tumbleX[i] * free;
|
|
3678
|
+
transform2.rotation.y = this.tumbleY[i] * free;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
2195
3681
|
}
|
|
2196
|
-
|
|
2197
|
-
if (!this.
|
|
2198
|
-
|
|
2199
|
-
return this.transitionSample("intro", t, this.intro, this.introStart);
|
|
3682
|
+
renderCollapse(now) {
|
|
3683
|
+
if (!this.captured) {
|
|
3684
|
+
this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
|
|
2200
3685
|
}
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
3686
|
+
const u = clamp015((now - this.collapseAt) / COLLAPSE_MS);
|
|
3687
|
+
const pull = easeInCubic(u);
|
|
3688
|
+
for (let i = 0; i < COUNT; i++) {
|
|
3689
|
+
const transform2 = this.handles[i].transform;
|
|
3690
|
+
const from = this.captured[i];
|
|
3691
|
+
transform2.position.x = from.x * (1 - pull);
|
|
3692
|
+
transform2.position.y = from.y * (1 - pull);
|
|
3693
|
+
transform2.position.z = from.z * (1 - pull);
|
|
3694
|
+
transform2.scale = this.size * (1 - 0.99 * pull);
|
|
3695
|
+
}
|
|
3696
|
+
if (u >= 1) {
|
|
3697
|
+
if (!this.popFading) {
|
|
3698
|
+
this.popFading = true;
|
|
3699
|
+
for (let i = 0; i < COUNT; i++) this.handles[i].transparency = this.fades[i];
|
|
3700
|
+
}
|
|
3701
|
+
const v = clamp015((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
|
|
3702
|
+
for (let i = 0; i < COUNT; i++) {
|
|
3703
|
+
this.fades[i].opacity = 1 - v;
|
|
3704
|
+
this.handles[i].transform.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
|
|
3705
|
+
}
|
|
2204
3706
|
}
|
|
2205
|
-
return { position: this.motion.positionAt(t), size: 1 };
|
|
2206
3707
|
}
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
3708
|
+
};
|
|
3709
|
+
|
|
3710
|
+
// src/composite-animation.ts
|
|
3711
|
+
var CompositeAnimation = class {
|
|
3712
|
+
constructor(layers) {
|
|
3713
|
+
this.elements = [];
|
|
3714
|
+
this.layers = layers.map((layer) => "animation" in layer ? layer : { animation: layer });
|
|
2213
3715
|
}
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
velocity,
|
|
2223
|
-
size: 1,
|
|
2224
|
-
durationMs,
|
|
2225
|
-
elapsedMs,
|
|
2226
|
-
phase
|
|
2227
|
-
};
|
|
3716
|
+
mount(target) {
|
|
3717
|
+
target.style.position = "relative";
|
|
3718
|
+
for (const [index, layer] of this.layers.entries()) {
|
|
3719
|
+
const element = document.createElement("div");
|
|
3720
|
+
element.style.cssText = `position:absolute;inset:0;z-index:${layer.zIndex ?? index}`;
|
|
3721
|
+
target.appendChild(element);
|
|
3722
|
+
this.elements.push(element);
|
|
3723
|
+
layer.animation.mount(element);
|
|
2228
3724
|
}
|
|
2229
|
-
return {
|
|
2230
|
-
delta,
|
|
2231
|
-
position: this.outroPosition,
|
|
2232
|
-
direction: this.outroDirection,
|
|
2233
|
-
velocity: this.outroVelocity,
|
|
2234
|
-
size: 1,
|
|
2235
|
-
durationMs,
|
|
2236
|
-
elapsedMs,
|
|
2237
|
-
phase
|
|
2238
|
-
};
|
|
2239
3725
|
}
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
3726
|
+
enter(now) {
|
|
3727
|
+
for (const layer of this.layers) layer.animation.enter(now);
|
|
3728
|
+
}
|
|
3729
|
+
exit(now) {
|
|
3730
|
+
for (const layer of this.layers) layer.animation.exit(now);
|
|
3731
|
+
}
|
|
3732
|
+
render(now, frame) {
|
|
3733
|
+
for (const layer of this.layers) layer.animation.render(now, frame);
|
|
3734
|
+
}
|
|
3735
|
+
isFinished() {
|
|
3736
|
+
return this.layers.every((layer) => layer.animation.isFinished());
|
|
3737
|
+
}
|
|
3738
|
+
destroy() {
|
|
3739
|
+
for (const layer of this.layers) layer.animation.destroy();
|
|
3740
|
+
for (const element of this.elements) element.remove();
|
|
3741
|
+
this.elements.length = 0;
|
|
2246
3742
|
}
|
|
2247
3743
|
};
|
|
2248
3744
|
|
|
2249
|
-
// src/
|
|
2250
|
-
function
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
3745
|
+
// src/prefabs/spinner.ts
|
|
3746
|
+
function spinner(animation, options) {
|
|
3747
|
+
return {
|
|
3748
|
+
type: "indeterminate",
|
|
3749
|
+
animation,
|
|
3750
|
+
loop: options.loop,
|
|
3751
|
+
periodMs: options.periodMs
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
function progressSpinner(animation, options) {
|
|
3755
|
+
return {
|
|
3756
|
+
type: "progress",
|
|
3757
|
+
animation,
|
|
3758
|
+
progress: options.progress ?? 1e-3,
|
|
3759
|
+
timeout: options.timeout,
|
|
3760
|
+
until: options.until
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
// src/prefabs/charged-orb.ts
|
|
3765
|
+
function chargedOrb(options = {}) {
|
|
3766
|
+
const particles = options.particles ?? {};
|
|
3767
|
+
const rate = particles.rate ?? 60;
|
|
3768
|
+
const orb = new ChargedOrbAnimation({
|
|
3769
|
+
backend: options.backend,
|
|
3770
|
+
...options.orb
|
|
3771
|
+
});
|
|
3772
|
+
const streams = new ParticlesAnimation({
|
|
3773
|
+
rate,
|
|
3774
|
+
lifeMs: 1200,
|
|
3775
|
+
size: 0.12,
|
|
3776
|
+
speed: 0.05,
|
|
3777
|
+
colors: ["#ffffff", "#a5f3fc", "#818cf8"],
|
|
3778
|
+
texture: particles.texture ?? shineTexture(),
|
|
3779
|
+
emitter: orb.satelliteEmitter(1e3 / rate),
|
|
3780
|
+
outroMs: orb.outroEmitMs,
|
|
3781
|
+
seed: 5,
|
|
3782
|
+
backend: options.backend,
|
|
3783
|
+
...particles,
|
|
3784
|
+
label: options.label ?? particles.label,
|
|
3785
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3786
|
+
});
|
|
3787
|
+
return progressSpinner(new CompositeAnimation([orb, streams]), options);
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// src/motion/figure-eight.ts
|
|
3791
|
+
var LOOP_X = 1.5;
|
|
3792
|
+
var LOOP_Y = 1;
|
|
3793
|
+
var LOOP_Z = 1.05;
|
|
3794
|
+
function figureEightMotion(options = {}) {
|
|
3795
|
+
const size = options.size ?? 1;
|
|
3796
|
+
const periodMs = options.periodMs ?? 3600;
|
|
2257
3797
|
return {
|
|
2258
3798
|
positionAt(t) {
|
|
2259
|
-
const
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
3799
|
+
const a = t / periodMs * Math.PI * 2;
|
|
3800
|
+
return {
|
|
3801
|
+
x: size * LOOP_X * Math.sin(a),
|
|
3802
|
+
y: size * LOOP_Y * Math.sin(a) * Math.cos(a),
|
|
3803
|
+
z: size * LOOP_Z * Math.cos(a)
|
|
3804
|
+
};
|
|
2263
3805
|
}
|
|
2264
3806
|
};
|
|
2265
3807
|
}
|
|
2266
3808
|
|
|
3809
|
+
// src/prefabs/crystal-comet.ts
|
|
3810
|
+
function crystalComet(options = {}) {
|
|
3811
|
+
const motion = options.object?.motion ?? figureEightMotion({ size: 0.66, periodMs: 7200 });
|
|
3812
|
+
const particles = options.particles ?? {};
|
|
3813
|
+
const object = new ObjectMotionAnimation({
|
|
3814
|
+
mesh: () => tetrahedron(1, ["#f0f9ff", "#7dd3fc", "#818cf8", "#e879f9"]),
|
|
3815
|
+
motion,
|
|
3816
|
+
size: 0.42,
|
|
3817
|
+
rotation: { spinX: 2e-3, spinY: 3e-3 },
|
|
3818
|
+
backend: options.backend,
|
|
3819
|
+
...options.object,
|
|
3820
|
+
label: options.object?.label
|
|
3821
|
+
});
|
|
3822
|
+
const animation = new CompositeAnimation([
|
|
3823
|
+
new ParticlesAnimation({
|
|
3824
|
+
rate: 44,
|
|
3825
|
+
lifeMs: 2300,
|
|
3826
|
+
size: 0.25,
|
|
3827
|
+
speed: 0.08,
|
|
3828
|
+
colors: ["#ffffff", "#bae6fd", "#818cf8"],
|
|
3829
|
+
texture: particles.texture ?? shineTexture(),
|
|
3830
|
+
emitter: object.trailEmitter(),
|
|
3831
|
+
outroMs: object.outroDurationMs,
|
|
3832
|
+
seed: 28,
|
|
3833
|
+
backend: options.backend,
|
|
3834
|
+
...particles,
|
|
3835
|
+
label: options.label ?? particles.label ?? "Polishing pixels",
|
|
3836
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3837
|
+
}),
|
|
3838
|
+
object
|
|
3839
|
+
]);
|
|
3840
|
+
return spinner(animation, options);
|
|
3841
|
+
}
|
|
3842
|
+
|
|
2267
3843
|
// src/motion/square.ts
|
|
2268
3844
|
function squareMotion(options = {}) {
|
|
2269
3845
|
const half = (options.size ?? 2.4) / 2;
|
|
@@ -2298,23 +3874,194 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
2298
3874
|
};
|
|
2299
3875
|
}
|
|
2300
3876
|
|
|
2301
|
-
// src/
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
3877
|
+
// src/prefabs/ghost-train.ts
|
|
3878
|
+
function ghostTrain(options = {}) {
|
|
3879
|
+
const motion = options.object?.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
3880
|
+
const particles = options.particles ?? {};
|
|
3881
|
+
const object = new ObjectMotionAnimation({
|
|
3882
|
+
mesh: () => cube(1, ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"]),
|
|
3883
|
+
motion,
|
|
3884
|
+
size: 0.3,
|
|
3885
|
+
transparency: { mode: "two-sided", opacity: 0.55 },
|
|
3886
|
+
tail: { count: 4, gapMs: 240 },
|
|
3887
|
+
backend: options.backend,
|
|
3888
|
+
...options.object,
|
|
3889
|
+
label: options.object?.label
|
|
3890
|
+
});
|
|
3891
|
+
const trail = new ParticlesAnimation({
|
|
3892
|
+
rate: 30,
|
|
3893
|
+
lifeMs: 1700,
|
|
3894
|
+
size: 0.13,
|
|
3895
|
+
speed: 0.07,
|
|
3896
|
+
colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
|
|
3897
|
+
texture: particles.texture ?? starTexture(),
|
|
3898
|
+
emitter: object.trailEmitter(),
|
|
3899
|
+
outroMs: object.outroDurationMs,
|
|
3900
|
+
seed: 17,
|
|
3901
|
+
backend: options.backend,
|
|
3902
|
+
...particles,
|
|
3903
|
+
label: options.label ?? particles.label,
|
|
3904
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3905
|
+
});
|
|
3906
|
+
return progressSpinner(new CompositeAnimation([trail, object]), options);
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
// src/prefabs/grid-assembly.ts
|
|
3910
|
+
function gridAssembly(options = {}) {
|
|
3911
|
+
return progressSpinner(
|
|
3912
|
+
new GridAssemblyAnimation({
|
|
3913
|
+
backend: options.backend,
|
|
3914
|
+
label: options.label,
|
|
3915
|
+
fadeLabel: options.fadeLabel,
|
|
3916
|
+
...options.assembly
|
|
3917
|
+
}),
|
|
3918
|
+
options
|
|
3919
|
+
);
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
// src/prefabs/monochrome-streak.ts
|
|
3923
|
+
function monochromeStreak(options = {}) {
|
|
3924
|
+
const particles = options.particles ?? {};
|
|
3925
|
+
return spinner(new ParticlesAnimation({
|
|
3926
|
+
rate: 70,
|
|
3927
|
+
lifeMs: 2800,
|
|
3928
|
+
size: 0.38,
|
|
3929
|
+
speed: 1.35,
|
|
3930
|
+
direction: { x: 0, y: 1, z: 0 },
|
|
3931
|
+
spread: 0.62,
|
|
3932
|
+
gravity: { x: 0, y: -1.45, z: 0 },
|
|
3933
|
+
colors: ["#fff", "#000"],
|
|
3934
|
+
texture: particles.texture ?? streakTexture(),
|
|
3935
|
+
spin: 0,
|
|
3936
|
+
alignToMotion: true,
|
|
3937
|
+
seed: 37,
|
|
3938
|
+
backend: options.backend,
|
|
3939
|
+
...particles,
|
|
3940
|
+
label: options.label ?? particles.label ?? "Loading...",
|
|
3941
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3942
|
+
}), options);
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
// src/prefabs/plane-star-trail.ts
|
|
3946
|
+
function planeStarTrail(options = {}) {
|
|
3947
|
+
const motion = options.object?.motion ?? figureEightMotion({ size: 0.72, periodMs: 6200 });
|
|
3948
|
+
const particles = options.particles ?? {};
|
|
3949
|
+
const object = new ObjectMotionAnimation({
|
|
3950
|
+
mesh: planeMesh,
|
|
3951
|
+
motion,
|
|
3952
|
+
size: 0.48,
|
|
3953
|
+
backend: options.backend,
|
|
3954
|
+
...options.object,
|
|
3955
|
+
label: options.object?.label
|
|
3956
|
+
});
|
|
3957
|
+
const animation = new CompositeAnimation([
|
|
3958
|
+
new ParticlesAnimation({
|
|
3959
|
+
rate: 34,
|
|
3960
|
+
lifeMs: 1900,
|
|
3961
|
+
size: 0.16,
|
|
3962
|
+
speed: 0.11,
|
|
3963
|
+
colors: ["#fde047", "#f472b6", "#7dd3fc"],
|
|
3964
|
+
texture: particles.texture ?? starTexture(),
|
|
3965
|
+
emitter: object.trailEmitter(),
|
|
3966
|
+
outroMs: object.outroDurationMs,
|
|
3967
|
+
seed: 11,
|
|
3968
|
+
backend: options.backend,
|
|
3969
|
+
...particles,
|
|
3970
|
+
label: options.label ?? particles.label ?? "Flying in...",
|
|
3971
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3972
|
+
}),
|
|
3973
|
+
object
|
|
3974
|
+
]);
|
|
3975
|
+
return spinner(animation, options);
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
// src/prefabs/pulsing-starfield.ts
|
|
3979
|
+
function pulsingLabel() {
|
|
3980
|
+
const label = document.createElement("div");
|
|
3981
|
+
label.innerHTML = `<style>
|
|
3982
|
+
@keyframes spinner-prefab-pulse { 0%,100% { color:#fff; transform:scale(1); } 50% { color:#93c5fd; transform:scale(1.06); } }
|
|
3983
|
+
</style><div style="animation:spinner-prefab-pulse 2.4s ease-in-out infinite;font-size:2rem">Loading the good stuff</div>`;
|
|
3984
|
+
return label;
|
|
3985
|
+
}
|
|
3986
|
+
function pulsingStarfield(options = {}) {
|
|
3987
|
+
const particles = options.particles ?? {};
|
|
3988
|
+
return spinner(new ParticlesAnimation({
|
|
3989
|
+
rate: 48,
|
|
3990
|
+
lifeMs: 4200,
|
|
3991
|
+
size: 0.3,
|
|
3992
|
+
speed: 0.34,
|
|
3993
|
+
colors: ["#ffffff", "#dbeafe", "#93c5fd", "#c4b5fd"],
|
|
3994
|
+
texture: particles.texture ?? shineTexture(),
|
|
3995
|
+
seed: 71,
|
|
3996
|
+
backend: options.backend,
|
|
3997
|
+
...particles,
|
|
3998
|
+
label: options.label ?? particles.label ?? pulsingLabel(),
|
|
3999
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
4000
|
+
}), options);
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
// src/motion/circle.ts
|
|
4004
|
+
function circleMotion(options = {}) {
|
|
4005
|
+
const radius = options.radius ?? 1.3;
|
|
4006
|
+
const periodMs = options.periodMs ?? 3e3;
|
|
4007
|
+
const tilt = options.tilt ?? 0.5;
|
|
4008
|
+
const direction = options.direction ?? 1;
|
|
4009
|
+
const cosTilt = Math.cos(tilt);
|
|
4010
|
+
const sinTilt = Math.sin(tilt);
|
|
2308
4011
|
return {
|
|
2309
4012
|
positionAt(t) {
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
4013
|
+
const angle = direction * t / periodMs * Math.PI * 2;
|
|
4014
|
+
const x = radius * Math.cos(angle);
|
|
4015
|
+
const flatY = radius * Math.sin(angle);
|
|
4016
|
+
return { x, y: flatY * cosTilt, z: flatY * sinTilt };
|
|
4017
|
+
}
|
|
4018
|
+
};
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
// src/prefabs/rocket-launch.ts
|
|
4022
|
+
var launchUp = ({ delta, position, velocity, durationMs }) => {
|
|
4023
|
+
const coast = durationMs * delta * (1 - 0.5 * delta);
|
|
4024
|
+
const climb = 5.5 * easeInCubic(delta);
|
|
4025
|
+
return {
|
|
4026
|
+
position: {
|
|
4027
|
+
x: position.x + (velocity?.x ?? 0) * coast,
|
|
4028
|
+
y: position.y + (velocity?.y ?? 0) * coast + climb,
|
|
4029
|
+
z: position.z + (velocity?.z ?? 0) * coast
|
|
2316
4030
|
}
|
|
2317
4031
|
};
|
|
4032
|
+
};
|
|
4033
|
+
function rocketLaunch(options = {}) {
|
|
4034
|
+
const motion = options.object?.motion ?? circleMotion({ radius: 0.55, periodMs: 7e3, tilt: 0.15 });
|
|
4035
|
+
const particles = options.particles ?? {};
|
|
4036
|
+
const object = new ObjectMotionAnimation({
|
|
4037
|
+
mesh: () => pyramid(1, ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"]),
|
|
4038
|
+
motion,
|
|
4039
|
+
size: 0.44,
|
|
4040
|
+
facing: "+y",
|
|
4041
|
+
outro: { transition: launchUp, durationMs: 1300 },
|
|
4042
|
+
backend: options.backend,
|
|
4043
|
+
...options.object,
|
|
4044
|
+
label: options.object?.label
|
|
4045
|
+
});
|
|
4046
|
+
const exhaust = new ParticlesAnimation({
|
|
4047
|
+
rate: 46,
|
|
4048
|
+
lifeMs: 1100,
|
|
4049
|
+
size: 0.18,
|
|
4050
|
+
speed: 0.5,
|
|
4051
|
+
direction: { x: 0, y: -1, z: 0 },
|
|
4052
|
+
spread: 0.3,
|
|
4053
|
+
gravity: { x: 0, y: -0.6, z: 0 },
|
|
4054
|
+
colors: ["#fde047", "#fb923c", "#ef4444", "#fef3c7"],
|
|
4055
|
+
texture: particles.texture ?? shineTexture(),
|
|
4056
|
+
emitter: object.trailEmitter(),
|
|
4057
|
+
outroMs: object.outroDurationMs,
|
|
4058
|
+
seed: 9,
|
|
4059
|
+
backend: options.backend,
|
|
4060
|
+
...particles,
|
|
4061
|
+
label: options.label ?? particles.label,
|
|
4062
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
4063
|
+
});
|
|
4064
|
+
return progressSpinner(new CompositeAnimation([exhaust, object]), options);
|
|
2318
4065
|
}
|
|
2319
4066
|
|
|
2320
4067
|
// src/motion/wander.ts
|
|
@@ -2358,16 +4105,69 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
2358
4105
|
};
|
|
2359
4106
|
}
|
|
2360
4107
|
|
|
4108
|
+
// src/prefabs/star-swarm.ts
|
|
4109
|
+
function starSwarm(options = {}) {
|
|
4110
|
+
const particles = options.particles ?? {};
|
|
4111
|
+
const emitter = particles.emitter ?? wanderMotion({
|
|
4112
|
+
bounds: { x: 1.1, y: 0.72, z: 0.35 },
|
|
4113
|
+
periodMs: 7200,
|
|
4114
|
+
seed: 19
|
|
4115
|
+
});
|
|
4116
|
+
return spinner(new ParticlesAnimation({
|
|
4117
|
+
rate: 38,
|
|
4118
|
+
lifeMs: 2600,
|
|
4119
|
+
size: 0.15,
|
|
4120
|
+
speed: 0.17,
|
|
4121
|
+
colors: ["#fef08a", "#f9a8d4", "#a5f3fc"],
|
|
4122
|
+
texture: particles.texture ?? starTexture(),
|
|
4123
|
+
emitter,
|
|
4124
|
+
seed: 91,
|
|
4125
|
+
backend: options.backend,
|
|
4126
|
+
...particles,
|
|
4127
|
+
label: options.label ?? particles.label ?? "Loading...",
|
|
4128
|
+
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
4129
|
+
}), options);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
// <stdin>
|
|
4133
|
+
init_webgl_textured();
|
|
4134
|
+
init_webgpu_textured();
|
|
4135
|
+
|
|
2361
4136
|
// src/engines/little-3d-engine/loaders/obj.ts
|
|
2362
|
-
var
|
|
4137
|
+
var DEFAULT_COLORS12 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
4138
|
+
function channelToHex(value) {
|
|
4139
|
+
const channel = Number.parseFloat(value);
|
|
4140
|
+
if (!Number.isFinite(channel)) return void 0;
|
|
4141
|
+
return Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0");
|
|
4142
|
+
}
|
|
4143
|
+
function parseMtlColors(text) {
|
|
4144
|
+
const colors = /* @__PURE__ */ new Map();
|
|
4145
|
+
let material;
|
|
4146
|
+
for (const line of text.split("\n")) {
|
|
4147
|
+
const trimmed = line.trim();
|
|
4148
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
4149
|
+
const parts = trimmed.split(/\s+/);
|
|
4150
|
+
if (parts[0] === "newmtl") {
|
|
4151
|
+
material = parts.slice(1).join(" ");
|
|
4152
|
+
} else if (parts[0] === "Kd" && material) {
|
|
4153
|
+
const channels = parts.slice(1, 4).map(channelToHex);
|
|
4154
|
+
if (channels.length === 3 && channels.every((channel) => channel !== void 0)) {
|
|
4155
|
+
colors.set(material, `#${channels.join("")}`);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
return colors;
|
|
4160
|
+
}
|
|
2363
4161
|
function resolveIndex(token, vertexCount) {
|
|
2364
4162
|
const n = parseInt(token, 10);
|
|
2365
4163
|
return n < 0 ? vertexCount + n : n - 1;
|
|
2366
4164
|
}
|
|
2367
4165
|
function parseObj(text, options = {}) {
|
|
2368
|
-
const colors = options.colors ??
|
|
4166
|
+
const colors = options.colors ?? DEFAULT_COLORS12;
|
|
4167
|
+
const materialColors = options.useMtlColors && options.mtl ? parseMtlColors(options.mtl) : void 0;
|
|
2369
4168
|
const vertices = [];
|
|
2370
4169
|
const faces = [];
|
|
4170
|
+
let material;
|
|
2371
4171
|
for (const line of text.split("\n")) {
|
|
2372
4172
|
const trimmed = line.trim();
|
|
2373
4173
|
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
@@ -2379,6 +4179,8 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
2379
4179
|
y: parseFloat(parts[2]),
|
|
2380
4180
|
z: parseFloat(parts[3])
|
|
2381
4181
|
});
|
|
4182
|
+
} else if (keyword === "usemtl") {
|
|
4183
|
+
material = parts.slice(1).join(" ");
|
|
2382
4184
|
} else if (keyword === "f") {
|
|
2383
4185
|
const indices = [];
|
|
2384
4186
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -2386,7 +4188,9 @@ fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
|
2386
4188
|
indices.push(resolveIndex(vertexToken, vertices.length));
|
|
2387
4189
|
}
|
|
2388
4190
|
if (indices.length >= 3) {
|
|
2389
|
-
|
|
4191
|
+
const mtlColor = material ? materialColors?.get(material) : void 0;
|
|
4192
|
+
const color = mtlColor ?? (materialColors ? colors[0] ?? "#888888" : colors[faces.length % colors.length]);
|
|
4193
|
+
faces.push({ indices, color });
|
|
2390
4194
|
}
|
|
2391
4195
|
}
|
|
2392
4196
|
}
|