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
Como pedir ao Claude Code para mexer em um único arquivo
Do desastre em que um 'deixa melhor' alterou 40 linhas nasceu um template de prompt que limita o escopo, valida e permite reverter.
Recuperar de negações de permissão no Claude Code sem enfraquecer guardrails
Transforme um comando negado em plano seguro com motivo, alternativa, provas e critérios de nova tentativa.
Claude Code Harness Smoke Test: prova de 15 minutos antes de confiar em um agente
Um smoke test para escopo, áreas bloqueadas, comandos de prova, URL pública e CTAs de receita no Claude Code.