前言

到目前為止,我們的作品大多是「自己動」的——粒子靠重力移動,流場靠 noise 驅動,碎形靠遞迴生成。但創意程式設計還有一個讓人興奮的維度:互動

當觀眾的動作能即時影響畫面,作品就不再只是靜態的展示,而是變成了一場對話。滑鼠一動,線條跟著飄;手在 webcam 前揮舞,像素跟著炸開。這種即時反饋帶來的驚喜和樂趣,是生成式藝術獨有的魅力。

p5.js 在互動方面特別強大——它把瀏覽器的各種輸入介面封裝成簡單的變數和函數,讓你幾乎不需要處理任何底層事件就能建立豐富的互動體驗。

這篇文章會帶你探索三種輸入源:滑鼠、鍵盤、以及 webcam 鏡頭,並用它們來驅動即時的視覺效果。


mouseX / mouseY 互動

p5.js 內建的 mouseXmouseY 是最基礎的互動工具。它們每一幀都會更新為滑鼠的當前座標。

基礎:滑鼠繪圖

function setup() {
  createCanvas(800, 600);
  background(30, 30, 40);
}

function draw() { // 滑鼠按下時繪製 if (mouseIsPressed) { let brushSize = map( dist(mouseX, mouseY, pmouseX, pmouseY), 0, 50, 20, 2 );

noStroke(); fill(255, 100, 100, 100); ellipse(mouseX, mouseY, brushSize); } }

pmouseXpmouseY 是上一幀的滑鼠位置——利用當前位置與前一幀位置的距離,我們可以判斷滑鼠移動的速度。移動越快,筆觸越細;移動越慢,筆觸越粗。這模擬了毛筆的行為。

滑鼠驅動的粒子場

讓粒子被滑鼠排斥(像磁場一樣):

let particles = [];

function setup() { createCanvas(800, 600); for (let i = 0; i < 500; i++) { particles.push({ pos: createVector(random(width), random(height)), vel: createVector(0, 0), home: createVector(random(width), random(height)), size: random(3, 8) }); } }

function draw() { background(30, 30, 40, 30); let mouse = createVector(mouseX, mouseY);

for (let p of particles) { // 被滑鼠排斥 let d = dist(p.pos.x, p.pos.y, mouse.x, mouse.y); if (d < 150) { let repel = p5.Vector.sub(p.pos, mouse); repel.normalize(); repel.mult(map(d, 0, 150, 5, 0)); p.vel.add(repel); }

// 回到原始位置的力 let homeForce = p5.Vector.sub(p.home, p.pos); homeForce.mult(0.02); p.vel.add(homeForce);

// 阻尼 p.vel.mult(0.9); p.pos.add(p.vel);

// 繪製 let alpha = map(d, 0, 150, 255, 100); fill(100, 180, 255, alpha); noStroke(); ellipse(p.pos.x, p.pos.y, p.size); } }

每個粒子有一個「家」的位置(home),會被一股彈簧般的力拉回去。同時,滑鼠靠近時會產生排斥力把粒子推開。兩股力的平衡創造了一種有機的互動感。

滑鼠速度追蹤

更進階的互動會用到滑鼠的速度和加速度:

let mouseVel;
let prevMouse;

function setup() { createCanvas(800, 600); mouseVel = createVector(0, 0); prevMouse = createVector(0, 0); background(30, 30, 40); }

function draw() { // 計算滑鼠速度 let currentMouse = createVector(mouseX, mouseY); mouseVel = p5.Vector.sub(currentMouse, prevMouse); prevMouse = currentMouse.copy();

let speed = mouseVel.mag();

if (speed > 1) { // 在滑鼠位置噴射粒子,方向垂直於滑鼠移動方向 let perpendicular = createVector(-mouseVel.y, mouseVel.x).normalize();

for (let i = 0; i < 3; i++) { let offset = p5.Vector.mult(perpendicular, random(-20, 20)); let pos = p5.Vector.add(currentMouse, offset);

noStroke(); fill( map(speed, 0, 30, 100, 255), map(speed, 0, 30, 200, 50), 100, 150 ); ellipse(pos.x, pos.y, random(2, 6)); } }

// 慢慢淡化 noStroke(); fill(30, 30, 40, 5); rect(0, 0, width, height); }


鍵盤事件

鍵盤互動適合用來控制模式切換、參數調整、或觸發特定效果。

keyPressed 與 keyTyped

let mode = 0;
let brushColor;
let particles = [];

function setup() { createCanvas(800, 600); background(30, 30, 40); brushColor = color(255, 100, 100); }

function draw() { // 半透明背景淡化 fill(30, 30, 40, 10); noStroke(); rect(0, 0, width, height);

// 根據模式繪製 switch (mode) { case 0: drawMode0(); break; // 圓形筆刷 case 1: drawMode1(); break; // 線條筆刷 case 2: drawMode2(); break; // 噴濺筆刷 }

// UI 提示 fill(200); textSize(14); text('Mode: ' + ['Circle', 'Line', 'Spray'][mode], 20, 30); text('Press 1/2/3 to switch mode, C to clear', 20, 50); text('Press R/G/B to change color', 20, 70); }

function drawMode0() { if (mouseIsPressed) { fill(brushColor); noStroke(); ellipse(mouseX, mouseY, 20); } }

function drawMode1() { if (mouseIsPressed) { stroke(brushColor); strokeWeight(2); line(pmouseX, pmouseY, mouseX, mouseY); } }

function drawMode2() { if (mouseIsPressed) { for (let i = 0; i < 10; i++) { let angle = random(TWO_PI); let radius = random(30); let x = mouseX + cos(angle) * radius; let y = mouseY + sin(angle) * radius; let size = random(1, 4);

noStroke(); fill(red(brushColor), green(brushColor), blue(brushColor), 150); ellipse(x, y, size); } } }

function keyPressed() { if (key === '1') mode = 0; if (key === '2') mode = 1; if (key === '3') mode = 2;

if (key === 'c' || key === 'C') { background(30, 30, 40); }

if (key === 'r' || key === 'R') brushColor = color(255, 100, 100); if (key === 'g' || key === 'G') brushColor = color(100, 255, 100); if (key === 'b' || key === 'B') brushColor = color(100, 150, 255);

// 方向鍵也可以用 if (keyCode === UP_ARROW) { // 增加筆刷大小 } if (keyCode === DOWN_ARROW) { // 減少筆刷大小 } }

注意 keykeyCode 的區別:

  • key:字元值(’a’, ‘b’, ‘1’ 等)
  • keyCode:鍵碼(UP_ARROW, LEFT_ARROW, ENTER 等特殊鍵)

即時鍵盤偵測

keyIsDown() 可以在 draw() 中即時檢測某個鍵是否按著,適合做持續性的控制:

let player;

function setup() { createCanvas(800, 600); player = { x: width / 2, y: height / 2, speed: 4 }; }

function draw() { background(30, 30, 40);

// WASD 控制 if (keyIsDown(87)) player.y -= player.speed; // W if (keyIsDown(83)) player.y += player.speed; // S if (keyIsDown(65)) player.x -= player.speed; // A if (keyIsDown(68)) player.x += player.speed; // D

// 包裹邊界 player.x = (player.x + width) % width; player.y = (player.y + height) % height;

fill(255, 100, 100); noStroke(); ellipse(player.x, player.y, 30);

// 留下軌跡 fill(255, 100, 100, 30); ellipse(player.x, player.y, 60); }


Webcam 驅動的視覺

這是最令人興奮的部分。p5.js 的 createCapture() 讓你用幾行程式碼就能接入 webcam。

基礎 webcam 顯示

let capture;

function setup() { createCanvas(640, 480); capture = createCapture(VIDEO); capture.size(640, 480); capture.hide(); // 隱藏原始 <video> 元素 }

function draw() { image(capture, 0, 0, width, height); }

capture.hide() 很重要——createCapture 會在 DOM 裡建立一個 <video> 元素,如果不隱藏它,你會看到兩個影像(一個是原始的 video 標籤,一個是 canvas 上畫的)。

像素操作:ASCII 藝術

把 webcam 畫面轉成 ASCII 字元:

let capture;
const density = ' .:-=+*#%@';

function setup() { createCanvas(800, 600); capture = createCapture(VIDEO); capture.size(160, 120); // 低解析度 capture.hide(); textFont('monospace'); textSize(10); textAlign(CENTER, CENTER); }

function draw() { background(0); capture.loadPixels();

let cellW = width / capture.width; let cellH = height / capture.height;

for (let y = 0; y < capture.height; y++) { for (let x = 0; x < capture.width; x++) { let index = (x + y capture.width) 4; let r = capture.pixels[index]; let g = capture.pixels[index + 1]; let b = capture.pixels[index + 2];

let brightness = (r + g + b) / 3; let charIndex = floor(map(brightness, 0, 255, density.length - 1, 0)); let ch = density[charIndex];

fill(r, g, b); noStroke(); text(ch, x cellW + cellW / 2, y cellH + cellH / 2); } } }

動態偵測:畫面差異

比較前後幀的差異,偵測到動作的區域才有視覺反應:

let capture;
let prevFrame;

function setup() { createCanvas(640, 480); capture = createCapture(VIDEO); capture.size(320, 240); capture.hide(); prevFrame = createImage(320, 240); }

function draw() { background(30, 30, 40); capture.loadPixels(); prevFrame.loadPixels();

let threshold = 50;

for (let y = 0; y < capture.height; y += 4) { for (let x = 0; x < capture.width; x += 4) { let index = (x + y capture.width) 4;

let r = capture.pixels[index]; let g = capture.pixels[index + 1]; let b = capture.pixels[index + 2];

let pr = prevFrame.pixels[index]; let pg = prevFrame.pixels[index + 1]; let pb = prevFrame.pixels[index + 2];

// 計算像素差異 let diff = abs(r - pr) + abs(g - pg) + abs(b - pb);

if (diff > threshold) { // 有動態的區域 → 畫亮點 let screenX = map(x, 0, capture.width, width, 0); // 鏡像 let screenY = map(y, 0, capture.height, 0, height);

fill(r, g, b, 200); noStroke(); let size = map(diff, threshold, 300, 4, 20); ellipse(screenX, screenY, size); } } }

// 儲存當前幀作為下一幀的比較基準 prevFrame.copy(capture, 0, 0, capture.width, capture.height, 0, 0, prevFrame.width, prevFrame.height); }

注意 screenX 的映射是反向的(width0)——這是為了鏡像效果,讓畫面像照鏡子一樣直覺。

像素化 webcam 效果

把 webcam 畫面用圓形像素重新繪製:

let capture;

function setup() { createCanvas(800, 600); capture = createCapture(VIDEO); capture.size(80, 60); capture.hide(); noStroke(); }

function draw() { background(0); capture.loadPixels();

let cellW = width / capture.width; let cellH = height / capture.height;

for (let y = 0; y < capture.height; y++) { for (let x = 0; x < capture.width; x++) { let index = (x + y capture.width) 4; let r = capture.pixels[index]; let g = capture.pixels[index + 1]; let b = capture.pixels[index + 2]; let bright = (r + g + b) / 3;

let size = map(bright, 0, 255, 1, cellW * 0.9);

// 鏡像 let screenX = width - (x * cellW + cellW / 2); let screenY = y * cellH + cellH / 2;

fill(r, g, b); ellipse(screenX, screenY, size); } } }


組合多種輸入

最強大的互動作品往往會組合多種輸入。比如:webcam 偵測動態,同時滑鼠控制效果的參數,鍵盤切換模式。

let capture;
let effectMode = 0;
let effectIntensity = 0.5;

function setup() { createCanvas(800, 600); capture = createCapture(VIDEO); capture.size(160, 120); capture.hide(); }

function draw() { // 滑鼠 Y 控制效果強度 effectIntensity = map(mouseY, 0, height, 0, 1);

switch (effectMode) { case 0: pixelateEffect(); break; case 1: asciiEffect(); break; case 2: motionEffect(); break; }

// UI fill(255); textSize(14); text('Mode: ' + effectMode + ' | Intensity: ' + nf(effectIntensity, 1, 2), 10, 20); }

function keyPressed() { if (key === '1') effectMode = 0; if (key === '2') effectMode = 1; if (key === '3') effectMode = 2; if (key === 's' || key === 'S') saveCanvas('webcam-art', 'png'); }


小結

互動是創意程式設計的靈魂之一。今天我們探索了三種輸入方式:

  1. 滑鼠互動:位置追蹤、速度計算、排斥力場
  2. 鍵盤控制:模式切換、參數調整、即時按鍵偵測
  3. Webcam 鏡頭:像素操作、ASCII 化、動態偵測、像素化效果

把這些輸入源組合起來,你可以做出非常豐富的互動裝置藝術。想像一下:一面牆上的投影,路人走過時畫面會產生漣漪——這就是互動藝術的魅力。

延伸閱讀

  • p5.js 官方文件中的 Events 章節 — 完整的事件 API 列表
  • Kyle McDonald 的 webcam 互動藝術作品集
  • Daniel Shiffman, Coding Train: “Video and Pixels” 系列教學
  • 《Interactive Art and Embodiment》— 探討互動藝術的理論框架
  • ml5.js — 在 p5.js 中加入機器學習,實現手勢辨識、姿態偵測等進階互動