feat: vrai fondu enchaîné par overlay image sur le heatmap
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Principe : capture du canvas heatmap actuel dans une <img> superposée (opacity 1), mise à jour immédiate du heatmap en dessous, puis dissolution de l'overlay (opacity 0 en 500ms). Les deux frames coexistent pendant la transition → vrai dissolve sans clignotement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+33
-14
@@ -33,6 +33,7 @@ export function HeatMap({ transactions }: HeatMapProps) {
|
|||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const mapRef = useRef<L.Map | null>(null);
|
const mapRef = useRef<L.Map | null>(null);
|
||||||
const heatRef = useRef<L.HeatLayer | null>(null);
|
const heatRef = useRef<L.HeatLayer | null>(null);
|
||||||
|
const overlayRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
// Initialize map once
|
// Initialize map once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,11 +65,12 @@ export function HeatMap({ transactions }: HeatMapProps) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Update heatmap data with fade transition when transactions change
|
// Crossfade: capture current heatmap → update underneath → fade out overlay
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!heatRef.current || !mapRef.current) return;
|
if (!heatRef.current || !mapRef.current) return;
|
||||||
|
|
||||||
const canvas = (heatRef.current as unknown as { _canvas?: HTMLCanvasElement })._canvas;
|
const canvas = (heatRef.current as unknown as { _canvas?: HTMLCanvasElement })._canvas;
|
||||||
|
const overlay = overlayRef.current;
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const maxAmount = Math.max(...transactions.map((t) => t.amount), 1);
|
const maxAmount = Math.max(...transactions.map((t) => t.amount), 1);
|
||||||
@@ -84,28 +86,45 @@ export function HeatMap({ transactions }: HeatMapProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!canvas) {
|
if (!canvas || !overlay) {
|
||||||
update();
|
update();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.style.transition = 'opacity 0.15s ease-out';
|
// Freeze current frame in the overlay
|
||||||
canvas.style.opacity = '0.15';
|
try {
|
||||||
|
overlay.src = canvas.toDataURL();
|
||||||
const t = setTimeout(() => {
|
} catch {
|
||||||
|
// canvas tainted (shouldn't happen with heatmap-only canvas)
|
||||||
update();
|
update();
|
||||||
canvas.style.transition = 'opacity 0.2s ease-in';
|
return;
|
||||||
canvas.style.opacity = '1';
|
}
|
||||||
}, 150);
|
overlay.style.transition = 'none';
|
||||||
|
overlay.style.opacity = '1';
|
||||||
|
|
||||||
return () => clearTimeout(t);
|
// Update heatmap underneath immediately
|
||||||
|
update();
|
||||||
|
|
||||||
|
// Then dissolve the overlay away
|
||||||
|
const raf = requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
overlay.style.transition = 'opacity 0.5s ease-in-out';
|
||||||
|
overlay.style.opacity = '0';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(raf);
|
||||||
}, [transactions]);
|
}, [transactions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full h-full relative" style={{ minHeight: 0 }}>
|
||||||
ref={containerRef}
|
<div ref={containerRef} className="absolute inset-0" />
|
||||||
className="w-full h-full"
|
<img
|
||||||
style={{ minHeight: 0 }}
|
ref={overlayRef}
|
||||||
|
alt=""
|
||||||
|
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||||
|
style={{ opacity: 0, zIndex: 500 }}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user