3d-spinner 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animations/ghost-train.d.ts +72 -0
- package/dist/animations/ghost-train.js +247 -0
- package/dist/animations/grid-assembly.d.ts +11 -8
- package/dist/animations/grid-assembly.js +44 -30
- package/dist/animations/rocket-launch.d.ts +58 -0
- package/dist/animations/rocket-launch.js +375 -0
- package/dist/cjs/animations/grid-assembly.cjs +28 -146
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +22 -10
- package/dist/cjs/prefabs/prefabs.cjs +635 -169
- package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +14 -2
- package/dist/engines/little-3d-engine/textures/dynamic/star.js +26 -11
- package/dist/prefabs/ghost-train.d.ts +5 -4
- package/dist/prefabs/ghost-train.js +17 -24
- package/dist/prefabs/rocket-launch.d.ts +7 -5
- package/dist/prefabs/rocket-launch.js +10 -53
- package/dist/umd/spinner.global.js +623 -105
- package/dist/umd/spinner.global.min.js +10 -10
- package/package.json +2 -2
|
@@ -1693,43 +1693,9 @@ function tetrahedron(size = 1, colors = DEFAULT_COLORS3) {
|
|
|
1693
1693
|
return { vertices, faces };
|
|
1694
1694
|
}
|
|
1695
1695
|
|
|
1696
|
-
// src/engines/little-3d-engine/shapes/primitives/octahedron.ts
|
|
1697
|
-
var DEFAULT_COLORS4 = [
|
|
1698
|
-
"#3b82f6",
|
|
1699
|
-
"#8b5cf6",
|
|
1700
|
-
"#ec4899",
|
|
1701
|
-
"#f59e0b",
|
|
1702
|
-
"#10b981",
|
|
1703
|
-
"#ef4444",
|
|
1704
|
-
"#06b6d4",
|
|
1705
|
-
"#eab308"
|
|
1706
|
-
];
|
|
1707
|
-
function octahedron(size = 1, colors = DEFAULT_COLORS4) {
|
|
1708
|
-
const r = size / 2;
|
|
1709
|
-
const vertices = [
|
|
1710
|
-
{ x: r, y: 0, z: 0 },
|
|
1711
|
-
{ x: -r, y: 0, z: 0 },
|
|
1712
|
-
{ x: 0, y: r, z: 0 },
|
|
1713
|
-
{ x: 0, y: -r, z: 0 },
|
|
1714
|
-
{ x: 0, y: 0, z: r },
|
|
1715
|
-
{ x: 0, y: 0, z: -r }
|
|
1716
|
-
];
|
|
1717
|
-
const faces = [
|
|
1718
|
-
{ indices: [4, 0, 2], color: colors[0 % colors.length] },
|
|
1719
|
-
{ indices: [4, 2, 1], color: colors[1 % colors.length] },
|
|
1720
|
-
{ indices: [4, 1, 3], color: colors[2 % colors.length] },
|
|
1721
|
-
{ indices: [4, 3, 0], color: colors[3 % colors.length] },
|
|
1722
|
-
{ indices: [5, 2, 0], color: colors[4 % colors.length] },
|
|
1723
|
-
{ indices: [5, 1, 2], color: colors[5 % colors.length] },
|
|
1724
|
-
{ indices: [5, 3, 1], color: colors[6 % colors.length] },
|
|
1725
|
-
{ indices: [5, 0, 3], color: colors[7 % colors.length] }
|
|
1726
|
-
];
|
|
1727
|
-
return { vertices, faces };
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
1696
|
// src/engines/little-3d-engine/shapes/primitives/pyramid.ts
|
|
1731
|
-
var
|
|
1732
|
-
function pyramid(size = 1, colors =
|
|
1697
|
+
var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
|
|
1698
|
+
function pyramid(size = 1, colors = DEFAULT_COLORS4) {
|
|
1733
1699
|
const h = size / 2;
|
|
1734
1700
|
const vertices = [
|
|
1735
1701
|
{ x: -h, y: -h, z: h },
|
|
@@ -1750,7 +1716,7 @@ function pyramid(size = 1, colors = DEFAULT_COLORS5) {
|
|
|
1750
1716
|
|
|
1751
1717
|
// src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
|
|
1752
1718
|
init_geometry();
|
|
1753
|
-
var
|
|
1719
|
+
var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1754
1720
|
var T = (1 + Math.sqrt(5)) / 2;
|
|
1755
1721
|
var SEED_VERTICES = [
|
|
1756
1722
|
{ x: -1, y: T, z: 0 },
|
|
@@ -1788,7 +1754,7 @@ var SEED_FACES = [
|
|
|
1788
1754
|
[8, 6, 7],
|
|
1789
1755
|
[9, 8, 1]
|
|
1790
1756
|
];
|
|
1791
|
-
function icosphere(size = 1, detail = 1, colors =
|
|
1757
|
+
function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5) {
|
|
1792
1758
|
return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
|
|
1793
1759
|
}
|
|
1794
1760
|
|
|
@@ -1796,8 +1762,8 @@ function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS6) {
|
|
|
1796
1762
|
init_geometry();
|
|
1797
1763
|
|
|
1798
1764
|
// src/engines/little-3d-engine/shapes/complex/plane.ts
|
|
1799
|
-
var
|
|
1800
|
-
function planeMesh(colors =
|
|
1765
|
+
var DEFAULT_COLORS6 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
|
|
1766
|
+
function planeMesh(colors = DEFAULT_COLORS6) {
|
|
1801
1767
|
return {
|
|
1802
1768
|
vertices: [
|
|
1803
1769
|
{ x: 0.9, y: 0, z: 0 },
|
|
@@ -1811,20 +1777,20 @@ function planeMesh(colors = DEFAULT_COLORS7) {
|
|
|
1811
1777
|
{ x: -0.52, y: 0.38, z: 0 }
|
|
1812
1778
|
],
|
|
1813
1779
|
faces: [
|
|
1814
|
-
{ indices: [6, 1, 0], color: colors[0] ??
|
|
1815
|
-
{ indices: [6, 2, 1], color: colors[3] ??
|
|
1816
|
-
{ indices: [6, 3, 2], color: colors[1] ??
|
|
1817
|
-
{ indices: [6, 4, 3], color: colors[2] ??
|
|
1818
|
-
{ indices: [6, 5, 4], color: colors[3] ??
|
|
1819
|
-
{ indices: [6, 0, 5], color: colors[0] ??
|
|
1820
|
-
{ indices: [7, 0, 1], color: colors[1] ??
|
|
1821
|
-
{ indices: [7, 1, 2], color: colors[2] ??
|
|
1822
|
-
{ indices: [7, 2, 3], color: colors[1] ??
|
|
1823
|
-
{ indices: [7, 3, 4], color: colors[2] ??
|
|
1824
|
-
{ indices: [7, 4, 5], color: colors[1] ??
|
|
1825
|
-
{ indices: [7, 5, 0], color: colors[2] ??
|
|
1826
|
-
{ indices: [3, 6, 8], color: colors[0] ??
|
|
1827
|
-
{ indices: [3, 8, 6], color: colors[1] ??
|
|
1780
|
+
{ indices: [6, 1, 0], color: colors[0] ?? DEFAULT_COLORS6[0] },
|
|
1781
|
+
{ indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS6[3] },
|
|
1782
|
+
{ indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS6[1] },
|
|
1783
|
+
{ indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS6[2] },
|
|
1784
|
+
{ indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS6[3] },
|
|
1785
|
+
{ indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS6[0] },
|
|
1786
|
+
{ indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS6[1] },
|
|
1787
|
+
{ indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS6[2] },
|
|
1788
|
+
{ indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS6[1] },
|
|
1789
|
+
{ indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS6[2] },
|
|
1790
|
+
{ indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS6[1] },
|
|
1791
|
+
{ indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS6[2] },
|
|
1792
|
+
{ indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS6[0] },
|
|
1793
|
+
{ indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS6[1] }
|
|
1828
1794
|
]
|
|
1829
1795
|
};
|
|
1830
1796
|
}
|
|
@@ -1839,18 +1805,30 @@ function canvasTexture(draw, size = 96) {
|
|
|
1839
1805
|
}
|
|
1840
1806
|
|
|
1841
1807
|
// src/engines/little-3d-engine/textures/dynamic/star.ts
|
|
1842
|
-
function
|
|
1808
|
+
function drawStar(ctx) {
|
|
1809
|
+
ctx.save();
|
|
1810
|
+
ctx.translate(48, 48);
|
|
1811
|
+
ctx.fillStyle = "#fff";
|
|
1812
|
+
ctx.beginPath();
|
|
1813
|
+
for (let index = 0; index < 10; index++) {
|
|
1814
|
+
const radius = index % 2 === 0 ? 43 : 16;
|
|
1815
|
+
const angle = index * Math.PI / 5 - Math.PI / 2;
|
|
1816
|
+
ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
|
|
1817
|
+
}
|
|
1818
|
+
ctx.closePath();
|
|
1819
|
+
ctx.fill();
|
|
1820
|
+
ctx.restore();
|
|
1821
|
+
}
|
|
1822
|
+
function starTexture(options = {}) {
|
|
1823
|
+
const glow = Math.max(0, options.glow ?? 0);
|
|
1843
1824
|
return canvasTexture((ctx) => {
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
const angle = index * Math.PI / 5 - Math.PI / 2;
|
|
1850
|
-
ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
|
|
1825
|
+
if (glow > 0) {
|
|
1826
|
+
ctx.save();
|
|
1827
|
+
ctx.filter = `blur(${glow}px)`;
|
|
1828
|
+
drawStar(ctx);
|
|
1829
|
+
ctx.restore();
|
|
1851
1830
|
}
|
|
1852
|
-
ctx
|
|
1853
|
-
ctx.fill();
|
|
1831
|
+
drawStar(ctx);
|
|
1854
1832
|
});
|
|
1855
1833
|
}
|
|
1856
1834
|
|
|
@@ -2034,7 +2012,7 @@ var Little3dEngine = class {
|
|
|
2034
2012
|
};
|
|
2035
2013
|
|
|
2036
2014
|
// src/animations/particles.ts
|
|
2037
|
-
var
|
|
2015
|
+
var DEFAULT_COLORS7 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
|
|
2038
2016
|
var FADE_IN_END = 0.15;
|
|
2039
2017
|
var FADE_OUT_START = 0.6;
|
|
2040
2018
|
function rand01(seed, index, salt) {
|
|
@@ -2129,7 +2107,7 @@ var ParticlesAnimation = class {
|
|
|
2129
2107
|
this.exitAt = Infinity;
|
|
2130
2108
|
this.finished = false;
|
|
2131
2109
|
this.field = particleField(options);
|
|
2132
|
-
this.colors = options.colors ??
|
|
2110
|
+
this.colors = options.colors ?? DEFAULT_COLORS7;
|
|
2133
2111
|
this.backend = options.backend;
|
|
2134
2112
|
this.texture = options.texture;
|
|
2135
2113
|
this.labelContent = options.label;
|
|
@@ -2952,46 +2930,249 @@ function squareMotion(options = {}) {
|
|
|
2952
2930
|
};
|
|
2953
2931
|
}
|
|
2954
2932
|
|
|
2933
|
+
// src/animations/ghost-train.ts
|
|
2934
|
+
var MAX_CARS = 50;
|
|
2935
|
+
var CAMERA_Z2 = 3;
|
|
2936
|
+
var FOV = 55 * Math.PI / 180;
|
|
2937
|
+
var HALF_HEIGHT = Math.tan(FOV / 2) * CAMERA_Z2;
|
|
2938
|
+
var RUN_GAP_MS = 130;
|
|
2939
|
+
var POP_MS = 320;
|
|
2940
|
+
var SAMPLE_MS2 = 8;
|
|
2941
|
+
var TURN_RATE = 0.4 * Math.PI / 180;
|
|
2942
|
+
var MAX_OUTRO_MS = 4e3;
|
|
2943
|
+
var WARP_ACCEL = 1e3;
|
|
2944
|
+
var TRAIL_OUTRO_MS = 1200;
|
|
2945
|
+
var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
|
|
2946
|
+
var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
|
|
2947
|
+
var WORLD_UP2 = { x: 0, y: 1, z: 0 };
|
|
2948
|
+
function clamp014(value) {
|
|
2949
|
+
return Math.max(0, Math.min(1, value));
|
|
2950
|
+
}
|
|
2951
|
+
function orientationFor2(forward) {
|
|
2952
|
+
const fwd = normalize(forward);
|
|
2953
|
+
let right = cross(fwd, WORLD_UP2);
|
|
2954
|
+
if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
|
|
2955
|
+
right = normalize(right);
|
|
2956
|
+
const up = cross(right, fwd);
|
|
2957
|
+
const w = normalize(cross(fwd, up));
|
|
2958
|
+
return {
|
|
2959
|
+
x: Math.atan2(cross(w, fwd).z, w.z),
|
|
2960
|
+
y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
|
|
2961
|
+
z: Math.atan2(fwd.y, fwd.x)
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
function rotateToward(from, to, maxRad) {
|
|
2965
|
+
const a = normalize(from);
|
|
2966
|
+
const b = normalize(to);
|
|
2967
|
+
const d = Math.max(-1, Math.min(1, dot(a, b)));
|
|
2968
|
+
const angle = Math.acos(d);
|
|
2969
|
+
if (angle <= maxRad || angle < 1e-4) return b;
|
|
2970
|
+
const sin = Math.sin(angle);
|
|
2971
|
+
if (sin < 1e-4) return b;
|
|
2972
|
+
const t = maxRad / angle;
|
|
2973
|
+
const w1 = Math.sin((1 - t) * angle) / sin;
|
|
2974
|
+
const w2 = Math.sin(t * angle) / sin;
|
|
2975
|
+
return normalize({ x: a.x * w1 + b.x * w2, y: a.y * w1 + b.y * w2, z: a.z * w1 + b.z * w2 });
|
|
2976
|
+
}
|
|
2977
|
+
var GhostTrainAnimation = class {
|
|
2978
|
+
constructor(options = {}) {
|
|
2979
|
+
this.cars = [];
|
|
2980
|
+
this.appear = new Array(MAX_CARS).fill(0);
|
|
2981
|
+
this.headings = new Array(MAX_CARS).fill(void 0);
|
|
2982
|
+
this.aspect = 16 / 9;
|
|
2983
|
+
this.enterAt = Infinity;
|
|
2984
|
+
this.outroAt = Infinity;
|
|
2985
|
+
this.carsAtOutro = 0;
|
|
2986
|
+
this.exitPathTime = 0;
|
|
2987
|
+
// lead car's path-time at blast-off (the escape switch point)
|
|
2988
|
+
this.exitPoint = { x: 0, y: 0, z: 0 };
|
|
2989
|
+
this.exitDir = { x: 1, y: 0, z: 0 };
|
|
2990
|
+
// shared escape direction, outward from the track
|
|
2991
|
+
this.exitSpeed = 1e-3;
|
|
2992
|
+
// path-units per path-millisecond at the switch (keeps speed continuous)
|
|
2993
|
+
this.lastNow = 0;
|
|
2994
|
+
this.finished = false;
|
|
2995
|
+
this.motion = options.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
2996
|
+
this.size = options.size ?? 0.15;
|
|
2997
|
+
this.backend = options.backend;
|
|
2998
|
+
this.labelContent = options.label;
|
|
2999
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
3000
|
+
}
|
|
3001
|
+
mount(target) {
|
|
3002
|
+
if (!target.style.position) target.style.position = "relative";
|
|
3003
|
+
const engine = new Little3dEngine({
|
|
3004
|
+
backend: this.backend,
|
|
3005
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z2 }, fov: FOV }
|
|
3006
|
+
});
|
|
3007
|
+
const mesh = cube(1, CAR_COLORS);
|
|
3008
|
+
for (let i = 0; i < MAX_CARS; i++) {
|
|
3009
|
+
this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
|
|
3010
|
+
}
|
|
3011
|
+
this.engine = engine;
|
|
3012
|
+
engine.mount(target).catch((error) => {
|
|
3013
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
3014
|
+
});
|
|
3015
|
+
const measure = () => {
|
|
3016
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
3017
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
measure();
|
|
3021
|
+
this.observer = new ResizeObserver(measure);
|
|
3022
|
+
this.observer.observe(target);
|
|
3023
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
3024
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
3025
|
+
}
|
|
3026
|
+
enter(now) {
|
|
3027
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
3028
|
+
}
|
|
3029
|
+
exit(now) {
|
|
3030
|
+
if (this.outroAt !== Infinity || this.enterAt === Infinity) return;
|
|
3031
|
+
this.outroAt = now;
|
|
3032
|
+
this.carsAtOutro = this.appear.filter((a) => a > 0.5).length;
|
|
3033
|
+
this.exitPathTime = now - this.enterAt;
|
|
3034
|
+
const from = this.motion.positionAt(this.exitPathTime);
|
|
3035
|
+
const velocity = subtract(
|
|
3036
|
+
this.motion.positionAt(this.exitPathTime + 1),
|
|
3037
|
+
this.motion.positionAt(this.exitPathTime - 1)
|
|
3038
|
+
);
|
|
3039
|
+
const speed = Math.hypot(velocity.x, velocity.y, velocity.z);
|
|
3040
|
+
this.exitPoint = from;
|
|
3041
|
+
if (speed > 1e-6) this.exitSpeed = speed / 2;
|
|
3042
|
+
this.exitDir = speed > 1e-6 ? normalize(velocity) : { x: 1, y: 0, z: 0 };
|
|
3043
|
+
}
|
|
3044
|
+
isFinished() {
|
|
3045
|
+
return this.finished;
|
|
3046
|
+
}
|
|
3047
|
+
/** Milliseconds the lead car keeps moving into the outro; feed a trail layer's `outroMs`. */
|
|
3048
|
+
get outroDurationMs() {
|
|
3049
|
+
return TRAIL_OUTRO_MS;
|
|
3050
|
+
}
|
|
3051
|
+
/**
|
|
3052
|
+
* A {@link MotionController} following the lead car's actual position, through
|
|
3053
|
+
* laps and the accelerating escape. Feed it to a particle layer's `emitter`
|
|
3054
|
+
* so the star trail stays behind the train.
|
|
3055
|
+
*/
|
|
3056
|
+
trailEmitter() {
|
|
3057
|
+
return {
|
|
3058
|
+
positionAt: (t) => this.enterAt === Infinity ? this.motion.positionAt(t) : this.pathPosition(t - this.enterAt + this.warp(t))
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
render(now, frame) {
|
|
3062
|
+
if (!this.engine || !this.label) return;
|
|
3063
|
+
for (const car of this.cars) car.transform.scale = 0;
|
|
3064
|
+
if (this.enterAt === Infinity) {
|
|
3065
|
+
this.engine.render();
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
3069
|
+
this.lastNow = now;
|
|
3070
|
+
const want = this.outroAt !== Infinity ? this.carsAtOutro : Math.min(MAX_CARS, Math.round(frame.progress * MAX_CARS));
|
|
3071
|
+
const halfWidth = HALF_HEIGHT * this.aspect;
|
|
3072
|
+
const warp = this.warp(now);
|
|
3073
|
+
let anyOnScreen = false;
|
|
3074
|
+
for (let k = 0; k < MAX_CARS; k++) {
|
|
3075
|
+
const target = k < want ? 1 : 0;
|
|
3076
|
+
this.appear[k] = clamp014(this.appear[k] + Math.sign(target - this.appear[k]) * (dt / POP_MS));
|
|
3077
|
+
if (this.appear[k] <= 0) {
|
|
3078
|
+
this.headings[k] = void 0;
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
const p = now - this.enterAt - k * RUN_GAP_MS + warp;
|
|
3082
|
+
const position = this.pathPosition(p);
|
|
3083
|
+
if (Math.abs(position.x) > halfWidth + this.size || Math.abs(position.y) > HALF_HEIGHT + this.size) {
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
const ahead = subtract(this.pathPosition(p + SAMPLE_MS2), position);
|
|
3087
|
+
const targetDir = Math.hypot(ahead.x, ahead.y, ahead.z) > 1e-5 ? ahead : this.headings[k] ?? { x: 1, y: 0, z: 0 };
|
|
3088
|
+
this.headings[k] = this.headings[k] ? rotateToward(this.headings[k], targetDir, TURN_RATE * dt) : normalize(targetDir);
|
|
3089
|
+
const orientation = orientationFor2(this.headings[k]);
|
|
3090
|
+
const transform2 = this.cars[k].transform;
|
|
3091
|
+
transform2.position.x = position.x;
|
|
3092
|
+
transform2.position.y = position.y;
|
|
3093
|
+
transform2.position.z = position.z;
|
|
3094
|
+
transform2.rotation.x = orientation.x;
|
|
3095
|
+
transform2.rotation.y = orientation.y;
|
|
3096
|
+
transform2.rotation.z = orientation.z;
|
|
3097
|
+
transform2.scale = this.size * easeOutBack(this.appear[k]);
|
|
3098
|
+
anyOnScreen = true;
|
|
3099
|
+
}
|
|
3100
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
3101
|
+
if (this.fadeLabel) {
|
|
3102
|
+
this.label.setOpacity(animationLabelOpacity(now, this.enterAt, POP_MS, this.outroAt, TRAIL_OUTRO_MS));
|
|
3103
|
+
}
|
|
3104
|
+
if (this.outroAt !== Infinity && now > this.outroAt + 300 && (!anyOnScreen || now >= this.outroAt + MAX_OUTRO_MS)) {
|
|
3105
|
+
this.finished = true;
|
|
3106
|
+
}
|
|
3107
|
+
this.engine.render();
|
|
3108
|
+
}
|
|
3109
|
+
destroy() {
|
|
3110
|
+
this.observer?.disconnect();
|
|
3111
|
+
this.observer = void 0;
|
|
3112
|
+
this.label?.container.remove();
|
|
3113
|
+
this.label = void 0;
|
|
3114
|
+
this.engine?.destroy();
|
|
3115
|
+
this.engine = void 0;
|
|
3116
|
+
this.cars.length = 0;
|
|
3117
|
+
}
|
|
3118
|
+
/** Extra path-time every car has accelerated forward by, `now` ms into the outro. */
|
|
3119
|
+
warp(now) {
|
|
3120
|
+
if (this.outroAt === Infinity) return 0;
|
|
3121
|
+
const seconds = (now - this.outroAt) / 1e3;
|
|
3122
|
+
return 0.5 * WARP_ACCEL * seconds * seconds;
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* The single trajectory every car rides, sampled at path-time `p`: the track
|
|
3126
|
+
* up to the exit switch point, then a straight escape outward. Because the
|
|
3127
|
+
* switch point and direction are shared, all cars follow the exact same path.
|
|
3128
|
+
*/
|
|
3129
|
+
pathPosition(p) {
|
|
3130
|
+
if (this.outroAt === Infinity || p <= this.exitPathTime) {
|
|
3131
|
+
return this.motion.positionAt(p);
|
|
3132
|
+
}
|
|
3133
|
+
const distance = this.exitSpeed * (p - this.exitPathTime);
|
|
3134
|
+
return {
|
|
3135
|
+
x: this.exitPoint.x + this.exitDir.x * distance,
|
|
3136
|
+
y: this.exitPoint.y + this.exitDir.y * distance,
|
|
3137
|
+
z: this.exitPoint.z + this.exitDir.z * distance
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
|
|
2955
3142
|
// src/prefabs/ghost-train.ts
|
|
2956
3143
|
function ghostTrain(options = {}) {
|
|
2957
|
-
const motion = options.object?.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
2958
3144
|
const particles = options.particles ?? {};
|
|
2959
|
-
const
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
size: 0.3,
|
|
2963
|
-
transparency: { mode: "two-sided", opacity: 0.55 },
|
|
2964
|
-
tail: { count: 4, gapMs: 240 },
|
|
2965
|
-
backend: options.backend,
|
|
2966
|
-
...options.object,
|
|
2967
|
-
label: options.object?.label
|
|
3145
|
+
const train = new GhostTrainAnimation({
|
|
3146
|
+
motion: options.object?.motion,
|
|
3147
|
+
backend: options.backend
|
|
2968
3148
|
});
|
|
2969
3149
|
const trail = new ParticlesAnimation({
|
|
2970
3150
|
rate: 30,
|
|
2971
3151
|
lifeMs: 1700,
|
|
2972
|
-
size: 0.
|
|
2973
|
-
speed: 0.
|
|
3152
|
+
size: 0.15,
|
|
3153
|
+
speed: 0.11,
|
|
2974
3154
|
colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
|
|
2975
|
-
texture: particles.texture ?? starTexture(),
|
|
2976
|
-
emitter:
|
|
2977
|
-
outroMs:
|
|
3155
|
+
texture: particles.texture ?? starTexture({ glow: 5 }),
|
|
3156
|
+
emitter: train.trailEmitter(),
|
|
3157
|
+
outroMs: train.outroDurationMs,
|
|
2978
3158
|
seed: 17,
|
|
2979
3159
|
backend: options.backend,
|
|
2980
3160
|
...particles,
|
|
2981
3161
|
label: options.label ?? particles.label,
|
|
2982
3162
|
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
2983
3163
|
});
|
|
2984
|
-
return progressSpinner(new CompositeAnimation([trail,
|
|
3164
|
+
return progressSpinner(new CompositeAnimation([trail, train]), options);
|
|
2985
3165
|
}
|
|
2986
3166
|
|
|
2987
3167
|
// src/animations/grid-assembly.ts
|
|
2988
3168
|
var GRID = 5;
|
|
2989
3169
|
var COUNT = GRID * GRID;
|
|
2990
|
-
var
|
|
2991
|
-
var
|
|
3170
|
+
var CAMERA_Z3 = 4;
|
|
3171
|
+
var FOV2 = 55 * Math.PI / 180;
|
|
2992
3172
|
var TWO_PI2 = Math.PI * 2;
|
|
2993
3173
|
var INTRO_MS = 900;
|
|
2994
3174
|
var INTRO_STAGGER_MS = 60;
|
|
3175
|
+
var INTRO_DONE_MS = (COUNT - 1) * INTRO_STAGGER_MS + INTRO_MS;
|
|
2995
3176
|
var GATE_DOCK = 0.35;
|
|
2996
3177
|
var GATE_UNDOCK = 0.65;
|
|
2997
3178
|
var EXIT_HURRY2 = 2.5;
|
|
@@ -3000,20 +3181,16 @@ var SPIN_MS = 380;
|
|
|
3000
3181
|
var SPIN_STAGGER_MS = 80;
|
|
3001
3182
|
var HOLD_MS = 1e3;
|
|
3002
3183
|
var COLLAPSE_MS = 700;
|
|
3003
|
-
var
|
|
3184
|
+
var COLLAPSE_SPREAD_MS = 500;
|
|
3185
|
+
var POP_MS2 = 170;
|
|
3004
3186
|
var LABEL_FADE_MS = 600;
|
|
3005
|
-
var
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
() => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
|
|
3009
|
-
() => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
|
|
3010
|
-
() => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
|
|
3011
|
-
];
|
|
3012
|
-
function clamp014(value) {
|
|
3187
|
+
var CUBE_COLORS = ["#8397c6", "#7186b8", "#6176a8", "#93a6cf", "#556a9c", "#7a8fc0"];
|
|
3188
|
+
var DEFAULT_MESHES = [() => cube(1, CUBE_COLORS)];
|
|
3189
|
+
function clamp015(value) {
|
|
3013
3190
|
return Math.max(0, Math.min(1, value));
|
|
3014
3191
|
}
|
|
3015
3192
|
function smooth01(value) {
|
|
3016
|
-
const x =
|
|
3193
|
+
const x = clamp015(value);
|
|
3017
3194
|
return x * x * (3 - 2 * x);
|
|
3018
3195
|
}
|
|
3019
3196
|
function resolveMesh2(mesh) {
|
|
@@ -3036,6 +3213,9 @@ var GridAssemblyAnimation = class {
|
|
|
3036
3213
|
this.dockedAt = new Array(COUNT).fill(Infinity);
|
|
3037
3214
|
this.tumbleX = [];
|
|
3038
3215
|
this.tumbleY = [];
|
|
3216
|
+
this.collapseDelay = [];
|
|
3217
|
+
this.popStarted = new Array(COUNT).fill(false);
|
|
3218
|
+
this.maxCollapseDelay = 0;
|
|
3039
3219
|
this.fades = [];
|
|
3040
3220
|
this.slots = [];
|
|
3041
3221
|
this.aspect = 16 / 9;
|
|
@@ -3044,7 +3224,6 @@ var GridAssemblyAnimation = class {
|
|
|
3044
3224
|
this.allDockedAt = Infinity;
|
|
3045
3225
|
this.collapseAt = Infinity;
|
|
3046
3226
|
this.lastNow = 0;
|
|
3047
|
-
this.popFading = false;
|
|
3048
3227
|
this.finished = false;
|
|
3049
3228
|
const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
|
|
3050
3229
|
this.meshes = sources.map(resolveMesh2);
|
|
@@ -3061,13 +3240,15 @@ var GridAssemblyAnimation = class {
|
|
|
3061
3240
|
this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
|
|
3062
3241
|
this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
|
|
3063
3242
|
this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
|
|
3243
|
+
this.collapseDelay.push(hash01(i, 7) * COLLAPSE_SPREAD_MS);
|
|
3064
3244
|
}
|
|
3245
|
+
this.maxCollapseDelay = Math.max(...this.collapseDelay);
|
|
3065
3246
|
}
|
|
3066
3247
|
mount(target) {
|
|
3067
3248
|
if (!target.style.position) target.style.position = "relative";
|
|
3068
3249
|
const engine = new Little3dEngine({
|
|
3069
3250
|
backend: this.backend,
|
|
3070
|
-
camera: { position: { x: 0, y: 0, z:
|
|
3251
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
|
|
3071
3252
|
});
|
|
3072
3253
|
for (let i = 0; i < COUNT; i++) {
|
|
3073
3254
|
this.fades.push({ mode: "one-sided", opacity: 1 });
|
|
@@ -3122,7 +3303,7 @@ var GridAssemblyAnimation = class {
|
|
|
3122
3303
|
COLLAPSE_MS
|
|
3123
3304
|
));
|
|
3124
3305
|
}
|
|
3125
|
-
if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS +
|
|
3306
|
+
if (this.collapseAt !== Infinity && now >= this.collapseAt + this.maxCollapseDelay + COLLAPSE_MS + POP_MS2) {
|
|
3126
3307
|
this.finished = true;
|
|
3127
3308
|
}
|
|
3128
3309
|
this.engine.render();
|
|
@@ -3139,7 +3320,8 @@ var GridAssemblyAnimation = class {
|
|
|
3139
3320
|
}
|
|
3140
3321
|
updateBlends(dt, progress, now) {
|
|
3141
3322
|
const exiting = this.exitAt !== Infinity;
|
|
3142
|
-
const
|
|
3323
|
+
const ringComplete = now - this.enterAt >= INTRO_DONE_MS;
|
|
3324
|
+
const want = !ringComplete ? 0 : exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
|
|
3143
3325
|
const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
|
|
3144
3326
|
for (let i = 0; i < COUNT; i++) {
|
|
3145
3327
|
const target = i < want ? 1 : 0;
|
|
@@ -3164,14 +3346,14 @@ var GridAssemblyAnimation = class {
|
|
|
3164
3346
|
}
|
|
3165
3347
|
renderStory(now, dt) {
|
|
3166
3348
|
const t = now - this.enterAt;
|
|
3167
|
-
const halfHeight = Math.tan(
|
|
3349
|
+
const halfHeight = Math.tan(FOV2 / 2) * CAMERA_Z3;
|
|
3168
3350
|
const halfWidth = halfHeight * this.aspect;
|
|
3169
3351
|
const radius = Math.max(0.6, Math.min(halfWidth, halfHeight) - this.size * 0.55);
|
|
3170
3352
|
const spawnFactor = (Math.max(halfWidth, halfHeight) * 1.25 + this.size * 2) / radius;
|
|
3171
3353
|
for (let i = 0; i < COUNT; i++) {
|
|
3172
3354
|
const transform2 = this.handles[i].transform;
|
|
3173
3355
|
const eased = smooth01(this.blends[i]);
|
|
3174
|
-
const introT =
|
|
3356
|
+
const introT = clamp015((t - i * INTRO_STAGGER_MS) / INTRO_MS);
|
|
3175
3357
|
const reach = 1 + (spawnFactor - 1) * (1 - easeOutCubic(introT));
|
|
3176
3358
|
const angle = Math.PI / 2 - i / COUNT * TWO_PI2 - t / this.orbitPeriodMs * TWO_PI2;
|
|
3177
3359
|
const orbitX = Math.cos(angle) * radius * reach;
|
|
@@ -3202,25 +3384,30 @@ var GridAssemblyAnimation = class {
|
|
|
3202
3384
|
if (!this.captured) {
|
|
3203
3385
|
this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
|
|
3204
3386
|
}
|
|
3205
|
-
const u = clamp014((now - this.collapseAt) / COLLAPSE_MS);
|
|
3206
|
-
const pull = easeInCubic(u);
|
|
3207
3387
|
for (let i = 0; i < COUNT; i++) {
|
|
3208
3388
|
const transform2 = this.handles[i].transform;
|
|
3209
3389
|
const from = this.captured[i];
|
|
3390
|
+
const local = now - this.collapseAt - this.collapseDelay[i];
|
|
3391
|
+
if (local <= 0) {
|
|
3392
|
+
transform2.position.x = from.x;
|
|
3393
|
+
transform2.position.y = from.y;
|
|
3394
|
+
transform2.position.z = from.z;
|
|
3395
|
+
transform2.scale = this.size;
|
|
3396
|
+
continue;
|
|
3397
|
+
}
|
|
3398
|
+
const pull = easeInCubic(clamp015(local / COLLAPSE_MS));
|
|
3210
3399
|
transform2.position.x = from.x * (1 - pull);
|
|
3211
3400
|
transform2.position.y = from.y * (1 - pull);
|
|
3212
3401
|
transform2.position.z = from.z * (1 - pull);
|
|
3213
3402
|
transform2.scale = this.size * (1 - 0.99 * pull);
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
const v = clamp014((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
|
|
3221
|
-
for (let i = 0; i < COUNT; i++) {
|
|
3403
|
+
if (local >= COLLAPSE_MS) {
|
|
3404
|
+
if (!this.popStarted[i]) {
|
|
3405
|
+
this.popStarted[i] = true;
|
|
3406
|
+
this.handles[i].transparency = this.fades[i];
|
|
3407
|
+
}
|
|
3408
|
+
const v = clamp015((local - COLLAPSE_MS) / POP_MS2);
|
|
3222
3409
|
this.fades[i].opacity = 1 - v;
|
|
3223
|
-
|
|
3410
|
+
transform2.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
|
|
3224
3411
|
}
|
|
3225
3412
|
}
|
|
3226
3413
|
}
|
|
@@ -3320,68 +3507,347 @@ function pulsingStarfield(options = {}) {
|
|
|
3320
3507
|
}), options);
|
|
3321
3508
|
}
|
|
3322
3509
|
|
|
3323
|
-
// src/
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3510
|
+
// src/animations/rocket-launch.ts
|
|
3511
|
+
var ROCKETS = 20;
|
|
3512
|
+
var CAMERA_Z4 = 3;
|
|
3513
|
+
var FOV3 = 55 * Math.PI / 180;
|
|
3514
|
+
var HALF_HEIGHT2 = Math.tan(FOV3 / 2) * CAMERA_Z4;
|
|
3515
|
+
var SIZE = 0.12;
|
|
3516
|
+
var ROW_Y = -0.5;
|
|
3517
|
+
var PARTICLE_Z = 0.3;
|
|
3518
|
+
var SLIDE_MS = 460;
|
|
3519
|
+
var SLIDE_GATE = 0.45;
|
|
3520
|
+
var EXIT_HURRY3 = 2.5;
|
|
3521
|
+
var LAUNCH_SPREAD_MS = 620;
|
|
3522
|
+
var ASCENT_G = 5.2;
|
|
3523
|
+
var FINISH_PAD_MS = 2e3;
|
|
3524
|
+
var TURNERS = 3;
|
|
3525
|
+
var TURN_MIN_Y = 0.2;
|
|
3526
|
+
var TURN_MAX_Y = 0.8;
|
|
3527
|
+
var TURN_MIN_DEG = 30;
|
|
3528
|
+
var TURN_MAX_DEG = 50;
|
|
3529
|
+
var DEG = Math.PI / 180;
|
|
3530
|
+
var SMOKE_LIFE_MS = 1400;
|
|
3531
|
+
var SMOKE_GAP_MS = 320;
|
|
3532
|
+
var SMOKE_RISE = 0.55;
|
|
3533
|
+
var SMOKE_SIZE = 0.17;
|
|
3534
|
+
var SMOKE_PEAK = 0.16;
|
|
3535
|
+
var SMOKE_POOL = 104;
|
|
3536
|
+
var FIRE_LIFE_MS = 420;
|
|
3537
|
+
var FIRE_GAP_MS = 55;
|
|
3538
|
+
var FIRE_ON_MS = 950;
|
|
3539
|
+
var FIRE_TRAIL = 0.25;
|
|
3540
|
+
var FIRE_SPREAD = 0.09;
|
|
3541
|
+
var FIRE_SIZE = 0.15;
|
|
3542
|
+
var FIRE_PEAK = 0.9;
|
|
3543
|
+
var FIRE_POOL = 140;
|
|
3544
|
+
var SMOKE_COLORS = ["#e2e8f0", "#cbd5e1"];
|
|
3545
|
+
var FIRE_COLORS = ["#fef3c7", "#fde047", "#fb923c", "#ef4444"];
|
|
3546
|
+
var ROCKET_COLORS = ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"];
|
|
3547
|
+
function clamp016(value) {
|
|
3548
|
+
return Math.max(0, Math.min(1, value));
|
|
3339
3549
|
}
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3550
|
+
function smoothstep2(edge0, edge1, value) {
|
|
3551
|
+
const x = clamp016((value - edge0) / (edge1 - edge0));
|
|
3552
|
+
return x * x * (3 - 2 * x);
|
|
3553
|
+
}
|
|
3554
|
+
function hash012(index, salt) {
|
|
3555
|
+
let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
|
|
3556
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
3557
|
+
h ^= h >>> 16;
|
|
3558
|
+
return (h >>> 0) / 4294967296;
|
|
3559
|
+
}
|
|
3560
|
+
function puffTexture(coreAlpha, coreStop) {
|
|
3561
|
+
return canvasTexture((ctx) => {
|
|
3562
|
+
const g = ctx.createRadialGradient(48, 48, 1, 48, 48, 47);
|
|
3563
|
+
g.addColorStop(0, `rgba(255,255,255,${coreAlpha})`);
|
|
3564
|
+
g.addColorStop(coreStop, `rgba(255,255,255,${coreAlpha * 0.6})`);
|
|
3565
|
+
g.addColorStop(1, "rgba(255,255,255,0)");
|
|
3566
|
+
ctx.fillStyle = g;
|
|
3567
|
+
ctx.fillRect(0, 0, 96, 96);
|
|
3568
|
+
});
|
|
3569
|
+
}
|
|
3570
|
+
var RocketLaunchAnimation = class {
|
|
3571
|
+
constructor(options = {}) {
|
|
3572
|
+
this.rockets = [];
|
|
3573
|
+
this.smoke = [];
|
|
3574
|
+
this.fire = [];
|
|
3575
|
+
this.smokeFades = [];
|
|
3576
|
+
this.fireFades = [];
|
|
3577
|
+
this.blends = new Array(ROCKETS).fill(0);
|
|
3578
|
+
this.groundedAt = new Array(ROCKETS).fill(Infinity);
|
|
3579
|
+
// Per-rocket veer parameters (turnS = Infinity for a rocket that climbs straight).
|
|
3580
|
+
this.turnS = new Array(ROCKETS).fill(Infinity);
|
|
3581
|
+
this.turnDir = [];
|
|
3582
|
+
this.turnRoll = new Array(ROCKETS).fill(0);
|
|
3583
|
+
this.stagger = new Array(ROCKETS).fill(0);
|
|
3584
|
+
this.aspect = 16 / 9;
|
|
3585
|
+
this.enterAt = Infinity;
|
|
3586
|
+
this.exitAt = Infinity;
|
|
3587
|
+
this.launchedAt = Infinity;
|
|
3588
|
+
this.lastNow = 0;
|
|
3589
|
+
this.finished = false;
|
|
3590
|
+
this.backend = options.backend;
|
|
3591
|
+
this.labelContent = options.label;
|
|
3592
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
3593
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
3594
|
+
this.turnDir.push({ x: 0, y: 1 });
|
|
3595
|
+
this.stagger[i] = hash012(i, 7) * LAUNCH_SPREAD_MS;
|
|
3350
3596
|
}
|
|
3351
|
-
|
|
3597
|
+
const order = Array.from({ length: ROCKETS }, (_, i) => i).sort(
|
|
3598
|
+
(a, b) => hash012(a, 11) - hash012(b, 11)
|
|
3599
|
+
);
|
|
3600
|
+
for (const i of order.slice(0, TURNERS)) {
|
|
3601
|
+
const height = TURN_MIN_Y + hash012(i, 12) * (TURN_MAX_Y - TURN_MIN_Y);
|
|
3602
|
+
const angle = (TURN_MIN_DEG + hash012(i, 13) * (TURN_MAX_DEG - TURN_MIN_DEG)) * DEG;
|
|
3603
|
+
const sign = hash012(i, 14) < 0.5 ? -1 : 1;
|
|
3604
|
+
this.turnS[i] = height - ROW_Y;
|
|
3605
|
+
this.turnDir[i] = { x: sign * Math.sin(angle), y: Math.cos(angle) };
|
|
3606
|
+
this.turnRoll[i] = -sign * angle;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
mount(target) {
|
|
3610
|
+
if (!target.style.position) target.style.position = "relative";
|
|
3611
|
+
const smokeMeshes = SMOKE_COLORS.map((color) => quad(1, [color]));
|
|
3612
|
+
const fireMeshes = FIRE_COLORS.map((color) => quad(1, [color]));
|
|
3613
|
+
const smokeTexture = puffTexture(0.85, 0.5);
|
|
3614
|
+
const fireTexture = puffTexture(1, 0.32);
|
|
3615
|
+
const backend = async (rendererOptions) => {
|
|
3616
|
+
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);
|
|
3617
|
+
for (const mesh of smokeMeshes) renderer.setTexture(mesh, smokeTexture);
|
|
3618
|
+
for (const mesh of fireMeshes) renderer.setTexture(mesh, fireTexture);
|
|
3619
|
+
return renderer;
|
|
3620
|
+
};
|
|
3621
|
+
const engine = new Little3dEngine({
|
|
3622
|
+
backend,
|
|
3623
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z4 }, fov: FOV3 }
|
|
3624
|
+
});
|
|
3625
|
+
const rocketMesh = pyramid(1, ROCKET_COLORS);
|
|
3626
|
+
for (let i = 0; i < ROCKETS; i++) this.rockets.push(engine.add(rocketMesh, { scale: 0 }));
|
|
3627
|
+
for (let s = 0; s < SMOKE_POOL; s++) {
|
|
3628
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
3629
|
+
this.smokeFades.push(fade);
|
|
3630
|
+
this.smoke.push(engine.add(smokeMeshes[s % smokeMeshes.length], { scale: 0, transparency: fade }));
|
|
3631
|
+
}
|
|
3632
|
+
for (let f = 0; f < FIRE_POOL; f++) {
|
|
3633
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
3634
|
+
this.fireFades.push(fade);
|
|
3635
|
+
this.fire.push(engine.add(fireMeshes[f % fireMeshes.length], { scale: 0, transparency: fade }));
|
|
3636
|
+
}
|
|
3637
|
+
this.engine = engine;
|
|
3638
|
+
engine.mount(target).catch((error) => {
|
|
3639
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
3640
|
+
});
|
|
3641
|
+
const measure = () => {
|
|
3642
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
3643
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
measure();
|
|
3647
|
+
this.observer = new ResizeObserver(measure);
|
|
3648
|
+
this.observer.observe(target);
|
|
3649
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
3650
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
3651
|
+
}
|
|
3652
|
+
enter(now) {
|
|
3653
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
3654
|
+
}
|
|
3655
|
+
exit(now) {
|
|
3656
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
3657
|
+
}
|
|
3658
|
+
isFinished() {
|
|
3659
|
+
return this.finished;
|
|
3660
|
+
}
|
|
3661
|
+
render(now, frame) {
|
|
3662
|
+
if (!this.engine || !this.label) return;
|
|
3663
|
+
for (const handle of this.rockets) handle.transform.scale = 0;
|
|
3664
|
+
for (const fade of this.smokeFades) fade.opacity = 0;
|
|
3665
|
+
for (const fade of this.fireFades) fade.opacity = 0;
|
|
3666
|
+
for (const handle of this.smoke) handle.transform.scale = 0;
|
|
3667
|
+
for (const handle of this.fire) handle.transform.scale = 0;
|
|
3668
|
+
if (this.enterAt === Infinity) {
|
|
3669
|
+
this.engine.render();
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
3673
|
+
this.lastNow = now;
|
|
3674
|
+
const exiting = this.exitAt !== Infinity;
|
|
3675
|
+
this.updateBlends(dt, frame.progress, now, exiting);
|
|
3676
|
+
if (exiting && this.launchedAt === Infinity && this.blends.every((blend) => blend >= 1)) {
|
|
3677
|
+
this.launchedAt = now;
|
|
3678
|
+
}
|
|
3679
|
+
const launched = this.launchedAt !== Infinity;
|
|
3680
|
+
const halfWidth = HALF_HEIGHT2 * this.aspect;
|
|
3681
|
+
const rowHalf = Math.min(halfWidth * 0.8, 1.18);
|
|
3682
|
+
const spawnX = halfWidth + 0.6;
|
|
3683
|
+
let smokeCursor = 0;
|
|
3684
|
+
let fireCursor = 0;
|
|
3685
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
3686
|
+
const homeX = -rowHalf + 2 * rowHalf * i / (ROCKETS - 1);
|
|
3687
|
+
const launchAt = launched ? this.launchedAt + this.stagger[i] : Infinity;
|
|
3688
|
+
const la = now - launchAt;
|
|
3689
|
+
if (launched && la >= 0) {
|
|
3690
|
+
this.renderAscent(i, homeX, la, halfWidth);
|
|
3691
|
+
fireCursor = this.emitFire(i, homeX, la, fireCursor);
|
|
3692
|
+
continue;
|
|
3693
|
+
}
|
|
3694
|
+
const blend = this.blends[i];
|
|
3695
|
+
if (blend <= 0) continue;
|
|
3696
|
+
const transform2 = this.rockets[i].transform;
|
|
3697
|
+
transform2.position.x = spawnX + (homeX - spawnX) * easeOutBack(blend);
|
|
3698
|
+
transform2.position.y = ROW_Y;
|
|
3699
|
+
transform2.position.z = 0;
|
|
3700
|
+
transform2.rotation.x = 0;
|
|
3701
|
+
transform2.rotation.y = 0;
|
|
3702
|
+
transform2.rotation.z = 0;
|
|
3703
|
+
transform2.scale = SIZE * smoothstep2(0, 0.6, blend);
|
|
3704
|
+
if (this.groundedAt[i] !== Infinity) {
|
|
3705
|
+
smokeCursor = this.emitSmoke(i, homeX, now, launchAt, smokeCursor);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
3709
|
+
if (this.fadeLabel) {
|
|
3710
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
3711
|
+
now,
|
|
3712
|
+
this.enterAt,
|
|
3713
|
+
SLIDE_MS,
|
|
3714
|
+
this.launchedAt,
|
|
3715
|
+
LAUNCH_SPREAD_MS
|
|
3716
|
+
));
|
|
3717
|
+
}
|
|
3718
|
+
if (launched && now >= this.launchedAt + LAUNCH_SPREAD_MS + FINISH_PAD_MS) {
|
|
3719
|
+
this.finished = true;
|
|
3720
|
+
}
|
|
3721
|
+
this.engine.render();
|
|
3722
|
+
}
|
|
3723
|
+
destroy() {
|
|
3724
|
+
this.observer?.disconnect();
|
|
3725
|
+
this.observer = void 0;
|
|
3726
|
+
this.label?.container.remove();
|
|
3727
|
+
this.label = void 0;
|
|
3728
|
+
this.engine?.destroy();
|
|
3729
|
+
this.engine = void 0;
|
|
3730
|
+
this.rockets.length = 0;
|
|
3731
|
+
this.smoke.length = 0;
|
|
3732
|
+
this.fire.length = 0;
|
|
3733
|
+
this.smokeFades.length = 0;
|
|
3734
|
+
this.fireFades.length = 0;
|
|
3735
|
+
}
|
|
3736
|
+
updateBlends(dt, progress, now, exiting) {
|
|
3737
|
+
const want = exiting ? ROCKETS : Math.min(ROCKETS, Math.round(progress * ROCKETS));
|
|
3738
|
+
const rate = dt / SLIDE_MS * (exiting ? EXIT_HURRY3 : 1);
|
|
3739
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
3740
|
+
const target = i < want ? 1 : 0;
|
|
3741
|
+
const blend = this.blends[i];
|
|
3742
|
+
if (target > blend && (i === 0 || this.blends[i - 1] >= SLIDE_GATE)) {
|
|
3743
|
+
this.blends[i] = Math.min(1, blend + rate);
|
|
3744
|
+
} else if (target < blend && (i === ROCKETS - 1 || this.blends[i + 1] <= 1 - SLIDE_GATE)) {
|
|
3745
|
+
this.blends[i] = Math.max(0, blend - rate);
|
|
3746
|
+
}
|
|
3747
|
+
if (this.blends[i] >= 1) {
|
|
3748
|
+
if (this.groundedAt[i] === Infinity) this.groundedAt[i] = now;
|
|
3749
|
+
} else {
|
|
3750
|
+
this.groundedAt[i] = Infinity;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
/** Along-track distance climbed `la` ms after this rocket's own blast-off. */
|
|
3755
|
+
ascentDistance(la) {
|
|
3756
|
+
const seconds = la / 1e3;
|
|
3757
|
+
return 0.5 * ASCENT_G * seconds * seconds;
|
|
3758
|
+
}
|
|
3759
|
+
/** Rocket center, nose direction, and roll `la` ms into its climb. */
|
|
3760
|
+
ascentPose(i, homeX, la) {
|
|
3761
|
+
const s = this.ascentDistance(la);
|
|
3762
|
+
const turnS = this.turnS[i];
|
|
3763
|
+
if (s <= turnS) {
|
|
3764
|
+
return { pos: { x: homeX, y: ROW_Y + s }, dir: { x: 0, y: 1 }, roll: 0 };
|
|
3765
|
+
}
|
|
3766
|
+
const post = s - turnS;
|
|
3767
|
+
const dir = this.turnDir[i];
|
|
3768
|
+
return {
|
|
3769
|
+
pos: { x: homeX + dir.x * post, y: ROW_Y + turnS + dir.y * post },
|
|
3770
|
+
dir,
|
|
3771
|
+
roll: this.turnRoll[i]
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
renderAscent(i, homeX, la, halfWidth) {
|
|
3775
|
+
const { pos, roll } = this.ascentPose(i, homeX, la);
|
|
3776
|
+
if (pos.y > HALF_HEIGHT2 + 0.4 || Math.abs(pos.x) > halfWidth + 0.4) return;
|
|
3777
|
+
const transform2 = this.rockets[i].transform;
|
|
3778
|
+
transform2.position.x = pos.x;
|
|
3779
|
+
transform2.position.y = pos.y;
|
|
3780
|
+
transform2.position.z = 0;
|
|
3781
|
+
transform2.rotation.x = 0;
|
|
3782
|
+
transform2.rotation.y = 0;
|
|
3783
|
+
transform2.rotation.z = roll;
|
|
3784
|
+
transform2.scale = SIZE;
|
|
3785
|
+
}
|
|
3786
|
+
emitFire(i, homeX, la, cursor) {
|
|
3787
|
+
const gap = FIRE_GAP_MS;
|
|
3788
|
+
const last = Math.min(Math.floor(la / gap), Math.floor(FIRE_ON_MS / gap));
|
|
3789
|
+
const first = Math.max(0, Math.ceil((la - FIRE_LIFE_MS) / gap));
|
|
3790
|
+
for (let n = first; n <= last; n++) {
|
|
3791
|
+
if (cursor >= this.fire.length) return cursor;
|
|
3792
|
+
const emitLa = n * gap;
|
|
3793
|
+
const age = la - emitLa;
|
|
3794
|
+
if (age < 0 || age >= FIRE_LIFE_MS) continue;
|
|
3795
|
+
const life = age / FIRE_LIFE_MS;
|
|
3796
|
+
const seconds = age / 1e3;
|
|
3797
|
+
const pose = this.ascentPose(i, homeX, emitLa);
|
|
3798
|
+
const back = { x: -pose.dir.x, y: -pose.dir.y };
|
|
3799
|
+
const perp = { x: -pose.dir.y, y: pose.dir.x };
|
|
3800
|
+
const lat = (hash012(i * 97 + n, 1) - 0.5) * FIRE_SPREAD;
|
|
3801
|
+
const baseX = pose.pos.x + back.x * SIZE * 0.5;
|
|
3802
|
+
const baseY = pose.pos.y + back.y * SIZE * 0.5;
|
|
3803
|
+
const transform2 = this.fire[cursor].transform;
|
|
3804
|
+
transform2.position.x = baseX + back.x * FIRE_TRAIL * seconds + perp.x * lat;
|
|
3805
|
+
transform2.position.y = baseY + back.y * FIRE_TRAIL * seconds + perp.y * lat - 0.12 * seconds * seconds;
|
|
3806
|
+
transform2.position.z = PARTICLE_Z;
|
|
3807
|
+
transform2.rotation.z = hash012(i * 97 + n, 2) * Math.PI * 2;
|
|
3808
|
+
transform2.scale = FIRE_SIZE * (0.7 + 0.5 * hash012(i * 97 + n, 3)) * (1 - 0.55 * life);
|
|
3809
|
+
this.fireFades[cursor].opacity = FIRE_PEAK * smoothstep2(0, 0.15, life) * (1 - smoothstep2(0.55, 1, life));
|
|
3810
|
+
cursor++;
|
|
3811
|
+
}
|
|
3812
|
+
return cursor;
|
|
3813
|
+
}
|
|
3814
|
+
emitSmoke(i, homeX, now, launchAt, cursor) {
|
|
3815
|
+
const start = this.groundedAt[i];
|
|
3816
|
+
const tr = now - start;
|
|
3817
|
+
const gap = SMOKE_GAP_MS;
|
|
3818
|
+
const emitUntil = Number.isFinite(launchAt) ? launchAt - start : tr;
|
|
3819
|
+
const last = Math.min(Math.floor(tr / gap), Math.floor(emitUntil / gap));
|
|
3820
|
+
const first = Math.max(0, Math.ceil((tr - SMOKE_LIFE_MS) / gap));
|
|
3821
|
+
const baseY = ROW_Y - SIZE * 0.4;
|
|
3822
|
+
for (let n = first; n <= last; n++) {
|
|
3823
|
+
if (cursor >= this.smoke.length) return cursor;
|
|
3824
|
+
const age = tr - n * gap;
|
|
3825
|
+
if (age < 0 || age >= SMOKE_LIFE_MS) continue;
|
|
3826
|
+
const life = age / SMOKE_LIFE_MS;
|
|
3827
|
+
const drift = (hash012(i * 131 + n, 1) - 0.5) * 0.14;
|
|
3828
|
+
const transform2 = this.smoke[cursor].transform;
|
|
3829
|
+
transform2.position.x = homeX + drift * life;
|
|
3830
|
+
transform2.position.y = baseY + SMOKE_RISE * life;
|
|
3831
|
+
transform2.position.z = PARTICLE_Z;
|
|
3832
|
+
transform2.rotation.z = hash012(i * 131 + n, 2) * Math.PI * 2;
|
|
3833
|
+
transform2.scale = SMOKE_SIZE * (0.5 + 0.8 * life) * (0.7 + 0.6 * hash012(i * 131 + n, 3));
|
|
3834
|
+
this.smokeFades[cursor].opacity = SMOKE_PEAK * smoothstep2(0, 0.25, life) * (1 - smoothstep2(0.5, 1, life));
|
|
3835
|
+
cursor++;
|
|
3836
|
+
}
|
|
3837
|
+
return cursor;
|
|
3838
|
+
}
|
|
3352
3839
|
};
|
|
3840
|
+
|
|
3841
|
+
// src/prefabs/rocket-launch.ts
|
|
3353
3842
|
function rocketLaunch(options = {}) {
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
backend: options.backend,
|
|
3363
|
-
...options.object,
|
|
3364
|
-
label: options.object?.label
|
|
3365
|
-
});
|
|
3366
|
-
const exhaust = new ParticlesAnimation({
|
|
3367
|
-
rate: 46,
|
|
3368
|
-
lifeMs: 1100,
|
|
3369
|
-
size: 0.18,
|
|
3370
|
-
speed: 0.5,
|
|
3371
|
-
direction: { x: 0, y: -1, z: 0 },
|
|
3372
|
-
spread: 0.3,
|
|
3373
|
-
gravity: { x: 0, y: -0.6, z: 0 },
|
|
3374
|
-
colors: ["#fde047", "#fb923c", "#ef4444", "#fef3c7"],
|
|
3375
|
-
texture: particles.texture ?? shineTexture(),
|
|
3376
|
-
emitter: object.trailEmitter(),
|
|
3377
|
-
outroMs: object.outroDurationMs,
|
|
3378
|
-
seed: 9,
|
|
3379
|
-
backend: options.backend,
|
|
3380
|
-
...particles,
|
|
3381
|
-
label: options.label ?? particles.label,
|
|
3382
|
-
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3383
|
-
});
|
|
3384
|
-
return progressSpinner(new CompositeAnimation([exhaust, object]), options);
|
|
3843
|
+
return progressSpinner(
|
|
3844
|
+
new RocketLaunchAnimation({
|
|
3845
|
+
backend: options.backend,
|
|
3846
|
+
label: options.label,
|
|
3847
|
+
fadeLabel: options.fadeLabel
|
|
3848
|
+
}),
|
|
3849
|
+
options
|
|
3850
|
+
);
|
|
3385
3851
|
}
|
|
3386
3852
|
|
|
3387
3853
|
// src/motion/wander.ts
|