AI Dev Lab
DoodleDrop

DoodleDrop ができるまで — シミュレーター 2本目を お絵描き物理 で開く

計算ツール 4本目 (side-tax) に続く ラボ 28本目で シミュレーター thesis 2本目 (pile-park に続く)。 matter.js + Canvas + 凸包 (Andrew's monotone chain) でストロークを polygon body 化、 黒板 motif の visual を組んだ設計記録。

·decision改善·stage公開中

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 をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — 文章embeddingsで意味検索をブラウザ内に

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →