cubeforge 0.4.10 → 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 +35 -1
- package/dist/index.js +170 -7
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -531,8 +531,42 @@ 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';
|
|
541
|
+
/**
|
|
542
|
+
* Formation mode: particles lerp toward fixed target positions instead of
|
|
543
|
+
* being emitted with velocity. Enables logo reveals, constellations, shape morphing.
|
|
544
|
+
* - `'standard'` (default) — normal emit/gravity/lifetime behaviour
|
|
545
|
+
* - `'formation'` — particles seek `formationPoints`; no gravity, no expiry
|
|
546
|
+
*/
|
|
547
|
+
mode?: 'standard' | 'formation';
|
|
548
|
+
/**
|
|
549
|
+
* Target positions for formation mode. One particle is spawned per point.
|
|
550
|
+
* Change this array to morph the formation — particles smoothly lerp to new targets.
|
|
551
|
+
*/
|
|
552
|
+
formationPoints?: {
|
|
553
|
+
x: number;
|
|
554
|
+
y: number;
|
|
555
|
+
}[];
|
|
556
|
+
/**
|
|
557
|
+
* How strongly particles seek their target each frame in formation mode.
|
|
558
|
+
* Exponential lerp factor (0–1). Default 0.055 (~5.5% per frame at 60 fps).
|
|
559
|
+
*/
|
|
560
|
+
seekStrength?: number;
|
|
561
|
+
/**
|
|
562
|
+
* Smoothly transition all particles to this color.
|
|
563
|
+
* Works in both standard and formation modes.
|
|
564
|
+
*/
|
|
565
|
+
targetColor?: string;
|
|
566
|
+
/** Duration of the global color transition in seconds. Default 0.5. */
|
|
567
|
+
colorTransitionDuration?: number;
|
|
534
568
|
}
|
|
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;
|
|
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;
|
|
536
570
|
|
|
537
571
|
interface VirtualJoystickProps {
|
|
538
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,
|
|
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);
|
|
@@ -3653,15 +3691,87 @@ var RenderSystem = class {
|
|
|
3653
3691
|
for (const id of world.query("Transform", "ParticlePool")) {
|
|
3654
3692
|
const t = world.getComponent(id, "Transform");
|
|
3655
3693
|
const pool = world.getComponent(id, "ParticlePool");
|
|
3694
|
+
if (pool.targetColor && pool._colorTransitionFrom !== void 0) {
|
|
3695
|
+
pool._colorTransitionElapsed = (pool._colorTransitionElapsed ?? 0) + dt;
|
|
3696
|
+
const dur = pool.colorTransitionDuration ?? 0.5;
|
|
3697
|
+
const ct = Math.min((pool._colorTransitionElapsed ?? 0) / dur, 1);
|
|
3698
|
+
const ease = ct * ct * (3 - 2 * ct);
|
|
3699
|
+
const [cr0, cg0, cb0] = parseCSSColor(pool._colorTransitionFrom);
|
|
3700
|
+
const [cr1, cg1, cb1] = parseCSSColor(pool.targetColor);
|
|
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);
|
|
3704
|
+
pool.color = `rgb(${ri},${gi},${bi})`;
|
|
3705
|
+
if (ct >= 1) {
|
|
3706
|
+
pool._colorTransitionFrom = void 0;
|
|
3707
|
+
pool._colorTransitionElapsed = void 0;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
const isFormation = pool.mode === "formation";
|
|
3656
3711
|
pool.particles = pool.particles.filter((p) => {
|
|
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
|
+
}
|
|
3718
|
+
if (pool.attractors) {
|
|
3719
|
+
for (const attr of pool.attractors) {
|
|
3720
|
+
const adx = p.x - attr.x;
|
|
3721
|
+
const ady = p.y - attr.y;
|
|
3722
|
+
const dist = Math.sqrt(adx * adx + ady * ady);
|
|
3723
|
+
if (dist < attr.radius && dist > 0) {
|
|
3724
|
+
const magnitude = -attr.strength * (1 - dist / attr.radius) * dt;
|
|
3725
|
+
p.x += adx / dist * magnitude;
|
|
3726
|
+
p.y += ady / dist * magnitude;
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
return true;
|
|
3731
|
+
}
|
|
3657
3732
|
p.life -= dt;
|
|
3733
|
+
if (pool.attractors) {
|
|
3734
|
+
for (const attr of pool.attractors) {
|
|
3735
|
+
const adx = attr.x - p.x;
|
|
3736
|
+
const ady = attr.y - p.y;
|
|
3737
|
+
const dist = Math.sqrt(adx * adx + ady * ady);
|
|
3738
|
+
if (dist < attr.radius && dist > 0) {
|
|
3739
|
+
const force = attr.strength * (1 - dist / attr.radius);
|
|
3740
|
+
p.vx += adx / dist * force * dt;
|
|
3741
|
+
p.vy += ady / dist * force * dt;
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3658
3745
|
p.x += p.vx * dt;
|
|
3659
3746
|
p.y += p.vy * dt;
|
|
3660
3747
|
p.vy += p.gravity * dt;
|
|
3661
3748
|
if (p.rotationSpeed !== void 0) p.rotation = (p.rotation ?? 0) + p.rotationSpeed * dt;
|
|
3662
3749
|
return p.life > 0;
|
|
3663
3750
|
});
|
|
3664
|
-
if (
|
|
3751
|
+
if (isFormation) {
|
|
3752
|
+
const fp = pool.formationPoints ?? [];
|
|
3753
|
+
while (pool.particles.length < fp.length) {
|
|
3754
|
+
const idx = pool.particles.length;
|
|
3755
|
+
const startSize = pool.sizeOverLife?.start ?? pool.particleSize;
|
|
3756
|
+
const endSize = pool.sizeOverLife?.end ?? pool.particleSize;
|
|
3757
|
+
pool.particles.push({
|
|
3758
|
+
x: t.x + (world.rng() - 0.5) * 20,
|
|
3759
|
+
y: t.y + (world.rng() - 0.5) * 20,
|
|
3760
|
+
vx: 0,
|
|
3761
|
+
vy: 0,
|
|
3762
|
+
life: 1,
|
|
3763
|
+
maxLife: 1,
|
|
3764
|
+
size: startSize,
|
|
3765
|
+
startSize,
|
|
3766
|
+
endSize,
|
|
3767
|
+
color: pool.color,
|
|
3768
|
+
gravity: 0,
|
|
3769
|
+
rotation: 0,
|
|
3770
|
+
targetX: fp[idx].x,
|
|
3771
|
+
targetY: fp[idx].y
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
} else if (pool.active && pool.particles.length < pool.maxParticles) {
|
|
3665
3775
|
let spawnCount;
|
|
3666
3776
|
if (pool.burstCount != null && pool.burstCount > 0) {
|
|
3667
3777
|
spawnCount = pool.burstCount;
|
|
@@ -3711,11 +3821,22 @@ var RenderSystem = class {
|
|
|
3711
3821
|
}
|
|
3712
3822
|
}
|
|
3713
3823
|
let pCount = 0;
|
|
3714
|
-
const
|
|
3824
|
+
const pShape = pool.particleShape ?? "soft";
|
|
3715
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
|
+
};
|
|
3716
3837
|
for (const p of pool.particles) {
|
|
3717
3838
|
if (pCount >= MAX_INSTANCES) {
|
|
3718
|
-
|
|
3839
|
+
flushParticles(pCount);
|
|
3719
3840
|
pCount = 0;
|
|
3720
3841
|
}
|
|
3721
3842
|
const lifeFrac = p.life / p.maxLife;
|
|
@@ -3762,7 +3883,7 @@ var RenderSystem = class {
|
|
|
3762
3883
|
);
|
|
3763
3884
|
pCount++;
|
|
3764
3885
|
}
|
|
3765
|
-
if (pCount > 0)
|
|
3886
|
+
if (pCount > 0) flushParticles(pCount);
|
|
3766
3887
|
}
|
|
3767
3888
|
for (const id of world.query("Transform", "Trail")) {
|
|
3768
3889
|
const t = world.getComponent(id, "Transform");
|
|
@@ -15556,7 +15677,13 @@ function ParticleEmitter({
|
|
|
15556
15677
|
sizeOverLife,
|
|
15557
15678
|
attractors,
|
|
15558
15679
|
colorOverLife,
|
|
15559
|
-
blendMode
|
|
15680
|
+
blendMode,
|
|
15681
|
+
mode,
|
|
15682
|
+
formationPoints,
|
|
15683
|
+
seekStrength,
|
|
15684
|
+
targetColor,
|
|
15685
|
+
colorTransitionDuration,
|
|
15686
|
+
particleShape
|
|
15560
15687
|
}) {
|
|
15561
15688
|
const presetConfig = preset ? PARTICLE_PRESETS[preset] : {};
|
|
15562
15689
|
const resolvedRate = rate ?? presetConfig.rate ?? 20;
|
|
@@ -15596,7 +15723,12 @@ function ParticleEmitter({
|
|
|
15596
15723
|
sizeOverLife,
|
|
15597
15724
|
attractors,
|
|
15598
15725
|
colorOverLife,
|
|
15599
|
-
blendMode
|
|
15726
|
+
blendMode,
|
|
15727
|
+
mode,
|
|
15728
|
+
formationPoints,
|
|
15729
|
+
seekStrength,
|
|
15730
|
+
colorTransitionDuration,
|
|
15731
|
+
particleShape
|
|
15600
15732
|
});
|
|
15601
15733
|
return () => engine.ecs.removeComponent(entityId, "ParticlePool");
|
|
15602
15734
|
}, []);
|
|
@@ -15605,6 +15737,37 @@ function ParticleEmitter({
|
|
|
15605
15737
|
if (!pool) return;
|
|
15606
15738
|
pool.active = active;
|
|
15607
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]);
|
|
15746
|
+
useEffect28(() => {
|
|
15747
|
+
const pool = engine.ecs.getComponent(entityId, "ParticlePool");
|
|
15748
|
+
if (!pool) return;
|
|
15749
|
+
pool.attractors = attractors;
|
|
15750
|
+
}, [attractors, engine, entityId]);
|
|
15751
|
+
useEffect28(() => {
|
|
15752
|
+
const pool = engine.ecs.getComponent(entityId, "ParticlePool");
|
|
15753
|
+
if (!pool || pool.mode !== "formation" || !formationPoints) return;
|
|
15754
|
+
pool.formationPoints = formationPoints;
|
|
15755
|
+
const shuffled = [...formationPoints].sort(() => Math.random() - 0.5);
|
|
15756
|
+
pool.particles.forEach((p, i) => {
|
|
15757
|
+
if (shuffled[i]) {
|
|
15758
|
+
p.targetX = shuffled[i].x;
|
|
15759
|
+
p.targetY = shuffled[i].y;
|
|
15760
|
+
}
|
|
15761
|
+
});
|
|
15762
|
+
}, [formationPoints, engine, entityId]);
|
|
15763
|
+
useEffect28(() => {
|
|
15764
|
+
const pool = engine.ecs.getComponent(entityId, "ParticlePool");
|
|
15765
|
+
if (!pool || !targetColor) return;
|
|
15766
|
+
pool._colorTransitionFrom = pool.color;
|
|
15767
|
+
pool._colorTransitionElapsed = 0;
|
|
15768
|
+
pool.targetColor = targetColor;
|
|
15769
|
+
pool.colorTransitionDuration = colorTransitionDuration;
|
|
15770
|
+
}, [targetColor, colorTransitionDuration, engine, entityId]);
|
|
15608
15771
|
return null;
|
|
15609
15772
|
}
|
|
15610
15773
|
|