Tips & Tricks (更新: 2026/6/6)

Claude CodeのCanvas開発でぼやけ・スマホ崩れを直す実装術

Canvasがスマホで崩れぼやける原因はHiDPIと座標設計。Claude Codeに渡す条件文、コピペで動くコード、Playwright検証まで実体験で解説。

Claude CodeのCanvas開発でぼやけ・スマホ崩れを直す実装術

「Canvasでかっこいいデモ作って」とClaude Codeに頼んだら、自分のMacでは完璧に動くものが出てきた。

そのままサイトに載せて、スマホで開いて、固まりました。線はぼやけ、Canvasの高さは潰れ、CTAボタンの上には謎の余白だけが残っている。スクリーンショットでは気づけなかった崩れです。

Canvasって、デモが派手なほど粗が隠れるんですよ。今日はそこで僕がハマった落とし穴を、全部先回りして潰す書き方を共有します。

この記事の要点

  • Canvasのぼやけ・スマホ崩れは、ほぼ全部HiDPI対応座標・状態の設計で決まる。モデルの賢さの問題じゃない
  • Claude Codeには「かっこいいCanvas」ではなく、devicePixelRatio対応・requestAnimationFrame・Pointer Events・スマホ375px幅といった境界条件を先に渡す
  • リサイズのたびにctx.scaleを呼ぶと座標がずれていく。ctx.setTransformで毎回上書きするのが正解
  • 入力はmousemoveではなくPointer Eventsで統一。touch-action: noneを忘れるとスマホで描画中にスクロールが割り込む
  • CanvasはDOMを見ても中身が分からないので、Playwrightで非ブランク検査(実際にピクセルが塗られたか)まで入れる

そもそもCanvasの何が難しいのか

Canvasは、ブラウザ上に線・画像・粒子・ゲーム画面・グラフを直接描くための「描画面」です。HTMLやCSSのレイアウトとは別世界で、JavaScriptが毎フレーム絵を描き直します。だから見た目は完全に自由。その代わり、CSSのように要素が残ってくれません。

ここが最初の壁でした。ボタンやdivなら、一度置けば画面に残ります。Canvasは違う。1秒間に60回まっさらにして描き直すので、過去に引いた線も、いま選んでいるツールも、Undo履歴も、ぜんぶ自分でJavaScript側に持っておかないと消えます。

そしてもう一つ、ピクセルの罠があります。CSSが言う「1ピクセル」と、画面の実ピクセルは一致しません。MacBookやスマホのような高密度ディスプレイでは、CSSの1ピクセルが実際には2ピクセル分あったりする。これを無視して描くと、線がにじんでぼやけます。冒頭の僕の失敗、半分はこれが原因でした。

Claude CodeでCanvasを書く価値は、APIの断片を吐かせることじゃありません。既存のReactコンポーネント、CSS、テスト、記事のCTA、スマホ幅の制約まで読ませて、「動くデモ」から「公開できるUI」に引き上げるところにあります。Canvasは低レベルなAPIなので、依頼の条件も低レベルに具体化するほど、出てくる品質が安定するんです。

Claude Codeへの依頼文:条件を先に渡す

Canvas開発でいちばん危ない依頼は「かっこいいCanvasを作って」です。これだと、固定サイズ・マウス専用・HiDPI未対応・テストなしのデモが返ってきます。僕がこのサイト用の検証デモを最初に雑に頼んだとき、まさにそうなりました。デスクトップでは動くのに、iPhone幅でCanvasの高さが潰れ、CTAの上に余白だけが残ったんです。

なので、絵ではなく「描画システム」を作らせるつもりで、境界条件を先に渡します。

Claude CodeでCanvas 2Dのデモを実装してください。
条件:
- CSSピクセルと内部ピクセルを分け、devicePixelRatioに対応する
- requestAnimationFrameで描画し、dtは最大値を丸める
- mouse/touch/penをPointer Eventsで統一する
- 描画状態は1つのstateにまとめ、render関数に副作用を持たせない
- ResizeObserverで親要素の幅変更に追従する
- スマホ幅375pxで横スクロールしない
- Playwrightで非ブランク描画、スクリーンショット、モバイル幅を検証する
- 変更ファイル、落とし穴、手動確認項目を最後に列挙する

devicePixelRatioはHiDPI、つまり高密度ディスプレイで1 CSSピクセルを複数の実ピクセルとして描くための値です。ここを条件に入れておくと、Claude Codeが最初から「ぼやけ対策込み」のコードを書いてくれます。後から「ぼやけてる、直して」と頼むより、ずっと手戻りが少ない。

雑な依頼と条件付きの依頼で、何がどう変わるか。僕の体感をまとめるとこうです。

観点「かっこいいCanvas作って」条件を先に渡した場合
HiDPI未対応でぼやけるsetTransformで鮮明
入力mousemoveのみ(スマホ無反応)Pointer Eventsで統一
リサイズ固定サイズで崩れるResizeObserverで追従
スマホ375px横スクロール発生はみ出しなし
検証なし(目視のみ)Playwrightで非ブランク確認

責務を分けておくと後がラク

コードを書く前に、Canvasの仕事を分けておくとレビューが一気に楽になります。Claude Codeにもこの構造を先に共有してから依頼すると、入力処理と描画処理が混ざりにくくなる。

Pointer Events
      |
      v
  input handler  --->  state更新  --->  update(dt)
                                      |
ResizeObserver ---> resize(dpr)       v
                                  render(ctx)
                                      |
                                      v
                             Playwright検証

肝は、render(ctx)の中でイベント登録やDOM更新を絶対にしないこと。イベントは入力、updateは状態の時間変化、renderは描画だけ、と担当を割り切る。こうしておくと、Claude Codeが後から機能を足しても破綻しにくいです。逆にここを混ぜると、「描画のたびにイベントリスナーが増殖する」みたいな見えない地雷を踏みます。僕は踏みました。

どんな場面で効くのか(4つ)

1. ダッシュボードや記事内のデータ可視化 標準のグラフライブラリでは表現しにくい粒子・軌跡・地図上の動き・リアルタイム波形は、Canvasが向いています。ただし広告やCTAがある記事では、Canvasが重すぎて本文やボタンの表示を遅らせないことが最優先。見た目より体感速度です。

2. 画像注釈ツール スクリーンショットに矢印・矩形・ハイライト・手書きメモを重ねるUI。ここはPointer Eventsと状態管理が中心になります。ペン入力のpressure(筆圧)を扱える端末なら、線の太さに反映できて気持ちいい。

3. 学習教材やミニゲーム 物理シミュレーション、タイピング練習、英単語カードのアニメーション。毎フレームの状態更新が自然にハマる用途です。ただしrequestAnimationFrameを複数起動したまま画面遷移すると、見えないCanvasがCPUを食い続けるので、停止処理まで必ず実装します。

4. 商品ページのインタラクティブな演出 マウスやタッチで商品色を切り替える、粒子で背景を反応させる、3Dに進む前の軽いプレビューを出す。収益導線に近い場所で使うほど、「CTAを邪魔しない」「スマホで崩れない」「広告枠と重ならない」を見た目より優先します。

コピペで動くCanvasデモ

次のHTMLは、単体でブラウザに開けるCanvas 2Dデモです。HiDPI対応・requestAnimationFrame・Pointer Events・ResizeObserver・スマホのtouch-action: noneをぜんぶ入れてあります。落とし穴回避の要点は1か所、リサイズのたびにctx.scale(dpr, dpr)を繰り返すのではなく、ctx.setTransform(dpr, 0, 0, dpr, 0, 0)で変換を上書きすること。scaleは累積するので、何度かリサイズすると座標がじわじわずれていきます。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Claude Code Canvas Demo</title>
    <style>
      body {
        margin: 0;
        font-family: system-ui, sans-serif;
        background: #111827;
        color: #f9fafb;
      }

      main {
        min-height: 100vh;
        box-sizing: border-box;
        display: grid;
        place-items: center;
        padding: 24px;
      }

      .canvas-shell {
        width: min(100%, 760px);
      }

      canvas {
        display: block;
        width: 100%;
        aspect-ratio: 16 / 9;
        border: 1px solid #374151;
        border-radius: 8px;
        background: #020617;
        touch-action: none;
      }
    </style>
  </head>
  <body>
    <main>
      <div class="canvas-shell">
        <canvas id="demo" aria-label="Pointer controlled particle demo"></canvas>
      </div>
    </main>

    <script type="module">
      const canvas = document.querySelector("#demo");
      const ctx = canvas.getContext("2d");

      // 描画に必要な状態は全部ここに集約する(renderは読むだけ)
      const state = {
        dpr: 1,
        width: 1,
        height: 1,
        last: 0,
        pointer: { x: 0, y: 0, down: false },
        particles: [],
      };

      // リサイズ時:CSSサイズと内部バッファを分け、変換は毎回上書きする
      function resize() {
        const rect = canvas.getBoundingClientRect();
        const dpr = Math.min(window.devicePixelRatio || 1, 2);
        state.width = Math.max(1, rect.width);
        state.height = Math.max(1, rect.height);
        state.dpr = dpr;
        canvas.width = Math.round(state.width * dpr);
        canvas.height = Math.round(state.height * dpr);
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      }

      // Canvas内の座標へ変換(rectのleft/topを引くのを忘れがち)
      function readPoint(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, count = 9) {
        for (let i = 0; i < count; i += 1) {
          const angle = Math.random() * Math.PI * 2;
          const speed = 80 + Math.random() * 220;
          state.particles.push({
            x,
            y,
            vx: Math.cos(angle) * speed,
            vy: Math.sin(angle) * speed,
            size: 3 + pressure * 10 * Math.random(),
            life: 0.7 + Math.random() * 0.5,
            maxLife: 1.2,
          });
        }
        // 粒子は上限を決めて捨てる。無限に貯めると重くなる
        state.particles = state.particles.slice(-420);
      }

      function handlePointerMove(event) {
        // getCoalescedEventsで高頻度入力の取りこぼしを拾う
        const events = event.getCoalescedEvents ? event.getCoalescedEvents() : [event];
        for (const item of events) {
          const point = readPoint(item);
          state.pointer.x = point.x;
          state.pointer.y = point.y;
          if (state.pointer.down) emit(point.x, point.y, point.pressure, 3);
        }
      }

      canvas.addEventListener("pointerdown", (event) => {
        canvas.setPointerCapture(event.pointerId);
        const point = readPoint(event);
        state.pointer = { x: point.x, y: point.y, down: true };
        emit(point.x, point.y, point.pressure, 24);
      });

      canvas.addEventListener("pointermove", handlePointerMove);
      canvas.addEventListener("pointerup", () => {
        state.pointer.down = false;
      });
      canvas.addEventListener("pointercancel", () => {
        state.pointer.down = false;
      });

      // 状態の時間変化だけを担当する。描画はしない
      function update(dt) {
        for (const particle of state.particles) {
          particle.vy += 240 * dt;
          particle.x += particle.vx * dt;
          particle.y += particle.vy * dt;
          particle.life -= dt;
        }
        state.particles = state.particles.filter((particle) => particle.life > 0);
      }

      function drawGrid() {
        ctx.strokeStyle = "rgba(148, 163, 184, 0.16)";
        ctx.lineWidth = 1;
        for (let x = 0; x < state.width; x += 40) {
          ctx.beginPath();
          ctx.moveTo(x, 0);
          ctx.lineTo(x, state.height);
          ctx.stroke();
        }
        for (let y = 0; y < state.height; y += 40) {
          ctx.beginPath();
          ctx.moveTo(0, y);
          ctx.lineTo(state.width, y);
          ctx.stroke();
        }
      }

      // 状態を読んで描くだけ。ここで副作用を持たせない
      function render() {
        ctx.clearRect(0, 0, state.width, state.height);
        ctx.fillStyle = "#020617";
        ctx.fillRect(0, 0, state.width, state.height);
        drawGrid();

        for (const particle of state.particles) {
          const alpha = Math.max(0, particle.life / particle.maxLife);
          ctx.fillStyle = `rgba(56, 189, 248, ${alpha})`;
          ctx.beginPath();
          ctx.arc(particle.x, particle.y, particle.size * alpha, 0, Math.PI * 2);
          ctx.fill();
        }

        ctx.strokeStyle = "#f97316";
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc(state.pointer.x, state.pointer.y, 14, 0, Math.PI * 2);
        ctx.stroke();

        ctx.fillStyle = "#e5e7eb";
        ctx.font = "14px system-ui, sans-serif";
        ctx.fillText(`dpr ${state.dpr.toFixed(2)} / particles ${state.particles.length}`, 16, 24);
      }

      // dtは上限を丸める。タブ復帰時に巨大なdtで飛ぶのを防ぐ
      function frame(now) {
        const dt = state.last ? Math.min((now - state.last) / 1000, 0.033) : 0;
        state.last = now;
        update(dt);
        render();
        requestAnimationFrame(frame);
      }

      new ResizeObserver(resize).observe(canvas);
      resize();
      requestAnimationFrame(frame);
    </script>
  </body>
</html>

このコードをClaude Codeにレビューさせるなら、「ctx.scaleの累積がないか」「スマホ幅でcanvasが親要素を超えないか」「Pointer Eventsがマウスだけに閉じていないか」の3点を見てもらいます。ここを確認しないと、PCのスクリーンショットだけきれいなデモが完成します。動きの調整までやりたくなったら、Claude Codeでアニメーション実装を効率化する方法も合わせて読むと、dtの扱いがつながって理解できます。

描画状態は純粋関数に寄せる

さっき書いたとおり、Canvasは毎フレーム消えます。過去の線・現在のツール・Undo/Redo・選択状態を、JavaScript側に持つしかない。ここをぐちゃっと書くと、Undoしたらアプリごと壊れた、みたいな事故が起きます。

僕のおすすめは、状態更新を純粋関数(reducer)に寄せること。入力に対して新しい状態を返すだけにすると、テストがめちゃくちゃ書きやすくなります。

type Tool = "pen" | "eraser";

type StrokePoint = {
  x: number;
  y: number;
  pressure: number;
  t: number;
};

type CanvasState = {
  tool: Tool;
  color: string;
  lineWidth: number;
  strokes: StrokePoint[][];
  redo: StrokePoint[][];
};

type Action =
  | { type: "start"; point: StrokePoint }
  | { type: "append"; point: StrokePoint }
  | { type: "finish" }
  | { type: "undo" }
  | { type: "redo" }
  | { type: "tool"; tool: Tool };

export function canvasReducer(state: CanvasState, action: Action): CanvasState {
  if (action.type === "start") {
    // 新しい線を始めたらredoはクリアする
    return { ...state, strokes: [...state.strokes, [action.point]], redo: [] };
  }

  if (action.type === "append") {
    const strokes = state.strokes.slice();
    const last = strokes.at(-1) ?? [];
    strokes[strokes.length - 1] = [...last, action.point];
    return { ...state, strokes };
  }

  if (action.type === "undo") {
    const strokes = state.strokes.slice(0, -1);
    const removed = state.strokes.at(-1);
    return removed ? { ...state, strokes, redo: [removed, ...state.redo] } : state;
  }

  if (action.type === "redo") {
    const [next, ...redo] = state.redo;
    return next ? { ...state, strokes: [...state.strokes, next], redo } : state;
  }

  if (action.type === "tool") {
    return { ...state, tool: action.tool };
  }

  return state;
}

状態更新を純粋関数に寄せると、Claude Codeに「UndoでRedoが消えるか」「空の状態でUndoしても壊れないか」「筆圧がない端末で既定値になるか」をテストさせやすくなります。境界ケースを言葉で指定できるのが効くんです。テスト全体の組み方はClaude Codeでテスト戦略を立てて品質を上げる方法も参考になります。

Reactで組み込む時の注意

ReactでCanvasを扱うときは、useEffectの中でイベント登録・ResizeObserver・アニメーションループを開始し、クリーンアップで必ず解除します。ここを忘れると、ページ遷移やホットリロードのたびにrequestAnimationFrameが増えて、見た目は同じなのにCPU使用率だけ上がっていく。開発中にファンが回り出したら、だいたいこれを疑ってください。

import { useEffect, useRef } from "react";

export function CanvasPanel() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas?.getContext("2d");
    if (!canvas || !ctx) return;

    let frameId = 0;
    let last = 0;
    const state = { width: 1, height: 1, dpr: 1, x: 40 };

    const 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);
    };

    const observer = new ResizeObserver(resize);
    observer.observe(canvas);
    resize();

    const tick = (now: number) => {
      const dt = last ? Math.min((now - last) / 1000, 0.033) : 0;
      last = now;
      state.x = (state.x + dt * 80) % state.width;

      ctx.clearRect(0, 0, state.width, state.height);
      ctx.fillStyle = "#020617";
      ctx.fillRect(0, 0, state.width, state.height);
      ctx.fillStyle = "#38bdf8";
      ctx.beginPath();
      ctx.arc(state.x, state.height / 2, 18, 0, Math.PI * 2);
      ctx.fill();

      frameId = requestAnimationFrame(tick);
    };

    frameId = requestAnimationFrame(tick);

    // アンマウント時にループとObserverを必ず止める
    return () => {
      cancelAnimationFrame(frameId);
      observer.disconnect();
    };
  }, []);

  return <canvas ref={canvasRef} className="w-full aspect-video touch-none rounded border" />;
}

Claude CodeにReact実装を頼むときは、「useEffectの戻り値で解除する」「依存配列を変えた時に二重起動しない」「CSSで高さが決まっている」をレビュー条件に入れてください。この3つを言っておくだけで、ループ残留の事故がほぼ消えます。

Playwrightで「本当に描けたか」を検証する

CanvasはDOMの中身を見ても描画結果が分かりません。<canvas>の中はタグとして空っぽだからです。だからスクリーンショットとピクセル検査をセットで入れます。ここで注意したいのが、PlaywrightのtoHaveScreenshotだけに頼ると環境差で揺れること。まずCanvasが表示され、スマホ幅で親要素を超えず、実際にピクセルが塗られていることを確認します。

import { expect, test } from "@playwright/test";

const viewports = [
  { name: "desktop", width: 1280, height: 800 },
  { name: "mobile", width: 390, height: 844 },
];

for (const viewport of viewports) {
  test(`canvas renders on ${viewport.name}`, async ({ page }) => {
    await page.setViewportSize({ width: viewport.width, height: viewport.height });
    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(viewport.width);

    if (box) {
      await page.mouse.move(box.x + box.width * 0.3, box.y + box.height * 0.5);
      await page.mouse.down();
      await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.6);
      await page.mouse.up();
    }

    // 非ブランク検査:不透明ピクセルが実際にあるか数える
    const paintedPixels = await canvas.evaluate((node) => {
      const context = node.getContext("2d");
      if (!context) return 0;
      const image = context.getImageData(0, 0, node.width, node.height).data;
      let painted = 0;
      for (let i = 3; i < image.length; i += 4) {
        if (image[i] > 0) painted += 1;
      }
      return painted;
    });

    expect(paintedPixels).toBeGreaterThan(1000);
    await expect(canvas).toHaveScreenshot(`canvas-${viewport.name}.png`, {
      maxDiffPixelRatio: 0.03,
    });
  });
}

初回はnpx playwright test tests/canvas.spec.ts --update-snapshotsで基準画像を作り、以後は通常実行で差分を見ます。Claude Codeには、失敗したスクリーンショットだけでなく、boundingBox・viewport・DPR・直近のCSS変更も読ませると原因特定が早い。Playwrightの組み方そのものはClaude CodeとPlaywrightでE2Eテストを自動化する方法にまとめています。

僕がやらかした落とし穴6つ

正直に書きます。最初のCanvasデモは、踏める地雷を全部踏みました。

1. CSSサイズだけ変えて内部サイズを変えない。 canvas.style.width = "100%"だけだと、内部の描画バッファが既定の300×150のまま拡大表示されて、ぼやけます。冒頭の「ぼやけ事件」の正体がこれ。

2. リサイズのたびにctx.scale(dpr, dpr)を呼ぶ。 scaleは累積するので、何度かリサイズすると線や座標が大きくずれます。setTransformで上書きするのが安全。

3. mousemoveだけで入力を実装する。 スマホ・タブレット・ペンで無反応になります。Pointer Eventsならマウス・タッチ・ペンを同じコードで扱える。さらにtouch-action: noneをCanvasに指定しないと、描画中にページスクロールが割り込みます。

4. requestAnimationFrameを止めない。 Reactのアンマウント、タブ切り替え、モーダルを閉じる操作でループが残ると、見えないCanvasが永遠に動き続けます。

5. スマホ幅でCanvasを固定ピクセルにする。 width: 800pxのまま記事本文に置くと、375px幅で横スクロールが出る。コードブロック・広告・CTA・関連記事の列と一緒に確認してください。

6. スクリーンショットが通るだけで安心する。 黒背景だけでもスクリーンショットは生成されます。非ブランク検査・イベント操作後の差分・モバイル幅のboundingBoxを合わせて見ないと、「真っ黒だけど合格」が起きます。

WebGLへ進む前の判断

Canvas 2Dで線・画像・数百個くらいの粒子を扱うなら、まず2Dコンテキストで十分です。何万個の点、3Dカメラ、ライティング、GPUシェーダーが必要になったら、はじめてWebGLやThree.jsを検討します。Claude CodeにWebGLシェーダーを書かせる場合も、「Canvas 2Dのフォールバック」「WebGLが使えない時の静止画像」「スマホGPUでの負荷」「スクリーンショット検証」を条件に含めてください。

WebGLの基礎はWebGL Fundamentalsが分かりやすく、実務で3Dに進むならClaude CodeでThree.js 3D表現を作るガイドにつなげると判断しやすくなります。データ表現寄りならClaude Codeでデータ可視化を実装する方法も見ておくと、Canvasかライブラリかの線引きが楽になります。

公式情報はMDN Canvas APIrequestAnimationFramePointer EventsPlaywright screenshotsClaude Code Docsを確認してください。

よくある質問

Q. Canvasがぼやけます。何を直せばいいですか。 ほぼHiDPI未対応です。CSSのwidth/height(見た目のサイズ)とcanvas.width/height(内部バッファ)を分けて、内部バッファをCSSサイズ × devicePixelRatioで確保してください。そのうえでctx.setTransform(dpr, 0, 0, dpr, 0, 0)を入れれば鮮明になります。

Q. PCでは動くのにスマホで反応しません。 入力をmousemoveで書いているはずです。Pointer Events(pointerdown/pointermove/pointerup)に置き換えると、マウス・タッチ・ペンが同じコードで動きます。あわせてCanvasのCSSにtouch-action: noneを付けないと、描画中にスクロールが割り込みます。

Q. ctx.scalectx.setTransform、どっちを使えばいいですか。 リサイズが絡むならsetTransformです。scaleは呼ぶたびに累積するので、ResizeObserverと組み合わせると座標が徐々にずれます。setTransformは毎回まっさらに上書きするので安全です。

Q. 開発中にCPUファンが回りっぱなしになります。 requestAnimationFrameが止まっていません。ReactならuseEffectのクリーンアップでcancelAnimationFrameobserver.disconnect()を呼んでください。ホットリロードのたびにループが多重起動している可能性が高いです。

Q. Playwrightのスクリーンショットが通れば品質は大丈夫ですか。 いいえ。真っ黒な画面でもスクリーンショットは生成されます。getImageDataで不透明ピクセルを数える非ブランク検査と、スマホ幅のboundingBoxチェックを必ず併用してください。

実際に試した結果

冒頭の「スマホで崩れた」事件のあと、僕はCanvasのレビュー方針を変えました。「きれいに描けたか」を先に見るのをやめて、どこで崩れるかを先に潰すようにしたんです。

具体的には、Claude Codeに実装を頼むとき、同じプロンプトの中で「失敗例のレビューもして」とセットで頼む。これが一番効きました。検証では、固定800pxのCanvas・ctx.scaleの累積・スマホでのスクロール干渉が、公開前の早い段階で全部見つかった。後から「なんか変」と気づいて直すより、はるかに手戻りが少ないです。

Canvasは派手なデモほど粗が隠れます。描画コード・状態管理・スクリーンショット・CTA周辺のレイアウトを、同じチェックリストで一度に見る。地味ですが、これが実務でいちばん安定する、というのが今の僕の結論です。

チームでCanvasデモ・教育コンテンツ・データ可視化・プロダクトUIを作るなら、レビュー観点やPlaywright検証を既存リポジトリに合わせて設計できます。具体的な進め方はClaude Code研修・導入相談で相談できますし、すぐ使える実装テンプレートは教材一覧にまとめてあります。

#Claude Code #Canvas #HiDPI #Pointer Events #TypeScript
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。