Enhance link glow and sparkles on preview segment
This commit is contained in:
@@ -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 },
|
||||
};
|
||||
|
||||
161
src/loop.js
161
src/loop.js
@@ -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 };
|
||||
|
||||
@@ -55,6 +55,14 @@
|
||||
...(defaults.clearAnimation || {}),
|
||||
...(link.clearAnimation || {}),
|
||||
},
|
||||
glowEffect: {
|
||||
...(defaults.glowEffect || {}),
|
||||
...(link.glowEffect || {}),
|
||||
},
|
||||
sparkles: {
|
||||
...(defaults.sparkles || {}),
|
||||
...(link.sparkles || {}),
|
||||
},
|
||||
});
|
||||
|
||||
const normalizeSceneConfig = (sceneConfig = {}, defaults = baseConfig) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user