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
|
@@ -2052,18 +2052,30 @@ void main() {
|
|
|
2052
2052
|
}
|
|
2053
2053
|
|
|
2054
2054
|
// src/engines/little-3d-engine/textures/dynamic/star.ts
|
|
2055
|
-
function
|
|
2055
|
+
function drawStar(ctx) {
|
|
2056
|
+
ctx.save();
|
|
2057
|
+
ctx.translate(48, 48);
|
|
2058
|
+
ctx.fillStyle = "#fff";
|
|
2059
|
+
ctx.beginPath();
|
|
2060
|
+
for (let index = 0; index < 10; index++) {
|
|
2061
|
+
const radius = index % 2 === 0 ? 43 : 16;
|
|
2062
|
+
const angle = index * Math.PI / 5 - Math.PI / 2;
|
|
2063
|
+
ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
|
|
2064
|
+
}
|
|
2065
|
+
ctx.closePath();
|
|
2066
|
+
ctx.fill();
|
|
2067
|
+
ctx.restore();
|
|
2068
|
+
}
|
|
2069
|
+
function starTexture(options = {}) {
|
|
2070
|
+
const glow = Math.max(0, options.glow ?? 0);
|
|
2056
2071
|
return canvasTexture((ctx) => {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
const angle = index * Math.PI / 5 - Math.PI / 2;
|
|
2063
|
-
ctx.lineTo(radius * Math.cos(angle), radius * Math.sin(angle));
|
|
2072
|
+
if (glow > 0) {
|
|
2073
|
+
ctx.save();
|
|
2074
|
+
ctx.filter = `blur(${glow}px)`;
|
|
2075
|
+
drawStar(ctx);
|
|
2076
|
+
ctx.restore();
|
|
2064
2077
|
}
|
|
2065
|
-
ctx
|
|
2066
|
-
ctx.fill();
|
|
2078
|
+
drawStar(ctx);
|
|
2067
2079
|
});
|
|
2068
2080
|
}
|
|
2069
2081
|
|
|
@@ -3473,6 +3485,7 @@ void main() {
|
|
|
3473
3485
|
var TWO_PI2 = Math.PI * 2;
|
|
3474
3486
|
var INTRO_MS = 900;
|
|
3475
3487
|
var INTRO_STAGGER_MS = 60;
|
|
3488
|
+
var INTRO_DONE_MS = (COUNT - 1) * INTRO_STAGGER_MS + INTRO_MS;
|
|
3476
3489
|
var GATE_DOCK = 0.35;
|
|
3477
3490
|
var GATE_UNDOCK = 0.65;
|
|
3478
3491
|
var EXIT_HURRY2 = 2.5;
|
|
@@ -3481,15 +3494,11 @@ void main() {
|
|
|
3481
3494
|
var SPIN_STAGGER_MS = 80;
|
|
3482
3495
|
var HOLD_MS = 1e3;
|
|
3483
3496
|
var COLLAPSE_MS = 700;
|
|
3497
|
+
var COLLAPSE_SPREAD_MS = 500;
|
|
3484
3498
|
var POP_MS = 170;
|
|
3485
3499
|
var LABEL_FADE_MS = 600;
|
|
3486
|
-
var
|
|
3487
|
-
|
|
3488
|
-
() => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
|
|
3489
|
-
() => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
|
|
3490
|
-
() => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
|
|
3491
|
-
() => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
|
|
3492
|
-
];
|
|
3500
|
+
var CUBE_COLORS = ["#8397c6", "#7186b8", "#6176a8", "#93a6cf", "#556a9c", "#7a8fc0"];
|
|
3501
|
+
var DEFAULT_MESHES = [() => cube(1, CUBE_COLORS)];
|
|
3493
3502
|
function clamp015(value) {
|
|
3494
3503
|
return Math.max(0, Math.min(1, value));
|
|
3495
3504
|
}
|
|
@@ -3517,6 +3526,9 @@ void main() {
|
|
|
3517
3526
|
this.dockedAt = new Array(COUNT).fill(Infinity);
|
|
3518
3527
|
this.tumbleX = [];
|
|
3519
3528
|
this.tumbleY = [];
|
|
3529
|
+
this.collapseDelay = [];
|
|
3530
|
+
this.popStarted = new Array(COUNT).fill(false);
|
|
3531
|
+
this.maxCollapseDelay = 0;
|
|
3520
3532
|
this.fades = [];
|
|
3521
3533
|
this.slots = [];
|
|
3522
3534
|
this.aspect = 16 / 9;
|
|
@@ -3525,7 +3537,6 @@ void main() {
|
|
|
3525
3537
|
this.allDockedAt = Infinity;
|
|
3526
3538
|
this.collapseAt = Infinity;
|
|
3527
3539
|
this.lastNow = 0;
|
|
3528
|
-
this.popFading = false;
|
|
3529
3540
|
this.finished = false;
|
|
3530
3541
|
const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
|
|
3531
3542
|
this.meshes = sources.map(resolveMesh3);
|
|
@@ -3542,7 +3553,9 @@ void main() {
|
|
|
3542
3553
|
this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
|
|
3543
3554
|
this.tumbleX.push(TWO_PI2 * hash01(i, 2) - Math.PI);
|
|
3544
3555
|
this.tumbleY.push(TWO_PI2 * hash01(i, 4) - Math.PI);
|
|
3556
|
+
this.collapseDelay.push(hash01(i, 7) * COLLAPSE_SPREAD_MS);
|
|
3545
3557
|
}
|
|
3558
|
+
this.maxCollapseDelay = Math.max(...this.collapseDelay);
|
|
3546
3559
|
}
|
|
3547
3560
|
mount(target) {
|
|
3548
3561
|
if (!target.style.position) target.style.position = "relative";
|
|
@@ -3603,7 +3616,7 @@ void main() {
|
|
|
3603
3616
|
COLLAPSE_MS
|
|
3604
3617
|
));
|
|
3605
3618
|
}
|
|
3606
|
-
if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS + POP_MS) {
|
|
3619
|
+
if (this.collapseAt !== Infinity && now >= this.collapseAt + this.maxCollapseDelay + COLLAPSE_MS + POP_MS) {
|
|
3607
3620
|
this.finished = true;
|
|
3608
3621
|
}
|
|
3609
3622
|
this.engine.render();
|
|
@@ -3620,7 +3633,8 @@ void main() {
|
|
|
3620
3633
|
}
|
|
3621
3634
|
updateBlends(dt, progress, now) {
|
|
3622
3635
|
const exiting = this.exitAt !== Infinity;
|
|
3623
|
-
const
|
|
3636
|
+
const ringComplete = now - this.enterAt >= INTRO_DONE_MS;
|
|
3637
|
+
const want = !ringComplete ? 0 : exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
|
|
3624
3638
|
const rate = dt / this.dockMs * (exiting ? EXIT_HURRY2 : 1);
|
|
3625
3639
|
for (let i = 0; i < COUNT; i++) {
|
|
3626
3640
|
const target = i < want ? 1 : 0;
|
|
@@ -3683,25 +3697,30 @@ void main() {
|
|
|
3683
3697
|
if (!this.captured) {
|
|
3684
3698
|
this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
|
|
3685
3699
|
}
|
|
3686
|
-
const u = clamp015((now - this.collapseAt) / COLLAPSE_MS);
|
|
3687
|
-
const pull = easeInCubic(u);
|
|
3688
3700
|
for (let i = 0; i < COUNT; i++) {
|
|
3689
3701
|
const transform2 = this.handles[i].transform;
|
|
3690
3702
|
const from = this.captured[i];
|
|
3703
|
+
const local = now - this.collapseAt - this.collapseDelay[i];
|
|
3704
|
+
if (local <= 0) {
|
|
3705
|
+
transform2.position.x = from.x;
|
|
3706
|
+
transform2.position.y = from.y;
|
|
3707
|
+
transform2.position.z = from.z;
|
|
3708
|
+
transform2.scale = this.size;
|
|
3709
|
+
continue;
|
|
3710
|
+
}
|
|
3711
|
+
const pull = easeInCubic(clamp015(local / COLLAPSE_MS));
|
|
3691
3712
|
transform2.position.x = from.x * (1 - pull);
|
|
3692
3713
|
transform2.position.y = from.y * (1 - pull);
|
|
3693
3714
|
transform2.position.z = from.z * (1 - pull);
|
|
3694
3715
|
transform2.scale = this.size * (1 - 0.99 * pull);
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
const v = clamp015((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
|
|
3702
|
-
for (let i = 0; i < COUNT; i++) {
|
|
3716
|
+
if (local >= COLLAPSE_MS) {
|
|
3717
|
+
if (!this.popStarted[i]) {
|
|
3718
|
+
this.popStarted[i] = true;
|
|
3719
|
+
this.handles[i].transparency = this.fades[i];
|
|
3720
|
+
}
|
|
3721
|
+
const v = clamp015((local - COLLAPSE_MS) / POP_MS);
|
|
3703
3722
|
this.fades[i].opacity = 1 - v;
|
|
3704
|
-
|
|
3723
|
+
transform2.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
|
|
3705
3724
|
}
|
|
3706
3725
|
}
|
|
3707
3726
|
}
|
|
@@ -3874,36 +3893,238 @@ void main() {
|
|
|
3874
3893
|
};
|
|
3875
3894
|
}
|
|
3876
3895
|
|
|
3896
|
+
// src/animations/ghost-train.ts
|
|
3897
|
+
var MAX_CARS = 50;
|
|
3898
|
+
var CAMERA_Z3 = 3;
|
|
3899
|
+
var FOV2 = 55 * Math.PI / 180;
|
|
3900
|
+
var HALF_HEIGHT = Math.tan(FOV2 / 2) * CAMERA_Z3;
|
|
3901
|
+
var RUN_GAP_MS = 130;
|
|
3902
|
+
var POP_MS2 = 320;
|
|
3903
|
+
var SAMPLE_MS2 = 8;
|
|
3904
|
+
var TURN_RATE = 0.4 * Math.PI / 180;
|
|
3905
|
+
var MAX_OUTRO_MS = 4e3;
|
|
3906
|
+
var WARP_ACCEL = 1e3;
|
|
3907
|
+
var TRAIL_OUTRO_MS = 1200;
|
|
3908
|
+
var TRANSPARENCY = { mode: "two-sided", opacity: 0.275 };
|
|
3909
|
+
var CAR_COLORS = ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"];
|
|
3910
|
+
var WORLD_UP2 = { x: 0, y: 1, z: 0 };
|
|
3911
|
+
function clamp016(value) {
|
|
3912
|
+
return Math.max(0, Math.min(1, value));
|
|
3913
|
+
}
|
|
3914
|
+
function orientationFor2(forward) {
|
|
3915
|
+
const fwd = normalize(forward);
|
|
3916
|
+
let right = cross(fwd, WORLD_UP2);
|
|
3917
|
+
if (Math.hypot(right.x, right.y, right.z) < 1e-4) right = { x: 0, y: 0, z: 1 };
|
|
3918
|
+
right = normalize(right);
|
|
3919
|
+
const up = cross(right, fwd);
|
|
3920
|
+
const w = normalize(cross(fwd, up));
|
|
3921
|
+
return {
|
|
3922
|
+
x: Math.atan2(cross(w, fwd).z, w.z),
|
|
3923
|
+
y: Math.asin(Math.max(-1, Math.min(1, -fwd.z))),
|
|
3924
|
+
z: Math.atan2(fwd.y, fwd.x)
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
function rotateToward(from, to, maxRad) {
|
|
3928
|
+
const a = normalize(from);
|
|
3929
|
+
const b = normalize(to);
|
|
3930
|
+
const d = Math.max(-1, Math.min(1, dot(a, b)));
|
|
3931
|
+
const angle = Math.acos(d);
|
|
3932
|
+
if (angle <= maxRad || angle < 1e-4) return b;
|
|
3933
|
+
const sin = Math.sin(angle);
|
|
3934
|
+
if (sin < 1e-4) return b;
|
|
3935
|
+
const t = maxRad / angle;
|
|
3936
|
+
const w1 = Math.sin((1 - t) * angle) / sin;
|
|
3937
|
+
const w2 = Math.sin(t * angle) / sin;
|
|
3938
|
+
return normalize({ x: a.x * w1 + b.x * w2, y: a.y * w1 + b.y * w2, z: a.z * w1 + b.z * w2 });
|
|
3939
|
+
}
|
|
3940
|
+
var GhostTrainAnimation = class {
|
|
3941
|
+
constructor(options = {}) {
|
|
3942
|
+
this.cars = [];
|
|
3943
|
+
this.appear = new Array(MAX_CARS).fill(0);
|
|
3944
|
+
this.headings = new Array(MAX_CARS).fill(void 0);
|
|
3945
|
+
this.aspect = 16 / 9;
|
|
3946
|
+
this.enterAt = Infinity;
|
|
3947
|
+
this.outroAt = Infinity;
|
|
3948
|
+
this.carsAtOutro = 0;
|
|
3949
|
+
this.exitPathTime = 0;
|
|
3950
|
+
// lead car's path-time at blast-off (the escape switch point)
|
|
3951
|
+
this.exitPoint = { x: 0, y: 0, z: 0 };
|
|
3952
|
+
this.exitDir = { x: 1, y: 0, z: 0 };
|
|
3953
|
+
// shared escape direction, outward from the track
|
|
3954
|
+
this.exitSpeed = 1e-3;
|
|
3955
|
+
// path-units per path-millisecond at the switch (keeps speed continuous)
|
|
3956
|
+
this.lastNow = 0;
|
|
3957
|
+
this.finished = false;
|
|
3958
|
+
this.motion = options.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
3959
|
+
this.size = options.size ?? 0.15;
|
|
3960
|
+
this.backend = options.backend;
|
|
3961
|
+
this.labelContent = options.label;
|
|
3962
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
3963
|
+
}
|
|
3964
|
+
mount(target) {
|
|
3965
|
+
if (!target.style.position) target.style.position = "relative";
|
|
3966
|
+
const engine = new Little3dEngine({
|
|
3967
|
+
backend: this.backend,
|
|
3968
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z3 }, fov: FOV2 }
|
|
3969
|
+
});
|
|
3970
|
+
const mesh = cube(1, CAR_COLORS);
|
|
3971
|
+
for (let i = 0; i < MAX_CARS; i++) {
|
|
3972
|
+
this.cars.push(engine.add(mesh, { scale: 0, transparency: { ...TRANSPARENCY } }));
|
|
3973
|
+
}
|
|
3974
|
+
this.engine = engine;
|
|
3975
|
+
engine.mount(target).catch((error) => {
|
|
3976
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
3977
|
+
});
|
|
3978
|
+
const measure = () => {
|
|
3979
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
3980
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
3981
|
+
}
|
|
3982
|
+
};
|
|
3983
|
+
measure();
|
|
3984
|
+
this.observer = new ResizeObserver(measure);
|
|
3985
|
+
this.observer.observe(target);
|
|
3986
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
3987
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
3988
|
+
}
|
|
3989
|
+
enter(now) {
|
|
3990
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
3991
|
+
}
|
|
3992
|
+
exit(now) {
|
|
3993
|
+
if (this.outroAt !== Infinity || this.enterAt === Infinity) return;
|
|
3994
|
+
this.outroAt = now;
|
|
3995
|
+
this.carsAtOutro = this.appear.filter((a) => a > 0.5).length;
|
|
3996
|
+
this.exitPathTime = now - this.enterAt;
|
|
3997
|
+
const from = this.motion.positionAt(this.exitPathTime);
|
|
3998
|
+
const velocity = subtract(
|
|
3999
|
+
this.motion.positionAt(this.exitPathTime + 1),
|
|
4000
|
+
this.motion.positionAt(this.exitPathTime - 1)
|
|
4001
|
+
);
|
|
4002
|
+
const speed = Math.hypot(velocity.x, velocity.y, velocity.z);
|
|
4003
|
+
this.exitPoint = from;
|
|
4004
|
+
if (speed > 1e-6) this.exitSpeed = speed / 2;
|
|
4005
|
+
this.exitDir = speed > 1e-6 ? normalize(velocity) : { x: 1, y: 0, z: 0 };
|
|
4006
|
+
}
|
|
4007
|
+
isFinished() {
|
|
4008
|
+
return this.finished;
|
|
4009
|
+
}
|
|
4010
|
+
/** Milliseconds the lead car keeps moving into the outro; feed a trail layer's `outroMs`. */
|
|
4011
|
+
get outroDurationMs() {
|
|
4012
|
+
return TRAIL_OUTRO_MS;
|
|
4013
|
+
}
|
|
4014
|
+
/**
|
|
4015
|
+
* A {@link MotionController} following the lead car's actual position, through
|
|
4016
|
+
* laps and the accelerating escape. Feed it to a particle layer's `emitter`
|
|
4017
|
+
* so the star trail stays behind the train.
|
|
4018
|
+
*/
|
|
4019
|
+
trailEmitter() {
|
|
4020
|
+
return {
|
|
4021
|
+
positionAt: (t) => this.enterAt === Infinity ? this.motion.positionAt(t) : this.pathPosition(t - this.enterAt + this.warp(t))
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
render(now, frame) {
|
|
4025
|
+
if (!this.engine || !this.label) return;
|
|
4026
|
+
for (const car of this.cars) car.transform.scale = 0;
|
|
4027
|
+
if (this.enterAt === Infinity) {
|
|
4028
|
+
this.engine.render();
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
4032
|
+
this.lastNow = now;
|
|
4033
|
+
const want = this.outroAt !== Infinity ? this.carsAtOutro : Math.min(MAX_CARS, Math.round(frame.progress * MAX_CARS));
|
|
4034
|
+
const halfWidth = HALF_HEIGHT * this.aspect;
|
|
4035
|
+
const warp = this.warp(now);
|
|
4036
|
+
let anyOnScreen = false;
|
|
4037
|
+
for (let k = 0; k < MAX_CARS; k++) {
|
|
4038
|
+
const target = k < want ? 1 : 0;
|
|
4039
|
+
this.appear[k] = clamp016(this.appear[k] + Math.sign(target - this.appear[k]) * (dt / POP_MS2));
|
|
4040
|
+
if (this.appear[k] <= 0) {
|
|
4041
|
+
this.headings[k] = void 0;
|
|
4042
|
+
continue;
|
|
4043
|
+
}
|
|
4044
|
+
const p = now - this.enterAt - k * RUN_GAP_MS + warp;
|
|
4045
|
+
const position = this.pathPosition(p);
|
|
4046
|
+
if (Math.abs(position.x) > halfWidth + this.size || Math.abs(position.y) > HALF_HEIGHT + this.size) {
|
|
4047
|
+
continue;
|
|
4048
|
+
}
|
|
4049
|
+
const ahead = subtract(this.pathPosition(p + SAMPLE_MS2), position);
|
|
4050
|
+
const targetDir = Math.hypot(ahead.x, ahead.y, ahead.z) > 1e-5 ? ahead : this.headings[k] ?? { x: 1, y: 0, z: 0 };
|
|
4051
|
+
this.headings[k] = this.headings[k] ? rotateToward(this.headings[k], targetDir, TURN_RATE * dt) : normalize(targetDir);
|
|
4052
|
+
const orientation = orientationFor2(this.headings[k]);
|
|
4053
|
+
const transform2 = this.cars[k].transform;
|
|
4054
|
+
transform2.position.x = position.x;
|
|
4055
|
+
transform2.position.y = position.y;
|
|
4056
|
+
transform2.position.z = position.z;
|
|
4057
|
+
transform2.rotation.x = orientation.x;
|
|
4058
|
+
transform2.rotation.y = orientation.y;
|
|
4059
|
+
transform2.rotation.z = orientation.z;
|
|
4060
|
+
transform2.scale = this.size * easeOutBack(this.appear[k]);
|
|
4061
|
+
anyOnScreen = true;
|
|
4062
|
+
}
|
|
4063
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
4064
|
+
if (this.fadeLabel) {
|
|
4065
|
+
this.label.setOpacity(animationLabelOpacity(now, this.enterAt, POP_MS2, this.outroAt, TRAIL_OUTRO_MS));
|
|
4066
|
+
}
|
|
4067
|
+
if (this.outroAt !== Infinity && now > this.outroAt + 300 && (!anyOnScreen || now >= this.outroAt + MAX_OUTRO_MS)) {
|
|
4068
|
+
this.finished = true;
|
|
4069
|
+
}
|
|
4070
|
+
this.engine.render();
|
|
4071
|
+
}
|
|
4072
|
+
destroy() {
|
|
4073
|
+
this.observer?.disconnect();
|
|
4074
|
+
this.observer = void 0;
|
|
4075
|
+
this.label?.container.remove();
|
|
4076
|
+
this.label = void 0;
|
|
4077
|
+
this.engine?.destroy();
|
|
4078
|
+
this.engine = void 0;
|
|
4079
|
+
this.cars.length = 0;
|
|
4080
|
+
}
|
|
4081
|
+
/** Extra path-time every car has accelerated forward by, `now` ms into the outro. */
|
|
4082
|
+
warp(now) {
|
|
4083
|
+
if (this.outroAt === Infinity) return 0;
|
|
4084
|
+
const seconds = (now - this.outroAt) / 1e3;
|
|
4085
|
+
return 0.5 * WARP_ACCEL * seconds * seconds;
|
|
4086
|
+
}
|
|
4087
|
+
/**
|
|
4088
|
+
* The single trajectory every car rides, sampled at path-time `p`: the track
|
|
4089
|
+
* up to the exit switch point, then a straight escape outward. Because the
|
|
4090
|
+
* switch point and direction are shared, all cars follow the exact same path.
|
|
4091
|
+
*/
|
|
4092
|
+
pathPosition(p) {
|
|
4093
|
+
if (this.outroAt === Infinity || p <= this.exitPathTime) {
|
|
4094
|
+
return this.motion.positionAt(p);
|
|
4095
|
+
}
|
|
4096
|
+
const distance = this.exitSpeed * (p - this.exitPathTime);
|
|
4097
|
+
return {
|
|
4098
|
+
x: this.exitPoint.x + this.exitDir.x * distance,
|
|
4099
|
+
y: this.exitPoint.y + this.exitDir.y * distance,
|
|
4100
|
+
z: this.exitPoint.z + this.exitDir.z * distance
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
|
|
3877
4105
|
// src/prefabs/ghost-train.ts
|
|
3878
4106
|
function ghostTrain(options = {}) {
|
|
3879
|
-
const motion = options.object?.motion ?? squareMotion({ size: 1.7, periodMs: 6800, tilt: 0.5 });
|
|
3880
4107
|
const particles = options.particles ?? {};
|
|
3881
|
-
const
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
size: 0.3,
|
|
3885
|
-
transparency: { mode: "two-sided", opacity: 0.55 },
|
|
3886
|
-
tail: { count: 4, gapMs: 240 },
|
|
3887
|
-
backend: options.backend,
|
|
3888
|
-
...options.object,
|
|
3889
|
-
label: options.object?.label
|
|
4108
|
+
const train = new GhostTrainAnimation({
|
|
4109
|
+
motion: options.object?.motion,
|
|
4110
|
+
backend: options.backend
|
|
3890
4111
|
});
|
|
3891
4112
|
const trail = new ParticlesAnimation({
|
|
3892
4113
|
rate: 30,
|
|
3893
4114
|
lifeMs: 1700,
|
|
3894
|
-
size: 0.
|
|
3895
|
-
speed: 0.
|
|
4115
|
+
size: 0.15,
|
|
4116
|
+
speed: 0.11,
|
|
3896
4117
|
colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
|
|
3897
|
-
texture: particles.texture ?? starTexture(),
|
|
3898
|
-
emitter:
|
|
3899
|
-
outroMs:
|
|
4118
|
+
texture: particles.texture ?? starTexture({ glow: 5 }),
|
|
4119
|
+
emitter: train.trailEmitter(),
|
|
4120
|
+
outroMs: train.outroDurationMs,
|
|
3900
4121
|
seed: 17,
|
|
3901
4122
|
backend: options.backend,
|
|
3902
4123
|
...particles,
|
|
3903
4124
|
label: options.label ?? particles.label,
|
|
3904
4125
|
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
3905
4126
|
});
|
|
3906
|
-
return progressSpinner(new CompositeAnimation([trail,
|
|
4127
|
+
return progressSpinner(new CompositeAnimation([trail, train]), options);
|
|
3907
4128
|
}
|
|
3908
4129
|
|
|
3909
4130
|
// src/prefabs/grid-assembly.ts
|
|
@@ -4000,68 +4221,347 @@ void main() {
|
|
|
4000
4221
|
}), options);
|
|
4001
4222
|
}
|
|
4002
4223
|
|
|
4003
|
-
// src/
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4224
|
+
// src/animations/rocket-launch.ts
|
|
4225
|
+
var ROCKETS = 20;
|
|
4226
|
+
var CAMERA_Z4 = 3;
|
|
4227
|
+
var FOV3 = 55 * Math.PI / 180;
|
|
4228
|
+
var HALF_HEIGHT2 = Math.tan(FOV3 / 2) * CAMERA_Z4;
|
|
4229
|
+
var SIZE = 0.12;
|
|
4230
|
+
var ROW_Y = -0.5;
|
|
4231
|
+
var PARTICLE_Z = 0.3;
|
|
4232
|
+
var SLIDE_MS = 460;
|
|
4233
|
+
var SLIDE_GATE = 0.45;
|
|
4234
|
+
var EXIT_HURRY3 = 2.5;
|
|
4235
|
+
var LAUNCH_SPREAD_MS = 620;
|
|
4236
|
+
var ASCENT_G = 5.2;
|
|
4237
|
+
var FINISH_PAD_MS = 2e3;
|
|
4238
|
+
var TURNERS = 3;
|
|
4239
|
+
var TURN_MIN_Y = 0.2;
|
|
4240
|
+
var TURN_MAX_Y = 0.8;
|
|
4241
|
+
var TURN_MIN_DEG = 30;
|
|
4242
|
+
var TURN_MAX_DEG = 50;
|
|
4243
|
+
var DEG = Math.PI / 180;
|
|
4244
|
+
var SMOKE_LIFE_MS = 1400;
|
|
4245
|
+
var SMOKE_GAP_MS = 320;
|
|
4246
|
+
var SMOKE_RISE = 0.55;
|
|
4247
|
+
var SMOKE_SIZE = 0.17;
|
|
4248
|
+
var SMOKE_PEAK = 0.16;
|
|
4249
|
+
var SMOKE_POOL = 104;
|
|
4250
|
+
var FIRE_LIFE_MS = 420;
|
|
4251
|
+
var FIRE_GAP_MS = 55;
|
|
4252
|
+
var FIRE_ON_MS = 950;
|
|
4253
|
+
var FIRE_TRAIL = 0.25;
|
|
4254
|
+
var FIRE_SPREAD = 0.09;
|
|
4255
|
+
var FIRE_SIZE = 0.15;
|
|
4256
|
+
var FIRE_PEAK = 0.9;
|
|
4257
|
+
var FIRE_POOL = 140;
|
|
4258
|
+
var SMOKE_COLORS = ["#e2e8f0", "#cbd5e1"];
|
|
4259
|
+
var FIRE_COLORS = ["#fef3c7", "#fde047", "#fb923c", "#ef4444"];
|
|
4260
|
+
var ROCKET_COLORS = ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"];
|
|
4261
|
+
function clamp017(value) {
|
|
4262
|
+
return Math.max(0, Math.min(1, value));
|
|
4019
4263
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4264
|
+
function smoothstep2(edge0, edge1, value) {
|
|
4265
|
+
const x = clamp017((value - edge0) / (edge1 - edge0));
|
|
4266
|
+
return x * x * (3 - 2 * x);
|
|
4267
|
+
}
|
|
4268
|
+
function hash012(index, salt) {
|
|
4269
|
+
let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
|
|
4270
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
4271
|
+
h ^= h >>> 16;
|
|
4272
|
+
return (h >>> 0) / 4294967296;
|
|
4273
|
+
}
|
|
4274
|
+
function puffTexture(coreAlpha, coreStop) {
|
|
4275
|
+
return canvasTexture((ctx) => {
|
|
4276
|
+
const g = ctx.createRadialGradient(48, 48, 1, 48, 48, 47);
|
|
4277
|
+
g.addColorStop(0, `rgba(255,255,255,${coreAlpha})`);
|
|
4278
|
+
g.addColorStop(coreStop, `rgba(255,255,255,${coreAlpha * 0.6})`);
|
|
4279
|
+
g.addColorStop(1, "rgba(255,255,255,0)");
|
|
4280
|
+
ctx.fillStyle = g;
|
|
4281
|
+
ctx.fillRect(0, 0, 96, 96);
|
|
4282
|
+
});
|
|
4283
|
+
}
|
|
4284
|
+
var RocketLaunchAnimation = class {
|
|
4285
|
+
constructor(options = {}) {
|
|
4286
|
+
this.rockets = [];
|
|
4287
|
+
this.smoke = [];
|
|
4288
|
+
this.fire = [];
|
|
4289
|
+
this.smokeFades = [];
|
|
4290
|
+
this.fireFades = [];
|
|
4291
|
+
this.blends = new Array(ROCKETS).fill(0);
|
|
4292
|
+
this.groundedAt = new Array(ROCKETS).fill(Infinity);
|
|
4293
|
+
// Per-rocket veer parameters (turnS = Infinity for a rocket that climbs straight).
|
|
4294
|
+
this.turnS = new Array(ROCKETS).fill(Infinity);
|
|
4295
|
+
this.turnDir = [];
|
|
4296
|
+
this.turnRoll = new Array(ROCKETS).fill(0);
|
|
4297
|
+
this.stagger = new Array(ROCKETS).fill(0);
|
|
4298
|
+
this.aspect = 16 / 9;
|
|
4299
|
+
this.enterAt = Infinity;
|
|
4300
|
+
this.exitAt = Infinity;
|
|
4301
|
+
this.launchedAt = Infinity;
|
|
4302
|
+
this.lastNow = 0;
|
|
4303
|
+
this.finished = false;
|
|
4304
|
+
this.backend = options.backend;
|
|
4305
|
+
this.labelContent = options.label;
|
|
4306
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
4307
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
4308
|
+
this.turnDir.push({ x: 0, y: 1 });
|
|
4309
|
+
this.stagger[i] = hash012(i, 7) * LAUNCH_SPREAD_MS;
|
|
4030
4310
|
}
|
|
4031
|
-
|
|
4311
|
+
const order = Array.from({ length: ROCKETS }, (_, i) => i).sort(
|
|
4312
|
+
(a, b) => hash012(a, 11) - hash012(b, 11)
|
|
4313
|
+
);
|
|
4314
|
+
for (const i of order.slice(0, TURNERS)) {
|
|
4315
|
+
const height = TURN_MIN_Y + hash012(i, 12) * (TURN_MAX_Y - TURN_MIN_Y);
|
|
4316
|
+
const angle = (TURN_MIN_DEG + hash012(i, 13) * (TURN_MAX_DEG - TURN_MIN_DEG)) * DEG;
|
|
4317
|
+
const sign = hash012(i, 14) < 0.5 ? -1 : 1;
|
|
4318
|
+
this.turnS[i] = height - ROW_Y;
|
|
4319
|
+
this.turnDir[i] = { x: sign * Math.sin(angle), y: Math.cos(angle) };
|
|
4320
|
+
this.turnRoll[i] = -sign * angle;
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
mount(target) {
|
|
4324
|
+
if (!target.style.position) target.style.position = "relative";
|
|
4325
|
+
const smokeMeshes = SMOKE_COLORS.map((color) => quad(1, [color]));
|
|
4326
|
+
const fireMeshes = FIRE_COLORS.map((color) => quad(1, [color]));
|
|
4327
|
+
const smokeTexture = puffTexture(0.85, 0.5);
|
|
4328
|
+
const fireTexture = puffTexture(1, 0.32);
|
|
4329
|
+
const backend = async (rendererOptions) => {
|
|
4330
|
+
const renderer = this.backend === "webgpu" ? new (await Promise.resolve().then(() => (init_webgpu_textured(), webgpu_textured_exports))).WebGPUTexturedRenderer(rendererOptions) : this.backend === "webgl" ? new (await Promise.resolve().then(() => (init_webgl_textured(), webgl_textured_exports))).WebGLTexturedRenderer(rendererOptions) : new (await Promise.resolve().then(() => (init_canvas2d_textured(), canvas2d_textured_exports))).Canvas2DTexturedRenderer(rendererOptions);
|
|
4331
|
+
for (const mesh of smokeMeshes) renderer.setTexture(mesh, smokeTexture);
|
|
4332
|
+
for (const mesh of fireMeshes) renderer.setTexture(mesh, fireTexture);
|
|
4333
|
+
return renderer;
|
|
4334
|
+
};
|
|
4335
|
+
const engine = new Little3dEngine({
|
|
4336
|
+
backend,
|
|
4337
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z4 }, fov: FOV3 }
|
|
4338
|
+
});
|
|
4339
|
+
const rocketMesh = pyramid(1, ROCKET_COLORS);
|
|
4340
|
+
for (let i = 0; i < ROCKETS; i++) this.rockets.push(engine.add(rocketMesh, { scale: 0 }));
|
|
4341
|
+
for (let s = 0; s < SMOKE_POOL; s++) {
|
|
4342
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
4343
|
+
this.smokeFades.push(fade);
|
|
4344
|
+
this.smoke.push(engine.add(smokeMeshes[s % smokeMeshes.length], { scale: 0, transparency: fade }));
|
|
4345
|
+
}
|
|
4346
|
+
for (let f = 0; f < FIRE_POOL; f++) {
|
|
4347
|
+
const fade = { mode: "one-sided", opacity: 0 };
|
|
4348
|
+
this.fireFades.push(fade);
|
|
4349
|
+
this.fire.push(engine.add(fireMeshes[f % fireMeshes.length], { scale: 0, transparency: fade }));
|
|
4350
|
+
}
|
|
4351
|
+
this.engine = engine;
|
|
4352
|
+
engine.mount(target).catch((error) => {
|
|
4353
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
4354
|
+
});
|
|
4355
|
+
const measure = () => {
|
|
4356
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
4357
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
4358
|
+
}
|
|
4359
|
+
};
|
|
4360
|
+
measure();
|
|
4361
|
+
this.observer = new ResizeObserver(measure);
|
|
4362
|
+
this.observer.observe(target);
|
|
4363
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
4364
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
4365
|
+
}
|
|
4366
|
+
enter(now) {
|
|
4367
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
4368
|
+
}
|
|
4369
|
+
exit(now) {
|
|
4370
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
4371
|
+
}
|
|
4372
|
+
isFinished() {
|
|
4373
|
+
return this.finished;
|
|
4374
|
+
}
|
|
4375
|
+
render(now, frame) {
|
|
4376
|
+
if (!this.engine || !this.label) return;
|
|
4377
|
+
for (const handle of this.rockets) handle.transform.scale = 0;
|
|
4378
|
+
for (const fade of this.smokeFades) fade.opacity = 0;
|
|
4379
|
+
for (const fade of this.fireFades) fade.opacity = 0;
|
|
4380
|
+
for (const handle of this.smoke) handle.transform.scale = 0;
|
|
4381
|
+
for (const handle of this.fire) handle.transform.scale = 0;
|
|
4382
|
+
if (this.enterAt === Infinity) {
|
|
4383
|
+
this.engine.render();
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
4387
|
+
this.lastNow = now;
|
|
4388
|
+
const exiting = this.exitAt !== Infinity;
|
|
4389
|
+
this.updateBlends(dt, frame.progress, now, exiting);
|
|
4390
|
+
if (exiting && this.launchedAt === Infinity && this.blends.every((blend) => blend >= 1)) {
|
|
4391
|
+
this.launchedAt = now;
|
|
4392
|
+
}
|
|
4393
|
+
const launched = this.launchedAt !== Infinity;
|
|
4394
|
+
const halfWidth = HALF_HEIGHT2 * this.aspect;
|
|
4395
|
+
const rowHalf = Math.min(halfWidth * 0.8, 1.18);
|
|
4396
|
+
const spawnX = halfWidth + 0.6;
|
|
4397
|
+
let smokeCursor = 0;
|
|
4398
|
+
let fireCursor = 0;
|
|
4399
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
4400
|
+
const homeX = -rowHalf + 2 * rowHalf * i / (ROCKETS - 1);
|
|
4401
|
+
const launchAt = launched ? this.launchedAt + this.stagger[i] : Infinity;
|
|
4402
|
+
const la = now - launchAt;
|
|
4403
|
+
if (launched && la >= 0) {
|
|
4404
|
+
this.renderAscent(i, homeX, la, halfWidth);
|
|
4405
|
+
fireCursor = this.emitFire(i, homeX, la, fireCursor);
|
|
4406
|
+
continue;
|
|
4407
|
+
}
|
|
4408
|
+
const blend = this.blends[i];
|
|
4409
|
+
if (blend <= 0) continue;
|
|
4410
|
+
const transform2 = this.rockets[i].transform;
|
|
4411
|
+
transform2.position.x = spawnX + (homeX - spawnX) * easeOutBack(blend);
|
|
4412
|
+
transform2.position.y = ROW_Y;
|
|
4413
|
+
transform2.position.z = 0;
|
|
4414
|
+
transform2.rotation.x = 0;
|
|
4415
|
+
transform2.rotation.y = 0;
|
|
4416
|
+
transform2.rotation.z = 0;
|
|
4417
|
+
transform2.scale = SIZE * smoothstep2(0, 0.6, blend);
|
|
4418
|
+
if (this.groundedAt[i] !== Infinity) {
|
|
4419
|
+
smokeCursor = this.emitSmoke(i, homeX, now, launchAt, smokeCursor);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
4423
|
+
if (this.fadeLabel) {
|
|
4424
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
4425
|
+
now,
|
|
4426
|
+
this.enterAt,
|
|
4427
|
+
SLIDE_MS,
|
|
4428
|
+
this.launchedAt,
|
|
4429
|
+
LAUNCH_SPREAD_MS
|
|
4430
|
+
));
|
|
4431
|
+
}
|
|
4432
|
+
if (launched && now >= this.launchedAt + LAUNCH_SPREAD_MS + FINISH_PAD_MS) {
|
|
4433
|
+
this.finished = true;
|
|
4434
|
+
}
|
|
4435
|
+
this.engine.render();
|
|
4436
|
+
}
|
|
4437
|
+
destroy() {
|
|
4438
|
+
this.observer?.disconnect();
|
|
4439
|
+
this.observer = void 0;
|
|
4440
|
+
this.label?.container.remove();
|
|
4441
|
+
this.label = void 0;
|
|
4442
|
+
this.engine?.destroy();
|
|
4443
|
+
this.engine = void 0;
|
|
4444
|
+
this.rockets.length = 0;
|
|
4445
|
+
this.smoke.length = 0;
|
|
4446
|
+
this.fire.length = 0;
|
|
4447
|
+
this.smokeFades.length = 0;
|
|
4448
|
+
this.fireFades.length = 0;
|
|
4449
|
+
}
|
|
4450
|
+
updateBlends(dt, progress, now, exiting) {
|
|
4451
|
+
const want = exiting ? ROCKETS : Math.min(ROCKETS, Math.round(progress * ROCKETS));
|
|
4452
|
+
const rate = dt / SLIDE_MS * (exiting ? EXIT_HURRY3 : 1);
|
|
4453
|
+
for (let i = 0; i < ROCKETS; i++) {
|
|
4454
|
+
const target = i < want ? 1 : 0;
|
|
4455
|
+
const blend = this.blends[i];
|
|
4456
|
+
if (target > blend && (i === 0 || this.blends[i - 1] >= SLIDE_GATE)) {
|
|
4457
|
+
this.blends[i] = Math.min(1, blend + rate);
|
|
4458
|
+
} else if (target < blend && (i === ROCKETS - 1 || this.blends[i + 1] <= 1 - SLIDE_GATE)) {
|
|
4459
|
+
this.blends[i] = Math.max(0, blend - rate);
|
|
4460
|
+
}
|
|
4461
|
+
if (this.blends[i] >= 1) {
|
|
4462
|
+
if (this.groundedAt[i] === Infinity) this.groundedAt[i] = now;
|
|
4463
|
+
} else {
|
|
4464
|
+
this.groundedAt[i] = Infinity;
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
/** Along-track distance climbed `la` ms after this rocket's own blast-off. */
|
|
4469
|
+
ascentDistance(la) {
|
|
4470
|
+
const seconds = la / 1e3;
|
|
4471
|
+
return 0.5 * ASCENT_G * seconds * seconds;
|
|
4472
|
+
}
|
|
4473
|
+
/** Rocket center, nose direction, and roll `la` ms into its climb. */
|
|
4474
|
+
ascentPose(i, homeX, la) {
|
|
4475
|
+
const s = this.ascentDistance(la);
|
|
4476
|
+
const turnS = this.turnS[i];
|
|
4477
|
+
if (s <= turnS) {
|
|
4478
|
+
return { pos: { x: homeX, y: ROW_Y + s }, dir: { x: 0, y: 1 }, roll: 0 };
|
|
4479
|
+
}
|
|
4480
|
+
const post = s - turnS;
|
|
4481
|
+
const dir = this.turnDir[i];
|
|
4482
|
+
return {
|
|
4483
|
+
pos: { x: homeX + dir.x * post, y: ROW_Y + turnS + dir.y * post },
|
|
4484
|
+
dir,
|
|
4485
|
+
roll: this.turnRoll[i]
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4488
|
+
renderAscent(i, homeX, la, halfWidth) {
|
|
4489
|
+
const { pos, roll } = this.ascentPose(i, homeX, la);
|
|
4490
|
+
if (pos.y > HALF_HEIGHT2 + 0.4 || Math.abs(pos.x) > halfWidth + 0.4) return;
|
|
4491
|
+
const transform2 = this.rockets[i].transform;
|
|
4492
|
+
transform2.position.x = pos.x;
|
|
4493
|
+
transform2.position.y = pos.y;
|
|
4494
|
+
transform2.position.z = 0;
|
|
4495
|
+
transform2.rotation.x = 0;
|
|
4496
|
+
transform2.rotation.y = 0;
|
|
4497
|
+
transform2.rotation.z = roll;
|
|
4498
|
+
transform2.scale = SIZE;
|
|
4499
|
+
}
|
|
4500
|
+
emitFire(i, homeX, la, cursor) {
|
|
4501
|
+
const gap = FIRE_GAP_MS;
|
|
4502
|
+
const last = Math.min(Math.floor(la / gap), Math.floor(FIRE_ON_MS / gap));
|
|
4503
|
+
const first = Math.max(0, Math.ceil((la - FIRE_LIFE_MS) / gap));
|
|
4504
|
+
for (let n = first; n <= last; n++) {
|
|
4505
|
+
if (cursor >= this.fire.length) return cursor;
|
|
4506
|
+
const emitLa = n * gap;
|
|
4507
|
+
const age = la - emitLa;
|
|
4508
|
+
if (age < 0 || age >= FIRE_LIFE_MS) continue;
|
|
4509
|
+
const life = age / FIRE_LIFE_MS;
|
|
4510
|
+
const seconds = age / 1e3;
|
|
4511
|
+
const pose = this.ascentPose(i, homeX, emitLa);
|
|
4512
|
+
const back = { x: -pose.dir.x, y: -pose.dir.y };
|
|
4513
|
+
const perp = { x: -pose.dir.y, y: pose.dir.x };
|
|
4514
|
+
const lat = (hash012(i * 97 + n, 1) - 0.5) * FIRE_SPREAD;
|
|
4515
|
+
const baseX = pose.pos.x + back.x * SIZE * 0.5;
|
|
4516
|
+
const baseY = pose.pos.y + back.y * SIZE * 0.5;
|
|
4517
|
+
const transform2 = this.fire[cursor].transform;
|
|
4518
|
+
transform2.position.x = baseX + back.x * FIRE_TRAIL * seconds + perp.x * lat;
|
|
4519
|
+
transform2.position.y = baseY + back.y * FIRE_TRAIL * seconds + perp.y * lat - 0.12 * seconds * seconds;
|
|
4520
|
+
transform2.position.z = PARTICLE_Z;
|
|
4521
|
+
transform2.rotation.z = hash012(i * 97 + n, 2) * Math.PI * 2;
|
|
4522
|
+
transform2.scale = FIRE_SIZE * (0.7 + 0.5 * hash012(i * 97 + n, 3)) * (1 - 0.55 * life);
|
|
4523
|
+
this.fireFades[cursor].opacity = FIRE_PEAK * smoothstep2(0, 0.15, life) * (1 - smoothstep2(0.55, 1, life));
|
|
4524
|
+
cursor++;
|
|
4525
|
+
}
|
|
4526
|
+
return cursor;
|
|
4527
|
+
}
|
|
4528
|
+
emitSmoke(i, homeX, now, launchAt, cursor) {
|
|
4529
|
+
const start = this.groundedAt[i];
|
|
4530
|
+
const tr = now - start;
|
|
4531
|
+
const gap = SMOKE_GAP_MS;
|
|
4532
|
+
const emitUntil = Number.isFinite(launchAt) ? launchAt - start : tr;
|
|
4533
|
+
const last = Math.min(Math.floor(tr / gap), Math.floor(emitUntil / gap));
|
|
4534
|
+
const first = Math.max(0, Math.ceil((tr - SMOKE_LIFE_MS) / gap));
|
|
4535
|
+
const baseY = ROW_Y - SIZE * 0.4;
|
|
4536
|
+
for (let n = first; n <= last; n++) {
|
|
4537
|
+
if (cursor >= this.smoke.length) return cursor;
|
|
4538
|
+
const age = tr - n * gap;
|
|
4539
|
+
if (age < 0 || age >= SMOKE_LIFE_MS) continue;
|
|
4540
|
+
const life = age / SMOKE_LIFE_MS;
|
|
4541
|
+
const drift = (hash012(i * 131 + n, 1) - 0.5) * 0.14;
|
|
4542
|
+
const transform2 = this.smoke[cursor].transform;
|
|
4543
|
+
transform2.position.x = homeX + drift * life;
|
|
4544
|
+
transform2.position.y = baseY + SMOKE_RISE * life;
|
|
4545
|
+
transform2.position.z = PARTICLE_Z;
|
|
4546
|
+
transform2.rotation.z = hash012(i * 131 + n, 2) * Math.PI * 2;
|
|
4547
|
+
transform2.scale = SMOKE_SIZE * (0.5 + 0.8 * life) * (0.7 + 0.6 * hash012(i * 131 + n, 3));
|
|
4548
|
+
this.smokeFades[cursor].opacity = SMOKE_PEAK * smoothstep2(0, 0.25, life) * (1 - smoothstep2(0.5, 1, life));
|
|
4549
|
+
cursor++;
|
|
4550
|
+
}
|
|
4551
|
+
return cursor;
|
|
4552
|
+
}
|
|
4032
4553
|
};
|
|
4554
|
+
|
|
4555
|
+
// src/prefabs/rocket-launch.ts
|
|
4033
4556
|
function rocketLaunch(options = {}) {
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
backend: options.backend,
|
|
4043
|
-
...options.object,
|
|
4044
|
-
label: options.object?.label
|
|
4045
|
-
});
|
|
4046
|
-
const exhaust = new ParticlesAnimation({
|
|
4047
|
-
rate: 46,
|
|
4048
|
-
lifeMs: 1100,
|
|
4049
|
-
size: 0.18,
|
|
4050
|
-
speed: 0.5,
|
|
4051
|
-
direction: { x: 0, y: -1, z: 0 },
|
|
4052
|
-
spread: 0.3,
|
|
4053
|
-
gravity: { x: 0, y: -0.6, z: 0 },
|
|
4054
|
-
colors: ["#fde047", "#fb923c", "#ef4444", "#fef3c7"],
|
|
4055
|
-
texture: particles.texture ?? shineTexture(),
|
|
4056
|
-
emitter: object.trailEmitter(),
|
|
4057
|
-
outroMs: object.outroDurationMs,
|
|
4058
|
-
seed: 9,
|
|
4059
|
-
backend: options.backend,
|
|
4060
|
-
...particles,
|
|
4061
|
-
label: options.label ?? particles.label,
|
|
4062
|
-
fadeLabel: options.fadeLabel ?? particles.fadeLabel
|
|
4063
|
-
});
|
|
4064
|
-
return progressSpinner(new CompositeAnimation([exhaust, object]), options);
|
|
4557
|
+
return progressSpinner(
|
|
4558
|
+
new RocketLaunchAnimation({
|
|
4559
|
+
backend: options.backend,
|
|
4560
|
+
label: options.label,
|
|
4561
|
+
fadeLabel: options.fadeLabel
|
|
4562
|
+
}),
|
|
4563
|
+
options
|
|
4564
|
+
);
|
|
4065
4565
|
}
|
|
4066
4566
|
|
|
4067
4567
|
// src/motion/wander.ts
|
|
@@ -4129,6 +4629,24 @@ void main() {
|
|
|
4129
4629
|
}), options);
|
|
4130
4630
|
}
|
|
4131
4631
|
|
|
4632
|
+
// src/motion/circle.ts
|
|
4633
|
+
function circleMotion(options = {}) {
|
|
4634
|
+
const radius = options.radius ?? 1.3;
|
|
4635
|
+
const periodMs = options.periodMs ?? 3e3;
|
|
4636
|
+
const tilt = options.tilt ?? 0.5;
|
|
4637
|
+
const direction = options.direction ?? 1;
|
|
4638
|
+
const cosTilt = Math.cos(tilt);
|
|
4639
|
+
const sinTilt = Math.sin(tilt);
|
|
4640
|
+
return {
|
|
4641
|
+
positionAt(t) {
|
|
4642
|
+
const angle = direction * t / periodMs * Math.PI * 2;
|
|
4643
|
+
const x = radius * Math.cos(angle);
|
|
4644
|
+
const flatY = radius * Math.sin(angle);
|
|
4645
|
+
return { x, y: flatY * cosTilt, z: flatY * sinTilt };
|
|
4646
|
+
}
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4132
4650
|
// <stdin>
|
|
4133
4651
|
init_webgl_textured();
|
|
4134
4652
|
init_webgpu_textured();
|