Enhance link glow and sparkles on preview segment

This commit is contained in:
Daddy32
2025-12-16 20:50:39 +01:00
parent 6c36abae0e
commit e3dfeb2e70
4 changed files with 194 additions and 19 deletions

View File

@@ -32,6 +32,28 @@
endOpacity: 0,
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 },
};

View File

@@ -141,25 +141,148 @@
stepTimer();
};
const drawChainPreview = () => {
if (!chain.active || !chain.pointer || chain.bodies.length === 0) return;
const last = chain.bodies[chain.bodies.length - 1];
const ctx = render.context;
const maxLinkDist = getMaxLinkDistance();
const delta = Vector.sub(chain.pointer, last.position);
const dist = Vector.magnitude(delta);
const previewMargin = config.ballRadius;
const cappedDist = Math.max(
0,
Math.min(dist, Math.max(0, maxLinkDist - previewMargin)),
const linkSparkles = [];
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];
segments.push({ from: last.position, to: previewTarget });
}
if (!segments.length) return;
const count = Math.max(
1,
Math.floor(segments.length * rate * (deltaMs / 16)),
);
const safeTarget =
dist > 0
? Vector.add(
last.position,
Vector.mult(Vector.normalise(delta), cappedDist),
)
: chain.pointer;
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 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 delta = Vector.sub(chain.pointer, last.position);
const dist = Vector.magnitude(delta);
const previewMargin = config.ballRadius;
const cappedDist = Math.max(
0,
Math.min(dist, Math.max(0, maxLinkDist - previewMargin)),
);
safeTarget =
dist > 0
? Vector.add(
last.position,
Vector.mult(Vector.normalise(delta), cappedDist),
)
: 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.strokeStyle = chain.color || "#fff";
ctx.lineWidth = 3;
@@ -173,7 +296,7 @@
Events.on(engine, "afterUpdate", keepBallsInBounds);
Events.on(engine, "beforeUpdate", beforeUpdateStep);
Events.on(render, "afterRender", drawChainPreview);
Events.on(render, "afterRender", drawLinkGlowAndSparkles);
};
window.PhysilinksLoop = { create };

View File

@@ -55,6 +55,14 @@
...(defaults.clearAnimation || {}),
...(link.clearAnimation || {}),
},
glowEffect: {
...(defaults.glowEffect || {}),
...(link.glowEffect || {}),
},
sparkles: {
...(defaults.sparkles || {}),
...(link.sparkles || {}),
},
});
const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => {

View File

@@ -71,6 +71,28 @@
sizeScale: 1, // multiplies the base size (radius) used for the visual
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)