Desenvolvimento Canvas com Claude Code: HiDPI, RAF, eventos e testes
Crie interfaces Canvas confiáveis com Claude Code: HiDPI, requestAnimationFrame, Pointer Events, estado e Playwright.
Canvas precisa de engenharia, não só de efeito visual
Canvas é ótimo para gráficos personalizados, anotações em imagens, partículas, jogos leves, simulações e visualizações que ficam difíceis com HTML comum. Mas ele é uma API de baixo nível. O navegador não guarda os traços como elementos DOM, não ajusta automaticamente a densidade de pixels e não encerra sua animação quando o componente some.
Por isso, pedir ao Claude Code apenas “faça um Canvas bonito” costuma gerar um demo de desktop: visualmente interessante, mas borrado em telas HiDPI, sem toque no celular, com loop duplicado ou com largura fixa que quebra o artigo. O pedido certo inclui as restrições de produção: layout responsivo, estado, Pointer Events, requestAnimationFrame, screenshots e impacto em CTA.
Leia também animações com Claude Code, Three.js com Claude Code e visualização de dados com Claude Code. As referências oficiais são Claude Code Docs, MDN Canvas API, requestAnimationFrame, Pointer Events e Playwright screenshots.
Prompt para o Claude Code
HiDPI é uma tela de alta densidade de pixels. Um pixel CSS pode corresponder a vários pixels físicos. Se o Canvas tem width: 100% no CSS, mas o buffer interno continua pequeno, o navegador estica a imagem e o desenho fica sem nitidez.
Implemente um demo Canvas 2D.
Requisitos:
- Separar pixels CSS e pixels internos usando devicePixelRatio
- Renderizar com requestAnimationFrame e limitar dt após pausas da aba
- Usar Pointer Events para mouse, touch e pen
- Manter o estado em um objeto state e deixar render(ctx) apenas desenhar
- Usar ResizeObserver para acompanhar o contêiner
- Não gerar scroll horizontal em 375px de largura
- Adicionar testes Playwright: visibilidade, pixels não vazios, screenshot e largura mobile
- Listar arquivos alterados, riscos, casos de falha e checagens manuais
Esse prompt faz o Claude Code pensar no Canvas como um sistema de renderização. Ele também reduz a chance de uma reescrita visual que mexe em componentes, anúncios ou botões sem necessidade.
Arquitetura antes do código
Separe entrada, estado, atualização no tempo, renderização e verificação.
Pointer Events
|
v
input handler ---> state update ---> update(dt)
|
ResizeObserver ---> resize(dpr) v
render(ctx)
|
v
Playwright checks
render(ctx) deve ler o estado e desenhar. Não deve registrar eventos, mexer em DOM externo ou iniciar outro loop. Essa regra facilita undo, borracha, fallback WebGL e testes visuais.
Casos de uso
O primeiro caso é visualização de dados em artigos e dashboards. Trilhas em tempo real, muitos pontos, ondas de áudio e mapas animados podem ser melhores em Canvas do que em uma biblioteca de gráficos padrão. Ainda assim, é preciso tratar carregamento, dados vazios, tela pequena e posição do CTA.
O segundo caso é anotação de imagens. Revisar prints, marcar telas e comentar material didático exige linhas, setas, retângulos, rótulos e undo. Pointer Events evita três implementações separadas para mouse, dedo e caneta. Quando disponível, pressure ajuda a variar a espessura.
O terceiro caso é educação e jogos simples. Simulações físicas, treino de digitação, cartões interativos e partículas funcionam bem com atualização por frame. O risco é esquecer de cancelar requestAnimationFrame quando a rota muda.
O quarto caso é página de produto. Um preview interativo pode explicar melhor uma compra ou uma consulta, mas só vale se não esconder o próximo passo.
Exemplo executável
Salve este trecho como HTML e abra no navegador. O ponto central é ctx.setTransform, que substitui a escala a cada resize em vez de acumular chamadas de ctx.scale.
<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>
Estado e mobile
Canvas não mantém traços como nós DOM. Para undo, redo, ferramenta ativa, cor e replay, você precisa guardar pontos e metadados no estado JavaScript. Peça ao Claude Code para separar atualização de estado e renderização; isso torna os testes e a revisão mais simples.
No celular, os erros mais comuns são largura fixa e altura indefinida. width: 800px cria scroll horizontal em telas de 375px; width: 100% sem aspect-ratio pode deixar o Canvas achatado. Verifique o componente junto com texto, blocos de código, anúncios, cards relacionados e CTA.
Validação com Playwright
Como Canvas é pixel, uma asserção de DOM não basta. Combine visibilidade, largura mobile, pixels pintados e 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 });
});
Armadilhas, monetização e resultado
As armadilhas mais frequentes são alterar só o CSS, acumular ctx.scale, ouvir apenas mousemove, não limpar o loop RAF, fixar largura de desktop e aceitar um screenshot preto como sucesso. Coloque essa lista no prompt de revisão do Claude Code.
Canvas ajuda a monetização quando explica algo que imagem estática não resolve: tutorial interativo, ferramenta de anotação, preview de produto ou visualização conectada a uma oferta. Prejudica quando empurra texto e CTA para baixo. Para transformar isso em processo de equipe, a página de treinamento e consultoria Claude Code pode ajudar a definir prompts, regras e testes Playwright.
Ao testar esse fluxo, a etapa mais útil foi pedir ao Claude Code uma segunda revisão focada apenas em falhas. Ela encontrou largura fixa, ctx.scale acumulado, touch incompleto e scroll mobile antes da publicação. O demo ficou menos exagerado, mas muito mais adequado para uma página real.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Escada de segurança de permissões no Claude Code
Amplie de read-only para edições limitadas, comandos de prova e deploy checks sem perder controle.
Claude Code Small PR Proof Pack: pequenas mudanças fáceis de revisar
Um pacote de prova para PRs do Claude Code: diff, checks, URL pública, CTA e rollback.
Gate de revisão antes do commit com Claude Code
Revisão antes do commit com Claude Code: diff, build, URL pública, Gumroad, consultoria, testes e arquivos fora do escopo.