feat(pulse): 55% probability of split via box (#13)
* feat: random light pulse along threads + box outlines A blurred light pulse glides along one of the 5 wave threads (wave1/2/3, thread1/2) every 2.5-7s. Color matches the active theme (cyan on home/portfolio/about, pink/magenta on contact). Pulse also follows the outline of designated boxes marked with data-trail-box: - Home: 'Looking for inspiration?' + 'Ready to collaborate?' - Contact: 'Waiting to hear from you' card Boxes are rebuilt on every pulse so they stay aligned during scroll and respect viewport (off-screen boxes are skipped). Pulse uses requestAnimationFrame + path.getPointAtLength for native smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend mode so it blends with the dark background. * feat(pulse): slower, brighter, comet-shaped pulse along paths Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape (soft halo + mid glow + bright core), rotated to path tangent, ease-in-out timing, mix-blend-mode screen. Fixes: pulse not tracking path was a parallax-offset coordinate mismatch. Cleaned up via getSpeedKey() helper. * fix(pulse): tone down white core so theme color dominates Core was pure white (#ffffff) which washed out the theme color. Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller (10x3 vs 14x4) with 75% opacity so the purple/pink and cyan glow layers come through more visibly. * feat(pulse): double the glide duration Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s). * feat(pulse): split into two halves around boxes When a thread-pulse hits a data-trail-box bound, it splits: one half glides along the top of the box outline, the other along the bottom. They re-merge on the far side and continue as one along the rest of the thread. Implementation: - Two pulse-blob groups (blob-1, blob-2) in SVG - Box-intersection scan samples path every 4px to find entry + exit points - Phase 1: blob-1 glides from start to entry - Phase 2: blob-1 follows top-half of box outline, blob-2 follows bottom-half (both spawned dynamically) - Phase 3: blob-1 continues from exit to end, blob-2 hides - Velocity kept constant across phases so split makes the animation longer rather than faster * fix(pulse): remove direct box-pulses + smooth split transition Two fixes after review: 1. Removed runBoxPulse() entirely. Pulses now spawn ONLY on the 5 wave threads. Box outlines are followed only via the split mechanism when a thread-pulse intersects a box. 2. Smooth split transition via Bezier curves: split-path now starts at the EXACT thread intersection point (entryX/Y) and ends at exact exit point, curving smoothly up/down to the box top/bottom edge. Previously the path jumped to box.cy which caused a visible teleport when entering the split phase. * feat(pulse): bias 55% of pulses to threads that hit a box Before each pulse, split wavePaths into intersecting (those that currently cross a data-trail-box) and non-intersecting. Roll a die: 55% of the time pick from intersecting, 45% from non-intersecting. Falls back to the other bucket if one is empty (e.g. no boxes visible on the page).
This commit is contained in:
@@ -395,6 +395,8 @@ const pulseCore = theme === 'purple' ? '#fdf4ff' : '#f0fdfa';
|
|||||||
requestAnimationFrame(step);
|
requestAnimationFrame(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SPLIT_PROBABILITY = 0.55;
|
||||||
|
|
||||||
function pickAndPulse() {
|
function pickAndPulse() {
|
||||||
if (isPulsing) {
|
if (isPulsing) {
|
||||||
schedule();
|
schedule();
|
||||||
@@ -404,7 +406,28 @@ const pulseCore = theme === 'purple' ? '#fdf4ff' : '#f0fdfa';
|
|||||||
schedule();
|
schedule();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const choice = wavePaths[Math.floor(Math.random() * wavePaths.length)];
|
const boxes = refreshBoxes();
|
||||||
|
const intersecting: SVGPathElement[] = [];
|
||||||
|
const nonIntersecting: SVGPathElement[] = [];
|
||||||
|
for (const p of wavePaths) {
|
||||||
|
const speedKey = getSpeedKey(p.id);
|
||||||
|
const pathTx = speedKey ? window.scrollY * speeds[speedKey] : 0;
|
||||||
|
const hit = boxes.length > 0 ? findBoxIntersection(p, pathTx, boxes) : null;
|
||||||
|
if (hit) intersecting.push(p);
|
||||||
|
else nonIntersecting.push(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
let choice: SVGPathElement;
|
||||||
|
const wantSplit = Math.random() < SPLIT_PROBABILITY;
|
||||||
|
if (wantSplit && intersecting.length > 0) {
|
||||||
|
choice = intersecting[Math.floor(Math.random() * intersecting.length)];
|
||||||
|
} else if (!wantSplit && nonIntersecting.length > 0) {
|
||||||
|
choice = nonIntersecting[Math.floor(Math.random() * nonIntersecting.length)];
|
||||||
|
} else if (intersecting.length > 0) {
|
||||||
|
choice = intersecting[Math.floor(Math.random() * intersecting.length)];
|
||||||
|
} else {
|
||||||
|
choice = nonIntersecting[Math.floor(Math.random() * nonIntersecting.length)];
|
||||||
|
}
|
||||||
runThreadPulse(choice, 18000 + Math.random() * 8000);
|
runThreadPulse(choice, 18000 + Math.random() * 8000);
|
||||||
schedule();
|
schedule();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user