前言

你花了好幾個小時打磨出一個美麗的生成式藝術作品,在瀏覽器裡跑得很漂亮。然後呢?你想分享到 Instagram、Twitter,或是做成作品集放在個人網站上。但瀏覽器裡的 canvas 要怎麼變成一張高品質的 PNG,或是一段流暢的 MP4 影片?

這就是今天要解決的問題。

p5.js 內建了 save()saveFrames() 函數,可以應付簡單的截圖需求。但如果你要錄製一段動畫、輸出 GIF、或是做成影片,就需要借助 CCapture.js 這個強大的函式庫。

我自己在做作品集的時候踩過不少坑——圖片解析度不對、影片掉幀、GIF 檔案太大……這篇文章會把我學到的經驗都整理出來,讓你少走彎路。


save():截取單張圖片

最簡單的方式——按下某個鍵,把當前畫面存成圖片:

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

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

// 你的藝術作品... for (let i = 0; i < 100; i++) { let x = width / 2 + cos(i 0.1 + frameCount 0.02) (100 + i 2); let y = height / 2 + sin(i 0.15 + frameCount 0.02) (80 + i 1.5); let size = map(i, 0, 100, 10, 2);

fill(100 + i * 1.5, 150, 255 - i, 180); noStroke(); ellipse(x, y, size); } }

function keyPressed() { if (key === 's' || key === 'S') { save('my-artwork.png'); } }

save() 可以輸出 PNG 或 JPG:

save('artwork.png');   // PNG(無損壓縮,支援透明度)
save('artwork.jpg');   // JPG(有損壓縮,檔案較小)

輸出高解析度圖片

Canvas 的預設解析度等於你設定的 widthheight。如果要輸出印刷品質的圖片,你需要更高的解析度:

function setup() {
  // 高解析度 canvas
  createCanvas(3200, 2400);  // 4x 倍數
  pixelDensity(1);           // 確保不受 Retina 影響
  noLoop();
}

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

// 所有的 size 和 position 也要相應放大 let scale = 4; // 放大倍數

for (let i = 0; i < 500; i++) { let x = width / 2 + cos(i 0.05) (200 scale + i scale); let y = height / 2 + sin(i 0.08) (150 scale + i 0.8 * scale); let size = map(i, 0, 500, 20 scale, 2 scale);

fill(100 + i 0.3, 150, 255 - i 0.5, 180); noStroke(); ellipse(x, y, size); }

// 存檔 save('high-res-artwork.png'); }

另一個更聰明的做法是用 pixelDensity() 來提高解析度,而不改變 canvas 的邏輯大小:

function setup() {
  pixelDensity(4);  // 4x 解析度
  createCanvas(800, 600);
  // canvas 邏輯上還是 800x600,但實際輸出是 3200x2400
}

這樣你的所有座標計算都不需要改。不過要注意,pixelDensity(4) 會讓所有的像素操作(loadPixels() 等)變慢四倍。


saveFrames():快速存多張圖

如果你想存一段動畫的每一幀:

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

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

let t = frameCount * 0.05; for (let i = 0; i < 50; i++) { let x = width / 2 + cos(t + i 0.2) (100 + i * 3); let y = height / 2 + sin(t + i 0.3) (80 + i * 2);

fill(100 + i 3, 150, 255 - i 3, 180); noStroke(); ellipse(x, y, 10); } }

function keyPressed() { if (key === 'r' || key === 'R') { // 存 60 幀(2 秒),PNG 格式 saveFrames('frame', 'png', 2, 30); // saveFrames(檔名前綴, 格式, 持續秒數, fps) } }

saveFrames() 會觸發瀏覽器下載大量檔案,對於超過幾秒的動畫並不實用。這時候就需要 CCapture.js。


CCapture.js 設定

CCapture.js 是一個專門為 canvas 動畫設計的錄影函式庫。它的原理是:每一幀暫停真實時間,等 canvas 完成繪製後再抓取畫面,確保不會掉幀。

安裝

在 HTML 中引入:

<script src="https://cdn.jsdelivr.net/npm/ccapture.js@1.1.0/build/CCapture.all.min.js"></script>

基本使用

let capturer;
let isRecording = false;

function setup() { createCanvas(800, 600); frameRate(30);

// 建立 capturer capturer = new CCapture({ format: 'webm', // 輸出格式 framerate: 30, // 幀率 quality: 95, // 品質 (0-100) name: 'my-animation', // 檔案名稱 verbose: true // 顯示除錯資訊 }); }

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

// 你的動畫... let t = frameCount * 0.03; for (let i = 0; i < 80; i++) { let angle = t + i * TWO_PI / 80; let r = 150 + sin(angle 3 + t) 50; let x = width / 2 + cos(angle) * r; let y = height / 2 + sin(angle) * r;

let hue = map(i, 0, 80, 0, 360); colorMode(HSB, 360, 100, 100); fill(hue, 80, 90, 80); noStroke(); ellipse(x, y, 8); colorMode(RGB); }

// 如果正在錄影,抓取這一幀 if (isRecording) { capturer.capture(document.getElementById('defaultCanvas0')); } }

function keyPressed() { if (key === 'r' || key === 'R') { if (!isRecording) { // 開始錄影 capturer.start(); isRecording = true; console.log('Recording started...'); } else { // 停止錄影並儲存 capturer.stop(); capturer.save(); isRecording = false; console.log('Recording saved!'); } } }

錄製固定長度的循環動畫

很多生成式藝術作品是循環的——動畫結束時剛好回到起始狀態。這種情況下,我們可以精確控制錄製的幀數:

let capturer;
let totalFrames = 300;  // 300 幀 @ 30fps = 10 秒
let currentFrame = 0;
let isRecording = false;

function setup() { createCanvas(800, 600); frameRate(30);

capturer = new CCapture({ format: 'webm', framerate: 30, quality: 95, name: 'loop-animation' }); }

function draw() { // 用 progress (0~1) 取代 frameCount let progress = currentFrame / totalFrames;

background(30, 30, 40);

// 利用 progress 來確保動畫完美循環 let t = progress * TWO_PI;

for (let i = 0; i < 60; i++) { let angle = i * TWO_PI / 60; let r = 150 + sin(angle 4 + t) 80; let x = width / 2 + cos(angle) * r; let y = height / 2 + sin(angle) * r; let size = 5 + sin(angle 6 + t 2) * 3;

let hue = (i 6 + progress 360) % 360; colorMode(HSB, 360, 100, 100); fill(hue, 80, 90); noStroke(); ellipse(x, y, size); colorMode(RGB); }

// 錄製邏輯 if (isRecording) { capturer.capture(document.getElementById('defaultCanvas0')); currentFrame++;

if (currentFrame >= totalFrames) { capturer.stop(); capturer.save(); isRecording = false; noLoop(); console.log('Recording complete!'); } } }

function keyPressed() { if (key === 'r' || key === 'R') { currentFrame = 0; isRecording = true; capturer.start(); console.log('Recording ' + totalFrames + ' frames...'); } }

關鍵技巧:用 progress(0 到 1)乘以 TWO_PI 來控制動畫。這樣當 progress 從 0 走到 1,sin()cos() 剛好完成一個完整的循環。


GIF 輸出

GIF 非常適合社群分享。CCapture.js 支援 GIF 格式,但需要額外引入 gif.js:

<script src="https://cdn.jsdelivr.net/npm/ccapture.js@1.1.0/build/CCapture.all.min.js"></script>
let capturer;

function setup() { createCanvas(400, 400); // GIF 建議用小尺寸 frameRate(24);

capturer = new CCapture({ format: 'gif', workersPath: './', // gif.worker.js 的路徑 framerate: 12, // GIF 幀率通常較低 quality: 10, // gif.js 的品質(數字越小越好) name: 'my-gif' }); }

GIF 的注意事項:

  1. 尺寸要小:400×400 是不錯的上限。越大,檔案越巨大
  2. 幀率要低:12-15 fps 對 GIF 來說就夠了
  3. 顏色限制:GIF 只支援 256 色,複雜漸層會出現色帶
  4. 時長要短:3-10 秒是理想長度

替代方案:p5.js 內建的 saveGif

p5.js 較新的版本內建了 saveGif() 函數:

function keyPressed() {
  if (key === 'g' || key === 'G') {
    saveGif('my-animation', 5);  // 錄 5 秒
  }
}

這比 CCapture.js 更簡單,但自訂選項較少。


MP4 輸出工作流程

CCapture.js 的 webm 格式已經是影片了,但如果你需要 MP4(相容性更好),有兩種做法:

方法一:存成圖片序列,用 FFmpeg 合併

// p5.js 端:存每一幀為 PNG
let frameNum = 0;

function draw() { // 你的動畫...

if (isRecording && frameNum < totalFrames) { let filename = 'frame-' + nf(frameNum, 4); // frame-0001, frame-0002... saveCanvas(filename, 'png'); frameNum++; } }

然後用命令列的 FFmpeg 合併:

# 圖片序列 → MP4
ffmpeg -framerate 30 -i frame-%04d.png -c:v libx264 -pix_fmt yuv420p -crf 18 output.mp4

# 加入音樂 ffmpeg -framerate 30 -i frame-%04d.png -i music.mp3 -c:v libx264 -c:a aac -shortest output.mp4

# 圖片序列 → 高品質 GIF ffmpeg -framerate 30 -i frame-%04d.png -vf "fps=15,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" output.gif

FFmpeg 的參數說明:

  • -crf 18:品質(0 最好,23 預設,18 非常好)
  • -pix_fmt yuv420p:確保大多數播放器能播
  • fps=15:GIF 的幀率
  • scale=480:-1:縮放寬度為 480,高度自動

方法二:用 MediaRecorder API

瀏覽器原生的錄製 API,不需要額外函式庫:

let mediaRecorder;
let recordedChunks = [];
let isRecording = false;

function startRecording() { let canvas = document.getElementById('defaultCanvas0'); let stream = canvas.captureStream(30); // 30 fps

mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9', videoBitsPerSecond: 5000000 // 5 Mbps });

mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) { recordedChunks.push(e.data); } };

mediaRecorder.onstop = function() { let blob = new Blob(recordedChunks, { type: 'video/webm' }); let url = URL.createObjectURL(blob); let a = document.createElement('a'); a.href = url; a.download = 'recording.webm'; a.click();

recordedChunks = []; URL.revokeObjectURL(url); };

mediaRecorder.start(); isRecording = true; console.log('Recording started'); }

function stopRecording() { mediaRecorder.stop(); isRecording = false; console.log('Recording stopped'); }

function keyPressed() { if (key === 'r' || key === 'R') { if (!isRecording) { startRecording(); } else { stopRecording(); } } }

MediaRecorder 的好處是不需要額外函式庫,但它是「即時錄製」的——如果動畫很複雜、幀率跟不上,影片就會掉幀。CCapture.js 不會有這個問題,因為它會暫停真實時間。


作品集建議

最後,分享一些建立生成式藝術作品集的經驗:

輸出格式選擇

| 用途 | 推薦格式 | 說明 |
|——|———|——|
| 社群分享 | GIF / MP4 | GIF 自動播放但檔案大;MP4 品質好但需要點擊播放 |
| 印刷/展覽 | PNG (高解析度) | 至少 300 DPI,建議 4000×4000 以上 |
| 個人網站 | WebM / MP4 | WebM 檔案小,MP4 相容性好 |
| NFT 平台 | PNG / MP4 | 依平台要求而定 |

作品集的呈現技巧

  1. 每件作品多個版本:同一個程式,不同的隨機種子產生不同的結果
function setup() {
  // 用固定種子確保可重現
  randomSeed(42);
  noiseSeed(42);

createCanvas(800, 800); noLoop(); }

function generateVariations() { let seeds = [42, 137, 256, 512, 1024]; for (let seed of seeds) { randomSeed(seed); noiseSeed(seed); redraw(); save('variation-' + seed + '.png'); } }

  1. 固定隨機種子:讓你的作品可以被精確重現
let seed;

function setup() { createCanvas(800, 800); seed = floor(random(100000)); console.log('Seed:', seed); }

function draw() { randomSeed(seed); noiseSeed(seed);

background(30, 30, 40); // 你的藝術作品...

// 種子不變,畫面就不變 }

function keyPressed() { if (key === 'n' || key === 'N') { seed = floor(random(100000)); console.log('New seed:', seed); } if (key === 's' || key === 'S') { save('artwork-seed-' + seed + '.png'); } }

  1. 記錄你的程式碼:作品集不只是圖片,也要附上程式碼。很多生成式藝術的觀眾對「怎麼做的」跟「做了什麼」一樣感興趣。
  1. 製作過程影片:錄一段從空白畫布到完成作品的過程。用 screen recorder 錄你寫程式和調參數的過程,這種「幕後花絮」在社群上往往比最終作品更受歡迎。

展示平台推薦

  • OpenProcessing — p5.js 社群平台,可以直接執行 sketch
  • fxhash — Tezos 鏈上的生成式藝術 NFT 平台
  • ArtBlocks — 以太坊上的頂級生成式藝術平台
  • Instagram — 用短影片展示動態作品
  • 個人網站 — 用 GitHub Pages 或 Netlify 免費託管

完整的錄製工作流程範本

最後提供一個我自己用的完整錄製工作流程:

// === 設定區 ===
const CANVAS_W = 800;
const CANVAS_H = 800;
const FPS = 30;
const DURATION = 10;  // 秒
const TOTAL_FRAMES = FPS * DURATION;
const RECORD_ON_START = false;

let capturer; let currentFrame = 0; let isRecording = false;

function setup() { createCanvas(CANVAS_W, CANVAS_H); frameRate(FPS);

capturer = new CCapture({ format: 'webm', framerate: FPS, quality: 95, name: 'artwork-' + year() + month() + day() });

if (RECORD_ON_START) { startRecording(); } }

function draw() { let progress = isRecording ? currentFrame / TOTAL_FRAMES : (frameCount % TOTAL_FRAMES) / TOTAL_FRAMES;

// === 你的藝術作品 === renderArtwork(progress); // ====================

// 錄製 if (isRecording) { capturer.capture(document.getElementById('defaultCanvas0')); currentFrame++;

// 進度顯示 let pct = floor(currentFrame / TOTAL_FRAMES * 100); document.title = 'Recording... ' + pct + '%';

if (currentFrame >= TOTAL_FRAMES) { stopRecording(); } }

// UI(錄製時不顯示) if (!isRecording) { fill(200); noStroke(); textSize(12); text('Press R to record | S to save frame', 10, height - 20); } }

function renderArtwork(progress) { background(30, 30, 40); let t = progress * TWO_PI;

// 你的作品程式碼放這裡 for (let i = 0; i < 100; i++) { let angle = i * TWO_PI / 100 + t; let r = 200 + sin(angle 5 + t 3) * 80; let x = width / 2 + cos(angle) * r; let y = height / 2 + sin(angle) * r;

colorMode(HSB, 360, 100, 100); fill((i 3.6 + progress 360) % 360, 70, 90, 80); noStroke(); ellipse(x, y, 6); colorMode(RGB); } }

function startRecording() { currentFrame = 0; isRecording = true; capturer.start(); console.log('Recording started: ' + TOTAL_FRAMES + ' frames'); }

function stopRecording() { capturer.stop(); capturer.save(); isRecording = false; document.title = 'Recording complete!'; console.log('Recording saved!'); }

function keyPressed() { if (key === 'r' || key === 'R') startRecording(); if (key === 's' || key === 'S') save('frame-' + frameCount + '.png'); }


小結

匯出作品是創作流程的最後一哩路,但它跟創作本身一樣重要。一張高品質的圖片、一段流暢的影片,能讓你的作品從「好玩的 demo」升級為「專業的藝術作品」。

這篇文章我們學了:

  1. save() 截取靜態圖片,包括高解析度輸出
  2. saveFrames() 快速存多張圖
  3. CCapture.js 的完整設定與使用
  4. GIF 輸出的注意事項
  5. FFmpeg 命令列工具的使用
  6. MediaRecorder API 的替代方案
  7. 作品集建立的實用建議

希望這整個系列的十篇文章,能幫助你在 p5.js 的創意程式設計世界裡走得更遠。從粒子系統到碰撞物理,從流場到碎形,從 L-System 到音訊視覺化,再到 3D 和作品匯出——這些都是你的工具箱裡的工具。真正的藝術,在於你怎麼用它們來表達自己。

Happy coding, happy creating.

延伸閱讀

  • CCapture.js GitHub 頁面 — 完整文件和範例
  • FFmpeg 官方文件 — 影片處理的瑞士刀
  • p5.js save() / saveFrames() / saveGif() 官方文件
  • 《Generative Design》一書 — 系統性的生成式設計教材
  • OpenProcessing.org — 分享你的 p5.js 作品的最佳平台