お絵描きキャンバス
ポインタ操作で線を描き、色パレットと太さスライダー、クリア機能を備えた簡易ペイント。署名入力やメモ、落書き機能の土台になります。
ライブデモ
使用例(お題: カフェ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。