前言
你花了好幾個小時打磨出一個美麗的生成式藝術作品,在瀏覽器裡跑得很漂亮。然後呢?你想分享到 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 的預設解析度等於你設定的 width 和 height。如果要輸出印刷品質的圖片,你需要更高的解析度:
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 的注意事項:
- 尺寸要小:400×400 是不錯的上限。越大,檔案越巨大
- 幀率要低:12-15 fps 對 GIF 來說就夠了
- 顏色限制:GIF 只支援 256 色,複雜漸層會出現色帶
- 時長要短: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 | 依平台要求而定 |
作品集的呈現技巧
- 每件作品多個版本:同一個程式,不同的隨機種子產生不同的結果
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');
}
}
- 固定隨機種子:讓你的作品可以被精確重現
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');
}
}
- 記錄你的程式碼:作品集不只是圖片,也要附上程式碼。很多生成式藝術的觀眾對「怎麼做的」跟「做了什麼」一樣感興趣。
- 製作過程影片:錄一段從空白畫布到完成作品的過程。用 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」升級為「專業的藝術作品」。
這篇文章我們學了:
save()截取靜態圖片,包括高解析度輸出saveFrames()快速存多張圖- CCapture.js 的完整設定與使用
- GIF 輸出的注意事項
- FFmpeg 命令列工具的使用
- MediaRecorder API 的替代方案
- 作品集建立的實用建議
希望這整個系列的十篇文章,能幫助你在 p5.js 的創意程式設計世界裡走得更遠。從粒子系統到碰撞物理,從流場到碎形,從 L-System 到音訊視覺化,再到 3D 和作品匯出——這些都是你的工具箱裡的工具。真正的藝術,在於你怎麼用它們來表達自己。
Happy coding, happy creating.
延伸閱讀
- CCapture.js GitHub 頁面 — 完整文件和範例
- FFmpeg 官方文件 — 影片處理的瑞士刀
- p5.js save() / saveFrames() / saveGif() 官方文件
- 《Generative Design》一書 — 系統性的生成式設計教材
- OpenProcessing.org — 分享你的 p5.js 作品的最佳平台