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.
@@ -2052,18 +2052,30 @@ void main() {
2052
2052
  }
2053
2053
 
2054
2054
  // src/engines/little-3d-engine/textures/dynamic/star.ts
2055
- function starTexture() {
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
- 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));
2072
+ if (glow > 0) {
2073
+ ctx.save();
2074
+ ctx.filter = `blur(${glow}px)`;
2075
+ drawStar(ctx);
2076
+ ctx.restore();
2064
2077
  }
2065
- ctx.closePath();
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 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
- ];
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 want = exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
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
- 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++) {
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
- this.handles[i].transform.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
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 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
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.13,
3895
- speed: 0.07,
4115
+ size: 0.15,
4116
+ speed: 0.11,
3896
4117
  colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
3897
- texture: particles.texture ?? starTexture(),
3898
- emitter: object.trailEmitter(),
3899
- outroMs: object.outroDurationMs,
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, object]), options);
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/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);
4011
- return {
4012
- positionAt(t) {
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
- };
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
- // 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
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
- 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);
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();