Tips & Tricks (Atualizado: 02/06/2026)

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.

Desenvolvimento Canvas com Claude Code: HiDPI, RAF, eventos e testes

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.

#Claude Code #Canvas #WebGL #gráficos #TypeScript
Grátis

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.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.