Add a continuous ping animation to the selected event marker
The selected marker was just a static halo ring around a dot, which made the focused event read as a flat target. Two phase-offset pulse rings now expand outwards continuously while the marker is shown, matching the visual language of the per-event rings so a focused event still feels alive. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -91,6 +91,12 @@ export function PlanetoraSvgGlobe({
|
||||
const ringsGroupRef = useRef<SVGGElement | null>(null);
|
||||
const selectedDotRef = useRef<SVGCircleElement | null>(null);
|
||||
const selectedHaloRef = useRef<SVGCircleElement | null>(null);
|
||||
// Two phase-offset rings give the selected event a continuous "ping",
|
||||
// separate from the one-shot rings emitted on every event.
|
||||
const selectedPulseRefs = [
|
||||
useRef<SVGCircleElement | null>(null),
|
||||
useRef<SVGCircleElement | null>(null),
|
||||
] as const;
|
||||
|
||||
// Live refs for the rAF tick — read latest props without retriggering effect.
|
||||
const ringsRef = useRef(rings);
|
||||
@@ -211,7 +217,10 @@ export function PlanetoraSvgGlobe({
|
||||
}
|
||||
|
||||
// ── Selected marker ───────────────────────────────────────────────
|
||||
if (sel && selectedDotRef.current && selectedHaloRef.current) {
|
||||
const dotEl = selectedDotRef.current;
|
||||
const haloEl = selectedHaloRef.current;
|
||||
const pulseEls = selectedPulseRefs.map((r) => r.current);
|
||||
if (sel && dotEl && haloEl) {
|
||||
const xy = projection([sel.coords[0], sel.coords[1]]);
|
||||
if (xy) {
|
||||
const dist = geoDistance(
|
||||
@@ -220,21 +229,41 @@ export function PlanetoraSvgGlobe({
|
||||
);
|
||||
const z = Math.cos(dist);
|
||||
const op = Math.max(0, Math.min(1, 0.4 + z * 0.7));
|
||||
selectedDotRef.current.setAttribute('cx', xy[0].toFixed(1));
|
||||
selectedDotRef.current.setAttribute('cy', xy[1].toFixed(1));
|
||||
selectedDotRef.current.setAttribute('opacity', op.toFixed(2));
|
||||
selectedDotRef.current.setAttribute('fill', t.highlightColor);
|
||||
selectedHaloRef.current.setAttribute('cx', xy[0].toFixed(1));
|
||||
selectedHaloRef.current.setAttribute('cy', xy[1].toFixed(1));
|
||||
selectedHaloRef.current.setAttribute('opacity', op.toFixed(2));
|
||||
selectedHaloRef.current.setAttribute('stroke', t.highlightColor);
|
||||
dotEl.setAttribute('cx', xy[0].toFixed(1));
|
||||
dotEl.setAttribute('cy', xy[1].toFixed(1));
|
||||
dotEl.setAttribute('opacity', op.toFixed(2));
|
||||
dotEl.setAttribute('fill', t.highlightColor);
|
||||
haloEl.setAttribute('cx', xy[0].toFixed(1));
|
||||
haloEl.setAttribute('cy', xy[1].toFixed(1));
|
||||
haloEl.setAttribute('opacity', op.toFixed(2));
|
||||
haloEl.setAttribute('stroke', t.highlightColor);
|
||||
|
||||
// Continuous ping — two rings, phase-offset by half a cycle, each
|
||||
// expanding outwards and fading. Cycle length matches the regular
|
||||
// event rings so the visual language stays consistent.
|
||||
const cycle = PLANETORA_RING_TTL_MS;
|
||||
const tNow = ts;
|
||||
for (let i = 0; i < pulseEls.length; i++) {
|
||||
const el = pulseEls[i];
|
||||
if (!el) continue;
|
||||
const phase = ((tNow / cycle + i / pulseEls.length) % 1 + 1) % 1;
|
||||
const r = 6 + phase * 26;
|
||||
const pulseOp = (1 - phase) * 0.7 * op;
|
||||
el.setAttribute('cx', xy[0].toFixed(1));
|
||||
el.setAttribute('cy', xy[1].toFixed(1));
|
||||
el.setAttribute('r', r.toFixed(1));
|
||||
el.setAttribute('opacity', pulseOp.toFixed(2));
|
||||
el.setAttribute('stroke', t.highlightColor);
|
||||
}
|
||||
} else {
|
||||
selectedDotRef.current.setAttribute('opacity', '0');
|
||||
selectedHaloRef.current.setAttribute('opacity', '0');
|
||||
dotEl.setAttribute('opacity', '0');
|
||||
haloEl.setAttribute('opacity', '0');
|
||||
for (const el of pulseEls) el?.setAttribute('opacity', '0');
|
||||
}
|
||||
} else if (selectedDotRef.current && selectedHaloRef.current) {
|
||||
selectedDotRef.current.setAttribute('opacity', '0');
|
||||
selectedHaloRef.current.setAttribute('opacity', '0');
|
||||
} else if (dotEl && haloEl) {
|
||||
dotEl.setAttribute('opacity', '0');
|
||||
haloEl.setAttribute('opacity', '0');
|
||||
for (const el of pulseEls) el?.setAttribute('opacity', '0');
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(tick);
|
||||
@@ -326,16 +355,18 @@ export function PlanetoraSvgGlobe({
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{/* Selected marker — halo ring + filled dot, rendered on top. */}
|
||||
<g pointerEvents="none">
|
||||
{/* Selected marker — continuous ping rings + bullseye + dot. */}
|
||||
<g pointerEvents="none" fill="none">
|
||||
{selectedPulseRefs.map((ref, i) => (
|
||||
<circle key={i} ref={ref} strokeWidth="1.5" opacity={0} />
|
||||
))}
|
||||
<circle
|
||||
ref={selectedHaloRef}
|
||||
r={14}
|
||||
fill="none"
|
||||
strokeWidth="2"
|
||||
opacity={0}
|
||||
/>
|
||||
<circle ref={selectedDotRef} r={5} opacity={0} />
|
||||
<circle ref={selectedDotRef} r={5} fill="currentColor" opacity={0} />
|
||||
</g>
|
||||
|
||||
{/* Highlight + rim sit above the land for a "lit ball" feel. */}
|
||||
|
||||
Reference in New Issue
Block a user