cubeforge 0.4.11 → 0.4.12

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,6 +531,13 @@ interface ParticleEmitterProps {
531
531
  * - `'screen'` — lightens, softer than additive
532
532
  */
533
533
  blendMode?: 'normal' | 'additive' | 'multiply' | 'screen';
534
+ /**
535
+ * Visual shape of each particle.
536
+ * - `'soft'` (default) — radial gradient with glow halo; pairs well with `blendMode="additive"`
537
+ * - `'circle'` — hard-edged anti-aliased circle, no glow falloff
538
+ * - `'square'` — solid quad (fastest, no texture lookup)
539
+ */
540
+ particleShape?: 'soft' | 'circle' | 'square';
534
541
  /**
535
542
  * Formation mode: particles lerp toward fixed target positions instead of
536
543
  * being emitted with velocity. Enables logo reveals, constellations, shape morphing.
@@ -559,7 +566,7 @@ interface ParticleEmitterProps {
559
566
  /** Duration of the global color transition in seconds. Default 0.5. */
560
567
  colorTransitionDuration?: number;
561
568
  }
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;
569
+ 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, particleShape, }: ParticleEmitterProps): null;
563
570
 
564
571
  interface VirtualJoystickProps {
565
572
  /** Diameter of the joystick base in pixels (default 120) */
package/dist/index.js CHANGED
@@ -2793,6 +2793,8 @@ var RenderSystem = class {
2793
2793
  this.uTexture = gl.getUniformLocation(this.program, "u_texture");
2794
2794
  this.uUseTexture = gl.getUniformLocation(this.program, "u_useTexture");
2795
2795
  this.whiteTexture = createWhiteTexture(gl);
2796
+ this.particleTextureSoft = this._createParticleTexture("soft");
2797
+ this.particleTextureCircle = this._createParticleTexture("circle");
2796
2798
  this.parallaxProgram = createProgram(gl, PARALLAX_VERT_SRC, PARALLAX_FRAG_SRC);
2797
2799
  const fsVerts = new Float32Array([-1, -1, 1, -1, -1, 1, 1, -1, 1, 1, -1, 1]);
2798
2800
  this.parallaxVAO = gl.createVertexArray();
@@ -2819,6 +2821,8 @@ var RenderSystem = class {
2819
2821
  instanceBuffer;
2820
2822
  instanceData;
2821
2823
  whiteTexture;
2824
+ particleTextureSoft;
2825
+ particleTextureCircle;
2822
2826
  textures = /* @__PURE__ */ new Map();
2823
2827
  /** Tracks texture access order for LRU eviction (most recent at end). */
2824
2828
  textureLRU = [];
@@ -3041,6 +3045,38 @@ var RenderSystem = class {
3041
3045
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
3042
3046
  gl.activeTexture(gl.TEXTURE0);
3043
3047
  }
3048
+ /** Pre-baked particle shape textures: white alpha mask, tinted by instance color at render time. */
3049
+ _createParticleTexture(shape) {
3050
+ const { gl } = this;
3051
+ const SIZE = 64;
3052
+ const canvas = document.createElement("canvas");
3053
+ canvas.width = SIZE;
3054
+ canvas.height = SIZE;
3055
+ const ctx = canvas.getContext("2d");
3056
+ const cx = SIZE / 2;
3057
+ if (shape === "soft") {
3058
+ const g = ctx.createRadialGradient(cx, cx, 0, cx, cx, cx);
3059
+ g.addColorStop(0, "rgba(255,255,255,1)");
3060
+ g.addColorStop(0.25, "rgba(255,255,255,0.9)");
3061
+ g.addColorStop(0.5, "rgba(255,255,255,0.2)");
3062
+ g.addColorStop(1, "rgba(255,255,255,0)");
3063
+ ctx.fillStyle = g;
3064
+ ctx.fillRect(0, 0, SIZE, SIZE);
3065
+ } else {
3066
+ ctx.fillStyle = "white";
3067
+ ctx.beginPath();
3068
+ ctx.arc(cx, cx, cx - 1, 0, Math.PI * 2);
3069
+ ctx.fill();
3070
+ }
3071
+ const tex = gl.createTexture();
3072
+ gl.bindTexture(gl.TEXTURE_2D, tex);
3073
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
3074
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
3075
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
3076
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
3077
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
3078
+ return tex;
3079
+ }
3044
3080
  // ── Texture management (sprite textures — CLAMP_TO_EDGE) ──────────────────
3045
3081
  loadTexture(src) {
3046
3082
  const cached = this.textures.get(src);
@@ -3283,6 +3319,8 @@ var RenderSystem = class {
3283
3319
  const { gl, canvas } = this;
3284
3320
  const W = canvas.width;
3285
3321
  const H = canvas.height;
3322
+ const Wl = canvas.clientWidth || W;
3323
+ const Hl = canvas.clientHeight || H;
3286
3324
  let camX = 0, camY = 0, zoom = 1;
3287
3325
  let background = this.defaultBackground;
3288
3326
  let shakeX = 0, shakeY = 0;
@@ -3472,7 +3510,7 @@ var RenderSystem = class {
3472
3510
  gl.useProgram(this.program);
3473
3511
  gl.uniform2f(this.uCamPos, camX, camY);
3474
3512
  gl.uniform1f(this.uZoom, zoom);
3475
- gl.uniform2f(this.uCanvasSize, W, H);
3513
+ gl.uniform2f(this.uCanvasSize, Wl, Hl);
3476
3514
  gl.uniform2f(this.uShake, shakeX, shakeY);
3477
3515
  gl.uniform1i(this.uTexture, 0);
3478
3516
  gl.activeTexture(gl.TEXTURE0);
@@ -3660,9 +3698,9 @@ var RenderSystem = class {
3660
3698
  const ease = ct * ct * (3 - 2 * ct);
3661
3699
  const [cr0, cg0, cb0] = parseCSSColor(pool._colorTransitionFrom);
3662
3700
  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);
3701
+ const ri = Math.round((cr0 + (cr1 - cr0) * ease) * 255);
3702
+ const gi = Math.round((cg0 + (cg1 - cg0) * ease) * 255);
3703
+ const bi = Math.round((cb0 + (cb1 - cb0) * ease) * 255);
3666
3704
  pool.color = `rgb(${ri},${gi},${bi})`;
3667
3705
  if (ct >= 1) {
3668
3706
  pool._colorTransitionFrom = void 0;
@@ -3672,27 +3710,23 @@ var RenderSystem = class {
3672
3710
  const isFormation = pool.mode === "formation";
3673
3711
  pool.particles = pool.particles.filter((p) => {
3674
3712
  if (isFormation) {
3713
+ if (p.targetX !== void 0 && p.targetY !== void 0) {
3714
+ const seek2 = pool.seekStrength ?? 0.055;
3715
+ p.x += (p.targetX - p.x) * seek2;
3716
+ p.y += (p.targetY - p.y) * seek2;
3717
+ }
3675
3718
  if (pool.attractors) {
3676
3719
  for (const attr of pool.attractors) {
3677
- const adx = attr.x - p.x;
3678
- const ady = attr.y - p.y;
3720
+ const adx = p.x - attr.x;
3721
+ const ady = p.y - attr.y;
3679
3722
  const dist = Math.sqrt(adx * adx + ady * ady);
3680
3723
  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;
3724
+ const magnitude = -attr.strength * (1 - dist / attr.radius) * dt;
3725
+ p.x += adx / dist * magnitude;
3726
+ p.y += ady / dist * magnitude;
3684
3727
  }
3685
3728
  }
3686
3729
  }
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
3730
  return true;
3697
3731
  }
3698
3732
  p.life -= dt;
@@ -3787,11 +3821,22 @@ var RenderSystem = class {
3787
3821
  }
3788
3822
  }
3789
3823
  let pCount = 0;
3790
- const pKey = `__color__`;
3824
+ const pShape = pool.particleShape ?? "soft";
3791
3825
  const pBlend = pool.blendMode ?? "normal";
3826
+ const flushParticles = (n) => {
3827
+ if (n === 0) return;
3828
+ if (pBlend !== "normal") this.applyBlendMode(pBlend);
3829
+ if (pShape === "square") {
3830
+ this.flush(n, "__color__");
3831
+ } else {
3832
+ const tex = pShape === "circle" ? this.particleTextureCircle : this.particleTextureSoft;
3833
+ this.flushWithTex(n, tex, true);
3834
+ }
3835
+ if (pBlend !== "normal") this.applyBlendMode("normal");
3836
+ };
3792
3837
  for (const p of pool.particles) {
3793
3838
  if (pCount >= MAX_INSTANCES) {
3794
- this.flush(pCount, pKey, void 0, pBlend);
3839
+ flushParticles(pCount);
3795
3840
  pCount = 0;
3796
3841
  }
3797
3842
  const lifeFrac = p.life / p.maxLife;
@@ -3838,7 +3883,7 @@ var RenderSystem = class {
3838
3883
  );
3839
3884
  pCount++;
3840
3885
  }
3841
- if (pCount > 0) this.flush(pCount, pKey, void 0, pBlend);
3886
+ if (pCount > 0) flushParticles(pCount);
3842
3887
  }
3843
3888
  for (const id of world.query("Transform", "Trail")) {
3844
3889
  const t = world.getComponent(id, "Transform");
@@ -15637,7 +15682,8 @@ function ParticleEmitter({
15637
15682
  formationPoints,
15638
15683
  seekStrength,
15639
15684
  targetColor,
15640
- colorTransitionDuration
15685
+ colorTransitionDuration,
15686
+ particleShape
15641
15687
  }) {
15642
15688
  const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
15643
15689
  const resolvedRate = rate ?? presetConfig.rate ?? 20;
@@ -15681,7 +15727,8 @@ function ParticleEmitter({
15681
15727
  mode,
15682
15728
  formationPoints,
15683
15729
  seekStrength,
15684
- colorTransitionDuration
15730
+ colorTransitionDuration,
15731
+ particleShape
15685
15732
  });
15686
15733
  return () => engine.ecs.removeComponent(entityId, "ParticlePool");
15687
15734
  }, []);
@@ -15690,6 +15737,12 @@ function ParticleEmitter({
15690
15737
  if (!pool) return;
15691
15738
  pool.active = active;
15692
15739
  }, [active, engine, entityId]);
15740
+ useEffect28(() => {
15741
+ const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15742
+ if (!pool) return;
15743
+ pool.blendMode = blendMode;
15744
+ pool.particleShape = particleShape;
15745
+ }, [blendMode, particleShape, engine, entityId]);
15693
15746
  useEffect28(() => {
15694
15747
  const pool = engine.ecs.getComponent(entityId, "ParticlePool");
15695
15748
  if (!pool) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cubeforge",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "React-first 2D browser game engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",