前言
到目前為止,我們的作品大多是「自己動」的——粒子靠重力移動,流場靠 noise 驅動,碎形靠遞迴生成。但創意程式設計還有一個讓人興奮的維度:互動。
當觀眾的動作能即時影響畫面,作品就不再只是靜態的展示,而是變成了一場對話。滑鼠一動,線條跟著飄;手在 webcam 前揮舞,像素跟著炸開。這種即時反饋帶來的驚喜和樂趣,是生成式藝術獨有的魅力。
p5.js 在互動方面特別強大——它把瀏覽器的各種輸入介面封裝成簡單的變數和函數,讓你幾乎不需要處理任何底層事件就能建立豐富的互動體驗。
這篇文章會帶你探索三種輸入源:滑鼠、鍵盤、以及 webcam 鏡頭,並用它們來驅動即時的視覺效果。
mouseX / mouseY 互動
p5.js 內建的 mouseX 和 mouseY 是最基礎的互動工具。它們每一幀都會更新為滑鼠的當前座標。
基礎:滑鼠繪圖
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);
}
}
pmouseX 和 pmouseY 是上一幀的滑鼠位置——利用當前位置與前一幀位置的距離,我們可以判斷滑鼠移動的速度。移動越快,筆觸越細;移動越慢,筆觸越粗。這模擬了毛筆的行為。
滑鼠驅動的粒子場
讓粒子被滑鼠排斥(像磁場一樣):
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) {
// 減少筆刷大小
}
}
注意 key 和 keyCode 的區別:
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 的映射是反向的(width 到 0)——這是為了鏡像效果,讓畫面像照鏡子一樣直覺。
像素化 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');
}
小結
互動是創意程式設計的靈魂之一。今天我們探索了三種輸入方式:
- 滑鼠互動:位置追蹤、速度計算、排斥力場
- 鍵盤控制:模式切換、參數調整、即時按鍵偵測
- Webcam 鏡頭:像素操作、ASCII 化、動態偵測、像素化效果
把這些輸入源組合起來,你可以做出非常豐富的互動裝置藝術。想像一下:一面牆上的投影,路人走過時畫面會產生漣漪——這就是互動藝術的魅力。
延伸閱讀
- p5.js 官方文件中的 Events 章節 — 完整的事件 API 列表
- Kyle McDonald 的 webcam 互動藝術作品集
- Daniel Shiffman, Coding Train: “Video and Pixels” 系列教學
- 《Interactive Art and Embodiment》— 探討互動藝術的理論框架
- ml5.js — 在 p5.js 中加入機器學習,實現手勢辨識、姿態偵測等進階互動