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.
@@ -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 DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
1732
- function pyramid(size = 1, colors = DEFAULT_COLORS5) {
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 DEFAULT_COLORS6 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
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 = DEFAULT_COLORS6) {
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 DEFAULT_COLORS7 = ["#e0f2fe", "#7dd3fc", "#38bdf8", "#f8fafc"];
1800
- function planeMesh(colors = DEFAULT_COLORS7) {
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] ?? DEFAULT_COLORS7[0] },
1815
- { indices: [6, 2, 1], color: colors[3] ?? DEFAULT_COLORS7[3] },
1816
- { indices: [6, 3, 2], color: colors[1] ?? DEFAULT_COLORS7[1] },
1817
- { indices: [6, 4, 3], color: colors[2] ?? DEFAULT_COLORS7[2] },
1818
- { indices: [6, 5, 4], color: colors[3] ?? DEFAULT_COLORS7[3] },
1819
- { indices: [6, 0, 5], color: colors[0] ?? DEFAULT_COLORS7[0] },
1820
- { indices: [7, 0, 1], color: colors[1] ?? DEFAULT_COLORS7[1] },
1821
- { indices: [7, 1, 2], color: colors[2] ?? DEFAULT_COLORS7[2] },
1822
- { indices: [7, 2, 3], color: colors[1] ?? DEFAULT_COLORS7[1] },
1823
- { indices: [7, 3, 4], color: colors[2] ?? DEFAULT_COLORS7[2] },
1824
- { indices: [7, 4, 5], color: colors[1] ?? DEFAULT_COLORS7[1] },
1825
- { indices: [7, 5, 0], color: colors[2] ?? DEFAULT_COLORS7[2] },
1826
- { indices: [3, 6, 8], color: colors[0] ?? DEFAULT_COLORS7[0] },
1827
- { indices: [3, 8, 6], color: colors[1] ?? DEFAULT_COLORS7[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 starTexture() {
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
- ctx.translate(48, 48);
1845
- ctx.fillStyle = "#fff";
1846
- ctx.beginPath();
1847
- for (let index = 0; index < 10; index++) {
1848
- const radius = index % 2 === 0 ? 43 : 16;
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.closePath();
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 DEFAULT_COLORS8 = ["#fde047", "#fb923c", "#f472b6", "#60a5fa"];
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 ?? DEFAULT_COLORS8;
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 object = new ObjectMotionAnimation({
2960
- mesh: () => cube(1, ["#bae6fd", "#7dd3fc", "#38bdf8", "#0ea5e9", "#a5f3fc", "#e0f2fe"]),
2961
- motion,
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.13,
2973
- speed: 0.07,
3152
+ size: 0.15,
3153
+ speed: 0.11,
2974
3154
  colors: ["#e0f2fe", "#a5f3fc", "#c4b5fd"],
2975
- texture: particles.texture ?? starTexture(),
2976
- emitter: object.trailEmitter(),
2977
- outroMs: object.outroDurationMs,
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, object]), options);
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 CAMERA_Z2 = 4;
2991
- var FOV = 55 * Math.PI / 180;
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 POP_MS = 170;
3184
+ var COLLAPSE_SPREAD_MS = 500;
3185
+ var POP_MS2 = 170;
3004
3186
  var LABEL_FADE_MS = 600;
3005
- var DEFAULT_MESHES = [
3006
- () => cube(1, ["#60a5fa", "#3b82f6", "#2563eb", "#38bdf8", "#0ea5e9", "#1d4ed8"]),
3007
- () => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
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 = clamp014(value);
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: CAMERA_Z2 }, fov: FOV }
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 + POP_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 want = exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
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(FOV / 2) * CAMERA_Z2;
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 = clamp014((t - i * INTRO_STAGGER_MS) / INTRO_MS);
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
- if (u >= 1) {
3216
- if (!this.popFading) {
3217
- this.popFading = true;
3218
- for (let i = 0; i < COUNT; i++) this.handles[i].transparency = this.fades[i];
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
- this.handles[i].transform.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
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/motion/circle.ts
3324
- function circleMotion(options = {}) {
3325
- const radius = options.radius ?? 1.3;
3326
- const periodMs = options.periodMs ?? 3e3;
3327
- const tilt = options.tilt ?? 0.5;
3328
- const direction = options.direction ?? 1;
3329
- const cosTilt = Math.cos(tilt);
3330
- const sinTilt = Math.sin(tilt);
3331
- return {
3332
- positionAt(t) {
3333
- const angle = direction * t / periodMs * Math.PI * 2;
3334
- const x = radius * Math.cos(angle);
3335
- const flatY = radius * Math.sin(angle);
3336
- return { x, y: flatY * cosTilt, z: flatY * sinTilt };
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
- // src/prefabs/rocket-launch.ts
3342
- var launchUp = ({ delta, position, velocity, durationMs }) => {
3343
- const coast = durationMs * delta * (1 - 0.5 * delta);
3344
- const climb = 5.5 * easeInCubic(delta);
3345
- return {
3346
- position: {
3347
- x: position.x + (velocity?.x ?? 0) * coast,
3348
- y: position.y + (velocity?.y ?? 0) * coast + climb,
3349
- z: position.z + (velocity?.z ?? 0) * coast
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
- const motion = options.object?.motion ?? circleMotion({ radius: 0.55, periodMs: 7e3, tilt: 0.15 });
3355
- const particles = options.particles ?? {};
3356
- const object = new ObjectMotionAnimation({
3357
- mesh: () => pyramid(1, ["#e2e8f0", "#f8fafc", "#cbd5e1", "#94a3b8", "#e2e8f0"]),
3358
- motion,
3359
- size: 0.44,
3360
- facing: "+y",
3361
- outro: { transition: launchUp, durationMs: 1300 },
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