Heatmap-Demo: ein Klick = ein Punkt an der Zeigerstelle, weich eingeblendet

Wunsch des Inhabers: ein simulierter Klick soll nicht mehrere Heatpunkte auf einmal
bringen. Die Heatmap der letzten 30 Tage ist jetzt von Anfang an ganz zu sehen; jeder
Demo-Klick legt genau einen Punkt an die Stelle des Zeigers, der in gut 0,4 s weich
hereinwächst. Beim Neustart blenden nur die Demo-Klicks aus, nicht die ganze Heatmap.

heat.ts: heatEbene() zeichnet den Grundbestand einmal und färbt je Bild nur den kleinen
Ausschnitt um die sich ändernden Punkte neu ein (gemessen ca. 2.000 Pixel je Klick),
nach der Überblendung läuft nichts mehr. Ohne Bewegung: alles sofort im Endzustand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-04 15:58:50 +00:00
1 parent adb1587a7b
commit 58ac7745e6
4 files changed
+179 -59

No files matched your search

@@ -1,7 +1,7 @@
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type MouseEvent, type RefObject } from "react";
import { DemoZeiger, useFeinerZeiger, zufall, type DemoVariante } from "../gemeinsam";
import { punkteInZone, zeichneHeatmap, type HeatPunkt } from "../heat";
import { HEATMAP_START, type HeatmapZustand } from "./ablauf";
import { DemoZeiger, useBewegungErlaubt, useFeinerZeiger, zufall, type DemoVariante } from "../gemeinsam";
import { heatEbene, punkteInZone, type HeatPunkt, type KlickPunkt } from "../heat";
import { HEATMAP_START, KLICK_ZIELE, type HeatmapZustand } from "./ablauf";
import { FLAECHE_GROESSE, TOP_ZONE, ZONEN, ZONEN_LAYOUT, schildText, zoneNach, type Zone, type PunktRechteck } from "./zonen";
import "./heatmap.css";
@@ -382,20 +382,56 @@ const HeatmapSeite = ({ zustand = HEATMAP_START, variante }: Props) => {
const rechtecke = useGemessen(wurzel, variante);
const punkte = useMemo(() => erzeugePunkte(rechtecke, variante), [rechtecke, variante]);
const { fuellung, statisch } = zustand;
const bewegung = useBewegungErlaubt();
const ebene = useRef<ReturnType<typeof heatEbene> | null>(null);
/** Stärke jedes Demo-Klicks, wie sie gerade gezeichnet ist (0 bis 1) */
const staerken = useRef<number[]>([]);
// Neu zeichnen nur, wenn sich die sichtbaren Punkte ändern (neuer Klick, neue Messung), nicht je Bild
// Grundbestand (Klicks der letzten 30 Tage): neu zeichnen nur bei neuer Messung, nicht je Bild
useEffect(() => {
const c = leinwand.current;
if (!c) return;
const sichtbar: HeatPunkt[] = [];
ebene.current = heatEbene(c, LEINWAND[variante].radius);
const grund: HeatPunkt[] = [];
for (const z of ZONEN) {
const alle = punkte[z.id];
const anzahl = Math.round((fuellung[z.id] ?? 0) * alle.length);
if (anzahl > 0) sichtbar.push(...alle.slice(0, anzahl));
grund.push(...alle.slice(0, Math.round((fuellung[z.id] ?? 0) * alle.length)));
}
zeichneHeatmap(c, sichtbar, LEINWAND[variante].radius);
ebene.current.basis(grund);
staerken.current = [];
}, [punkte, fuellung, variante]);
// Demo-Klicks: jeder Klick ist genau EIN Punkt an der Stelle des Zeigers. Ein neuer Punkt wächst in gut 0,4 s weich
// herein, beim Neustart blenden alle Demo-Klicks ebenso weich aus. Neu eingefärbt wird je Bild nur der kleine
// Ausschnitt um die Punkte, die sich ändern; nach der Überblendung läuft nichts mehr.
const gesetzt = zustand.klickPunkte[variante].length;
useEffect(() => {
const e = ebene.current;
if (!e) return;
const { breite, hoehe } = FLAECHE_GROESSE[variante];
const lage = KLICK_ZIELE[variante].map((p) => ({ x: p.x / breite, y: p.y / hoehe }));
const von = lage.map((_, i) => staerken.current[i] ?? 0);
const nach = lage.map((_, i) => (i < gesetzt ? 1 : 0));
const setze = (t: number) => {
staerken.current = von.map((v, i) => v + (nach[i] - v) * t);
e.klicks(lage.map((p, i): KlickPunkt => ({ ...p, staerke: staerken.current[i] })));
};
if (!bewegung || von.every((v, i) => v === nach[i])) {
setze(1);
return;
}
const dauer = nach.some((n, i) => n < von[i]) ? 700 : 420;
let raf = 0;
const beginn = performance.now();
const bild = (jetzt: number) => {
const t = Math.min(1, (jetzt - beginn) / dauer);
setze(1 - (1 - t) ** 3); // sanft abbremsend
if (t < 1) raf = requestAnimationFrame(bild);
};
raf = requestAnimationFrame(bild);
return () => cancelAnimationFrame(raf);
}, [gesetzt, variante, bewegung, punkte, fuellung]);
const zoneUnter = (e: MouseEvent) => {
const z = (e.target as Element).closest?.("[data-zone]");
return z?.getAttribute("data-zone") ?? null;