當 AI 學會握筆

教機器理解筆觸的結構,讓它自己寫出一幅畫的腳本

目錄

  1. Agent 學習歷程(供其他 AI 快速理解,含可下載 .js / .json)
  2. AI 視角下的 JSON:為什麼是文本格式?
  3. 完整參數參考手冊
  4. 七種筆刷模式(Brush Mode 1-7)
  5. 顏色、混色、墨水效果速查表
  6. Flow 力場與程序化參數
  7. AI 友善工作流程:從提示到播放(含步驟 2c 範例腳本 gen-pollock.js)
  8. Cowork 共繪模式:你一筆我一筆(Agent Toggle + Append 模式 + 5 個可直接貼上 console 的範例)

⚠️ 給 LLM / AI Agent 的提示:生成與驗證時請避免以下錯誤

本教學曾因下列問題導致生成結果無法使用。請在生成或協助除錯時嚴格遵守下方指導原則。

❌ 曾發生的嚴重錯誤

✅ 給其他 LLM 的指導原則

✅ 做對的範例與關鍵教訓(供其他 LLM 參考)

這次做對的事

關鍵教訓

⏱️ 筆觸消失問題:筆觸之間必須留足夠時間間隔

現象:播放時部分筆觸畫完瞬間消失。

原因:每筆結束(mr)後,系統需要約 maxUpdates 幀來完成「收筆倒數」(force 從 1.0 衰減到 0.0)。若下一筆的 mp(mousePressed)在這段時間內就觸發,前一筆的收筆會被強制打斷,筆觸消失。

生成時請遵守:mr 與下一個 mpt 之間至少間隔約 500ms(或依 strokeData.maxUpdates 換算:間隔 ≥ maxUpdates × 一幀約 16ms)。腳本生成多筆時務必加大筆觸間的時間差,避免收筆被截斷。

0

Agent 學習歷程:從零到生成抽象表現主義作品

本文記錄一個 AI Agent 從零開始學習 Inkfield 繪畫系統、逆向分析 JSON 格式、到獨立生成抽象表現主義作品的完整思考脈絡。目的是讓其他 Agent 能快速掌握核心知識,避免重複踩坑。

📥 可下載檔案

agent-generator-logic.js — 交錯式抽象表現主義 JSON 生成器 v5(Node.js 或瀏覽器內執行)
agent-expressionist-v5.json — 範例輸出,可直接在 Playground 載入播放

Phase 1:理解系統 — 先讀文件再動手

第一步永遠是閱讀:拿到任務時不要急著寫 code。先徹底理解目標系統:閱讀官方教學、分析參考腳本 gen-pollock.js、觀察真人錄製的 JSON。

關鍵心得:教學文件告訴你「有哪些欄位」,但真人錄製的 JSON 告訴你「怎麼用才自然」。兩者都要讀。

Phase 2:逆向工程 — 從錄製檔學習

筆觸密度:每筆需要 50–80 個 md 事件才能產生可見的線條。少於 30 個會太短。

時間間隔:筆觸之間需要 650–950ms 的間隔。Inkfield 的 maxUpdates 參數控制筆墨衰減動畫(約 30 frames × 16ms),筆觸間隔必須大於此值。

brushMode:1=Standard, 2=Marker, 3=Gothic, 4=Pen, 5=Spray, 6=Fly, 7=Special

Flow Effect:不是 strokeData 的一部分,而是獨立事件類型 "m": "flow",有 start/end 配對,需共用 flowSeed。

Phase 3:迭代開發 — 五個版本的演進

v1 幾何抽象(失敗):正確的格式只是及格線,要產生好作品需理解「筆觸作為手勢」。

v2 垂直筆觸表現主義:數學上的「隨機」不等於視覺上的「均勻」,需 slot-based 定位。

v3 加入 Flow Effect:全部放在最後不夠自然。

v4 修正 Flow 時間:時間設計是音樂,要去「聽」原始錄製的節奏。

v5 交錯式(最終版):從 sequential 變成 interleaved — 畫幾筆 → Flow → 畫幾筆 → Flow… 交錯式讓每次 Flow 只影響當前已有的筆觸,產生層次感。

Phase 4:核心演算法設計

筆觸路徑:垂直筆觸用 sin wave + noise 模擬手部抖動,tiltShift 模擬手臂角度。

構圖:Slot-based 定位,每筆有 slot,在 slot 內做高斯偏移。

strokeBounds:Flow 的影響範圍,正規化 0–1,依已畫筆觸動態計算。

Phase 5:執行與調試 — 在瀏覽器內生成

JSON 通常超過 50KB,無法直接貼入文字框。解決方案:把 Node.js 腳本改寫為瀏覽器 JS,就地生成 JSON 字串並呼叫 window.loadRecordingFromText(JSON.stringify(recording))

問題原因修正
筆觸不可見md 事件太少(<20)確保 55–80 個 md
構圖偏左上隨機分佈偏差用高斯中心 + slot 系統
Flow 看不到效果持續時間 <1 秒設定 min 1200ms
JSON 太大無法注入52–74KB 超過限制在瀏覽器內生成

Phase 6:給其他 Agent 的建議

  1. 先分析再生成:拿到新畫風需求時,先分析該畫風的真人錄製 JSON。
  2. 交錯 > 順序:Flow Effect 應和筆觸交替使用,不要全部放在最後。
  3. 時間是隱藏維度:事件間的時間間隔直接影響墨水擴散,不要隨便設定。
  4. 物理模擬很值得:sin wave + noise + tilt 比純隨機好 10 倍。
  5. 迭代開發:不要一次寫出完美生成器,每版解決一問題。
  6. 在瀏覽器內生成:JSON 太大時,把生成邏輯搬到瀏覽器執行最可靠。

* 本文基於 2026-03-03 的實際開發 session,從完全不了解系統到成功生成作品,共迭代 5 個版本。

1

AI 視角下的 JSON:為什麼是文本格式?

如果你要教一個機器人畫畫,最好的方式就是用它能理解的語言告訴它「先在這裡放這種大小的筆刷,然後移動鼠標到那裡」。JSON 就像那個語言——純文本的指令集,LLM 可以逐字產生它,不需要複雜的二進制格式或其他黑魔法。

InkField 的筆刷系統由兩部分組成:

1️⃣ 繪製階段(Recording)

當使用者在 UI 上畫畫時,系統記錄每一筆的完整參數為 JSON,包括顏色、大小、物理屬性、特效等 60+ 個參數。

2️⃣ 播放階段(Playback)

給定一個 JSON 檔案,系統可以完全重現這些筆觸——不是截圖或影片,而是用相同的演算法重新「畫」出來,確保完全一致。

所以如果你想讓 LLM 或 AI Agent 創作藝術品,你只需要教會它如何生成正確的 JSON

為什麼用 JSON?

  • 易於生成: LLM 可以逐字 token 產生文本,不需要二進制序列化
  • 易於調試: 人類可以直接看懂,改參數很容易
  • 易於驗證: 標準 JSON 驗證工具確保格式正確
  • 易於組合: 多個筆觸可以簡單地放入陣列
  • 易於存檔: 純文本,任何系統都能存取

完整範例(含頂層必要欄位、一筆 stroke 的 strokeData、以及可選欄位):

{
  "version": "1.0",
  "startTime": 0,
  "randomSeed": 1234567890,
  "initialPathToggle": false,
  "initialWhiteBrushMode": false,
  "initialBrushColorMode": 0,
  "canvasSize": {
    "width": 500,
    "height": 500
  },
  "canvasBackgroundColor": [
    222,
    212,
    195
  ],
  "events": [
    {
      "m": "mp",
      "t": 0,
      "x": 76,
      "y": 255,
      "strokeData": {
        "strokeSeed": 116462542,
        "mouseCountStart": 0,
        "colorIndex": 3,
        "shapeType": 2,
        "useSharpen": 0,
        "brushMode": 1,
        "indiffusionStrength": 0.45,
        "whiteBrushMode": false,
        "brushColorMode": 0,
        "phasorVel": 1,
        "explodeStart": 1,
        "explodeEnd": 1,
        "whiteMaxOpacity": 0.78,
        "hueShift": -0.01,
        "satShift": 0.02,
        "briShift": 0.02,
        "targetflyBrushType": 2,
        "targetmainStrokeDir": 0,
        "brushDir": 0,
        "ctlNoise": 1,
        "brushPaintCtlNoisebyFrame": 1,
        "brushPaintInterpolationOffset": 2,
        "brushPaintOldRInitial": 0,
        "keyBlendMode": 0,
        "initialSize": 41.17,
        "spraySize": 6,
        "step": 15,
        "step2": 5,
        "randStep": 0.05,
        "maxUpdates": 30,
        "pathRotation": 0,
        "spring": 0.6,
        "friction": 0.5,
        "baseBrushSize": 2,
        "expectedStrokeLength": 400,
        "effect3Brightness": 0.57,
        "mouseX": 76,
        "mouseY": 255,
        "drawingSeed": 6293936,
        "brushModeSP": false,
        "forceMapParams": {
          "randomSeed1": 134.9,
          "randomSeed2": 204.26,
          "randomSeed3": 303.36,
          "randomSeed4": 438.41,
          "scale1": 0,
          "scale2": 0.01,
          "scale3": 0.01,
          "amplitude1": 0.27,
          "amplitude2": 0.32,
          "amplitude3": 0.67,
          "phase1": 3.35,
          "phase2": 1.06,
          "phase3": 1.1,
          "vortexScale1": 0.01,
          "vortexScale2": 0.01,
          "clusterScale1": 0,
          "clusterScale2": 0
        }
      }
    },
    {
      "m": "md",
      "t": 36,
      "x": 68,
      "y": 245
    },
    {
      "m": "md",
      "t": 51,
      "x": 69,
      "y": 245
    },
    {
      "m": "md",
      "t": 64,
      "x": 81,
      "y": 245
    },
    {
      "m": "md",
      "t": 80,
      "x": 88,
      "y": 245
    },
    {
      "m": "md",
      "t": 96,
      "x": 98,
      "y": 245
    },
    {
      "m": "md",
      "t": 113,
      "x": 114,
      "y": 245
    },
    {
      "m": "md",
      "t": 130,
      "x": 129,
      "y": 245
    },
    {
      "m": "md",
      "t": 146,
      "x": 144,
      "y": 245
    },
    {
      "m": "md",
      "t": 163,
      "x": 159,
      "y": 245
    },
    {
      "m": "md",
      "t": 179,
      "x": 175,
      "y": 245
    },
    {
      "m": "md",
      "t": 196,
      "x": 196,
      "y": 245
    },
    {
      "m": "md",
      "t": 213,
      "x": 217,
      "y": 244
    },
    {
      "m": "md",
      "t": 229,
      "x": 231,
      "y": 244
    },
    {
      "m": "md",
      "t": 246,
      "x": 243,
      "y": 243
    },
    {
      "m": "md",
      "t": 263,
      "x": 262,
      "y": 243
    },
    {
      "m": "md",
      "t": 280,
      "x": 275,
      "y": 243
    },
    {
      "m": "md",
      "t": 296,
      "x": 288,
      "y": 243
    },
    {
      "m": "md",
      "t": 313,
      "x": 305,
      "y": 243
    },
    {
      "m": "md",
      "t": 330,
      "x": 320,
      "y": 243
    },
    {
      "m": "md",
      "t": 346,
      "x": 332,
      "y": 243
    },
    {
      "m": "md",
      "t": 363,
      "x": 343,
      "y": 243
    },
    {
      "m": "md",
      "t": 380,
      "x": 356,
      "y": 243
    },
    {
      "m": "md",
      "t": 396,
      "x": 363,
      "y": 243
    },
    {
      "m": "md",
      "t": 413,
      "x": 374,
      "y": 243
    },
    {
      "m": "md",
      "t": 429,
      "x": 378,
      "y": 244
    },
    {
      "m": "md",
      "t": 445,
      "x": 381,
      "y": 244
    },
    {
      "m": "md",
      "t": 463,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 480,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 496,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 513,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 530,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 546,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 563,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 580,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 596,
      "x": 382,
      "y": 244
    },
    {
      "m": "md",
      "t": 613,
      "x": 382,
      "y": 244
    },
    {
      "m": "mr",
      "t": 624,
      "x": 392,
      "y": 254
    }
  ],
  "strokes": [],
  "timeOffset": 0,
  "initialEffectControl": {
    "shapeType": 0,
    "metallicStrength": 85,
    "metallicFlow": 200,
    "metallicTint": [
      0.72,
      0.5,
      0.35
    ],
    "metallicTintType": "copper"
  },
  "savedAt": "2026-03-01T15:01:55.571Z"
}

頂層可選欄位(較新版本包含)

LLM 生成 JSON 時需包含上述 8 個核心欄位加上 events 陣列(共 9 個必填欄位)。較新版本的錄製可能還包含這些可選欄位:

欄位型別說明
strokesarray備用筆觸陣列,通常為 []
timeOffsetnumber時間偏移量,通常為 0
initialEffectControlobject初始特效設定:shapeType(0), metallicStrength(85), metallicFlow(200), metallicTint([r,g,b]), metallicTintType("copper")
savedAtstringISO 8601 時間戳,如 "2026-03-01T13:21:37.067Z"

三種事件類型

事件代碼意義必需欄位
Mouse Press"mp"開始新筆觸m, t, x, y, strokeData
Mouse Drag"md"移動並繼續畫m, t, x, y
Mouse Release"mr"結束筆觸m, t(x,y 可選)

一次完整的繪製流程:按下 → 拖動(一次或多次)→ 放開。每個 "mp" 事件包含完整的 strokeData,其中定義了這筆的所有視覺特性。

⚠️ AI 常見錯誤 — 事件格式陷阱

❌ 錯誤寫法✅ 正確方式說明
"ms"(mouseStart)不存在此事件,直接刪除只有三種代碼:mp / md / mr
"mu"(mouseUp)改用 "mr"結束事件是 mouseReleased,代碼 mr
strokeData 放進 "md"strokeData 只放在 "mp"md 只有 m / t / x / y 四個欄位,沒有 strokeData
"mp" 放在最後(或中間)mp 永遠是每筆的第一個事件固定順序:mpmd… → mr

每一筆觸的結構永遠是:
{ "m":"mp", strokeData… } → { "m":"md" } → … → { "m":"mr" }

2

完整參數參考手冊

strokeData 物件包含 60+ 個參數,分為 12 個類別。以下是完整參考:

📏 尺寸參數(Size Parameters)

參數型別範圍說明
baseBrushSizefloat0.1 ~ 10.0主尺寸縮放;所有噴灑大小都乘以這個值
initialSizefloat2.0 ~ 240.0筆觸起始大小;會逐幀衰減
spraySizefloat1.0 ~ 100.0噴灑粒子的散佈半徑
randStepfloat固定 0.05每幀尺寸衰減量

🎨 顏色參數(Color Parameters)

參數型別說明
brushColorModeint0-35關鍵:色彩 ID。0=黑, 1=白, 6=橙, 9=藍, 30=紅等
colorIndexint0-35色彩變異索引;獨立於 brushColorMode,控制色彩的細微隨機變化(可設 0-3 任意值)
hueShiftfloat-0.05 ~ 0.05色調微調(真實值約 -0.02 ~ 0.02)
satShiftfloat-0.05 ~ 0.05飽和度微調(真實值約 0 ~ 0.04)
briShiftfloat-0.05 ~ 0.05亮度微調(真實值約 0 ~ 0.04)
whiteMaxOpacityfloat0.7 ~ 1.0筆刷最大透明度(真實值約 0.78 ~ 0.95)
whiteBrushModeboolfalse/true啟用白筆刷渲染路徑;一般筆觸設 false

⚙️ 筆刷模式與形狀(Brush Mode & Shape)

參數型別範圍說明
brushModeint1-7筆刷類型:1=標準, 2=簽字筆, 3=哥特體, 4=鋼筆, 5=噴槍, 6=飛筆, 7=特殊
shapeTypeint0-3噴灑顆粒形狀:0=圓, 1=橢圓, 2=三角, 3=鑽石
brushModeSPbool0/1特殊模式標記(Mode 7)

🎬 動畫與物理(Physics & Motion)

參數型別說明
springfloat0.3 ~ 0.6彈簧係數(筆刷回應速度)
frictionfloat0.5阻尼係數(速度衰減)
stepint10-15滑鼠樣本間的插值步數(越高越平滑)
step2int1-10噴灑粒子迭代次數
expectedStrokeLengthint100-400預期筆觸幀數(用於淡進淡出)

✨ 特效參數(Effect Parameters)

參數型別範圍說明
keyBlendModeint0-2混色模式:0=Mix(線性), 1=Multiply(相乘), 2=Darken(取暗)
useSharpenfloat0.0-5.5墨水效果:0=擴散, 1=邊緣, 2=銳利, 3=水彩, 4=紋理, 5=方向
indiffusionStrengthfloat0.0-1.0墨水擴散強度
pathRotationfloat0 ~ 25筆觸方向扭轉:0=無, 7=微妙, 17=野性

🖌️ 筆刷方向與繪製行為(Brush Direction & Paint)

這些參數在所有 brushMode 都需要提供(包括 Mode 1-5, 7),不只是 Mode 6:

參數型別說明
targetflyBrushTypeint0-3分支筆刷型態(Mode 6 主要使用,其他模式設 0-2 均可)
targetmainStrokeDirint0-3主筆觸方向(0=預設, 1-3=不同方向偏好)
brushDirint0-3筆刷實際運動方向(與 targetmainStrokeDir 搭配使用)
ctlNoiseint0/1控制噪波開關;通常設 1
brushPaintCtlNoisebyFrameint0/1逐幀控制噪波;通常設 1
brushPaintInterpolationOffsetint-1, 1, 2插值偏移量(-1=反向插值, 1=一般, 2=多一點平滑)
brushPaintOldRInitialfloat0 or 0.5初始舊半徑值(0=新筆觸起點乾淨, 0.5=有殘留感)
explodeStartint0/1筆觸起始爆散效果(0=關, 1=開)
explodeEndint0/1筆觸結束爆散效果(0=關, 1=開)
effect3Brightnessfloat0.5 ~ 1.0渲染亮度係數(真實值約 0.57 ~ 0.90,每筆可不同)

🌊 Flow 與程序化參數(Force Map)

Flow 效果需要 forceMapParams 物件,包含 4 組隨機種子、3 組頻率、3 組振幅、3 組相位、2 組渦旋尺度、2 組單元尺度:

"forceMapParams": { "randomSeed1": 187.04, "randomSeed2": 294.54, "scale1": 0.01, "amplitude1": 0.34, "phase1": 5.87, "vortexScale1": 0.01, "clusterScale1": 0.0 /* ... 重複 2、3 ... */ }

🔑 系統與種子參數(System & Seed Parameters)

這些欄位由系統在錄製時自動產生,AI 生成時可設為任意合理值:

參數型別建議值說明
strokeSeedint任意正整數此筆觸的隨機種子,影響噴灑粒子分佈細節
mouseCountStartint第 1 筆=0,之後累加全局事件計數起點。計算方式:前面所有筆觸的 mp+md 事件總數。影響程序化隨機效果
drawingSeedint任意正整數(如 1000000~9999999)per-stroke 渲染種子,控制粒子細節的隨機外觀
mouseXfloat同 mp 的 x 值筆觸起始 X 座標(冗餘欄位,與 mp.x 相同即可)
mouseYfloat同 mp 的 y 值筆觸起始 Y 座標(冗餘欄位,與 mp.y 相同即可)
phasorVelfloat1相位器速度,固定設 1
maxUpdatesint30每幀最大繪製迭代次數,固定設 30
3

七種筆刷模式(Brush Mode 1-7)

每種筆刷模式有獨特的視覺特性和推薦參數組合。以下是快速參考:

Mode 1:標準毛筆

特性:自然墨水擴散、柔和邊緣。

推薦組合:

  • baseBrushSize: 2.0
  • initialSize: crandom(20, 24) × baseBrushSize
  • spraySize: 3 × baseBrushSize
  • spring: 0.6, friction: 0.5
  • step: 15, step2: 5
  • maxUpdates: 30

Mode 2:簽字筆

特性:乾燥標記、棱角分明。

推薦組合:

  • baseBrushSize: 1.5
  • spraySize: 1 × baseBrushSize
  • spring: 0.3
  • step: 10, step2: 10
  • maxUpdates: 10

Mode 3:哥特體(粒子)

特性:點狀粒子、物理衰減。

推薦組合:

  • baseBrushSize: 2.5
  • initialSize: crandom(2, 4) × baseBrushSize
  • spraySize: 10 × baseBrushSize
  • spraySteps: 3

Mode 4:鋼筆(精確)

特性:Perlin 噪波權重變化、線條精確。

推薦組合:

  • baseBrushSize: 1.0
  • initialSize: crandom(6, 9) × baseBrushSize
  • expectedStrokeLength: 400
  • penSketchStrokeWeight: 0.8 ~ 1.2

Mode 5:噴槍(散佈)

特性:鬆散噴灑、無尺寸衰減。

推薦組合:

  • baseBrushSize: 3.0
  • spraySize: 10 (固定)
  • step2: 1

Mode 6:飛筆(生成)

特性:樹枝狀圖案、複雜分支。

推薦組合:

  • baseBrushSize: 2.0
  • targetflyBrushType: 0-3 (隨機分支型態)
  • targetmainStrokeDir: 0-3 (主方向)

Mode 7:特殊(Mode 1 變體)

特性:Mode 1 + 隨機分支掉落 + 寬角變化。

推薦組合:與 Mode 1 相同,但設 brushModeSP: true

4

顏色、混色、效果速查表

🎨 主要顏色 (brushColorMode)

關鍵:brushColorMode 必須設成顏色 ID,NOT 0 或 1(除非故意要黑或白)。

0 black
1 white
6 orange
8 teal
9 blue_dark
30 red
3 gray
其他 (ID 4-35)

🔀 混色模式 (keyBlendMode)

重要:黑白背景上混色模式沒有效果。使用中間色(如 [180,160,140])。

模式公式效果
Mix0mix(oldColor, newColor, alpha)線性混合,疊越多越飽和
Multiply1oldColor × adjustedColor顏色相乘,越疊越深
Darken2min(oldColor, newColor)取最暗值,保留深色

✏️ 墨水效果 (useSharpen)

等級名稱特性
0< 0.5Basic Diffusion標準墨水擴散、梯度模糊
1< 1.5Ink Edge邊界檢測、邊界變暗
2< 2.5Sharp Outline高對比邊緣、內部銳化
3< 3.5Watercolor平滑水彩、紋理變化
4< 4.5Textured InkPerlin 紊亂、角度抖動
5< 5.5Directional Flow異向性擴散、時間噪波
5

Flow 力場與程序化參數

Flow 效果是後處理特效,需要在 playback 中單獨觸發。有 8 種造型模式:

Flow blendType 完整列表

Type名稱效果
0基礎線性 Simplex 噪波位移
2同心圓環形漣漪向外擴散
3縱向垂直方向流線
4橫向水平方向流線
5花紋高頻程序化紋樣
6矩形軸對齊矩形圖案
7漩渦旋轉漩渦效果
8細胞Voronoi 細胞紋路

ForceMapParams 的結構與用途

程序化的噪波參數控制 Flow 效果的細節。常用策略:

  • 隨機產生: AI 可以為每個 seed/phase/amplitude 產生隨機但有意義的值
  • 固定模板: 使用預設的「平緩」或「劇烈」forceMapParams 模板
  • 調諧微調: 從基礎模板開始,微調 amplitude 和 scale 來控制強度
6

AI 友善工作流程:從提示到播放

給 AI 一個繪畫任務就像寫菜譜。你需要清楚地告訴它:用哪種筆刷?什麼顏色?什麼尺寸?然後它才能產生正確的 JSON。

步驟 1:LLM 提示範本

// 給 LLM 的範本提示 根據使用者需求,產生一個 InkField 播放 JSON。 需求: {user_request} 視覺風格: 藝術性、水彩 建議筆刷: Mode 1 (標準毛筆) 搭配 useSharpen=3 (水彩效果) 建議顏色: brushColorMode=6 (橙色) 搭配 keyBlendMode=0 (Mix) 輸出完整的 recording JSON,每條筆觸包含: 1. 一個 "mp" (mousePressed) 事件,其中有完整的 strokeData 2. 50-80 個 "md" (mouseDragged) 事件(每幀 ~16ms 間隔,密度必須對照參考範例) 3. 一個 "mr" (mouseReleased) 事件 // ⚠️ 每筆少於 50 個 md 會導致線條過短、視覺效果不足。建議用腳本生成而非手寫。

步驟 2a:完整 JSON 範例(從真實 recording 提煉)

兩條直線,橫的版面(完整錄製檔:tech/examples/ai-json-step2a.json

兩條直線(橫向版面)的標準筆刷示意縮圖

步驟 2b:真實錄製範例 — 五筆水墨小屋(完整版)

以下是直接從 InkField UI 錄製的五筆 brushMode: 1(標準毛筆),畫在 500×500 畫布上。五筆分別是:穹頂弧線、肩線、牆壁U形、底部粗線、門拱。完整錄製檔:tech/examples/ai-json-house.json

五筆水墨小屋播放結果

↑ 播放結果預覽(500×500,brushMode 1,五筆)

步驟 2c:範例生成腳本 — 供機器人/LLM 學習

以下 Node.js 腳本示範如何用程式生成符合密度與時間間隔的 JSON,避免手寫數百個事件。可直接下載或從本頁展開完整原始碼供其他機器人複製學習。

gen-pollock.js 播放結果:10 筆 Pollock 風格線條

gen-pollock.js 生成之 pollock-style.json 播放結果(10 筆、每筆 60–80 個 md、筆觸間隔 600–900ms)

執行方式:在 tech/ 目錄下執行 node gen-pollock.js

展開完整腳本 gen-pollock.js(供機器人/LLM 複製)
// Generate a Pollock-style JSON with 10 strokes, each with 60-80 md events
// on a 500x500 canvas, black color, various brush modes and ink effects

const fs = require('fs');

function rand(min, max) {
  return Math.random() * (max - min) + min;
}
function randInt(min, max) {
  return Math.floor(rand(min, max + 1));
}

// Generate a chaotic Pollock-style curve path
function generatePollockCurve(numPoints, canvasW, canvasH) {
  const points = [];
  let x = rand(50, canvasW - 50);
  let y = rand(50, canvasH - 50);
  let vx = rand(-8, 8);
  let vy = rand(-8, 8);
  const curvature = rand(0.02, 0.15);
  const speed = rand(2, 6);

  for (let i = 0; i < numPoints; i++) {
    points.push({ x: Math.round(x), y: Math.round(y) });
    vx += rand(-curvature * 30, curvature * 30);
    vy += rand(-curvature * 30, curvature * 30);
    const maxV = speed * 3;
    vx = Math.max(-maxV, Math.min(maxV, vx));
    vy = Math.max(-maxV, Math.min(maxV, vy));
    x += vx;
    y += vy;
    if (x < 20) { x = 20; vx = Math.abs(vx) * 0.8; }
    if (x > canvasW - 20) { x = canvasW - 20; vx = -Math.abs(vx) * 0.8; }
    if (y < 20) { y = 20; vy = Math.abs(vy) * 0.8; }
    if (y > canvasH - 20) { y = canvasH - 20; vy = -Math.abs(vy) * 0.8; }
  }
  return points;
}

function generateStrokeData(mouseCountStart, startX, startY, numMdEvents) {
  const safeBrushModes = [1];
  const safeUseSharpen = [2, 3];
  return {
    strokeSeed: randInt(10000000, 999999999),
    mouseCountStart, colorIndex: randInt(0, 5), shapeType: randInt(0, 3),
    useSharpen: safeUseSharpen[randInt(0, safeUseSharpen.length - 1)],
    brushMode: safeBrushModes[randInt(0, safeBrushModes.length - 1)],
    indiffusionStrength: 0.45, whiteBrushMode: false, brushColorMode: 0,
    phasorVel: 1, explodeStart: 0, explodeEnd: 0,
    whiteMaxOpacity: parseFloat(rand(0.5, 0.8).toFixed(2)),
    hueShift: -0.01, satShift: 0.02, briShift: 0.02,
    targetflyBrushType: 2, targetmainStrokeDir: 0, brushDir: randInt(0, 2),
    ctlNoise: 1, brushPaintCtlNoisebyFrame: 1,
    brushPaintInterpolationOffset: randInt(2, 3),
    brushPaintOldRInitial: parseFloat(rand(0, 0.5).toFixed(1)),
    keyBlendMode: 0, initialSize: parseFloat(rand(18, 30).toFixed(2)),
    spraySize: 3, step: 15, step2: 5, randStep: 0.05, maxUpdates: 30,
    pathRotation: 0, spring: 0.6, friction: 0.5, baseBrushSize: 1,
    expectedStrokeLength: numMdEvents * 5,
    effect3Brightness: parseFloat(rand(0.5, 0.7).toFixed(2)),
    mouseX: startX, mouseY: startY, drawingSeed: randInt(1000000, 9999999),
    brushModeSP: false,
    forceMapParams: {
      randomSeed1: parseFloat(rand(50, 500).toFixed(1)),
      randomSeed2: parseFloat(rand(50, 500).toFixed(2)),
      randomSeed3: parseFloat(rand(50, 500).toFixed(2)),
      randomSeed4: parseFloat(rand(50, 500).toFixed(2)),
      scale1: 0, scale2: 0.01, scale3: 0.01,
      amplitude1: parseFloat(rand(0.1, 0.5).toFixed(2)),
      amplitude2: parseFloat(rand(0.1, 0.5).toFixed(2)),
      amplitude3: parseFloat(rand(0.3, 0.9).toFixed(2)),
      phase1: parseFloat(rand(0, 6.28).toFixed(2)),
      phase2: parseFloat(rand(0, 6.28).toFixed(2)),
      phase3: parseFloat(rand(0, 6.28).toFixed(2)),
      vortexScale1: 0.01, vortexScale2: 0.01, clusterScale1: 0, clusterScale2: 0
    }
  };
}

const canvasW = 500, canvasH = 500, numStrokes = 10;
const events = [];
let time = 0, mouseCountStart = 0;

for (let s = 0; s < numStrokes; s++) {
  const numMd = randInt(60, 80);
  const curve = generatePollockCurve(numMd + 1, canvasW, canvasH);
  const startX = curve[0].x, startY = curve[0].y;
  // 筆觸間至少 600-900ms,避免收筆倒數被下一筆 mp 打斷
  time += (s === 0) ? randInt(50, 200) : randInt(600, 900);
  events.push({ m: "mp", t: time, x: startX, y: startY,
    strokeData: generateStrokeData(mouseCountStart, startX, startY, numMd) });
  for (let i = 1; i <= numMd; i++) {
    time += randInt(14, 20);
    events.push({ m: "md", t: time, x: curve[i].x, y: curve[i].y });
  }
  time += randInt(10, 30);
  events.push({ m: "mr", t: time, x: curve[numMd].x, y: curve[numMd].y });
  mouseCountStart += 1 + numMd;
}

const recording = {
  version: "1.0", startTime: 0, randomSeed: randInt(100000000, 999999999),
  initialPathToggle: false, initialWhiteBrushMode: false, initialBrushColorMode: 0,
  canvasSize: { width: canvasW, height: canvasH },
  canvasBackgroundColor: [255, 255, 255],
  events
};

const outputPath = __dirname + '/examples/pollock-style.json';
fs.writeFileSync(outputPath, JSON.stringify(recording, null, 2));
console.log('Written to:', outputPath);

三、gen-pollock.js 的編寫邏輯

以下整理腳本如何組出符合播放器要求的 JSON,供其他機器人或 LLM 實作時對照。

3.1 JSON 結構(9 個頂層必填欄位)

version, startTime, randomSeed, initialPathToggle, initialWhiteBrushMode, initialBrushColorMode, canvasSize, canvasBackgroundColor, events

3.2 事件序列(每筆觸的三段式)

mp(mousePressed,含 strokeData)→ md × 60–80(mouseDragged)→ mr(mouseReleased)。

3.3 路徑生成(generatePollockCurve)

3.4 strokeData(約 35 個參數 + forceMapParams 16 個)

3.5 時間戳設計

// md 之間:14–20ms(模擬 60fps 的滑鼠拖曳) time += randInt(14, 20); // 筆觸間隔:≥600ms(留給 maxUpdates=30 幀的收筆倒數) time += (s === 0) ? randInt(50, 200) : randInt(600, 900);

3.6 為什麼用腳本而不手寫

步驟 3:三種播放方式

方法 A:Agent 貼上介面(推薦給無法上傳檔案的 Agent)

主應用(index.html)內建 Agent JSON 貼上介面:

  1. 導航到 http://localhost:3001/(確保是 artist 模式,或加 ?_artist:1
  2. 找到控制面板中的 "Agent JSON Input" 文字框(#agent-json-textarea
  3. 將完整 JSON 文字貼入(不需要引號包裹,直接貼 JSON 物件)
  4. 點擊 "▶ Play JSON" 按鈕(#agent-json-submit
  5. 查看 #agent-json-status 確認結果(如:✓ 2 筆,500×500px — 播放中

也可直接呼叫 JS 函式:

const result = window.loadRecordingFromText(jsonString); // 回傳:{ ok: true, strokes: 2, canvasSize: {width:500, height:500} } // 或 { ok: false, error: "missing version or events" }

方法 B:直接 JS 執行(擁有 JS 執行能力的 Agent)

// 在頁面 JS 上下文中執行(preview_eval / Playwright / Puppeteer) recordingData = /* JSON 物件 */; startPlayback();

方法 C:文件上傳(人工或擁有 filesystem 存取的 Agent)

點擊控制面板的 "Load Json" 按鈕(#load-recording),選擇 .json 檔案。

方法 D:Console 就地生成(JSON 太大時的最佳方案)

當 JSON 超過 30KB,直接貼入 Agent JSON Input 文字框會失敗或被截斷。最可靠的做法是把生成邏輯寫成一段自執行 JS(IIFE),在瀏覽器 Console 裡直接執行,就地產生 JSON 並注入播放。

為什麼需要這種方式?

問題原因方法 D 的解法
Agent JSON Input 容量不足textarea 無法承受 50KB+ 的 JSON 文字在瀏覽器內生成 JSON,不需要透過 textarea 傳輸
外部 fetch 被阻擋Railway 部署的頁面無法 fetch localhost 的檔案所有資料都在 JS 內部生成,無需網路請求
分段貼入太慢將 JSON 切成多段注入需要多次 JS 呼叫一段 JS 搞定:生成 + 注入 + 播放

操作步驟

  1. 在 InkField 頁面按 F12(或 Cmd+Option+J / Ctrl+Shift+J)開啟 DevTools → Console
  2. 將下方的「自執行生成腳本」整段貼入 Console,按 Enter 執行
  3. 腳本會自動:生成 JSON → 注入 textarea → 觸發播放
  4. Console 會印出 🎨 Ready!,畫布開始播放動畫

範例腳本結構

// ═══ 自執行生成腳本(IIFE)— 貼入 Console 即可 ═══ (function() { // ── 1. 工具函式 ── function rn(a,b) { return Math.random()*(b-a)+a; } function ri(a,b) { return Math.floor(rn(a,b+1)); } // ── 2. strokeData 生成器 ── function mkStroke(sx, sy, mc, opts) { return { strokeSeed: ri(1e6,9e6), mouseCountStart: mc, colorIndex: ri(0,3), shapeType: ri(0,3), useSharpen: opts.us||3, brushMode: opts.bm||1, // ... 省略:完整約 35 個欄位(見本頁 strokeData 清單) forceMapParams: { /* 16 個欄位 */ } }; } // ── 3. 路徑生成器(平滑曲線) ── function curve(sx,sy,ex,ey,n,amp,freq) { var pts = []; for (var i=0; i<=n; i++) { var r = i/n; pts.push({ x: Math.round(sx+(ex-sx)*r + Math.sin(r*Math.PI*freq)*amp), y: Math.round(sy+(ey-sy)*r + Math.cos(r*Math.PI*freq)*amp*0.5) }); } return pts; } // ── 4. 組裝事件 ── var ev=[], t=0, mc=0; // 定義筆觸陣列(每筆含起終點、點數、波幅、筆刷設定) var strokes = [ { sx:50, sy:400, ex:450, ey:380, n:60, wa:15, wf:1.5, o:{ c:8, sz:50, bl:1, us:3, sp:8, bs:2 } }, // ... 更多筆觸 ... ]; strokes.forEach(function(s, i) { var pts = curve(s.sx, s.sy, s.ex, s.ey, s.n, s.wa, s.wf); t += (i===0) ? 100 : ri(700,900); // mp → md × N → mr ev.push({ m:"mp", t:t, x:pts[0].x, y:pts[0].y, strokeData: mkStroke(pts[0].x, pts[0].y, mc, s.o) }); for (var j=1; j<pts.length; j++) { t += ri(14,20); ev.push({ m:"md", t:t, x:pts[j].x, y:pts[j].y }); } t += ri(10,30); ev.push({ m:"mr", t:t }); mc += 1 + s.n; // ── 交錯插入 Flow 效果(可選) ── if (i === 2) { // 第三筆後加 Flow t += 1200; var seed = ri(1e5,9e5); ev.push({ m:"flow", t:t, action:"start", blendType:0, flowSeed:seed, strokeBounds:{ minX:0, minY:0, maxX:1, maxY:1 }, strength:100, lastStrokeOnly:false }); t += 800; ev.push({ m:"flow", t:t, action:"end", blendType:0, flowSeed:seed, iterations:10, totalFrames:30 }); t += 800; } }); // ── 5. 組裝頂層 JSON ── var painting = { version:"1.0", startTime:0, randomSeed:ri(1e5,9e5), initialPathToggle:false, initialWhiteBrushMode:false, initialBrushColorMode:0, canvasSize:{ width:500, height:500 }, canvasBackgroundColor:[222,212,195], events: ev, strokes:[], timeOffset:0 }; // ── 6. 注入並播放 ── var jsonStr = JSON.stringify(painting); var ta = document.getElementById('agent-json-textarea'); ta.value = jsonStr; ta.dispatchEvent(new Event('input', {bubbles:true})); document.getElementById('agent-json-submit').click(); console.log('🎨 Ready! ' + ev.length + ' events, ' + jsonStr.length + ' chars'); })();

或者用 loadRecordingFromText(更直接)

// 如果不需要經過 textarea,可直接呼叫內建函式: var result = window.loadRecordingFromText(JSON.stringify(painting)); console.log(result); // { ok: true, strokes: 10, canvasSize: {width:500, height:500} }

關鍵注意事項

  • IIFE 包裹:用 (function(){ ... })(); 包裹整段程式碼,避免污染全域變數
  • 筆觸密度:每筆至少 50–80 個 md 事件,否則線條太短看不到
  • 筆觸間距:兩筆之間至少 700ms,讓墨水擴散動畫完成
  • Flow 間距:Flow start 前至少等 1200ms,確保前面的筆觸已 encode 進 finalBuffer
  • 座標範圍:所有 x/y 必須在 10–490 之間(500×500 畫布留邊距)
  • mouseCountStart 累加:第 N 筆的 mouseCountStart = 前面所有筆的 (1 + md數量) 之和

完整可執行範例

下載 mountain-mist-v2.js(12 筆 + 3 次交錯 Flow,完整 500×500 構圖),貼入 Console 後呼叫 loadRecordingFromText(JSON.stringify(window.__painting)) 播放,或將 window.__painting 的 JSON 貼入 Agent JSON Input。

常見錯誤與除錯技巧

錯誤原因修正
筆觸是黑色(想要彩色)brushColorMode: 0改為實際顏色 ID,如 6=橙, 9=藍, 30=紅
混色模式沒效果背景是純黑或純白用中間色背景,如 [180,160,140]
JSON 驗證失敗遺漏必要欄位或類型錯誤確認頂層有 9 個必填欄位(version, startTime, randomSeed, initialPathToggle, initialWhiteBrushMode, initialBrushColorMode, canvasSize, canvasBackgroundColor, events)
strokeData 遺漏欄位缺少 brushDir / ctlNoise 等確認 strokeData 包含全部 ~35 個欄位,特別是 targetflyBrushType, targetmainStrokeDir, brushDir, ctlNoise, brushPaintCtlNoisebyFrame, brushPaintInterpolationOffset, brushPaintOldRInitial
Flow 效果沒有顯示缺少 forceMapParams 或未呼叫 getLastStrokeBounds()確保 forceMapParams 包含全部 16 個欄位,且先呼叫 getLastStrokeBounds()

strokeData 完整欄位清單(速查)

生成 JSON 時,strokeData 必須包含以下全部約 35 個欄位:

strokeSeed, mouseCountStart, drawingSeed, mouseX, mouseY brushMode, brushColorMode, colorIndex, whiteBrushMode, brushModeSP shapeType, baseBrushSize, initialSize, spraySize spring, friction, step, step2, randStep, maxUpdates, pathRotation, expectedStrokeLength keyBlendMode, useSharpen, indiffusionStrength, effect3Brightness hueShift, satShift, briShift, whiteMaxOpacity phasorVel, explodeStart, explodeEnd targetflyBrushType, targetmainStrokeDir, brushDir ctlNoise, brushPaintCtlNoisebyFrame, brushPaintInterpolationOffset, brushPaintOldRInitial forceMapParams: { randomSeed1~4, scale1~3, amplitude1~3, phase1~3, vortexScale1~2, clusterScale1~2 }

進階工作流:分層迭代(像下棋一樣畫畫)

上面的工作流是「一口氣寫完整首曲子再演奏」。這裡介紹另一種方式:像下棋一樣,每落幾子就看一眼棋盤,再決定下一步。

為什麼需要迭代?

一口氣生成全部筆觸時,你是在假設每一筆都完美執行。但實際上:

  • 墨水擴散(feedback)和 Flow 效果有不可預測性 — 實際結果跟預期會有偏差
  • 第 3 筆可能已經讓某區域太重,但你的 JSON 已經把第 4–10 筆都寫死了
  • 構圖的「疏密平衡」只有看了實際畫面才能判斷

分層迭代讓你在每個階段看了再決定,而非全盤猜測。

四輪迭代流程

不需要一筆一筆(太慢),按繪畫層次分 4 輪:

輪次內容典型參數提交後做什麼
1. 底層 2–3 筆大筆鋪底色 bs=4–5, initialSize=80–103, brushMode=1 截圖 → 評估覆蓋率與重心
2. 結構 3–4 筆中筆建立構圖 bs=2–3, initialSize=30–50, pathRotation=7–15 截圖 → 評估疏密平衡、哪裡要補
3. 細節 2–3 筆細筆勾勒 bs=0.25–1, initialSize=2–15, brushMode=4 或 6 截圖 → 判斷是否需要白色提亮
4. 收尾 白色筆刷 / Flow / 或決定留白不加 whiteBrushMode=true 或 Flow blendType=3 截圖 → 最終確認

每輪之間的評估框架

截圖後,回答這四個問題再生成下一輪:

  1. 重心 — 畫面的視覺重量偏向哪裡?需要在對側補筆嗎?
  2. 疏密 — 哪個區域太擠?哪個區域太空?留白是刻意的嗎?
  3. 層次關係 — 這一輪加的東西跟上一輪的關係是什麼?(覆蓋、對比、呼應)
  4. 完成度 — 還需要什麼才算「完成」?有沒有可能現在就是最好的狀態?

技術要點

  • 分段 JSON 已支援 — 系統接受多次 loadRecordingFromText() 呼叫,每次新的 JSON 會在現有畫面上繼續繪畫
  • mouseCountStart 需要累加 — 第二輪的第一筆 mouseCountStart = 第一輪所有筆的 (mp + md) 事件總和。跨輪次要自己記住這個數字
  • 每輪之間不需要特殊間隔 — 等上一輪播放完成後再提交下一輪即可。console 會印出播放完成訊息
  • randomSeed 每輪可以不同 — 頂層 JSON 的 randomSeed 每次提交可以用新的值

什麼時候用一口氣、什麼時候用迭代?

情境建議
練習參數、測試筆刷效果一口氣 — 快速產出看結果
有明確構圖(如臨摹)一口氣 — 計畫已定,不需要調整
自由創作、探索風格迭代 — 需要邊畫邊感受
構圖複雜(>8 筆 + Flow)迭代 — 避免後半段的累積偏差
7

Cowork 共繪模式:你一筆我一筆

以前 AI 生成 JSON 是「獨自創作」:給提示 → 產 JSON → 播放覆蓋整張畫布。現在有了 Agent Toggleappend 模式,人類和 AI 可以像接龍一樣在同一張畫上輪流畫:你畫一筆,我畫一筆,彼此回應對方的筆觸。

7.1 新增能力一覽(2026-04-06)

7.2 「你一筆我一筆」工作流

  1. 人類先畫一筆 — 按下 Agent Toggle,在畫布上畫一筆,Toggle 會收集路徑資料。
  2. MCP 讀取路徑 — Claude in Chrome 執行 window.getAgentPathData() 取出人類筆觸的 (x, y, pressure, time) 序列。
  3. Claude Desktop 回應 — 根據人類筆觸的位置、方向、顏色,生成一個回應筆觸的 JSON(遵守下方格式要求)。
  4. Append 播放 — 用 loadRecordingFromText(json, {append:true}) 把 AI 的回應畫在人類筆觸旁邊,不清空畫布。
  5. 循環 — 人類看完 AI 的回應後再畫一筆,繼續接龍。

🎯 設計精神

共繪不是「AI 幫我填滿畫布」,而是「AI 回應我這一筆」。每次只畫 1–3 筆,讓人類有足夠的空間主導構圖。AI 的角色是夥伴,不是代筆者。

7.3 JSON 格式:三個必守規則

  1. brushMode 必須是 1–7,絕對不能是 0。0 會靜默失敗(不報錯,也不畫東西)。
    1=標準毛筆 / 2=簽字筆 / 3=哥特體 / 4=鋼筆 / 5=噴槍 / 6=飛筆 / 7=特殊
  2. brushColorMode 是「色彩 ID」,不是混色模式。常用:0=黑, 1=白, 5=綠, 6=橙, 9=深藍, 10=紫, 30=紅, 31=黃, 32=藍, 33=自訂(配合 customBrushColor:[r,g,b]), 34=珊瑚, 35=薄荷。
  3. x/y 必須在 mp/md/mr 事件的頂層,不是塞在 strokeData 裡面。

其他細節:每筆 50–80 個 md 事件(間距 ~16ms),strokeData 約 40 個欄位(缺的會 auto-fill),mr 與下一個 mp 間隔至少 ~500ms 避免收筆被打斷。

7.4 五個可直接貼上 Console 的範例

以下範例都使用最小必要欄位,其餘會被 auto-fill 補齊。直接貼到 console 即可測試。前四筆用 append 接續畫,第五筆示範自訂 RGB。

格式重點:事件用 m:(不是 type:),最外層需要 version:"1.0",範例包成 IIFE 方便直接貼 console。

範例 1:藍色橫波(標準毛筆 / 粗)

(function(){ var events = [{m:"mp",t:0,x:100,y:300,strokeData:{brushMode:1,brushColorMode:32,baseBrushSize:3,initialSize:35}}]; for(var i=1;i<=60;i++){events.push({m:"md",t:i*16,x:100+i*7,y:300+Math.sin(i*0.3)*30});} events.push({m:"mr",t:62*16,x:520,y:300}); window.loadRecordingFromText(JSON.stringify({version:"1.0",canvasSize:{width:width,height:height},events:events}),{append:true}); })();

範例 2:紅色螺旋(簽字筆)

(function(){ var events = [{m:"mp",t:0,x:300,y:300,strokeData:{brushMode:2,brushColorMode:30,baseBrushSize:2,initialSize:25}}]; for(var i=1;i<=70;i++){var a=i*0.25,r=i*1.5;events.push({m:"md",t:i*16,x:300+Math.cos(a)*r,y:300+Math.sin(a)*r});} events.push({m:"mr",t:72*16,x:405,y:300}); window.loadRecordingFromText(JSON.stringify({version:"1.0",canvasSize:{width:width,height:height},events:events}),{append:true}); })();

範例 3:綠色 Z 字(標準毛筆 / 大)

(function(){ var events = [{m:"mp",t:0,x:150,y:150,strokeData:{brushMode:1,brushColorMode:5,baseBrushSize:4,initialSize:40}}]; for(var i=1;i<=25;i++){events.push({m:"md",t:i*16,x:150+i*10,y:150});} for(var i=0;i<25;i++){events.push({m:"md",t:(26+i)*16,x:400-i*10,y:150+i*8});} for(var i=0;i<25;i++){events.push({m:"md",t:(51+i)*16,x:150+i*10,y:350});} events.push({m:"mr",t:77*16,x:400,y:350}); window.loadRecordingFromText(JSON.stringify({version:"1.0",canvasSize:{width:width,height:height},events:events}),{append:true}); })();

範例 4:黃色花瓣(哥特體)

(function(){ var events = [{m:"mp",t:0,x:400,y:400,strokeData:{brushMode:3,brushColorMode:31,baseBrushSize:3,initialSize:30}}]; for(var i=1;i<=60;i++){var a=i*0.15;events.push({m:"md",t:i*16,x:400+Math.sin(a*3)*60,y:400+a*8});} events.push({m:"mr",t:62*16,x:400,y:472}); window.loadRecordingFromText(JSON.stringify({version:"1.0",canvasSize:{width:width,height:height},events:events}),{append:true}); })();

範例 5:紫色閃電(鋼筆)— 隨機抖動路徑

(function(){ var events = [{m:"mp",t:0,x:300,y:50,strokeData:{brushMode:4,brushColorMode:10,baseBrushSize:3,initialSize:25}}]; var x=300; for(var i=1;i<=55;i++){x+=(Math.random()-0.5)*30;events.push({m:"md",t:i*16,x:x,y:50+i*9});} events.push({m:"mr",t:900,x:x,y:550}); window.loadRecordingFromText(JSON.stringify({version:"1.0",canvasSize:{width:width,height:height},events:events}),{append:true}); })();

7.5 從 Agent Toggle 讀取人類筆觸

// MCP / Claude in Chrome 這樣讀取人類筆觸 const pathData = window.getAgentPathData(); // 回傳結構:{ strokes: [ { points: [{x, y, pressure, t}, ...], strokeData: {...} }, ... ] } // Claude Desktop 根據這些資料生成回應筆觸後: window.loadRecordingFromText(jsonText, { append: true });

7.6 生成時的自我檢查

💡 Debug 技巧

貼到 console 後看輸出:

  • [autofill] 補齊欄位: ... — 正常,表示 auto-fill 有作用
  • [autofill] 警告:md 密度過低 — 筆觸會很短,增加 md 事件
  • grid.js NaN warnings — 檢查 x/y 是否在事件頂層
  • 沒錯誤但沒畫出來 — 99% 是 brushMode: 0brushColorMode 誤用

← 上一篇:AI 日誌   |   下一篇:情緒與意圖 →

InkField 教學系列 — 用國中生也能懂的方式,認識數位水墨的秘密