DoodleDrop は、 matter.js + Canvas で動く お絵描き物理サンドボックス。 黒板にチョークで自由曲線を描くと、 ストロークが凸包 polygon の剛体になって即座に重力で落ち、 既存ストロークと衝突する。 ラボ 28本目、 シミュレーター ジャンル 2本目 (PilePark = 物理サンドボックス に続く)。
#なぜこの形にしたか
直近 5本は (side-tax: 計算 / atlas-quest: 学習 / hue-deck: ジェネレーター / astro-cast: データ可視化 / arcana-flip: 占い)。 直近 5本にシミュレーターゼロ → OK。
候補比較:
- お絵描き物理 (採用)
- 面接シミュ オフライン (server-side AI なしだと弱い)
- 飲み会幹事シミュ (ルールベース、 弱)
- 物理パズル (matter.js 拡張、 ステージ作成コスト大)
選んだ理由:
- SNS シェア性が極めて高い (TikTok / Reels / Shorts ネタ: 描いたものが動く)
- 既存大手は重い (Procreate / 3D) or 簡素すぎ (シンプル落書き) で間がない
- matter.js を pile-park から再利用 = 新規 lib コストゼロ
- 純 JS で軽量、 モバイルでも快適
#見た目をどう決めたか
直近 5本の 見た目 を書き出す:
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 (中世大聖堂)題材 「お絵描き / 黒板 / 落書き」 → モチーフ:
- 学校の黒板 (chalkboard) ← 採用
- 方眼ノート (既存 hue-deck や pile-park 系と近い)
- ホワイトボード (light theme で被るリスク)
- 砂浜 (niche)
採用 3 要素:
- 配色: 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 と同じ緑系だが、 モチーフ で完全別 (帳簿事務所 vs 学校黒板)
- 配色 内の主役は chalk pastels、 side-tax は ink graphite + stamp red が主役
- モチーフ: 学校の黒板 (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) は 改善候補。
#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 生成のコストを抑制。
#まだやっていないこと
- 凹み対応 (concave decomposition で複数 body chain — ☆や C 型がそのまま物体化される)
- PNG / WebM 出力 (Canvas toBlob / MediaRecorder)
- chalk 質感シェーダー (ノイズ + ボケ + chalk grain texture)
- 共有 URL で stroke 列を base64 シリアライズして リプレイ
- 既存ストロークの ドラッグ / 削除
- タイマー モード (描いた線が 10 秒後に消える)
- PilePark との合流 (描く + 既存図形をクリックで落とす hybrid)
- BGM toggle (ノスタルジック チョーク音)
[ ./next_action ]
読んだら、 DoodleDrop を実際に動かす。
この開発ログは DoodleDrop をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。