App InventorからJavaScriptへどうやって移行するのか?:2025年8月21日現在

考えたことメモ > App InventorからJavaScriptへどうやって移行するのか?

Google Gemini 2.5 Pro Deep Researchによれば、「一度App Inventorで作ったアプリをPythonで再現してみるというアプローチは、目的が明確なため非常に効果的な学習方法です」とのことでした。これは、AI時代における実践的なプログラミング学習の一例といえます。

Pythonは文法が比較的シンプルで、初心者にも扱いやすい言語です。AI開発、データ分析、Webサイト制作、作業の自動化など、幅広い分野に応用できる点が特徴です。

一方、JavaScriptPythonにならんで非常にポピュラーなプログラミング言語で、ウェブページで使われるのでウェブサイトを作るには必須と言えるプログラミング言語です。node.jsを使えばサーバーサイドの開発もできるので、ウェブアプリの開発にはこれ一つ知っていれば十分です。

App InventorからPythonへどうやって移行するのか?」でChatGPT-4oがApp Inventorのaiaファイルを直接読み込み、アプリの要件定義書を書いてそのまま動作するPythonコードを書けました。

ここではChatGPT-5を使ってApp Inventorのaiaファイルを直接読み込み、アプリの要件定義書を書いてJavaScriptコードを書いてみます。

出発点のApp Inventorアプリは「オンデマンドアカデミー」で紹介されている カラフルお絵かきアプリです。このアプリは、基本的なUIの構成やイベント処理の理解に適しています。

aiaファイルの読み込みと解析

要件定義書作成

JavaScriptプログラミング

動作確認

サマリー

ChatGPT-5はApp Inventorのaiaファイルを直接読み込み、アプリの要件定義書を書いてそのまま動作するJavaScriptコードを書けました。

aiaファイルの読み込みと解析

プロンプト

あなたはApp Inventor 2の内部構造に精通したプロのソフトウェアエンジニアです。このaiaファイルを完全に解析してください。

返答

了解です。アップロードいただいた **DoodlePlus.aia** を、中身(.scm / .bky / project.properties)まで読み解いて、構造・ロジック・設定を“丸裸”にしてご報告します。以下は App Inventor 2 の内部表現(JSON/Blockly XML)をベースにした、技術者向けの完全解析です。

# プロジェクト概要

* アプリ名: **DoodlePlus**(画面タイトル: **お絵かきアプリ+**)
* スクリーン数: **1(Screen1)**
* 作成/保存タイムスタンプ: **Sat Apr 12 01:36:28 UTC 2025**
* バージョン: **versionCode=1 / versionName=1.0**
* レイアウト: **sizing=Responsive**, **ActionBar=True**, **Theme=AppTheme.Light.DarkActionBar**
* 既定の権限/設定: **useslocation=False**, **defaultfilescope=App**, **showlistsasjson=True**
* 配色(プロジェクト設定)

  * Primary: `&HFF3F51B5`
  * Primary Dark: `&HFF303F9F`
  * Accent: `&HFFFF4081`

# 画面とUI階層(Screen1)

Screen1 直下に下記 12 コンポーネントが存在します。

* **キャンバス1**(Canvas): `Width=-2`, `Height=-2` → いずれも **Fill Parent**
* **水平配置1**(HorizontalArrangement)
  ├─ ボタン **黒**(BackgroundColor `#000000`, FontSize 10)
  ├─ ボタン **青**(`#0000FF`, FontSize 10)
  ├─ ボタン **グリーン**(`#00FF00`, FontSize 10)
  └─ ボタン **赤**(`#FF0000`, FontSize 10)
* **水平配置2**
  ├─ ボタン **細**(Text=「細」)
  └─ ボタン **中**(Text=「中」)
* **水平配置3**
  ├─ ボタン **画面消去**(Text=「画面消去」)
  └─ ボタン **消しゴム**(Text=「消しゴム」)

> 補足: Canvas は全面に描画。3 つの水平配置がパレット(色/太さ/アクション)になっています。

# グローバル変数(global\_declaration)

* **選択マーク** : 文字列 `"✔︎"`
* **選択色ボタン** : Button 参照(初期値 **黒**)
* **色ボタンリスト** : `[黒, 青, グリーン, 赤]`
* **ペン太さ** : Button 参照(初期値 **細**)
* **太さボタンリスト** : `[細, 中]`
* **太さリスト** : `[3.0, 5.0]`  ← 太さボタンの位置と対応
* **消しゴム** : `false`(ブール)

# 手続き(procedures)

1. **色ボタンリセット**

   * `色ボタンリスト` を全走査し、各ボタンの **Text を空文字**にする(✔︎マーク消去)。

2. **色ボタン選択(ボタン)**

   * 以前の **選択色ボタン** の **TextColor を黒**に戻す。
   * 引数 **ボタン** の **TextColor を白**にし、\*\*Text に「✔︎」\*\*を表示。
   * **選択色ボタン ← ボタン** に更新。
   * **キャンバス1.PaintColor ← 選択色ボタン.BackgroundColor** に設定。
   * (実装全体として)色選択時には **消しゴム ← false** に戻す挙動が組まれています。

3. **太さボタンリセット**

   * `太さボタンリスト` を全走査し、各ボタンの **TextColor を黒**に戻す。

4. **太さボタン選択(ボタン)**

   * **ボタン.TextColor ← 赤**(選択強調)。
   * **ペン太さ ← ボタン** に更新。
   * `太さボタンリスト` 内での **ペン太さ** の位置に基づき、`太さリスト` から対応値を取得し、
     **キャンバス1.LineWidth** に反映(`細→3.0px`, `中→5.0px`)。

5. **消しゴムボタン選択**

   * **消しゴム** をトグル。

     * ON 時: **キャンバス1.PaintColor ← 白**。
     * OFF 時: **キャンバス1.PaintColor ← 選択色ボタン.BackgroundColor** に戻す。

# イベント駆動フロー(重要)

* **Screen1.Initialize**

  * `色ボタン選択(選択色ボタン)` を最初に呼び(既定は **黒**)、
  * `太さボタン選択(ペン太さ)` を次に呼ぶ(既定は **細**)。

* **任意の Button.Click(Any Button)**
  クリックされた **component** に応じて分岐:

  1. `色ボタンリスト` に含まれる →
     **色ボタンリセット** → **消しゴム ← false** → **色ボタン選択(component)**
  2. `太さボタンリスト` に含まれる →
     **太さボタンリセット** → **太さボタン選択(component)**
  3. **画面消去** → **キャンバス1.Clear()**
  4. **消しゴム** → **消しゴムボタン選択()**

* **キャンバス1.Dragged(prevX, prevY, currentX, currentY, ...)**

  * **キャンバス1.DrawLine(prevX, prevY, currentX, currentY)**
    (線の色=`PaintColor`、太さ=`LineWidth` はボタン操作で設定済み)

# ブロック統計(Screen1.bky)

* 総ブロック数: **126**
* 代表的なブロック種別(上位)

  * `lexical_variable_get` 34 / `component_component_block` 14 / `component_set_get` 14
  * `global_declaration` 7 / `procedures_callnoreturn` 7 / `procedures_defnoreturn` 5
  * 制御系 `controls_if` 4 / `controls_forEach` 2
  * 色定数: `color_black` 3, `color_white` 2, `color_red` 2 など
* コンポーネントメソッド呼び出し

  * **Canvas.DrawLine**(ドラッグ時の描画)
  * **Canvas.Clear**(「画面消去」)

# 擬似コード(挙動イメージ)

```text
On Initialize:
  色ボタン選択(選択色ボタン=黒)
  太さボタン選択(ペン太さ=細)

Any Button.Click(component):
  if component ∈ 色ボタンリスト:
    色ボタンリセット()
    消しゴム := false
    色ボタン選択(component)          // ✔︎を付けて PaintColor を更新
  else if component ∈ 太さボタンリスト:
    太さボタンリセット()
    太さボタン選択(component)        // TextColor=赤, LineWidth を更新
  else if component == 画面消去:
    Canvas.Clear()
  else if component == 消しゴム:
    消しゴムボタン選択()             // 白/元色 へ PaintColor 切替

Canvas.Dragged(prevX, prevY, x, y):
  Canvas.DrawLine(prevX, prevY, x, y)
```

# 仕様上のポイント/注意

* **色選択と消しゴムは排他**:色ボタンを押すと必ず **消しゴム=false** に戻り、色の BackgroundColor が `PaintColor` に復帰します。
* **太さは 2 段階**(3px / 5px)。必要に応じて `太さボタンリスト` と `太さリスト` に要素を追加すれば拡張容易。
* **選択状態の可視化**:

  * 色…✔︎(Text)と TextColor(白/黒)で表現
  * 太さ…Button の TextColor(赤)で表現
* **永続化や共有は未実装**:描画の保存(画像出力/共有)、Undo/Redo、ズームなどはなし。

# 改善提案(実装のしやすさ順)

1. **太さ段階の拡張**:`太さボタンリスト` と `太さリスト` に「太」「極太」などを追加。
2. **Undo/Redo**:ドラッグで引いた線分の履歴(座標+色+太さ)をスタックに積む設計。
3. **保存/共有**:`Canvas.Save`(拡張/外部コンポーネント)や `ActivityStarter` 連携で画像保存・共有。
4. **筆圧/スムージング**:ドラッグ点列の間引きや補間、丸めを入れて滑らかなストロークに。
5. **UI アクセシビリティ**:選択強調をテキスト色だけに依存せず、アウトラインやアイコン化で視認性UP。
6. **消しゴムの太さ別管理**:消しゴム使用中の LineWidth を色ペンとは別に持つと使い勝手向上。

# 解析アーカイブ(生データ)

必要であれば、内部ファイルを直接ご確認いただけます:

* [Screen1.scm(JSON 抜粋テキスト)](sandbox:/mnt/data/Screen1_scm.txt)
* Blockly XML(抜粋)

  * [Screen1.Initialize イベント](sandbox:/mnt/data/event_init.xml)
  * [Any Button.Click イベント](sandbox:/mnt/data/event_any_button_click.xml)
  * [手続き: 色ボタン選択](sandbox:/mnt/data/proc_color_select.xml)
  * [手続き: 太さボタン選択](sandbox:/mnt/data/proc_thick_select.xml)
  * [手続き: 消しゴムボタン選択](sandbox:/mnt/data/proc_erasure.xml)
* コンポーネント一覧(CSV): [components.csv](sandbox:/mnt/data/components.csv)

---

さらに深掘り(たとえば「Undo をこう設計して」「太さを 4 段階にしてブロックを出して」等)も、そのままこちらで具体ブロックをご提案できます。どこから手を付けるか、希望があれば教えてください!

要件定義書作成

プロンプト

このアプリの要件定義書をMarkdownで書いて

返答

了解しました。先ほど解析した **DoodlePlus.aia** の内容を基に、ソフトウェアエンジニアが利用できる形式の **要件定義書(Markdown)** をまとめました。

---

# 要件定義書

## プロジェクト概要

* **アプリ名**: DoodlePlus
* **概要**: スマートフォンやタブレット上で指先を用いて自由にお絵かきできるアプリ。色や線の太さを変更でき、消しゴムや画面消去機能を備える。
* **対象プラットフォーム**: Android(MIT App Inventor 2 ベース)
* **バージョン**: 1.0

---

## 目的

* 子供から大人まで誰でも直感的に絵を描ける「シンプルなお絵かきアプリ」を提供する。
* 学習や創作の補助ツールとして利用可能。

---

## 機能要件

### 描画機能

* 指のドラッグ操作により線を描画できる。
* 線の色と太さをユーザーが選択可能。

### 色選択

* 黒、青、緑、赤の 4 色から選択可能。
* 選択中の色は ✔︎ マークで可視化される。
* 色ボタンを選択した際、消しゴムモードは解除される。

### 太さ選択

* 細(3px)、中(5px)の 2 段階。
* 選択中の太さは赤文字で表示される。

### 消しゴム機能

* ON/OFF 切り替え可能。
* ON 時はキャンバスの背景色(白)で描画し、既存の線を消せる。
* OFF 時は直前に選択した色へ戻る。

### 画面消去

* ワンタップでキャンバス全体をクリアする。

---

## 非機能要件

* **UI**: シンプルで直感的。文字よりも色やアイコンを重視。
* **操作性**: 子供でも使える直感的な操作。
* **パフォーマンス**: 描画時に遅延が発生しないこと。
* **可搬性**: Android スマホ/タブレット両対応。

---

## UI設計

### 画面構成

1. **キャンバス領域**

   * 画面全体に広がる描画スペース。

2. **色選択バー(水平配置1)**

   * 黒、青、緑、赤のボタン。

3. **太さ選択バー(水平配置2)**

   * 細、中のボタン。

4. **操作バー(水平配置3)**

   * 「画面消去」ボタン。
   * 「消しゴム」ボタン。

---

## データ要件

* 永続化なし(保存機能なし)。
* アプリ起動ごとにキャンバスは初期化される。

---

## 制約条件

* MIT App Inventor 2 による開発。
* デバイス依存: Android 4.4 以上を想定。

---

## 今後の拡張要件(任意)

* 線の太さ段階の追加(太、極太)。
* 描画内容の保存(端末ストレージ/共有)。
* Undo/Redo 機能。
* カラーパレットによる色追加。

---

---

この文書は「現状の DoodlePlus.aia の実装仕様」=「要件定義書」としてまとめています。

👉 ご希望があれば、このまま **設計書(UIフローや処理シーケンス図付き)** に発展させることも可能です。やりますか?

JavaScriptプログラミング

プロンプト

この要件定義書に基づいてJavaScriptのコードを書いて。初心者にも何をしているかわかるように、また、App Inventorアプリとの対応関係がわかるように、詳しいコメントをつけて。

返答

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>DoodlePlus (JavaScript 版)</title>
  <style>
    /* =============================
       シンプルな見た目のためのCSS
       ============================= */
    :root {
      --primary: #3f51b5; /* App Inventor プロジェクト設定のPrimary色に合わせた雰囲気 */
      --accent: #ff4081;
      --bg: #ffffff;
      --text: #222;
      --muted: #888;
    }
    * { box-sizing: border-box; }
    html, body { height: 100%; margin: 0; background: var(--bg); color: var(--text); font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans JP", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif; }
    main { display: grid; grid-template-rows: auto 1fr; height: 100%; }
    header { padding: .5rem .75rem; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: .75rem; }
    header h1 { font-size: 1rem; font-weight: 600; margin: 0; color: var(--primary); }

    /* ツールバー(App Inventorの「水平配置」に相当) */
    #toolbar { display: flex; gap: .75rem; padding: .5rem; border-bottom: 1px solid #eee; flex-wrap: wrap; }
    .group { display: flex; gap: .5rem; align-items: center; }
    .group-label { font-size: .85rem; color: var(--muted); }

    /* ボタン(App InventorのButtonに相当) */
    button { appearance: none; border: 1px solid #ddd; background: #fff; color: #222; padding: .4rem .6rem; border-radius: .5rem; cursor: pointer; font-size: .95rem; }
    button:focus { outline: 2px solid #d0d8ff; outline-offset: 2px; }
    button.selected { border-color: var(--primary); box-shadow: 0 0 0 2px rgba(63,81,181,0.12) inset; }

    /* 色ボタンは背景色で色を示す。選択状態はチェックマークで表現(App InventorのTextに"✔︎"を出すのに対応) */
    .color-btn { width: 44px; height: 32px; border-radius: .5rem; position: relative; }
    .color-btn .check { position: absolute; right: 6px; top: 4px; font-size: 16px; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.35); display: none; }
    .color-btn.selected .check { display: block; }

    /* 太さボタンは選択中を赤文字(App InventorのTextColor=赤)で表す */
    .thick-btn.selected { color: #d32f2f; font-weight: 700; }

    /* アクションボタン */
    #eraser.selected { background: #fafafa; border-color: #bbb; }
    #clear { border-color: #ffe0e0; }

    /* キャンバス(App InventorのCanvasに相当) */
    #stage-wrap { position: relative; }
    #stage { width: 100%; height: 100%; display: block; background: #fff; touch-action: none; /* ジェスチャーでのスクロール/ズームを抑制 */ }

    /* 小さなヘルプ */
    footer { padding: .35rem .75rem; font-size: .8rem; color: var(--muted); border-top: 1px solid #eee; }
  </style>
</head>
<body>
  <main>
    <!-- App Inventor: Screen1.Title 相当 -->
    <header>
      <h1>お絵かきアプリ+(DoodlePlus / JS版)</h1>
      <span style="font-size:.85rem;color:#888">App Inventorのブロック構成をJavaScript実装に写経</span>
    </header>

    <!-- App Inventor: 水平配置1/2/3 をまとめたツールバー -->
    <div id="toolbar" role="toolbar" aria-label="Drawing toolbar">
      <!-- 色ボタン群(水平配置1) -->
      <div class="group" id="colors" aria-label="colors">
        <span class="group-label">色:</span>
        <button class="color-btn" id="btn-black"  data-color="#000000" title="黒" aria-pressed="true" style="background:#000000"><span class="check">✔︎</span></button>
        <button class="color-btn" id="btn-blue"   data-color="#0000ff" title="青" style="background:#0000ff"><span class="check">✔︎</span></button>
        <button class="color-btn" id="btn-green"  data-color="#00ff00" title="グリーン" style="background:#00ff00"><span class="check">✔︎</span></button>
        <button class="color-btn" id="btn-red"    data-color="#ff0000" title="赤" style="background:#ff0000"><span class="check">✔︎</span></button>
      </div>

      <!-- 太さボタン群(水平配置2) -->
      <div class="group" id="thickness" aria-label="thickness">
        <span class="group-label">太さ:</span>
        <button class="thick-btn" id="btn-thin"   data-width="3">細</button>
        <button class="thick-btn" id="btn-medium" data-width="5">中</button>
      </div>

      <!-- アクション群(水平配置3) -->
      <div class="group" id="actions" aria-label="actions">
        <button id="clear" title="画面消去 (Canvas.Clear)">画面消去</button>
        <button id="eraser" title="消しゴム (白で描く)">消しゴム</button>
      </div>
    </div>

    <!-- キャンバス領域(Canvas) -->
    <div id="stage-wrap">
      <canvas id="stage"></canvas>
    </div>

    <footer>
      ヒント: ドラッグして線を描画。色を押すと消しゴムは解除されます(App Inventorの仕様を踏襲)。
    </footer>
  </main>

  <script>
    // =============================================================
    //  App Inventorの状態と手続きをJavaScriptで表現
    // =============================================================
    // ▼App Inventorでのグローバル変数に相当
    //   - 選択色ボタン(Button参照)
    //   - 色ボタンリスト
    //   - ペン太さ(Button参照)
    //   - 太さボタンリスト
    //   - 太さリスト [3.0, 5.0]
    //   - 消しゴム(boolean)

    const colorButtons = [
      document.getElementById('btn-black'),
      document.getElementById('btn-blue'),
      document.getElementById('btn-green'),
      document.getElementById('btn-red')
    ];
    let selectedColorButton = colorButtons[0]; // 既定は「黒」

    const thicknessButtons = [
      document.getElementById('btn-thin'),
      document.getElementById('btn-medium')
    ];
    const thicknessList = [3, 5]; // App Inventor: 太さリスト [3.0, 5.0]
    let selectedThicknessButton = thicknessButtons[0]; // 既定は「細」

    let eraser = false; // 消しゴムモードかどうか

    // ▼Canvas関連
    const canvas = document.getElementById('stage');
    const ctx = canvas.getContext('2d');

    // 高DPIでもクッキリ描けるように、CSSサイズと内部ピクセルを分けて設定
    function resizeCanvasToDisplaySize() {
      const dpr = window.devicePixelRatio || 1;
      const rect = canvas.getBoundingClientRect();
      const width = Math.floor(rect.width * dpr);
      const height = Math.floor(rect.height * dpr);
      if (canvas.width !== width || canvas.height !== height) {
        // 既存の描画は消えるので、必要ならイメージを保持してからリサイズしてください。
        const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
        canvas.width = width;
        canvas.height = height;
        ctx.putImageData(img, 0, 0); // 単純復元(拡大縮小はしない)
        ctx.scale(dpr, dpr); // 以降の描画はCSSピクセル基準
      }
      // 背景が透明になっている場合、白で塗りつぶしてApp Inventorの見た目に合せる
      // (初回のみ塗る・クリア時は別処理)
    }

    // レイアウト: ツールバーを除いた残りの領域いっぱいにキャンバスを広げる
    function layout() {
      const headerH = document.querySelector('header').offsetHeight;
      const toolbarH = document.getElementById('toolbar').offsetHeight;
      const footerH = document.querySelector('footer').offsetHeight;
      const wrap = document.getElementById('stage-wrap');
      wrap.style.height = `calc(100vh - ${headerH + toolbarH + footerH}px)`;
      resizeCanvasToDisplaySize();
    }

    window.addEventListener('resize', layout);

    // =============================================================
    //  App Inventor: 手続き(procedures)の移植
    // =============================================================
    // 手続き: 色ボタンリセット(全ボタンから✔︎を外す)
    function resetColorButtons() {
      colorButtons.forEach(btn => btn.classList.remove('selected'));
      colorButtons.forEach(btn => btn.setAttribute('aria-pressed', 'false'));
    }

    // 手続き: 色ボタン選択(ボタン)
    function selectColorButton(btn) {
      // App Inventor: 選択色ボタン.TextColor=白 相当 → ここではUI強調として✔︎表示
      btn.classList.add('selected');
      btn.setAttribute('aria-pressed', 'true');
      selectedColorButton = btn;

      // キャンバスの線色(PaintColor)を更新
      updateStrokeStyle();
    }

    // 手続き: 太さボタンリセット(全ボタンのTextColor=黒 相当)
    function resetThicknessButtons() {
      thicknessButtons.forEach(btn => btn.classList.remove('selected'));
    }

    // 手続き: 太さボタン選択(ボタン)
    function selectThicknessButton(btn) {
      btn.classList.add('selected'); // App Inventor: TextColor=赤 相当
      selectedThicknessButton = btn;
      // LineWidthを更新
      updateLineWidth();
    }

    // 手続き: 消しゴムボタン選択(トグル)
    function toggleEraser() {
      eraser = !eraser;
      document.getElementById('eraser').classList.toggle('selected', eraser);
      updateStrokeStyle(); // PaintColorを白 or 選択色へ
    }

    // 付帯: 線色を状態に合わせて更新
    function updateStrokeStyle() {
      if (eraser) {
        ctx.strokeStyle = '#ffffff'; // App Inventor: 白で上書き
      } else {
        ctx.strokeStyle = selectedColorButton.dataset.color;
      }
    }

    // 付帯: 線の太さを状態に合わせて更新
    function updateLineWidth() {
      const w = Number(selectedThicknessButton.dataset.width) || thicknessList[0];
      ctx.lineWidth = w;
      ctx.lineCap = 'round';
      ctx.lineJoin = 'round';
    }

    // 付帯: 画面消去(Canvas.Clear)
    function clearCanvas() {
      // 背景を白で塗りつぶし、App InventorのCanvas.Clearに近い見た目に
      ctx.save();
      ctx.setTransform(1,0,0,1,0,0); // 変換を一時的にリセット
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      // 白背景
      ctx.fillStyle = '#ffffff';
      const rect = canvas.getBoundingClientRect();
      ctx.fillRect(0, 0, rect.width, rect.height);
      ctx.restore();
    }

    // =============================================================
    //  App Inventor: Screen1.Initialize 相当(初期化)
    // =============================================================
    function initialize() {
      layout();
      // 既定の選択状態をApp Inventorと同じにする
      resetColorButtons();
      selectColorButton(selectedColorButton); // 黒

      resetThicknessButtons();
      selectThicknessButton(selectedThicknessButton); // 細(3px)

      // 背景を白で初期化
      clearCanvas();
    }

    // =============================================================
    //  App Inventor: Any Button.Click 相当のイベント分岐
    // =============================================================
    function onButtonClick(e) {
      const el = e.target.closest('button');
      if (!el) return;

      if (colorButtons.includes(el)) {
        // 色ボタン: 色ボタンリセット → 消しゴム=false → 色ボタン選択
        resetColorButtons();
        eraser = false;
        document.getElementById('eraser').classList.remove('selected');
        selectColorButton(el);
        return;
      }

      if (thicknessButtons.includes(el)) {
        // 太さボタン: 太さボタンリセット → 太さボタン選択
        resetThicknessButtons();
        selectThicknessButton(el);
        return;
      }

      if (el.id === 'clear') {
        clearCanvas();
        return;
      }

      if (el.id === 'eraser') {
        toggleEraser();
        return;
      }
    }

    document.getElementById('toolbar').addEventListener('click', onButtonClick);

    // =============================================================
    //  App Inventor: Canvas.Dragged(prevX, prevY, currentX, currentY)
    //  → JSではPointer Eventsで実装
    // =============================================================
    let drawing = false;
    let last = { x: 0, y: 0 };

    function toCanvasCoords(evt) {
      const rect = canvas.getBoundingClientRect();
      const x = (evt.clientX - rect.left);
      const y = (evt.clientY - rect.top);
      return { x, y };
    }

    function pointerDown(evt) {
      drawing = true;
      last = toCanvasCoords(evt);
    }

    function pointerMove(evt) {
      if (!drawing) return;
      const p = toCanvasCoords(evt);
      // Canvas.DrawLine(prevX, prevY, x, y) 相当
      ctx.beginPath();
      ctx.moveTo(last.x, last.y);
      ctx.lineTo(p.x, p.y);
      ctx.stroke();
      last = p;
    }

    function pointerUp() {
      drawing = false;
    }

    canvas.addEventListener('pointerdown', pointerDown);
    canvas.addEventListener('pointermove', pointerMove);
    window.addEventListener('pointerup', pointerUp);

    // スクロールなどで外に出た場合の保険
    canvas.addEventListener('pointerleave', pointerUp);

    // =============================================================
    // 起動
    // =============================================================
    initialize();
  </script>
</body>
</html>
できました!JS版の実装をキャンバスに置きました。

App Inventorの Screen1 / Canvas / 水平配置 / Button / 手続き を、それぞれ HTML要素 / <canvas> / ツールバー群 / <button> / JS関数 に対応づけ、ブロック名どおりの処理と詳しい日本語コメントを入れています。

そのままブラウザで動きます。色ボタンで✔︎が出て、太さは選択で赤表示、消しゴムは白で上書き、画面消去は白塗りクリアです。

他に欲しい拡張(保存・Undo/Redo・太さ追加・色数拡張・モバイル最適化など)があれば、この実装をベースにすぐに組み込みます。

動作確認

DoodlePlus.htmlに保存しGoogle Chromeで実行してみました。言葉が少し変なところもありますが、動作自体は以下のように問題無いです。

公開済みAI関連アプリチュートリアル:シルバー会員特典

ゴールド会員特典はAIが中心

初心者にはこちらも