# 常用指令

正式網址：https://newmediaart.course.monolab.world/snippets

上課一直會用到的 p5.js 小段程式：存圖、重來、全螢幕、粒子模板、麥克風測試，複製就能貼進 OpenProcessing。

## 起手模板

### 粒子起手模板：Hahow Week 3-0 Particle Template

講師的粒子模板（OpenProcessing 上直接 Fork）：Particle 的 update() 寫規則、draw() 寫長相；開頭三行 OPC 是拉桿、顏色、色盤，邊跑邊調。這份多補了四條常用規則（重力、noise 力場、追滑鼠、分裂），先註解掉，一次打開一條。之後有粒子的示範都從這裡開始。

最快：到 OpenProcessing 打開 https://openprocessing.org/@cheyuwu/3033176 按 Fork（OPC 函式庫已經開好）。自己貼進空白草圖的話，要到草圖設定的 Libraries 打開 OP Configurator 3000，不然會報 OPC is not defined。先跑起來，拉拉桿；再到 update() 打開一條規則、改數字看它變成什麼；想換長相就改 draw()。

```js
// Hahow Week 3-0 Particle Template（講師的 OpenProcessing 版：https://openprocessing.org/@cheyuwu/3033176 ，直接 Fork 最快）
// 用到 OPC 拉桿：OpenProcessing 草圖設定的 Libraries 要打開「OP Configurator 3000」（Fork 就有）
let particles = [];
OPC.slider('radius', 50, 10, 100); // 拉桿：名字、預設、最小、最大
OPC.color('ballColor', '#fff'); // 顏色
OPC.palette('currentPalette', [ // 色盤：點一下換下一組
  ['#eabfcb', '#c191a1', '#a4508b', '#5f0a87', '#2f004f'],
  ['#c3dfe0', '#bcd979', '#9dad6f', '#7d6d61', '#5e574d'],
  ['#4464ad', '#a4b0f5', '#f58f29', '#7d4600', '#466995'],
]);

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(100);
}

function draw() {
  background(100);
  // 新增粒子
  for (let i = 0; i < 10; i++) {
    // 用物件格式傳遞參數
    let newP = new Particle({
      p: createVector(mouseX, mouseY),
      v: p5.Vector.random2D(),
      // color: ballColor,
      color: random(currentPalette),
      r: radius,
    });
    particles.push(newP);
  }
  // 更新與繪製所有粒子
  particles.forEach((p) => {
    p.update();
    p.draw();
  });
  // 保留存活的粒子
  particles = particles.filter((p) => p.alive);
}

// ── 以下是 ParticleClass 分頁 ──
class Particle {
  // 用物件格式接收引數 obj {a:1} -> obj.a
  constructor(args) {
    let def = {
      p: createVector(0, 0), // 位置
      v: createVector(0, 0), // 速度
      a: createVector(0, 0), // 加速度
      r: 50, // 半徑
      color: color(255),
      life: 100, // 還能活幾格
      alive: true, // 是否活著
    };
    // 覆蓋預設值（如果有引數）
    Object.assign(def, args);
    Object.assign(this, def);
  }
  // 每次更新的邏輯
  update() {
    this.life--; // 生命 -1
    this.r *= 0.99; // 半徑逐漸變小
    if (this.life < 0) {
      this.alive = false;
    }
    // ── 常用規則（這份多補的，OpenProcessing 模板上沒有）：複製一行、拿掉 // 就打開 ──
    // 重力：this.a = createVector(0, 0.1);
    // noise 力場：this.a = p5.Vector.fromAngle(noise(this.p.x * 0.005, this.p.y * 0.005) * TWO_PI * 2).mult(0.1);
    // 追滑鼠：this.a = p5.Vector.sub(createVector(mouseX, mouseY), this.p).setMag(0.1);
    // 分裂：if (this.life === 0 && particles.length < 3000) for (let k = 0; k < 2; k++) particles.push(new Particle({ p: this.p.copy(), v: p5.Vector.random2D(), r: this.r, color: this.color }));
    // 位置/速度/加速度更新
    this.p.add(this.v);
    this.v.add(this.a);
  }
  // 繪製
  draw() {
    push();
    fill(this.color);
    translate(this.p.x, this.p.y);
    circle(0, 0, this.r);
    pop();
  }
}
```

### OPC 拉桿：把數字變成邊跑邊調的控制項

OpenProcessing 的函式庫 OP Configurator 3000（OPC）：把 let radius = 10 換成 OPC.slider(...)，畫面旁邊就多一根拉桿，不用改程式、不用重跑就能試數字。拉桿、開關、顏色、下拉選單都有。

在 OpenProcessing 的草圖設定打開 Libraries 的 OP Configurator 3000（沒打開會報 OPC is not defined）。把想試的數字從 let 改成 OPC.slider，畫面旁邊出現拉桿就可以邊跑邊調；調到喜歡的數字記下來。分享時網址加 ?sketch_radius=30 可以換預設值。

```js
// OPC（OP Configurator 3000）：把數字變成拉桿，邊跑邊調。說明：https://github.com/msawired/OPC
// OpenProcessing：草圖設定的 Libraries 打開「OP Configurator 3000」（Fork 講師的粒子模板就已經開好）
// 原本寫 let radius = 10; 的地方換成下面這行；radius 是我取的名字，OPC.slider 是函式庫的指令。不要再 let 一次 radius
OPC.slider('radius', 10, 10, 100); // 名字、預設、最小、最大
OPC.slider('count', 10, 1, 50, 1); // 第五個數字：每格跳多少
OPC.toggle('trail', false); // 開關：true／false
OPC.color('bg', '#646464'); // 顏色

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(bg);
}

function draw() {
  if (!trail) background(bg); // trail 關掉才清畫面
  for (let i = 0; i < count; i++) {
    circle(mouseX + random(-radius, radius), mouseY + random(-radius, radius), radius * 0.3);
  }
}

// 拉桿一動就會叫這個函式（名字是 OPC 規定的）
function parameterChanged(name, value) {
  if (name === 'bg') background(value);
}
```

## 自然的演算法

### 海龜繪圖：照字串畫線

一個 turtle(cmds, len, angle) 函式，讀一串指令照著走：F 前進畫線、+ 和 - 轉向、[ 記住位置、] 回到位置，改字串就改圖形。

只把最底下的 turtle() 整個複製進自己的 sketch，在 draw() 裡先 translate() 到出發點，再呼叫 turtle(字串, 步長, 角度)；字串裡的 [ ] 一定要成對。

```js
// 海龜繪圖：照字串畫。F 前進畫線，+ 右轉，- 左轉，[ 記住位置，] 回到位置
function setup() {
  createCanvas(640, 420);
}

function draw() {
  background(18, 25, 40);
  stroke(135, 218, 160);
  strokeWeight(2);
  push();
  translate(110, 150); // 海龜從這裡出發，預設朝右
  turtle('F+F+F+F+F', 110, 144); // 五角星：走一邊轉 144 度，共五次
  pop();
  push();
  translate(470, 400);
  rotate(-HALF_PI); // 先轉成朝上
  turtle('FF[+F[+F]-F][-F[-F]+F]', 40, 25); // 小樹：括號裡是分枝，畫完回到分岔點
  pop();
}

// 海龜：cmds 是指令字串，len 是每步長度，angle 是每次轉幾度
function turtle(cmds, len, angle) {
  for (let c of cmds) {
    if (c === 'F') {
      line(0, 0, len, 0);
      translate(len, 0); // 往前走一步：座標原點跟著海龜移動
    } else if (c === '+') rotate(radians(angle));
    else if (c === '-') rotate(-radians(angle));
    else if (c === '[') push(); // 記住現在的位置和方向
    else if (c === ']') pop(); // 回到上一次記住的地方
  }
}
```

### L-System：規則換字長出一棵樹

一個 grow(axiom, rules, n) 函式，把起始字串照規則改寫 n 代（F 換成 F[+F]F[-F]F）再交給畫字串函式，字串越長樹越茂密，點一下多長一代。

把 grow() 和 drawString() 複製進自己的 sketch，在 setup() 用 grow(起始字, 規則, 代數) 算好字串，draw() 裡 translate() 到出發點再 drawString()；改 RULES 就是改樹的長相。

```js
// L-System：每一代把字串裡的 F 換成規則，再用海龜畫出來。點一下多長一代
const RULES = { F: 'F[+F]F[-F]F' };
let gen = 3;
let sentence;

function setup() {
  createCanvas(640, 420);
  sentence = grow('F', RULES, gen);
}

function draw() {
  background(18, 25, 40);
  stroke(135, 218, 160);
  strokeWeight(1.5);
  translate(width / 2, height - 20); // 從畫面底部中間長出來
  rotate(-HALF_PI); // 朝上
  drawString(sentence, 60 / pow(2, gen), 25); // 每多一代，步長減半
}

function mousePressed() {
  gen = gen % 4 + 1; // 1 到 4 代輪流
  sentence = grow('F', RULES, gen);
}

// 長 n 代：每一代都讀舊字串、寫進新字串（不能邊讀邊改自己）
function grow(axiom, rules, n) {
  let s = axiom;
  for (let i = 0; i < n; i++) {
    let next = '';
    for (let c of s) next += rules[c] || c; // 有規則就換，沒有就原樣留下
    s = next;
  }
  return s;
}

// 照字串畫（跟 turtle-draw 的 turtle 是同一套，兩段一起用只要留一份）
function drawString(cmds, len, angle) {
  for (let c of cmds) {
    if (c === 'F') {
      line(0, 0, len, 0);
      translate(len, 0);
    } else if (c === '+') rotate(radians(angle));
    else if (c === '-') rotate(-radians(angle));
    else if (c === '[') push();
    else if (c === ']') pop();
  }
}
```

### 遞迴樹：branch 自己呼叫自己

一個 branch(len, depth, angle) 函式，畫一段樹枝再往左右各長兩根短一點的，depth 減到 0 就停；範例從畫面底部長一棵，滑鼠左右移動改分枝角度。

複製 branch() 進自己的 sketch，在 draw() 裡先 translate() 到樹根，再呼叫 branch(長度, 層數, 角度)；層數每加 1，枝條數量翻倍，電腦跑不動就調小。

```js
// 遞迴樹：一根樹枝畫完，在尖端分出左右兩根更短的，一直分到 depth 歸零
function setup() {
  createCanvas(640, 420);
}

function draw() {
  background(18, 25, 40);
  let angle = map(mouseX, 0, width, 0, HALF_PI); // 滑鼠 x 控制分枝張開多大
  translate(width / 2, height); // 原點移到畫面底部中間
  branch(90, 9, angle);
}

// len 這根枝的長度，depth 還要再分幾層，angle 左右各轉多少（弧度）
function branch(len, depth, angle) {
  if (depth === 0) return; // 停止條件：沒有它會無限呼叫下去
  stroke(190, 150, 100);
  strokeWeight(depth * 0.9);
  line(0, 0, 0, -len);
  translate(0, -len); // 走到枝尖
  if (depth === 1) {
    noStroke();
    fill(255, 150, 190);
    circle(0, 0, 8); // 最末端開一朵小花
  }
  push();
  rotate(angle);
  branch(len * 0.7, depth - 1, angle); // 右邊那根
  pop();
  push();
  rotate(-angle);
  branch(len * 0.7, depth - 1, angle); // 左邊那根
  pop();
}
```

### Noise 年輪：接得起來的不規則圈

一個 noiseRing(x, y, r, amp, k, z) 函式，用 noise 讓圓的半徑忽大忽小，起點跟終點接得起來；範例疊 30 圈變年輪，按 R 換一棵樹。

複製 noiseRing() 進自己的 sketch，先 noFill() 再呼叫；想畫年輪就用迴圈，每圈的 r 加大、z 加一點；k 越大越碎，amp 越大起伏越誇張。

```js
// Noise 年輪：每圈用 noise 擾動半徑，一圈一圈往外疊。按 R 換一組形狀
function setup() {
  createCanvas(640, 420);
  noiseSeed(1);
}

function draw() {
  background(30, 22, 18);
  noFill();
  strokeWeight(1.5);
  for (let i = 0; i < 30; i++) {
    stroke(lerpColor(color(240, 215, 170), color(105, 62, 32), i / 30)); // 淺到深
    // 半徑隨圈數變大；越外圈越不規則；z 用 i * 0.05：相鄰兩圈像、但不一樣
    noiseRing(width / 2, height / 2, 10 + i * 4.5, 6 + i * 0.9, 0.8, i * 0.05);
  }
}

function keyPressed() {
  if (key === 'r' || key === 'R') {
    noiseSeed(floor(random(99999)));
  }
}

// 畫一圈不規則的圈。x y 圓心，r 基本半徑，amp 起伏多大，k 起伏多碎，z 第三軸（換圈就換它）
function noiseRing(x, y, r, amp, k, z) {
  beginShape();
  for (let a = 0; a < TWO_PI; a += 0.05) {
    // 用 cos 和 sin 當 noise 的座標：走完一圈剛好回到起點，圈才接得起來
    let n = noise(cos(a) * k + 1, sin(a) * k + 1, z);
    let rr = r + n * amp;
    vertex(x + cos(a) * rr, y + sin(a) * rr);
  }
  endShape(CLOSE);
}
```

### Boid 三條規則：分離、對齊、聚合

一個 flock(me, others, radius) 函式，回傳分離、對齊、聚合三條規則合成的力；粒子模板的 p／v 或 Boid 的 pos／vel 都認得，在粒子模板的 update() 加一行就變鳥群，範例用最小的 class 跑 60 隻。

把最底下的 flock() 複製進自己的 sketch；粒子模板（Hahow Week 3-0）在 update() 的規則區加一行 this.a = flock(this, particles, 60); 就變鳥群，不用另外寫 class。

```js
// Boid：60 隻，每一格先算好所有人的力、再一起更新
let boids = [];

function setup() {
  createCanvas(640, 420);
  for (let i = 0; i < 60; i++) boids.push(new Boid(random(width), random(height)));
}

function draw() {
  background(18, 25, 40);
  // 先讓每一隻算完力，全部算完再更新位置，順序才不會讓前面的人佔便宜
  for (let b of boids) b.steerWith(boids);
  for (let b of boids) { b.update(); b.draw(); }
}

class Boid {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D().mult(2);
    this.acc = createVector(0, 0);
  }
  steerWith(all) {
    this.acc = flock(this, all, 60); // 呼叫底下那個 flock() 函式
  }
  update() {
    this.vel.add(this.acc).limit(2.3);
    this.pos.add(this.vel);
    this.pos.x = (this.pos.x + width) % width; // 穿過邊界從另一邊出來
    this.pos.y = (this.pos.y + height) % height;
  }
  draw() {
    push();
    translate(this.pos.x, this.pos.y);
    rotate(this.vel.heading());
    noStroke();
    fill(100, 205, 245);
    triangle(8, 0, -5, -4, -5, 4);
    pop();
  }
}

// 三條規則合成的力。me 要有 p、v（粒子模板）或 pos、vel；others 是大家；radius 看得到多遠的鄰居
function flock(me, others, radius) {
  const maxSpeed = 2.3, maxForce = 0.08;
  const P = (b) => b.p || b.pos, V = (b) => b.v || b.vel; // 粒子模板用 p／v，這裡的 Boid 用 pos／vel，兩種都認得
  let sep = createVector(0, 0), ali = createVector(0, 0), coh = createVector(0, 0);
  let near = 0, close = 0;
  for (let o of others) {
    if (o === me) continue;
    let d = p5.Vector.dist(P(me), P(o));
    if (d > 0 && d < radius) {
      ali.add(V(o)); // 對齊：加總鄰居的速度
      coh.add(P(o)); // 聚合：加總鄰居的位置
      near++;
      if (d < radius * 0.4) { sep.add(p5.Vector.sub(P(me), P(o)).div(d * d)); close++; } // 分離：太近的推開
    }
  }
  // 想去的方向 → 轉向的力：想要的速度減掉現在的速度，再限制力道
  function steer(want) {
    if (want.magSq() === 0) return createVector(0, 0);
    return want.setMag(maxSpeed).sub(V(me)).limit(maxForce);
  }
  let force = createVector(0, 0);
  if (close > 0) force.add(steer(sep.div(close)).mult(1.5));
  if (near > 0) {
    force.add(steer(ali.div(near))); // 往鄰居平均的方向走
    force.add(steer(coh.div(near).sub(P(me)))); // 往鄰居的中心靠
  }
  return force.limit(maxForce * 2.5);
}
```

## 存檔與輸出

### 按 S 存 JPG（檔名帶日期時間）

按一下 S 就把畫面存成 JPG，檔名像 sketch-2026-09-29-1905-12.jpg，存再多張也不會互相蓋掉，還會照時間排好。

把 keyPressed、pad、timestamp 三段貼到自己的 sketch 最下面；已經有 keyPressed 就把 if 那段併進去。在 OpenProcessing 要先點一下畫布，按鍵才會送進作品。

```js
// 按 S 存一張 JPG，檔名帶日期時間：sketch-2026-09-29-1905-12.jpg
function setup() {
  createCanvas(600, 600);
  background(20); // JPG 沒有透明：沒塗到的地方會變黑，先鋪一層底色
}

function draw() {
  // 你的作品畫在這裡（示範：跟著滑鼠畫半透明的圓）
  noStroke();
  fill(255, random(120, 220), 80, 90);
  circle(mouseX, mouseY, 40);
}

function keyPressed() {
  if (key === 's' || key === 'S') {
    saveCanvas('sketch-' + timestamp(), 'jpg');
  }
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

### 按 S 存 PNG（透明背景）

draw() 裡用 clear() 取代 background()，存成 PNG 背景就是透明的，可以直接疊到海報、簡報或影片上。

把自己 draw() 第一行的 background() 換成 clear()，再貼上 keyPressed、pad、timestamp。只是要存 PNG、不需要透明的話，background() 留著、把 jpg 改成 png 就好。

```js
// 按 S 存 PNG：背景是透明的，可以直接疊到海報、簡報、影片上
// 畫面上看到的底色是網頁的顏色，存出來的檔案才是透明
function setup() {
  createCanvas(600, 600);
}

function draw() {
  clear(); // 用 clear() 取代 background()：每一格擦成透明，而不是塗滿顏色
  noFill();
  stroke(255, 120, 80);
  strokeWeight(10);
  for (let i = 0; i < 6; i++) {
    circle(width / 2, height / 2, 80 + i * 80);
  }
}

function keyPressed() {
  if (key === 's' || key === 'S') {
    // 只有 PNG 留得住透明；存成 JPG 的話，透明的地方會變成黑色
    saveCanvas('sketch-' + timestamp(), 'png');
  }
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

### 存高解析度圖（pixelDensity）

pixelDensity(2) 讓 600×600 的畫布存出 1200×1200 的圖；要印刷就調到 3、4，存出來的尺寸＝寬高 × 密度。

在自己的 setup() 裡、createCanvas 的下一行加上 pixelDensity(2)。動態作品密度太高會變慢：平常用 1，要存圖前再改成 3 或 4。

```js
// 高解析度存圖：畫布 600×600，pixelDensity(2) 存出來是 1200×1200
function setup() {
  createCanvas(600, 600);
  // 放在 createCanvas 之後。要印海報就改 3 或 4（存出 1800、2400 像素），
  // 數字越大越吃效能
  // Retina 螢幕預設就是 2；寫死一個數字，換哪台電腦存出來都一樣大
  pixelDensity(2);
  noLoop(); // 靜態作品畫一次就好，密度調高也不會卡
}

function draw() {
  background(245, 240, 230);
  noFill();
  stroke(30);
  for (let r = 20; r < 580; r += 14) {
    circle(width / 2, height / 2, r);
  }
}

function keyPressed() {
  if (key === 's' || key === 'S') {
    // 存出來的尺寸＝寬 × 密度、高 × 密度；檔名順便記下尺寸
    const w = width * pixelDensity();
    const h = height * pixelDensity();
    saveCanvas('sketch-' + timestamp() + '-' + w + 'x' + h, 'png');
  }
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

### 按 G 存 3 秒 GIF（saveGif）

saveGif() 把動畫一格一格重畫、錄成 GIF；p5.js 1.5 以上與 2.x 都有，存好之前畫面會停一下是正常的。

把 keyPressed 裡的 if、pad、timestamp 貼進自己的 sketch。saveGif 會把 frameCount 從 0 重新算起（1.x、2.x 都是），用 frameCount 做的動畫錄出來最準。

```js
// 按 G 把 3 秒動畫存成 GIF（p5.js 1.5 以上、2.x 都有 saveGif）
function setup() {
  createCanvas(400, 400); // GIF 很吃記憶體：畫布小一點、秒數短一點
}

function draw() {
  background(20);
  // 動畫用 frameCount 算：saveGif 是一格一格重畫再存，
  // 用 millis() 做的動畫錄出來速度會不對
  const r = 120 + sin(frameCount * 0.05) * 80;
  noFill();
  stroke(255, 200, 80);
  strokeWeight(6);
  circle(width / 2, height / 2, r);
}

function keyPressed() {
  if (key === 'g' || key === 'G') {
    // 3 秒；左上角會出現進度，存好之前畫面停住是正常的
    saveGif('sketch-' + timestamp(), 3);
  }
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

### 按 V 錄 5 秒影片（WebM）

用瀏覽器內建的 MediaRecorder 把畫布錄成影片，不用裝函式庫；檔案比 GIF 小很多，錄影時畫面照常跑、滑鼠互動也會錄進去。

把 SECONDS、recorder 兩行放在最上面，keyPressed 裡的 if、startRecording、pad、timestamp 貼到下面。要上 IG 可以再用剪輯軟體把 WebM 轉成 MP4。

```js
// 按 V 把畫布錄成 5 秒影片（瀏覽器內建 MediaRecorder，不用裝函式庫）
// Chrome、Firefox 存成 .webm；Safari 存成 .mp4
const SECONDS = 5;
let recorder = null;

function setup() {
  createCanvas(600, 600);
}

function draw() {
  background(15);
  noStroke();
  for (let i = 0; i < 12; i++) {
    const a = frameCount * 0.02 + (i * TWO_PI) / 12;
    fill(255, 150 + i * 8, 80);
    circle(width / 2 + cos(a) * 180, height / 2 + sin(a) * 180, 40);
  }
}

function keyPressed() {
  if ((key === 'v' || key === 'V') && !recorder) {
    startRecording();
  }
}

function startRecording() {
  const stream = drawingContext.canvas.captureStream(60); // 每秒 60 格
  const chunks = [];
  recorder = new MediaRecorder(stream);
  recorder.ondataavailable = (e) => chunks.push(e.data);
  recorder.onstop = () => {
    const type = recorder.mimeType || 'video/webm';
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob(chunks, { type: type }));
    const ext = type.includes('mp4') ? '.mp4' : '.webm';
    a.download = 'sketch-' + timestamp() + ext;
    a.click(); // 跟 saveCanvas 一樣，交給瀏覽器下載
    recorder = null;
  };
  recorder.start();
  setTimeout(() => recorder.stop(), SECONDS * 1000);
  print('開始錄影 ' + SECONDS + ' 秒');
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

## 鍵盤滑鼠

### 按 R 換一組隨機

把 random 的種子存在變數裡，按 R 抽一個新種子、重畫一次；同一個種子永遠畫出同一張。

動態作品把 noLoop() 和 redraw() 拿掉，只留 randomSeed(seed) 在 draw() 第一行：每一格抽到同一組隨機，畫面就不會閃。有用 noise() 的話再加一行 noiseSeed(seed)。

```js
// 按 R 換一組隨機：記住種子，同一個種子永遠畫出同一張
let seed;

function setup() {
  createCanvas(600, 600);
  seed = floor(random(1000000));
  noLoop(); // 靜態構圖：畫一次就停，按 R 才重畫
}

function draw() {
  randomSeed(seed); // 每次重畫都從同一個種子開始抽籤
  background(245);
  noStroke();
  for (let i = 0; i < 60; i++) {
    fill(random(255), random(255), random(255), 180);
    circle(random(width), random(height), random(20, 120));
  }
  fill(0);
  textSize(14);
  text('seed ' + seed + '　按 R 換一組', 16, height - 16);
}

function keyPressed() {
  if (key === 'r' || key === 'R') {
    seed = floor(random(1000000)); // 抽一個新種子
    redraw(); // noLoop() 的時候，要叫 redraw() 才會再跑一次 draw()
  }
}
```

### 按空白鍵暫停／繼續

noLoop() 讓 draw() 停下、loop() 讓它繼續，isLooping() 知道現在是哪一種；想截某一格或停下來慢慢看時最好用。

把 keyPressed 裡的 if 貼進自己的 sketch。暫停時再按 S（配上存圖那段），存到的就是停住的這一格。

```js
// 按空白鍵暫停／繼續：想截某一格、或停下來慢慢看的時候用
function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(20);
  const x = (frameCount * 3) % width;
  noStroke();
  fill(255, 200, 80);
  circle(x, height / 2, 60);
}

function keyPressed() {
  if (key === ' ') {
    if (isLooping()) {
      noLoop(); // 停：draw() 不再重複
    } else {
      loop(); // 繼續
    }
    return false; // 不讓空白鍵把網頁往下捲
  }
}
```

### 按 C 清空畫布（筆刷作品）

不畫 background() 的筆刷作品會一直疊上去；按 C 用同一個底色蓋掉整張，就是清空重來。

把 keyPressed 裡的 if 貼進自己的筆刷作品，background() 的顏色改成跟 setup() 裡一樣。

```js
// 筆刷：按住滑鼠畫線；按 C 清空重來
function setup() {
  createCanvas(600, 600);
  background(250); // 底色只在 setup 畫一次，畫過的線才會留下來
}

function draw() {
  if (mouseIsPressed) {
    stroke(30);
    strokeWeight(6);
    // 上一格的滑鼠位置連到這一格，線才不會斷
    line(pmouseX, pmouseY, mouseX, mouseY);
  }
}

function keyPressed() {
  if (key === 'c' || key === 'C') {
    background(250); // 用同一個底色蓋掉整張，就是清空
  }
}
```

### 取座標小工具：點一下印出 vertex

在畫布上點一下，console 就印出一行 vertex(x, y);；點出形狀的頂點，整串貼回 beginShape() 和 endShape(CLOSE) 之間，就是自己的形狀。

整段貼進一個新的 sketch 當工具用：點出想要的形狀頂點，從 console 複製那幾行 vertex 貼回自己作品的 beginShape() 與 endShape(CLOSE) 之間；按 C 清掉重點。

```js
// 取座標小工具：在畫布上點一下，console 就印出一行 vertex(x, y);
// 點出想要的形狀頂點，把那幾行複製貼回 beginShape() 和 endShape(CLOSE) 之間
let points = []; // 點過的位置，畫在畫面上當參考

function setup() {
  createCanvas(600, 600);
}

function draw() {
  background(245);

  // 示範：用 vertex 一點一點圍出來的花盆（歪七扭八也沒關係）
  fill(200, 110, 70);
  stroke(60);
  strokeWeight(2);
  beginShape();
  vertex(163, 297);
  vertex(441, 289);
  vertex(429, 347);
  vertex(396, 347);
  vertex(371, 527);
  vertex(222, 533);
  vertex(197, 352);
  vertex(170, 350);
  endShape(CLOSE); // CLOSE：最後一點自動連回第一點

  // 點過的位置：紅線連起來，看得出形狀
  noFill();
  stroke(230, 40, 40);
  strokeWeight(1.5);
  beginShape();
  for (const p of points) {
    vertex(p.x, p.y);
  }
  endShape();
  noStroke();
  fill(230, 40, 40);
  for (const p of points) {
    circle(p.x, p.y, 7);
  }

  // 滑鼠旁邊顯示座標
  fill(0);
  textSize(14);
  text(round(mouseX) + ', ' + round(mouseY), mouseX + 10, mouseY - 10);
}

function mousePressed() {
  const x = round(mouseX);
  const y = round(mouseY);
  points.push({ x: x, y: y });
  print('vertex(' + x + ', ' + y + ');'); // 到 console 複製這一行
}

// 按 C 清掉點過的點，重新點一組
function keyPressed() {
  if (key === 'c' || key === 'C') {
    points = [];
    print('// ---- 重新開始 ----');
  }
}
```

### MIDI 一行接上：鍵盤、螢幕上的控制器、手機都能彈

在草圖最上面貼一行 Open Audio Visual，就能用 MIDI 鍵盤（MiniLab 3、LPD8）演奏作品：knob1～knob8 像 mouseX 一樣直接讀，notePressed() 像 keyPressed() 一樣按了就叫。沒有鍵盤就按畫面角落的 🎹 用螢幕上的控制器，或 📱 用手機。草圖裡有 OPC.slider（像 3-0 粒子模板）的話，旋鈕會照順序自動接上那些拉桿。

把第一行貼在自己草圖的最上面，之後 knob1～knob8、fader1～fader4、pad1～pad8、notePressed(note, velocity)、noteIsDown(60) 都能用。草圖裡已經有 OPC.slider 的話（像 3-0 粒子模板），旋鈕 1 會接第一根拉桿、旋鈕 2 接第二根，不用改程式。

```js
// 第一行：Open Audio Visual 一行接上 MIDI。說明：https://openaudiovisual.com/zh/docs/#p5
// 接上 MIDI 鍵盤（Chrome 會問一次要不要允許），或按畫面角落的 🎹 用螢幕上的控制器、📱 用手機
document.head.prepend(Object.assign(document.createElement('script'), { src: 'https://openaudiovisual.com/p5/oav-p5.js' }));

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(20);
  noStroke();
}

function draw() {
  // 旋鈕 1、2 跟 mouseX 一樣直接讀，數值 0 到 1
  background(20, 10 + knob1 * 60);
  fill(255, 60);
  circle(width / 2, height / 2, 40 + knob2 * 400);
}

// 跟 keyPressed() 一樣：按下任何一個鍵（或打擊墊）就會叫它
// note 是哪個鍵（MIDI 編號，中央 C＝60），velocity 是多用力（0 到 1）
function notePressed(note, velocity) {
  fill(random(255), random(255), 255);
  circle(map(note, 36, 84, 0, width, true), random(height), 20 + velocity * 160);
}
```

## 畫布與視窗

### 按 F 全螢幕＋畫布跟著視窗

fullscreen() 切換全螢幕；視窗大小一變 p5 會呼叫 windowResized()，在裡面 resizeCanvas() 畫布就永遠填滿視窗。

把 createCanvas 改成 windowWidth、windowHeight，再貼上 keyPressed 和 windowResized；按 Esc 也能離開全螢幕。沒畫 background() 的筆刷作品，換尺寸時畫過的東西會被清掉。

```js
// 按 F 切換全螢幕；視窗大小一變，畫布跟著變
function setup() {
  createCanvas(windowWidth, windowHeight);
}

function draw() {
  background(15);
  // 位置和大小用 width、height 算，不要寫死數字：畫布怎麼變都會置中
  noFill();
  stroke(255);
  strokeWeight(3);
  circle(width / 2, height / 2, min(width, height) * 0.6);
}

function keyPressed() {
  if (key === 'f' || key === 'F') {
    fullscreen(!fullscreen()); // 現在不是全螢幕就打開，是就關掉
  }
}

// 視窗大小改變（包含進出全螢幕）時，p5 會自動呼叫這個函式
function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // 注意：resizeCanvas 會把畫布清空
}
```

## 時間與隨機

### 用時間當參數：秒數與 sin 呼吸

millis() ÷ 1000 就是開始到現在幾秒，丟進 sin() 再用 map() 換算成大小和亮度，圓就會像呼吸一樣一脹一縮。

在自己 draw() 的開頭加 const t = millis() / 1000;，想要會動的數值就寫成 sin(t * TWO_PI / 週期秒數)，再用 map() 換算到要的範圍。

```js
// 用時間當參數：millis() 是開始到現在的毫秒，除以 1000 就是秒
function setup() {
  createCanvas(600, 600);
}

function draw() {
  background(20);
  const t = millis() / 1000; // 秒：電腦快慢都一樣
  // const t = frameCount / 60; // 另一種：用第幾格換算，電腦卡的時候會跟著變慢

  // 4 秒呼吸一次：sin 在 -1 到 1 之間來回
  // （有寫 angleMode(DEGREES) 的話，TWO_PI 改成 360）
  const breath = sin((t * TWO_PI) / 4);
  const d = map(breath, -1, 1, 120, 360); // 直徑
  const bright = map(breath, -1, 1, 90, 255);

  noStroke();
  fill(bright, 180, 120);
  circle(width / 2, height / 2, d);

  fill(255);
  textSize(16);
  text('t = ' + t.toFixed(1) + ' 秒', 16, 30);
}
```

### 在畫面上印出日期時間

year()、month()、day()、hour()、minute()、second() 拿到現在的時間，用跟存檔檔名同一個 pad() 補零，印成時鐘。

貼上 pad、timestamp 兩個函式，在 draw() 裡用 text() 印出來；已經貼過存檔那段就不用再貼一次。

```js
// 在畫面上印出現在的日期時間（跟存檔檔名同一組補零函式）
function setup() {
  createCanvas(600, 300);
}

function draw() {
  background(15);
  fill(255);
  textAlign(CENTER, CENTER);
  const date = year() + ' / ' + pad(month()) + ' / ' + pad(day());
  const clock = pad(hour()) + ':' + pad(minute()) + ':' + pad(second());
  textSize(36);
  text(date, width / 2, height / 2 - 50);
  textSize(72);
  text(clock, width / 2, height / 2 + 30);
  textSize(14);
  fill(150);
  text('存檔檔名會是 sketch-' + timestamp() + '.jpg', width / 2, height - 24);
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

### 存檔名帶種子：喜歡的那張找得回來

按 R 換一組隨機、按 S 存 PNG，檔名記下種子；之後把 seed 設回那個數字，就能畫回同一張、再存一次高解析度。

draw() 第一行放 randomSeed(seed) 和 noiseSeed(seed)，存檔名加上 seed。找到喜歡的就把 seed 固定成檔名裡的數字，再配合 pixelDensity 存一張大圖。

```js
// 按 R 換一組、按 S 存檔：檔名記下種子，喜歡的那張永遠找得回來
let seed;

function setup() {
  createCanvas(600, 600);
  seed = floor(random(1000000));
  // seed = 123456; // 想畫回某一張：把檔名裡的數字填進來
  noLoop();
}

function draw() {
  randomSeed(seed);
  noiseSeed(seed); // 有用 noise() 也要固定
  background(250);
  noFill();
  stroke(20, 90);
  strokeWeight(1.5);
  for (let i = 0; i < 120; i++) {
    let x = random(width);
    let y = random(height);
    beginShape();
    for (let j = 0; j < 80; j++) {
      vertex(x, y);
      const a = noise(x * 0.005, y * 0.005) * TWO_PI * 2;
      x += cos(a) * 6;
      y += sin(a) * 6;
    }
    endShape();
  }
}

function keyPressed() {
  if (key === 'r' || key === 'R') {
    seed = floor(random(1000000));
    redraw();
  }
  if (key === 's' || key === 'S') {
    // 檔名像 sketch-seed48213-2026-09-29-1905-12.png
    saveCanvas('sketch-seed' + seed + '-' + timestamp(), 'png');
  }
}

// 補零：7 → "07"。檔名長度一致，資料夾裡才會照時間排好
function pad(n) {
  return String(n).padStart(2, '0');
}

// 現在的日期時間：2026-09-29-1905-12（年-月-日-時分-秒）
function timestamp() {
  return year() + '-' + pad(month()) + '-' + pad(day()) + '-' +
    pad(hour()) + pad(minute()) + '-' + pad(second());
}
```

## 顏色

### 色票陣列＋random(palette)

先挑好幾個顏色放進陣列，random(palette) 每次抽一個；整張作品只用這幾色，配色馬上就統一。

把 palette 那一行放在最上面，原本寫 fill(random(255), …) 的地方改成 fill(random(palette))。配色可以到 coolors.co 挑好，把色碼整串貼進陣列。

```js
// 色票：先挑好 5 個顏色放進陣列，random(palette) 每次抽一個
const palette = ['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'];

function setup() {
  createCanvas(600, 600);
  noLoop();
}

function draw() {
  background('#fdf6e3');
  noStroke();
  for (let i = 0; i < 80; i++) {
    fill(random(palette)); // 從陣列裡隨機抽一個顏色
    circle(random(width), random(height), random(20, 90));
  }
}

// 點一下畫布換一張
function mousePressed() {
  redraw();
}
```

### HSB 彩虹：色相跟著時間轉

colorMode(HSB) 之後第一個數字是色相，0 到 360 繞一圈就是彩虹；飽和度、亮度分開調，比 RGB 直覺。

在 setup() 裡加 colorMode(HSB, 360, 100, 100, 1)；之後所有 fill、stroke、background 的數字都照 HSB 解讀，原本的 RGB 顏色要跟著改。

```js
// HSB 色彩：色相 0–360 繞一圈就是彩虹
function setup() {
  createCanvas(600, 600);
  // 色相 0–360、飽和 0–100、亮度 0–100、透明 0–1
  colorMode(HSB, 360, 100, 100, 1);
  noStroke();
}

function draw() {
  background(0, 0, 10); // HSB 模式下的深灰
  for (let i = 0; i < 12; i++) {
    // 色相：每一格往前轉一點，% 360 讓它繞回 0
    const h = (frameCount + i * 30) % 360;
    fill(h, 80, 100);
    const a = (TWO_PI * i) / 12;
    circle(width / 2 + cos(a) * 200, height / 2 + sin(a) * 200, 70);
  }
}
```

## 除錯

### 左上角顯示 fps 與 frameCount

作品卡不卡看數字最準：frameRate() 是現在每秒幾格、frameCount 是第幾格，畫在 draw() 最後才會蓋在作品上面。

把 drawDebug() 貼進自己的 sketch，在 draw() 最後一行呼叫它；作品完成、要展示時把那一行刪掉就好。

```js
// 左上角顯示 fps 與 frameCount：動畫卡不卡，看數字最準
function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(30);
  // 你的作品畫在這裡（示範：很多個會動的圓）
  noStroke();
  fill(120, 200, 255, 120);
  for (let i = 0; i < 300; i++) {
    circle((i * 37 + frameCount) % width, (i * 53) % height, 20);
  }
  drawDebug(); // 放在 draw() 的最後，才會蓋在作品上面
}

function drawDebug() {
  push(); // 用 push／pop 包起來，不會改到作品的顏色、位置設定
  resetMatrix(); // 前面有 translate、rotate 也一樣畫在左上角
  noStroke();
  fill(0, 180);
  rect(0, 0, 150, 48);
  fill(255);
  textFont('monospace');
  textSize(13);
  textAlign(LEFT, TOP);
  text('fps   ' + round(frameRate()), 10, 8);
  text('frame ' + frameCount, 10, 26);
  pop();
}
```

### 滑鼠十字線＋座標

跟著滑鼠畫一條直線、一條橫線，旁邊寫出 (mouseX, mouseY)；想知道某個點的座標，移過去看一眼就知道。

把 drawCrosshair() 貼進自己的 sketch，在 draw() 最後一行呼叫它；對好位置、把數字抄進程式之後，刪掉那一行。

```js
// 滑鼠十字線：(mouseX, mouseY) 在畫布的哪裡，一眼看出來
function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(240);
  // 你的作品畫在這裡
  fill(255, 180, 0);
  noStroke();
  circle(300, 200, 120);
  drawCrosshair(); // 放在 draw() 的最後
}

function drawCrosshair() {
  push();
  resetMatrix();
  stroke(255, 0, 0, 160);
  strokeWeight(1);
  line(mouseX, 0, mouseX, height); // 直線：這條線上每一點的 x 都一樣
  line(0, mouseY, width, mouseY); // 橫線：這條線上每一點的 y 都一樣
  noStroke();
  fill(255, 0, 0);
  textSize(14);
  // 靠近右邊或上面時，字換到另一邊，才不會跑出畫布
  const right = mouseX > width - 110;
  const top = mouseY < 30;
  textAlign(right ? RIGHT : LEFT, top ? TOP : BOTTOM);
  const label = '(' + round(mouseX) + ', ' + round(mouseY) + ')';
  text(label, mouseX + (right ? -8 : 8), mouseY + (top ? 8 : -8));
  pop();
}
```

### 每秒在 console 印一次變數

print() 放在 draw() 裡一秒會洗掉 60 行；用 frameCount % 60 === 0 只在每 60 格印一次，看得清楚數值怎麼變。

把 if (frameCount % 60 === 0) { print(…); } 放進自己的 draw()，括號裡換成想看的變數；OpenProcessing 的 console 在畫布下方。

```js
// 每秒在 console 印一次：print() 直接放在 draw() 裡，一秒會洗掉 60 行
let x = 0;

function setup() {
  createCanvas(600, 400);
}

function draw() {
  background(30);
  x = (x + 2.5) % width;
  fill(255);
  circle(x, height / 2, 40);

  if (frameCount % 60 === 0) {
    // 每 60 格（大約一秒）才印一次
    // print() 一次放一個值：要印好幾個，用 + 接成一串（p5 2.x 放兩個以上會跳警告）
    print('第 ' + frameCount + ' 格　x = ' + x.toFixed(1));
  }
}
```

### 麥克風測試

點一下畫面開始，畫一條跟著聲音跳的音量條；沒聲音時畫面上直接寫三個檢查點（瀏覽器權限、系統麥克風、OpenProcessing 要點進作品頁放大看，或改用 p5 網頁編輯器）。第 4 堂開播前先點開來測一次。

貼進 OpenProcessing 或 p5 網頁編輯器，點一下畫面開始；看到條跟著聲音跳就 OK。條不跳時，畫面上的三個檢查點照著查。這支只測瀏覽器拿不拿得到麥克風，第 4 堂的示範是同一個權限。

```js
// 麥克風測試：點一下畫面開始（瀏覽器規定要使用者點一下，才會問你能不能用麥克風）
// 看到下面的條跟著聲音跳，麥克風就 OK。沒用 p5.sound，只用瀏覽器內建的 Web Audio，所以 p5 1.x、2.x 都能跑。
let audioCtx;
let analyser;
let samples;
let state = 'idle'; // idle 還沒點｜asking 等你按「允許」｜ok 拿到麥克風｜fail 沒拿到
let reason = ''; // 沒拿到的原因（瀏覽器回報的名字）
let level = 0; // 現在的音量（0 到 1，已平滑）
let peak = 0; // 最近的最大音量，會慢慢往下掉
let heardAt = 0; // 最後一次聽到聲音的時間（毫秒）

function setup() {
  createCanvas(600, 400);
  textWrap(CHAR); // 中文沒有空白，要用「逐字換行」text() 的寬度才會生效
  textSize(18);
}

function draw() {
  background(20);
  noStroke();
  fill(240);
  textAlign(CENTER, CENTER);

  if (state === 'idle') {
    textSize(24);
    text('點一下畫面開始測試麥克風', width / 2, height / 2);
    return;
  }
  if (state === 'asking') {
    text('請在瀏覽器跳出的視窗按「允許」', width / 2, height / 2);
    return;
  }
  if (state === 'fail') {
    textSize(24);
    text('沒有拿到麥克風', width / 2, 50);
    textSize(14);
    fill(160);
    text('原因：' + reason + '　（點一下畫面再試一次）', width / 2, 84);
    drawChecklist(120);
    return;
  }

  // state === 'ok'：量音量（RMS），畫成一條橫的條
  analyser.getFloatTimeDomainData(samples);
  let sum = 0;
  for (let i = 0; i < samples.length; i++) {
    sum += samples[i] * samples[i];
  }
  level = lerp(level, sqrt(sum / samples.length), 0.3);
  peak = max(peak * 0.985, level);
  if (level > 0.02) {
    heardAt = millis();
  }

  const barW = map(constrain(level, 0, 0.3), 0, 0.3, 0, 500);
  const peakX = 50 + map(constrain(peak, 0, 0.3), 0, 0.3, 0, 500);
  textSize(20);
  text('對著麥克風說話　看到條在跳就 OK', width / 2, 70);
  noFill();
  stroke(90);
  rect(50, 110, 500, 44);
  noStroke();
  fill(110, 220, 140);
  rect(50, 110, barW, 44);
  fill(255, 210, 90);
  rect(peakX - 2, 104, 4, 56);

  // 麥克風開了，但連續 3 秒都沒聽到聲音：條不會跳，把檢查點列出來
  if (millis() - heardAt > 3000) {
    fill(255, 210, 90);
    textSize(18);
    text('條沒有跳？照下面三點查', width / 2, 200);
    drawChecklist(225);
  }
}

// 三個檢查點（一格一行，textWrap 會自己折行）
function drawChecklist(y) {
  textAlign(LEFT, TOP);
  textSize(15);
  fill(230);
  text('1. 瀏覽器權限：網址列左邊的鎖頭或麥克風圖示，把麥克風改成「允許」，重新整理再點一次。', 40, y, 520);
  text('2. 系統麥克風：電腦的系統設定、隱私權、麥克風，這個瀏覽器要打開；輸入裝置選對（筆電內建或你的麥克風）。', 40, y + 46, 520);
  text('3. OpenProcessing：作品要點進全螢幕（或開新分頁）才會給麥克風；還是不行，就把程式貼到 p5 網頁編輯器（editor.p5js.org）。', 40, y + 92, 520);
}

function mousePressed() {
  if (state === 'idle' || state === 'fail') {
    startMic();
  }
}

function startMic() {
  state = 'asking';
  reason = '';
  const AC = window.AudioContext || window.webkitAudioContext;
  if (!AC || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    state = 'fail';
    reason = '這個瀏覽器或這個頁面不能用麥克風';
    return;
  }
  // AudioContext 要在「點一下」這一刻就建（Safari、手機規定），拿到麥克風之後才接上去
  audioCtx = audioCtx || new AC();
  audioCtx.resume();
  navigator.mediaDevices.getUserMedia({ audio: true })
    .then(function (stream) {
      analyser = audioCtx.createAnalyser();
      analyser.fftSize = 1024;
      samples = new Float32Array(analyser.fftSize);
      audioCtx.createMediaStreamSource(stream).connect(analyser);
      heardAt = millis();
      state = 'ok';
    })
    .catch(function (err) {
      state = 'fail';
      reason = err && err.name ? err.name : '不明';
    });
}
```

[到 Hahow 報名](https://hahow.in/cr/creative-code)。價格與優惠以 Hahow 頁面為準。
