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:
sam
2026-05-21 15:16:08 +07:00
parent 80b56b3318
commit e04342668b
+49 -18
View File
@@ -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. */}