Canvas-Entwicklung mit Claude Code: HiDPI, RAF, Pointer und Tests
So baust du robuste Canvas-UIs mit Claude Code: HiDPI, requestAnimationFrame, Pointer Events, State und Playwright.
Canvas scheitert oft an den Details
Canvas ist stark, wenn normale DOM-Elemente nicht reichen: eigene Diagramme, Bildannotationen, Partikel, kleine Spiele, Lernsimulationen oder Produktkonfiguratoren. Die Freiheit hat aber einen Preis. Der Browser merkt sich keine gezeichneten Linien als DOM-Knoten, korrigiert nicht automatisch die Pixeldichte und beendet keine Animationsschleife für dich. Ein Demo kann auf dem Desktop sauber aussehen und auf dem Smartphone unscharf, unbedienbar oder zu breit sein.
Claude Code ist dann hilfreich, wenn du nicht nur einen Effekt, sondern einen Produktionspfad anforderst. Dazu gehören HiDPI, requestAnimationFrame, Pointer Events, State-Management, Resize-Verhalten, Playwright-Screenshots und die Frage, ob Text, Anzeigen und CTA weiter funktionieren. Gerade bei Content-Seiten ist Canvas selten allein auf der Seite.
Passende Vertiefungen sind Claude Code Animation, Claude Code Three.js 3D und Claude Code Datenvisualisierung. Offizielle Referenzen: Claude Code Docs, MDN Canvas API, requestAnimationFrame, Pointer Events und Playwright Screenshots.
Der richtige Prompt
HiDPI bedeutet hohe Pixeldichte. Ein CSS-Pixel kann auf dem Gerät mehrere physische Pixel belegen. Wenn du nur die CSS-Breite setzt, aber den internen Canvas-Puffer nicht anpasst, skaliert der Browser ein zu kleines Bild. Linien wirken dann weich.
Implementiere ein Canvas-2D-Demo.
Anforderungen:
- CSS-Pixel und interne Canvas-Pixel trennen und devicePixelRatio nutzen
- Mit requestAnimationFrame rendern und dt nach Tab-Pausen begrenzen
- Pointer Events für mouse, touch und pen verwenden
- Zeichenzustand in einem state halten, render(ctx) zeichnet nur
- ResizeObserver für Container-Änderungen nutzen
- Bei 375px Breite darf kein horizontaler Scroll entstehen
- Playwright prüft Sichtbarkeit, nicht-leere Pixel, Screenshot und Mobile-Breite
- Geänderte Dateien, Risiken, Fehlerfälle und manuelle Checks ausgeben
Dieser Prompt verhindert, dass Claude Code nur ein hübsches Desktop-Beispiel erzeugt. Er beschreibt den Vertrag, den der Canvas später erfüllen muss.
Architektur vor Code
Teile Eingabe, Zustand, Zeitupdate, Rendering und Prüfung. Diese Skizze kann direkt in den Kontext für Claude Code.
Pointer Events
|
v
input handler ---> state update ---> update(dt)
|
ResizeObserver ---> resize(dpr) v
render(ctx)
|
v
Playwright checks
render(ctx) sollte keine Events registrieren, keine DOM-Struktur ändern und keine weitere Schleife starten. Es liest Zustand und zeichnet. Diese Trennung macht Undo, Radierer, WebGL-Fallbacks und visuelle Tests deutlich einfacher.
Typische Anwendungsfälle
Erster Fall: redaktionelle Visualisierung und Dashboards. Live-Spuren, dichte Punktwolken, freie Formen, Audiowellen oder Kartenbewegungen lassen sich mit Canvas oft besser abbilden als mit Standarddiagrammen. Trotzdem müssen leere Daten, Ladezustände und mobile Breite geprüft werden.
Zweiter Fall: Bildannotation. Screenshots prüfen, Designs markieren, Kursaufgaben korrigieren: Linien, Rechtecke, Pfeile, Labels und Undo sind hier normal. Pointer Events sparen getrennte Implementierungen für Maus, Finger und Stift. Auf passenden Geräten kann pressure die Linienstärke steuern.
Dritter Fall: Lernen und kleine Spiele. Physikdemos, Vokabelkarten, Tipptrainer und Partikeleffekte passen gut zu Frame-basiertem State. Das Risiko liegt im Lifecycle: Läuft requestAnimationFrame nach einem Routenwechsel weiter, sieht der Nutzer nichts, aber CPU und Akku zahlen.
Vierter Fall: Produktseiten. Ein interaktiver Preview kann Farbe, Vergleich oder Bediengefühl erklären. Er muss aber die Kauf- oder Beratungs-CTA stützen, nicht verdrängen.
Ausführbares Beispiel
Speichere diesen Ausschnitt als HTML-Datei und öffne ihn im Browser. Wichtig ist ctx.setTransform, weil es die Skalierung nach jedem Resize ersetzt und nicht stapelt.
<style>
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #111827; }
canvas { width: min(100%, 720px); aspect-ratio: 16 / 9; display: block; background: #020617; border: 1px solid #374151; border-radius: 8px; touch-action: none; }
</style>
<canvas id="demo" aria-label="Canvas particle demo"></canvas>
<script type="module">
const canvas = document.querySelector("#demo");
const ctx = canvas.getContext("2d");
const state = { width: 1, height: 1, dpr: 1, last: 0, pointer: { x: 0, y: 0, down: false }, dots: [] };
function resize() {
const rect = canvas.getBoundingClientRect();
state.width = Math.max(1, rect.width);
state.height = Math.max(1, rect.height);
state.dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(state.width * state.dpr);
canvas.height = Math.round(state.height * state.dpr);
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
}
function point(event) {
const rect = canvas.getBoundingClientRect();
return { x: event.clientX - rect.left, y: event.clientY - rect.top, pressure: event.pressure || 0.5 };
}
function emit(x, y, pressure = 0.5) {
for (let i = 0; i < 8; i += 1) {
const angle = Math.random() * Math.PI * 2;
const speed = 90 + Math.random() * 180;
state.dots.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, life: 1, size: 4 + pressure * 8 });
}
state.dots = state.dots.slice(-360);
}
canvas.addEventListener("pointerdown", (event) => {
canvas.setPointerCapture(event.pointerId);
const p = point(event);
state.pointer = { x: p.x, y: p.y, down: true };
emit(p.x, p.y, p.pressure);
});
canvas.addEventListener("pointermove", (event) => {
const events = event.getCoalescedEvents ? event.getCoalescedEvents() : [event];
for (const item of events) {
const p = point(item);
state.pointer.x = p.x;
state.pointer.y = p.y;
if (state.pointer.down) emit(p.x, p.y, p.pressure);
}
});
canvas.addEventListener("pointerup", () => (state.pointer.down = false));
canvas.addEventListener("pointercancel", () => (state.pointer.down = false));
function frame(now) {
const dt = state.last ? Math.min((now - state.last) / 1000, 0.033) : 0;
state.last = now;
for (const dot of state.dots) {
dot.vy += 220 * dt;
dot.x += dot.vx * dt;
dot.y += dot.vy * dt;
dot.life -= dt;
}
state.dots = state.dots.filter((dot) => dot.life > 0);
ctx.clearRect(0, 0, state.width, state.height);
ctx.fillStyle = "#020617";
ctx.fillRect(0, 0, state.width, state.height);
for (const dot of state.dots) {
ctx.fillStyle = `rgba(56,189,248,${dot.life})`;
ctx.beginPath();
ctx.arc(dot.x, dot.y, dot.size * dot.life, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = "#e5e7eb";
ctx.fillText(`dpr ${state.dpr.toFixed(2)} / dots ${state.dots.length}`, 16, 24);
requestAnimationFrame(frame);
}
new ResizeObserver(resize).observe(canvas);
resize();
requestAnimationFrame(frame);
</script>
State, Mobile und Review
Canvas speichert keine Striche als DOM-Elemente. Wenn du Undo, Redo, Werkzeugwechsel oder Replay brauchst, gehören Punkte, Farbe, Linienstärke und Tool in JavaScript-State. Bitte Claude Code darum, State-Änderungen in kleinen Funktionen zu halten und Rendering deterministisch zu machen.
Mobile Fehler entstehen oft durch feste Breiten und fehlende Höhe. width: 800px erzeugt auf 375px Breite horizontalen Scroll; width: 100% ohne aspect-ratio kann den Canvas zusammenquetschen. Prüfe ihn zusammen mit Text, Codeblöcken, Anzeigen, Related Cards und CTA.
Playwright-Prüfung
DOM-Assertions beweisen nicht, dass der Canvas gemalt hat. Prüfe Sichtbarkeit, mobile Breite, bemalte Pixel und Screenshot.
import { expect, test } from "@playwright/test";
test("canvas renders on mobile", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/canvas-demo");
const canvas = page.locator("canvas").first();
await expect(canvas).toBeVisible();
const box = await canvas.boundingBox();
expect(box?.width ?? 0).toBeLessThanOrEqual(390);
const paintedPixels = await canvas.evaluate((node) => {
const context = node.getContext("2d");
if (!context) return 0;
const data = context.getImageData(0, 0, node.width, node.height).data;
let painted = 0;
for (let i = 3; i < data.length; i += 4) if (data[i] > 0) painted += 1;
return painted;
});
expect(paintedPixels).toBeGreaterThan(1000);
await expect(canvas).toHaveScreenshot("canvas-mobile.png", { maxDiffPixelRatio: 0.03 });
});
Fehler, Monetarisierung und Ergebnis
Häufige Fehler sind: nur CSS-Größe ändern, ctx.scale stapeln, nur mousemove verwenden, RAF nicht aufräumen, Desktop-Breite fixieren und einen schwarzen Screenshot als Erfolg werten. Diese Punkte gehören explizit in die Claude-Code-Review.
Canvas unterstützt Monetarisierung, wenn es etwas erklärt, das ein statisches Bild nicht leisten kann: interaktives Tutorial, Annotationstool, Produktpreview oder Datenwerkzeug. Es schadet, wenn es Inhalt und CTA verdeckt. Für Teams kann Claude Code Training und Beratung helfen, Prompts, Implementierungsregeln und Playwright-Prüfungen als Workflow aufzusetzen.
Beim Ausprobieren dieses Ablaufs war der größte Gewinn eine zweite Claude-Code-Runde nur für Fehlerfälle. Sie fand feste Breiten, kumulatives ctx.scale, fehlende Touch-Unterstützung und mobilen Scroll vor der Veröffentlichung. Das Ergebnis war weniger Show-Demo, aber deutlich näher an einer stabilen Seite.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Permission Safety Ladder: Zugriff kontrolliert erweitern
Von read-only zu begrenzten Änderungen, Prüfbefehlen und Deploy-Checks mit klarer Kontrolle.
Claude Code Small PR Proof Pack: kleine Änderungen reviewbar machen
Ein Proof Pack für Claude-Code-PRs: Diff, Checks, öffentliche URL, CTA-Pfad und Rollback.
Claude-Code-Review-Gate vor dem Commit
Vor dem Commit mit Claude Code prüfen: Diff, Build, öffentliche URL, Gumroad-Links, Beratung-CTA, fehlende Tests und fremde Dateien.