feat: vrai fondu enchaîné par overlay image sur le heatmap
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:
syoul
2026-03-23 21:55:51 +01:00
parent d50b30666b
commit 14d218e4ff
+34 -15
View File
@@ -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>
); );
} }