From 14d218e4ffcf08544b080b1f5c3eee6aa934c134 Mon Sep 17 00:00:00 2001 From: syoul Date: Mon, 23 Mar 2026 21:55:51 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20vrai=20fondu=20encha=C3=AEn=C3=A9=20par?= =?UTF-8?q?=20overlay=20image=20sur=20le=20heatmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Principe : capture du canvas heatmap actuel dans une 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 --- src/components/HeatMap.tsx | 49 ++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/components/HeatMap.tsx b/src/components/HeatMap.tsx index bff36d4..98db3e7 100644 --- a/src/components/HeatMap.tsx +++ b/src/components/HeatMap.tsx @@ -33,6 +33,7 @@ export function HeatMap({ transactions }: HeatMapProps) { const containerRef = useRef(null); const mapRef = useRef(null); const heatRef = useRef(null); + const overlayRef = useRef(null); // Initialize map once 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(() => { if (!heatRef.current || !mapRef.current) return; const canvas = (heatRef.current as unknown as { _canvas?: HTMLCanvasElement })._canvas; + const overlay = overlayRef.current; const update = () => { 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(); return; } - canvas.style.transition = 'opacity 0.15s ease-out'; - canvas.style.opacity = '0.15'; - - const t = setTimeout(() => { + // Freeze current frame in the overlay + try { + overlay.src = canvas.toDataURL(); + } catch { + // canvas tainted (shouldn't happen with heatmap-only canvas) update(); - canvas.style.transition = 'opacity 0.2s ease-in'; - canvas.style.opacity = '1'; - }, 150); + return; + } + 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]); return ( -
+
+
+ +
); }