cubeforge 0.4.10 → 0.4.11

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/index.d.ts CHANGED
@@ -531,8 +531,35 @@ interface ParticleEmitterProps {
531
531
  * - `'screen'` — lightens, softer than additive
532
532
  */
533
533
  blendMode?: 'normal' | 'additive' | 'multiply' | 'screen';
534
+ /**
535
+ * Formation mode: particles lerp toward fixed target positions instead of
536
+ * being emitted with velocity. Enables logo reveals, constellations, shape morphing.
537
+ * - `'standard'` (default) — normal emit/gravity/lifetime behaviour
538
+ * - `'formation'` — particles seek `formationPoints`; no gravity, no expiry
539
+ */
540
+ mode?: 'standard' | 'formation';
541
+ /**
542
+ * Target positions for formation mode. One particle is spawned per point.
543
+ * Change this array to morph the formation — particles smoothly lerp to new targets.
544
+ */
545
+ formationPoints?: {
546
+ x: number;
547
+ y: number;
548
+ }[];
549
+ /**
550
+ * How strongly particles seek their target each frame in formation mode.
551
+ * Exponential lerp factor (0–1). Default 0.055 (~5.5% per frame at 60 fps).
552
+ */
553
+ seekStrength?: number;
554
+ /**
555
+ * Smoothly transition all particles to this color.
556
+ * Works in both standard and formation modes.
557
+ */
558
+ targetColor?: string;
559
+ /** Duration of the global color transition in seconds. Default 0.5. */
560
+ colorTransitionDuration?: number;
534
561
  }
535
- declare function ParticleEmitter({ active, preset, rate, speed, spread, angle, particleLife, particleSize, color, gravity, maxParticles, burstCount, emitShape, emitRadius, emitWidth, emitHeight, textureSrc, enableRotation, rotationSpeedRange, sizeOverLife, attractors, colorOverLife, blendMode, }: ParticleEmitterProps): null;
562
+ declare function ParticleEmitter({ active, preset, rate, speed, spread, angle, particleLife, particleSize, color, gravity, maxParticles, burstCount, emitShape, emitRadius, emitWidth, emitHeight, textureSrc, enableRotation, rotationSpeedRange, sizeOverLife, attractors, colorOverLife, blendMode, mode, formationPoints, seekStrength, targetColor, colorTransitionDuration, }: ParticleEmitterProps): null;
536
563
 
537
564
  interface VirtualJoystickProps {
538
565
  /** Diameter of the joystick base in pixels (default 120) */
package/dist/index.js CHANGED
@@ -3653,15 +3653,91 @@ var RenderSystem = class {
3653
3653
  for (const id of world.query("Transform", "ParticlePool")) {
3654
3654
  const t = world.getComponent(id, "Transform");
3655
3655
  const pool = world.getComponent(id, "ParticlePool");
3656
+ if (pool.targetColor && pool._colorTransitionFrom !== void 0) {
3657
+ pool._colorTransitionElapsed = (pool._colorTransitionElapsed ?? 0) + dt;
3658
+ const dur = pool.colorTransitionDuration ?? 0.5;
3659
+ const ct = Math.min((pool._colorTransitionElapsed ?? 0) / dur, 1);
3660
+ const ease = ct * ct * (3 - 2 * ct);
3661
+ const [cr0, cg0, cb0] = parseCSSColor(pool._colorTransitionFrom);
3662
+ const [cr1, cg1, cb1] = parseCSSColor(pool.targetColor);
3663
+ const ri = Math.round(cr0 + (cr1 - cr0) * ease);
3664
+ const gi = Math.round(cg0 + (cg1 - cg0) * ease);
3665
+ const bi = Math.round(cb0 + (cb1 - cb0) * ease);
3666
+ pool.color = `rgb(${ri},${gi},${bi})`;
3667
+ if (ct >= 1) {
3668
+ pool._colorTransitionFrom = void 0;
3669
+ pool._colorTransitionElapsed = void 0;
3670
+ }
3671
+ }
3672
+ const isFormation = pool.mode === "formation";
3656
3673
  pool.particles = pool.particles.filter((p) => {
3674
+ if (isFormation) {
3675
+ if (pool.attractors) {
3676
+ for (const attr of pool.attractors) {
3677
+ const adx = attr.x - p.x;
3678
+ const ady = attr.y - p.y;
3679
+ const dist = Math.sqrt(adx * adx + ady * ady);
3680
+ if (dist < attr.radius && dist > 0) {
3681
+ const force = attr.strength * (1 - dist / attr.radius);
3682
+ p.vx += adx / dist * force * dt;
3683
+ p.vy += ady / dist * force * dt;
3684
+ }
3685
+ }
3686
+ }
3687
+ p.vx *= 0.82;
3688
+ p.vy *= 0.82;
3689
+ p.x += p.vx * dt;
3690
+ p.y += p.vy * dt;
3691
+ if (p.targetX !== void 0 && p.targetY !== void 0) {
3692
+ const seek2 = pool.seekStrength ?? 0.055;
3693
+ p.x += (p.targetX - p.x) * seek2;
3694
+ p.y += (p.targetY - p.y) * seek2;
3695
+ }
3696
+ return true;
3697
+ }
3657
3698
  p.life -= dt;
3699
+ if (pool.attractors) {
3700
+ for (const attr of pool.attractors) {
3701
+ const adx = attr.x - p.x;
3702
+ const ady = attr.y - p.y;
3703
+ const dist = Math.sqrt(adx * adx + ady * ady);
3704
+ if (dist < attr.radius && dist > 0) {
3705
+ const force = attr.strength * (1 - dist / attr.radius);
3706
+ p.vx += adx / dist * force * dt;
3707
+ p.vy += ady / dist * force * dt;
3708
+ }
3709
+ }
3710
+ }
3658
3711
  p.x += p.vx * dt;
3659
3712
  p.y += p.vy * dt;
3660
3713
  p.vy += p.gravity * dt;
3661
3714
  if (p.rotationSpeed !== void 0) p.rotation = (p.rotation ?? 0) + p.rotationSpeed * dt;
3662
3715
  return p.life > 0;
3663
3716
  });
3664
- if (pool.active && pool.particles.length < pool.maxParticles) {
3717
+ if (isFormation) {
3718
+ const fp = pool.formationPoints ?? [];
3719
+ while (pool.particles.length < fp.length) {
3720
+ const idx = pool.particles.length;
3721
+ const startSize = pool.sizeOverLife?.start ?? pool.particleSize;
3722
+ const endSize = pool.sizeOverLife?.end ?? pool.particleSize;
3723
+ pool.particles.push({
3724
+ x: t.x + (world.rng() - 0.5) * 20,
3725
+ y: t.y + (world.rng() - 0.5) * 20,
3726
+ vx: 0,
3727
+ vy: 0,
3728
+ life: 1,
3729
+ maxLife: 1,
3730
+ size: startSize,
3731
+ startSize,
3732
+ endSize,
3733
+ color: pool.color,
3734
+ gravity: 0,
3735
+ rotation: 0,
3736
+ targetX: fp[idx].x,
3737
+ targetY: fp[idx].y
3738
+ });
3739
+ }
3740
+ } else if (pool.active && pool.particles.length < pool.maxParticles) {
3665
3741
  let spawnCount;
3666
3742
  if (pool.burstCount != null && pool.burstCount > 0) {
3667
3743
  spawnCount = pool.burstCount;
@@ -15556,7 +15632,12 @@ function ParticleEmitter({
15556
15632
  sizeOverLife,
15557
15633
  attractors,
15558
15634
  colorOverLife,
15559
- blendMode
15635
+ blendMode,
15636
+ mode,
15637
+ formationPoints,
15638
+ seekStrength,
15639
+ targetColor,
15640
+ colorTransitionDuration
15560
15641
  }) {
15561
15642
  const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
15562
15643
  const resolvedRate = rate ?? presetConfig.rate ?? 20;
@@ -15596,7 +15677,11 @@ function ParticleEmitter({
15596
15677
  sizeOverLife,
15597
15678
  attractors,
15598
15679
  colorOverLife,
15599
- blendMode
15680
+ blendMode,
15681
+ mode,
15682
+ formationPoints,
15683
+ seekStrength,
15684
+ colorTransitionDuration
15600
15685
  });
15601
15686
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
15602
15687
  }, []);
@@ -15605,6 +15690,31 @@ function ParticleEmitter({
15605
15690
  if (!pool) return;
15606
15691
  pool.active = active;
15607
15692
  }, [active, engine, entityId]);
15693
+ useEffect28(() => {
15694
+ const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15695
+ if (!pool) return;
15696
+ pool.attractors = attractors;
15697
+ }, [attractors, engine, entityId]);
15698
+ useEffect28(() => {
15699
+ const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15700
+ if (!pool || pool.mode !== "formation" || !formationPoints) return;
15701
+ pool.formationPoints = formationPoints;
15702
+ const shuffled = [...formationPoints].sort(() => Math.random() - 0.5);
15703
+ pool.particles.forEach((p, i) => {
15704
+ if (shuffled[i]) {
15705
+ p.targetX = shuffled[i].x;
15706
+ p.targetY = shuffled[i].y;
15707
+ }
15708
+ });
15709
+ }, [formationPoints, engine, entityId]);
15710
+ useEffect28(() => {
15711
+ const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15712
+ if (!pool || !targetColor) return;
15713
+ pool._colorTransitionFrom = pool.color;
15714
+ pool._colorTransitionElapsed = 0;
15715
+ pool.targetColor = targetColor;
15716
+ pool.colorTransitionDuration = colorTransitionDuration;
15717
+ }, [targetColor, colorTransitionDuration, engine, entityId]);
15608
15718
  return null;
15609
15719
  }
15610
15720
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",