お絵描きキャンバス

ポインタ操作で線を描き、色パレットと太さスライダー、クリア機能を備えた簡易ペイント。署名入力やメモ、落書き機能の土台になります。

#canvas#interactive#drawing#tool

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW:店頭メッセージボード(手書きお絵描き) -->
<section class="mb-board">
  <header class="mb-board__head">
    <span class="mb-board__logo">☾ MOON BREW</span>
    <span class="mb-board__sub">今日のひとこと、描いてみてください</span>
  </header>

  <div class="mb-board__panel">
    <!-- 主役:手書きキャンバス -->
    <canvas class="mb-board__canvas" id="mbPaint"></canvas>
    <span class="mb-board__hint" id="mbHint">☕ ここにお絵描き・サインを</span>
  </div>

  <!-- 前景UI:パレット・太さ・クリア -->
  <div class="mb-board__tools">
    <div class="mb-swatches" id="mbSwatches">
      <button class="mb-sw is-on" style="--c:#2b1d12" data-color="#2b1d12" type="button" aria-label="ブラウン"></button>
      <button class="mb-sw" style="--c:#c98a3b" data-color="#c98a3b" type="button" aria-label="琥珀"></button>
      <button class="mb-sw" style="--c:#7c9c6a" data-color="#7c9c6a" type="button" aria-label="抹茶"></button>
      <button class="mb-sw" style="--c:#c45b5b" data-color="#c45b5b" type="button" aria-label="ベリー"></button>
    </div>
    <label class="mb-size">
      太さ
      <input type="range" id="mbSize" min="2" max="18" value="6">
    </label>
    <button class="mb-clear" id="mbClear" type="button">クリア</button>
  </div>
</section>
CSS
/* MOON BREW:店頭メッセージボード(お絵描き) */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.mb-board {
  height: 400px;
  padding: 18px 22px;
  background: var(--brown);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.mb-board__head { display: flex; align-items: baseline; gap: 14px; }
.mb-board__logo {
  font-size: 16px;
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--cream);
}
.mb-board__sub {
  font-size: 12px;
  color: rgba(245,237,225,0.65);
}

/* 黒板風パネル:主役のキャンバスを内包 */
.mb-board__panel {
  position: relative;
  flex: 1;
  border-radius: 14px;
  overflow: hidden;
  background:
    repeating-linear-gradient(45deg, rgba(255,255,255,0.02) 0 12px, transparent 12px 24px),
    #f8f3ea;
  border: 3px solid #4a3625;
  box-shadow: inset 0 0 0 2px rgba(201,138,59,0.25);
}
.mb-board__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  cursor: crosshair;
  touch-action: none;
}
.mb-board__hint {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-size: 14px;
  color: rgba(43,29,18,0.32);
  pointer-events: none;
  transition: opacity 0.3s ease;
}
.mb-board__hint.is-hidden { opacity: 0; }

/* ツールバー */
.mb-board__tools {
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}
.mb-swatches { display: flex; gap: 8px; }
.mb-sw {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid rgba(245,237,225,0.4);
  background: var(--c);
  cursor: pointer;
  padding: 0;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.mb-sw:hover { transform: scale(1.1); }
.mb-sw.is-on {
  border-color: var(--cream);
  box-shadow: 0 0 0 2px var(--amber);
}
.mb-size {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: rgba(245,237,225,0.8);
}
.mb-size input { accent-color: var(--amber); width: 90px; }
.mb-clear {
  margin-left: auto;
  padding: 7px 16px;
  border-radius: 999px;
  border: 1px solid rgba(245,237,225,0.35);
  background: transparent;
  color: var(--cream);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s ease;
}
.mb-clear:hover { background: rgba(245,237,225,0.12); }

@media (prefers-reduced-motion: reduce) {
  .mb-sw, .mb-clear, .mb-board__hint { transition: none; }
}
JavaScript
// MOON BREW:手書きお絵描き(パレット・太さ・クリア)
(() => {
  const canvas = document.getElementById('mbPaint');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;
  const swatches = document.getElementById('mbSwatches');
  const sizeInput = document.getElementById('mbSize');
  const clearBtn = document.getElementById('mbClear');
  const hint = document.getElementById('mbHint');

  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  let drawing = false, color = '#2b1d12', size = 6, last = null;

  // リサイズ時も描画内容を保持(一時退避→再描画)
  function resize() {
    const r = canvas.getBoundingClientRect();
    let snapshot = null;
    if (canvas.width > 0 && canvas.height > 0) {
      try { snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height); } catch (e) { snapshot = null; }
    }
    const prevW = canvas.width, prevH = canvas.height;
    canvas.width = Math.max(1, Math.floor(r.width * dpr));
    canvas.height = Math.max(1, Math.floor(r.height * dpr));
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    // 退避した内容を可能なら戻す
    if (snapshot && prevW === canvas.width && prevH === canvas.height) {
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.putImageData(snapshot, 0, 0);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    }
  }
  resize();

  // ポインタ座標(CSSピクセル)
  function pos(e) {
    const r = canvas.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }

  function startDraw(e) {
    drawing = true;
    last = pos(e);
    if (hint) hint.classList.add('is-hidden'); // ヒントを消す
    canvas.setPointerCapture && canvas.setPointerCapture(e.pointerId);
  }
  function moveDraw(e) {
    if (!drawing || !last) return;
    const p = pos(e);
    ctx.strokeStyle = color;
    ctx.lineWidth = size;
    ctx.beginPath();
    ctx.moveTo(last.x, last.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    last = p;
  }
  function endDraw() { drawing = false; last = null; }

  canvas.addEventListener('pointerdown', startDraw);
  canvas.addEventListener('pointermove', moveDraw);
  canvas.addEventListener('pointerup', endDraw);
  canvas.addEventListener('pointerleave', endDraw);

  // パレット選択
  if (swatches) {
    swatches.addEventListener('click', (e) => {
      const btn = e.target.closest('.mb-sw');
      if (!btn) return;
      color = btn.dataset.color || color;
      swatches.querySelectorAll('.mb-sw').forEach((b) => b.classList.remove('is-on'));
      btn.classList.add('is-on');
    });
  }
  // 太さ
  if (sizeInput) {
    sizeInput.addEventListener('input', () => { size = Number(sizeInput.value) || 6; });
  }
  // クリア
  if (clearBtn) {
    clearBtn.addEventListener('click', () => {
      const r = canvas.getBoundingClientRect();
      ctx.clearRect(0, 0, r.width, r.height);
      if (hint) hint.classList.remove('is-hidden');
    });
  }

  window.addEventListener('resize', resize);
})();

コード

HTML
<!-- お絵描きキャンバス -->
<div class="stage">
  <canvas id="paintCanvas"></canvas>
  <div class="toolbar">
    <div class="swatches" id="swatches"></div>
    <label class="size">
      太さ
      <input type="range" id="brushSize" min="1" max="40" value="6">
    </label>
    <button type="button" id="clearBtn">クリア</button>
  </div>
</div>
CSS
/* お絵描きキャンバス */
:root { --ink: #2b2f3a; }
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  /* うっすら方眼の用紙風背景 */
  background:
    linear-gradient(#eef1f7 0 0) padding-box,
    repeating-linear-gradient(0deg, #e3e7f0 0 1px, transparent 1px 24px),
    repeating-linear-gradient(90deg, #e3e7f0 0 1px, transparent 1px 24px),
    #f7f9fc;
}
#paintCanvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: crosshair;
  touch-action: none; /* タッチでスクロールさせない */
}
/* 下部のツールバー */
.toolbar {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 8px 14px;
  background: rgba(255, 255, 255, .85);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 14px;
  box-shadow: 0 6px 20px rgba(30, 40, 70, .12);
  backdrop-filter: blur(6px);
}
.swatches { display: flex; gap: 6px; }
.swatches button {
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
  cursor: pointer;
  padding: 0;
  transition: transform .12s;
}
.swatches button:hover { transform: scale(1.15); }
.swatches button.is-active {
  box-shadow: 0 0 0 2px var(--ink);
  transform: scale(1.15);
}
.size {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--ink);
}
.size input { width: 80px; }
#clearBtn {
  appearance: none;
  border: 1px solid rgba(0, 0, 0, .12);
  background: #fff;
  color: var(--ink);
  font-size: 12px;
  padding: 5px 12px;
  border-radius: 8px;
  cursor: pointer;
}
#clearBtn:hover { background: #f0f2f7; }
#clearBtn:active { transform: scale(.96); }
JavaScript
// お絵描きキャンバスデモ(色・太さ変更/クリア対応)
(() => {
  const canvas = document.getElementById('paintCanvas');
  const swatchBox = document.getElementById('swatches');
  const sizeInput = document.getElementById('brushSize');
  const clearBtn = document.getElementById('clearBtn');
  if (!canvas || !swatchBox || !sizeInput || !clearBtn) return; // null安全
  const ctx = canvas.getContext('2d');

  const colors = ['#2b2f3a', '#e23b5a', '#3b82f6', '#22c55e', '#f59e0b', '#a855f7'];
  let color = colors[0];
  let drawing = false;
  let last = null;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);

  // リサイズ時は内容を退避してCSSサイズに合わせて復元(消えない/伸び縮みも追従)
  function resize() {
    const r = canvas.getBoundingClientRect();
    let snapshot = null;
    if (canvas.width && canvas.height) {
      snapshot = document.createElement('canvas');
      snapshot.width = canvas.width;
      snapshot.height = canvas.height;
      snapshot.getContext('2d').drawImage(canvas, 0, 0);
    }
    canvas.width = r.width * dpr;
    canvas.height = r.height * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    if (snapshot) ctx.drawImage(snapshot, 0, 0, r.width, r.height);
  }
  resize();
  window.addEventListener('resize', resize);

  // 色見本ボタンを生成
  colors.forEach((c, i) => {
    const b = document.createElement('button');
    b.type = 'button';
    b.style.background = c;
    b.setAttribute('aria-label', `色 ${i + 1}`);
    if (i === 0) b.classList.add('is-active');
    b.addEventListener('click', () => {
      color = c;
      swatchBox.querySelectorAll('button').forEach((x) => x.classList.toggle('is-active', x === b));
    });
    swatchBox.appendChild(b);
  });

  function pos(e) {
    const r = canvas.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }

  function start(e) { drawing = true; last = pos(e); draw(e); }
  function draw(e) {
    if (!drawing) return;
    const p = pos(e);
    ctx.strokeStyle = color;
    ctx.lineWidth = Number(sizeInput.value);
    ctx.beginPath();
    ctx.moveTo(last.x, last.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    last = p;
  }
  function end() { drawing = false; last = null; }

  canvas.addEventListener('pointerdown', start);
  canvas.addEventListener('pointermove', draw);
  window.addEventListener('pointerup', end);
  canvas.addEventListener('pointerleave', end);

  clearBtn.addEventListener('click', () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  });
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「お絵描きキャンバス」の効果を追加してください。

# 追加してほしい効果
お絵描きキャンバス(Canvas エフェクト)
ポインタ操作で線を描き、色パレットと太さスライダー、クリア機能を備えた簡易ペイント。署名入力やメモ、落書き機能の土台になります。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- お絵描きキャンバス -->
<div class="stage">
  <canvas id="paintCanvas"></canvas>
  <div class="toolbar">
    <div class="swatches" id="swatches"></div>
    <label class="size">
      太さ
      <input type="range" id="brushSize" min="1" max="40" value="6">
    </label>
    <button type="button" id="clearBtn">クリア</button>
  </div>
</div>

【CSS】
/* お絵描きキャンバス */
:root { --ink: #2b2f3a; }
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  /* うっすら方眼の用紙風背景 */
  background:
    linear-gradient(#eef1f7 0 0) padding-box,
    repeating-linear-gradient(0deg, #e3e7f0 0 1px, transparent 1px 24px),
    repeating-linear-gradient(90deg, #e3e7f0 0 1px, transparent 1px 24px),
    #f7f9fc;
}
#paintCanvas {
  display: block;
  width: 100%;
  height: 100%;
  cursor: crosshair;
  touch-action: none; /* タッチでスクロールさせない */
}
/* 下部のツールバー */
.toolbar {
  position: absolute;
  left: 50%;
  bottom: 12px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 8px 14px;
  background: rgba(255, 255, 255, .85);
  border: 1px solid rgba(0, 0, 0, .08);
  border-radius: 14px;
  box-shadow: 0 6px 20px rgba(30, 40, 70, .12);
  backdrop-filter: blur(6px);
}
.swatches { display: flex; gap: 6px; }
.swatches button {
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .15);
  cursor: pointer;
  padding: 0;
  transition: transform .12s;
}
.swatches button:hover { transform: scale(1.15); }
.swatches button.is-active {
  box-shadow: 0 0 0 2px var(--ink);
  transform: scale(1.15);
}
.size {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--ink);
}
.size input { width: 80px; }
#clearBtn {
  appearance: none;
  border: 1px solid rgba(0, 0, 0, .12);
  background: #fff;
  color: var(--ink);
  font-size: 12px;
  padding: 5px 12px;
  border-radius: 8px;
  cursor: pointer;
}
#clearBtn:hover { background: #f0f2f7; }
#clearBtn:active { transform: scale(.96); }

【JavaScript】
// お絵描きキャンバスデモ(色・太さ変更/クリア対応)
(() => {
  const canvas = document.getElementById('paintCanvas');
  const swatchBox = document.getElementById('swatches');
  const sizeInput = document.getElementById('brushSize');
  const clearBtn = document.getElementById('clearBtn');
  if (!canvas || !swatchBox || !sizeInput || !clearBtn) return; // null安全
  const ctx = canvas.getContext('2d');

  const colors = ['#2b2f3a', '#e23b5a', '#3b82f6', '#22c55e', '#f59e0b', '#a855f7'];
  let color = colors[0];
  let drawing = false;
  let last = null;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);

  // リサイズ時は内容を退避してCSSサイズに合わせて復元(消えない/伸び縮みも追従)
  function resize() {
    const r = canvas.getBoundingClientRect();
    let snapshot = null;
    if (canvas.width && canvas.height) {
      snapshot = document.createElement('canvas');
      snapshot.width = canvas.width;
      snapshot.height = canvas.height;
      snapshot.getContext('2d').drawImage(canvas, 0, 0);
    }
    canvas.width = r.width * dpr;
    canvas.height = r.height * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    if (snapshot) ctx.drawImage(snapshot, 0, 0, r.width, r.height);
  }
  resize();
  window.addEventListener('resize', resize);

  // 色見本ボタンを生成
  colors.forEach((c, i) => {
    const b = document.createElement('button');
    b.type = 'button';
    b.style.background = c;
    b.setAttribute('aria-label', `色 ${i + 1}`);
    if (i === 0) b.classList.add('is-active');
    b.addEventListener('click', () => {
      color = c;
      swatchBox.querySelectorAll('button').forEach((x) => x.classList.toggle('is-active', x === b));
    });
    swatchBox.appendChild(b);
  });

  function pos(e) {
    const r = canvas.getBoundingClientRect();
    return { x: e.clientX - r.left, y: e.clientY - r.top };
  }

  function start(e) { drawing = true; last = pos(e); draw(e); }
  function draw(e) {
    if (!drawing) return;
    const p = pos(e);
    ctx.strokeStyle = color;
    ctx.lineWidth = Number(sizeInput.value);
    ctx.beginPath();
    ctx.moveTo(last.x, last.y);
    ctx.lineTo(p.x, p.y);
    ctx.stroke();
    last = p;
  }
  function end() { drawing = false; last = null; }

  canvas.addEventListener('pointerdown', start);
  canvas.addEventListener('pointermove', draw);
  window.addEventListener('pointerup', end);
  canvas.addEventListener('pointerleave', end);

  clearBtn.addEventListener('click', () => {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
  });
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。