Enhance link glow and sparkles on preview segment
This commit is contained in:
@@ -32,6 +32,28 @@
|
|||||||
endOpacity: 0,
|
endOpacity: 0,
|
||||||
sizeScale: 1,
|
sizeScale: 1,
|
||||||
},
|
},
|
||||||
|
glowEffect: {
|
||||||
|
enabled: true,
|
||||||
|
opacity: 0.26,
|
||||||
|
baseWidth: 6,
|
||||||
|
pulseWidth: 3,
|
||||||
|
pulseSpeedMs: 900,
|
||||||
|
shadowBlur: 18,
|
||||||
|
color: null,
|
||||||
|
lengthScale: 0.4,
|
||||||
|
opacityBoostPerLink: 0.02,
|
||||||
|
maxOpacity: 0.7,
|
||||||
|
},
|
||||||
|
sparkles: {
|
||||||
|
enabled: true,
|
||||||
|
rate: 0.12,
|
||||||
|
size: 4,
|
||||||
|
lifeMs: 260,
|
||||||
|
opacity: 0.9,
|
||||||
|
jitter: 6,
|
||||||
|
lengthRateBoost: 0.08,
|
||||||
|
sizeBoost: 0.2,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
messages: { ...defaultMessageConfig },
|
messages: { ...defaultMessageConfig },
|
||||||
};
|
};
|
||||||
|
|||||||
131
src/loop.js
131
src/loop.js
@@ -141,10 +141,68 @@
|
|||||||
stepTimer();
|
stepTimer();
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawChainPreview = () => {
|
const linkSparkles = [];
|
||||||
if (!chain.active || !chain.pointer || chain.bodies.length === 0) return;
|
|
||||||
|
const spawnSparkles = (now, previewTarget) => {
|
||||||
|
const sparkCfg = config.link?.sparkles || {};
|
||||||
|
if (!sparkCfg.enabled || !chain.active || chain.bodies.length < 1) return;
|
||||||
|
const deltaMs = engine.timing?.delta || 16;
|
||||||
|
const extraLinks = Math.max(0, chain.bodies.length - 1);
|
||||||
|
const rate =
|
||||||
|
(sparkCfg.rate ?? 0.12) *
|
||||||
|
(1 + (sparkCfg.lengthRateBoost ?? 0.08) * extraLinks);
|
||||||
|
const segments = [];
|
||||||
|
chain.constraints.forEach((c) => {
|
||||||
|
if (c?.bodyA?.position && c?.bodyB?.position) {
|
||||||
|
segments.push({ from: c.bodyA.position, to: c.bodyB.position });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (previewTarget && chain.bodies.length > 0) {
|
||||||
const last = chain.bodies[chain.bodies.length - 1];
|
const last = chain.bodies[chain.bodies.length - 1];
|
||||||
|
segments.push({ from: last.position, to: previewTarget });
|
||||||
|
}
|
||||||
|
if (!segments.length) return;
|
||||||
|
const count = Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor(segments.length * rate * (deltaMs / 16)),
|
||||||
|
);
|
||||||
|
const jitter = sparkCfg.jitter ?? 6;
|
||||||
|
const size =
|
||||||
|
(sparkCfg.size ?? 4) + (sparkCfg.sizeBoost ?? 0.2) * extraLinks;
|
||||||
|
const life = sparkCfg.lifeMs ?? 260;
|
||||||
|
const opacity = sparkCfg.opacity ?? 0.9;
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
const seg = segments[Math.floor(Math.random() * segments.length)];
|
||||||
|
if (!seg) continue;
|
||||||
|
const t = Math.random();
|
||||||
|
const from = seg.from;
|
||||||
|
const to = seg.to;
|
||||||
|
const pos = Vector.add(
|
||||||
|
from,
|
||||||
|
Vector.mult(Vector.sub(to, from), t || 0.5),
|
||||||
|
);
|
||||||
|
linkSparkles.push({
|
||||||
|
x: pos.x + (Math.random() - 0.5) * jitter,
|
||||||
|
y: pos.y + (Math.random() - 0.5) * jitter,
|
||||||
|
created: now,
|
||||||
|
life,
|
||||||
|
size,
|
||||||
|
color: chain.color || "#fff",
|
||||||
|
opacity,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawLinkGlowAndSparkles = () => {
|
||||||
|
if (!chain.active || chain.bodies.length === 0) return;
|
||||||
const ctx = render.context;
|
const ctx = render.context;
|
||||||
|
const now = engine.timing?.timestamp || performance.now();
|
||||||
|
const linkCfg = config.link || {};
|
||||||
|
const glowCfg = linkCfg.glowEffect || {};
|
||||||
|
|
||||||
|
let safeTarget = null;
|
||||||
|
if (chain.pointer) {
|
||||||
|
const last = chain.bodies[chain.bodies.length - 1];
|
||||||
const maxLinkDist = getMaxLinkDistance();
|
const maxLinkDist = getMaxLinkDistance();
|
||||||
const delta = Vector.sub(chain.pointer, last.position);
|
const delta = Vector.sub(chain.pointer, last.position);
|
||||||
const dist = Vector.magnitude(delta);
|
const dist = Vector.magnitude(delta);
|
||||||
@@ -153,13 +211,78 @@
|
|||||||
0,
|
0,
|
||||||
Math.min(dist, Math.max(0, maxLinkDist - previewMargin)),
|
Math.min(dist, Math.max(0, maxLinkDist - previewMargin)),
|
||||||
);
|
);
|
||||||
const safeTarget =
|
safeTarget =
|
||||||
dist > 0
|
dist > 0
|
||||||
? Vector.add(
|
? Vector.add(
|
||||||
last.position,
|
last.position,
|
||||||
Vector.mult(Vector.normalise(delta), cappedDist),
|
Vector.mult(Vector.normalise(delta), cappedDist),
|
||||||
)
|
)
|
||||||
: chain.pointer;
|
: chain.pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glowCfg.enabled && (chain.bodies.length > 1 || safeTarget)) {
|
||||||
|
const color = glowCfg.color || chain.color || "#fff";
|
||||||
|
const baseWidth = glowCfg.baseWidth ?? 6;
|
||||||
|
const pulseWidth = glowCfg.pulseWidth ?? 3;
|
||||||
|
const speed = glowCfg.pulseSpeedMs ?? 900;
|
||||||
|
const extraLinks = Math.max(0, chain.bodies.length - 1);
|
||||||
|
const lengthScale = glowCfg.lengthScale ?? 0.4;
|
||||||
|
const pulse =
|
||||||
|
speed > 0
|
||||||
|
? Math.sin(((now % speed) / speed) * Math.PI * 2) * 0.5 + 0.5
|
||||||
|
: 0;
|
||||||
|
ctx.save();
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth =
|
||||||
|
baseWidth + pulseWidth * pulse + lengthScale * extraLinks;
|
||||||
|
const maxOpacity = glowCfg.maxOpacity ?? 0.7;
|
||||||
|
const alphaBase = glowCfg.opacity ?? 0.26;
|
||||||
|
const alpha =
|
||||||
|
alphaBase + (glowCfg.opacityBoostPerLink ?? 0.02) * extraLinks;
|
||||||
|
ctx.globalAlpha = Math.min(maxOpacity, alpha);
|
||||||
|
ctx.shadowColor = color;
|
||||||
|
ctx.shadowBlur = glowCfg.shadowBlur ?? 18;
|
||||||
|
ctx.beginPath();
|
||||||
|
chain.bodies.forEach((b, idx) => {
|
||||||
|
if (idx === 0) {
|
||||||
|
ctx.moveTo(b.position.x, b.position.y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(b.position.x, b.position.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (safeTarget) {
|
||||||
|
const last = chain.bodies[chain.bodies.length - 1];
|
||||||
|
ctx.moveTo(last.position.x, last.position.y);
|
||||||
|
ctx.lineTo(safeTarget.x, safeTarget.y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
spawnSparkles(now, safeTarget);
|
||||||
|
|
||||||
|
if (linkSparkles.length > 0) {
|
||||||
|
ctx.save();
|
||||||
|
for (let i = linkSparkles.length - 1; i >= 0; i -= 1) {
|
||||||
|
const s = linkSparkles[i];
|
||||||
|
const age = now - s.created;
|
||||||
|
if (age > s.life) {
|
||||||
|
linkSparkles.splice(i, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const t = Math.max(0, Math.min(1, age / s.life));
|
||||||
|
const alpha = (s.opacity ?? 0.9) * (1 - t);
|
||||||
|
ctx.fillStyle = s.color || "#fff";
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(s.x, s.y, s.size * (1 - t), 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!safeTarget) return;
|
||||||
|
const last = chain.bodies[chain.bodies.length - 1];
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.strokeStyle = chain.color || "#fff";
|
ctx.strokeStyle = chain.color || "#fff";
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3;
|
||||||
@@ -173,7 +296,7 @@
|
|||||||
|
|
||||||
Events.on(engine, "afterUpdate", keepBallsInBounds);
|
Events.on(engine, "afterUpdate", keepBallsInBounds);
|
||||||
Events.on(engine, "beforeUpdate", beforeUpdateStep);
|
Events.on(engine, "beforeUpdate", beforeUpdateStep);
|
||||||
Events.on(render, "afterRender", drawChainPreview);
|
Events.on(render, "afterRender", drawLinkGlowAndSparkles);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.PhysilinksLoop = { create };
|
window.PhysilinksLoop = { create };
|
||||||
|
|||||||
@@ -55,6 +55,14 @@
|
|||||||
...(defaults.clearAnimation || {}),
|
...(defaults.clearAnimation || {}),
|
||||||
...(link.clearAnimation || {}),
|
...(link.clearAnimation || {}),
|
||||||
},
|
},
|
||||||
|
glowEffect: {
|
||||||
|
...(defaults.glowEffect || {}),
|
||||||
|
...(link.glowEffect || {}),
|
||||||
|
},
|
||||||
|
sparkles: {
|
||||||
|
...(defaults.sparkles || {}),
|
||||||
|
...(link.sparkles || {}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => {
|
const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => {
|
||||||
|
|||||||
@@ -71,6 +71,28 @@
|
|||||||
sizeScale: 1, // multiplies the base size (radius) used for the visual
|
sizeScale: 1, // multiplies the base size (radius) used for the visual
|
||||||
render: null, // optional function({ targets, config, scene, ui }) to fully override rendering
|
render: null, // optional function({ targets, config, scene, ui }) to fully override rendering
|
||||||
},
|
},
|
||||||
|
glowEffect: {
|
||||||
|
enabled: true, // set false to disable link glow while dragging
|
||||||
|
opacity: 0.26, // alpha for the glow stroke
|
||||||
|
baseWidth: 6, // base stroke width
|
||||||
|
pulseWidth: 3, // additional width added via pulse
|
||||||
|
pulseSpeedMs: 900, // speed of the width pulse
|
||||||
|
shadowBlur: 18, // blur radius for glow
|
||||||
|
color: null, // optional override; defaults to active chain color
|
||||||
|
lengthScale: 0.4, // extra stroke width per additional linked ball
|
||||||
|
opacityBoostPerLink: 0.02, // extra alpha per linked ball
|
||||||
|
maxOpacity: 0.7, // cap for glow alpha
|
||||||
|
},
|
||||||
|
sparkles: {
|
||||||
|
enabled: true, // set false to disable sparkle particles on links
|
||||||
|
rate: 0.12, // spawn density multiplier per link per frame
|
||||||
|
size: 4, // base sparkle size
|
||||||
|
lifeMs: 260, // lifetime of each sparkle
|
||||||
|
opacity: 0.9, // starting opacity
|
||||||
|
jitter: 6, // positional jitter in pixels
|
||||||
|
lengthRateBoost: 0.08, // extra spawn rate per linked ball
|
||||||
|
sizeBoost: 0.2, // additional sparkle size per linked ball
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Messaging overlays (goal hints / milestones)
|
// Messaging overlays (goal hints / milestones)
|
||||||
|
|||||||
Reference in New Issue
Block a user