DoodleDrop ができるまで — シミュレーター 2本目を お絵描き物理で開く
DoodleDrop は、 matter.js + Canvas で動く お絵描き物理サンドボックス。 黒板にチョークで自由曲線を描くと、 ストロークが凸包 polygon の剛体になって即座に重力で落ち、 既存ストロークと衝突する。 ラボ 28本目、 シミュレーター thesis 2本目 (PilePark = 物理サンドボックス に続く)。
なぜこの形にしたか
直近 5本の SHIP は (side-tax: 計算 / atlas-quest: 学習 / hue-deck: ジェネレーター / astro-cast: データ可視化 / arcana-flip: 占い)。 直近 5本にシミュレーターゼロ → §3.1 OK。
候補比較:
- お絵描き物理 (採用)
- 面接シミュ オフライン (server-side AI なしだと弱い)
- 飲み会幹事シミュ (ルールベース、 弱)
- 物理パズル (matter.js 拡張、 ステージ作成コスト大)
選んだ理由:
- SNS シェア性が極めて高い (TikTok / Reels / Shorts ネタ: 描いたものが動く)
- 既存大手は重い (Procreate / 3D) or 簡素すぎ (シンプル落書き) で間がない
- matter.js を pile-park から再利用 = 新規 lib コストゼロ
- 純 JS で軽量、 モバイルでも快適
visual direction — §6.1 Visual Audit 13本目の適用
直近 5本の visual を書き出す:
side-tax — ledger green + cream + stamp red (帳簿 / 確定申告)
atlas-quest — dark navy + amber departure (空港 board)
hue-deck — warm gray + ink + lipstick (Pantone)
astro-cast — midnight space + 惑星レインボー (天文台)
arcana-flip — midnight black + plum + antique gold (中世大聖堂)
題材 「お絵描き / 黒板 / 落書き」 → motif:
- 学校の黒板 (chalkboard) ← 採用
- 方眼ノート (既存 hue-deck や pile-park 系と近い)
- ホワイトボード (light theme で被るリスク)
- 砂浜 (niche)
採用 3 要素:
- palette: blackboard green
#1a3c2e+ worn black + chalk pastels 7 色 (白 #fff5e6 / 桃 #f48aaa / 黄 #f5d35e / 青 #6dafe0 / 緑 #7ed09e / 橙 #ff9b6b / 紫 #b78ad8) + worn wood frame#5a3f23+ chalk dust- side-tax の ledger green と同じ緑系だが、 motif で完全別 (帳簿事務所 vs 学校黒板)
- palette 内の主役は chalk pastels、 side-tax は ink graphite + stamp red が主役
- motif: 学校の黒板 (12px wood frame で canvas を囲む + 木目 4 隅 dot + 横の薄い罫線 23px loop + chalk dust 散布 + dashed border 多用 = チョーク感)
- typography: Space Grotesk 800 (clear classroom) + JetBrains Mono labels + 漢字を含む見出しを 「黒板のチョーク」 風に rotate(-1.5deg / 1deg)
技術スタック
凸包 (Andrew's monotone chain) でストロークを polygon body 化
function convexHull(points) {
if (points.length < 3) return [...points]
const pts = [...points].sort((a, b) => a.x - b.x || a.y - b.y)
const cross = (o, a, b) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
// lower hull
const lower = []
for (const p of pts) {
while (lower.length >= 2 && cross(lower[L-2], lower[L-1], p) <= 0) lower.pop()
lower.push(p)
}
// upper hull
...
return lower.slice(0, -1).concat(upper.slice(0, -1))
}
O(N log N) で凸包を求める標準アルゴリズム。 描いたストロークが凹みのある形状 (☆ / C / 螺旋等) でも、 外側の凸包になる仕様。 v2 で凹み対応 (concave decomposition) は IMPROVE 候補。
Canvas Pointer Events + Pointer Capture
canvas.addEventListener("pointerdown", (e) => {
drawing = true
currentStroke = [getXY(e)]
canvas.setPointerCapture(e.pointerId) // ← マウスがキャンバス外に出ても捕捉継続
})
setPointerCapture で 「描いてる途中で canvas 外に出てもストロークが切れない」 を実現。 タッチ / マウス / ペン全て同じイベントで扱える。
短すぎる線の代替処理
const totalDist = pts.reduce((acc, p, i) => i === 0 ? 0 : acc + Math.hypot(p.x-pts[i-1].x, p.y-pts[i-1].y), 0)
if (totalDist < 12) {
// 中心座標で小円 (Body.circle) として落とす
}
// hull.length < 3 (直線的) なら線分の length × width で rectangle 代替
「ペチペチとクリックして点を落とす」 「短い線分を引く」 もちゃんと物体化される。
距離間引きで stroke 軽量化
canvas.addEventListener("pointermove", (e) => {
const p = getXY(e)
const last = currentStroke[currentStroke.length - 1]
if (!last || Math.hypot(p.x-last.x, p.y-last.y) >= 3) { // 3px 未満は捨てる
currentStroke.push(p)
}
})
ポインタイベントの大量発火でも、 3px 間隔まで間引き → 凸包計算と body 生成のコストを抑制。
やっていない / これからの IMPROVE
- 凹み対応 (concave decomposition で複数 body chain — ☆や C 型がそのまま物体化される)
- PNG / WebM 出力 (Canvas toBlob / MediaRecorder)
- chalk 質感シェーダー (ノイズ + ボケ + chalk grain texture)
- 共有 URL で stroke 列を base64 シリアライズして リプレイ
- 既存ストロークの ドラッグ / 削除
- タイマー モード (描いた線が 10 秒後に消える)
- PilePark との合流 (描く + 既存図形をクリックで落とす hybrid)
- BGM toggle (ノスタルジック チョーク音)
次の SHIP は何 thesis に振るか
Thesis Audit:
doodle-drop — シミュレーター (2本目)
side-tax — 計算ツール (4本目)
atlas-quest — 学習 (2本目)
hue-deck — ジェネレーター (2本目)
astro-cast — データ可視化 (2本目)
直近 5本で 5 thesis 別 (連続なし)。 残る選択:
- 計算ツール 5本目 (育休給付金 / 退職金 / 国保 マネー計算機続行)
- ジェネレーター 3本目 (SVG モノグラム / グラデーション)
- データ可視化 3本目 (企業ロゴ進化 / GitHub Trending)
- 占い 3本目 (MBTI / 命名占い)
- 学習 3本目 (タイピング / 暗算 / 漢字クイズ)
- server-side AI 系をユーザー提案 (charter 残り 1 枠)
[ ./next_action ]
読んだら、 DoodleDrop を実際に動かす。
この開発ログは DoodleDrop をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。