前言
有一天我用 Stable Diffusion 生成了一張圖——那是一幅以深海為主題的抽象畫,有著幽暗的藍色漸層、漂浮的發光粒子、和有機的流動線條。我盯著它看了很久,然後想:「如果這不是一張靜態的圖片,而是一個可以互動、會呼吸的程式——那會多美?」
這個念頭開啟了我一系列的實驗:用 AI 生成圖作為「視覺參考」,然後用 p5.js 程式碼重新詮釋它。不是像素級的複製,而是捕捉圖像的「精神」——它的色彩氛圍、動態暗示、空間感——然後用演算法賦予它生命。
這篇文章記錄我的工作流程、技術手段、和過程中的思考。
工作流程概覽
整個流程分為四個階段:
1. AI 生成參考圖
↓
- 分析視覺元素
↓
- p5.js 程式化重現
↓
- 風格融合與迭代
每個階段都有其獨特的挑戰和樂趣。
第一階段:用 Stable Diffusion 生成參考圖
為什麼用 AI 生成參考圖?
傳統的程式藝術工作流程是「先想好要什麼效果,再寫程式實現」。但有時候你不知道自己想要什麼——你只有一個模糊的方向,比如「深海感的抽象畫」。
AI 圖像生成的價值在於快速視覺化你的模糊想法。你可以在幾分鐘內生成幾十張變化,從中挑選最有啟發性的那一張。
有效的 Stable Diffusion Prompt
生成適合做為 p5.js 參考的圖片,prompt 有一些技巧:
強調抽象和程式化元素:
abstract generative art, procedural patterns, flowing particles,
bioluminescent deep ocean, dark blue gradient background,
organic curves, scattered glowing dots, ethereal atmosphere,
digital art, creative coding aesthetic
加入風格修飾:
style of creative coding, reminiscent of processing/p5.js sketches,
mathematical beauty, algorithmic patterns, generative design
避免太寫實的元素:
Negative prompt: photorealistic, text, watermark, human face,
detailed objects, sharp edges
建議的生成參數
- 解析度:512×512 或 768×768(不需要太高,因為這只是參考)
- Steps:30-50
- CFG Scale:7-9(太高會太銳利,失去抽象感)
- Sampler:DPM++ 2M Karras 或 Euler a
生成多個變體
不要只生成一張。我通常會:
- 先用一個基本 prompt 生成 4-8 張
- 從中挑 2-3 張有潛力的
- 用這些作為起點,調整 prompt 或 seed 生成更多變體
- 最終選定 1-2 張作為程式化重現的目標
第二階段:分析視覺元素
這是最關鍵的一步。你需要把一張圖片「拆解」成可以用程式實現的元素。
分析框架
我使用以下的分析框架:
1. 色彩分析
- 主色調是什麼?(例:深藍 #0a1628)
- 輔色是什麼?(例:青綠 #1a8f7a、橙 #e8a544)
- 色彩的分佈比例?(70% 深色、20% 中間色、10% 亮色)
- 色彩之間如何過渡?(漸層?突變?噪音混合?)
2. 元素識別
- 有哪些可辨識的視覺元素?
– 粒子/光點
– 線條/曲線
– 形狀(圓、矩形、不規則形)
– 紋理/圖案
- 這些元素的密度和分佈?
- 大小的變化範圍?
3. 空間結構
- 有沒有明顯的焦點?
- 深度感(近大遠小、漸層模糊)?
- 對稱性?
- 構圖法則(三分法、黃金比例、中心放射)?
4. 動態暗示
- 靜態圖片中有沒有暗示方向的元素?
- 流動感(曲線的方向)?
- 擴散或收縮的趨勢?
- 如果讓這張圖動起來,最自然的動態是什麼?
實際分析範例
假設我的參考圖是一幅深海主題的抽象畫:
色彩分析:
- 背景:從 #050a14(幾乎黑色)到 #0a2040(深藍)的徑向漸層
- 發光粒子:#40e0d0(藍綠色),帶光暈
- 有一些橙色的小點作為對比色:#ff7043
- 整體偏冷,但有少量暖色作為視覺焦點
元素識別:
- 大量小粒子(50-200 個),大小不等(2-10px)
- 若干曲線,像海流或水母觸鬚
- 底部有類似海底地形的模糊形狀
- 頂部較空曠,有「向上看」的空間感
空間結構:
- 焦點在中央偏下
- 粒子從焦點向外擴散
- 邊緣漸暗(暈影效果)
動態暗示:
- 粒子應該緩慢漂浮,有隨機的布朗運動
- 曲線應該微微搖擺,像在水中
- 整體節奏緩慢,有「呼吸」的韻律感
第三階段:p5.js 程式化重現
建立基本結構
根據分析,先建立最基本的色彩和空間:
function setup() {
createCanvas(800, 800);
colorMode(HSB, 360, 100, 100, 100);
}
function draw() {
// 背景漸層(從中心向外漸暗)
drawRadialGradient();
// 海流曲線
drawFlowCurves();
// 發光粒子
drawParticles();
// 暈影
drawVignette();
}
function drawRadialGradient() {
loadPixels();
let cx = width / 2;
let cy = height * 0.6; // 焦點偏下
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let d = dist(x, y, cx, cy) / (width * 0.7);
d = constrain(d, 0, 1);
// 深藍到接近黑色
let h = lerp(210, 220, d);
let s = lerp(70, 90, d);
let b = lerp(20, 4, d);
let idx = (y width + x) 4;
let rgb = hsbToRgb(h, s, b);
pixels[idx] = rgb[0];
pixels[idx + 1] = rgb[1];
pixels[idx + 2] = rgb[2];
pixels[idx + 3] = 255;
}
}
updatePixels();
}
粒子系統
class DeepSeaParticle {
constructor() {
this.reset();
}
reset() {
// 集中在中下方
this.x = randomGaussian(width 0.5, width 0.3);
this.y = randomGaussian(height 0.6, height 0.25);
this.size = random(2, 10);
this.glowSize = this.size * random(3, 6);
// 大部分是青色,少數是橙色
if (random() < 0.15) {
this.hue = random(15, 35); // 橙色
} else {
this.hue = random(160, 190); // 青藍色
}
this.brightness = random(60, 100);
this.pulseSpeed = random(0.01, 0.03);
this.pulsePhase = random(TWO_PI);
// 緩慢漂移
this.vx = random(-0.1, 0.1);
this.vy = random(-0.2, 0.05); // 微微上升
this.noiseOffsetX = random(1000);
this.noiseOffsetY = random(1000);
}
update() {
// 噪音驅動的布朗運動
this.x += this.vx + (noise(this.noiseOffsetX) - 0.5) * 0.5;
this.y += this.vy + (noise(this.noiseOffsetY) - 0.5) * 0.3;
this.noiseOffsetX += 0.005;
this.noiseOffsetY += 0.005;
// 邊界循環
if (this.y < -this.glowSize) this.y = height + this.glowSize;
if (this.x < -this.glowSize || this.x > width + this.glowSize) {
this.reset();
}
}
display() {
let pulse = sin(frameCount * this.pulseSpeed + this.pulsePhase);
let currentBrightness = this.brightness + pulse * 15;
let currentGlow = this.glowSize (1 + pulse 0.2);
// 光暈(多層半透明圓)
noStroke();
for (let i = 3; i >= 0; i--) {
let r = map(i, 0, 3, this.size, currentGlow);
let alpha = map(i, 0, 3, 40, 5);
fill(this.hue, 60, currentBrightness, alpha);
ellipse(this.x, this.y, r * 2);
}
// 核心亮點
fill(this.hue, 20, 100, 80);
ellipse(this.x, this.y, this.size);
}
}
海流曲線
class FlowCurve {
constructor(startY) {
this.points = [];
this.startY = startY;
this.amplitude = random(20, 60);
this.frequency = random(0.005, 0.015);
this.speed = random(0.005, 0.015);
this.alpha = random(5, 20);
// 生成曲線控制點
for (let x = -50; x < width + 50; x += 10) {
this.points.push(createVector(x, startY));
}
}
update() {
for (let i = 0; i < this.points.length; i++) {
let x = this.points[i].x;
this.points[i].y = this.startY +
sin(x this.frequency + frameCount this.speed) * this.amplitude +
noise(x 0.01, frameCount 0.003) * 30;
}
}
display() {
noFill();
stroke(180, 40, 50, this.alpha);
strokeWeight(1);
beginShape();
for (let p of this.points) {
curveVertex(p.x, p.y);
}
endShape();
}
}
組合與精煉
let particles = [];
let curves = [];
function setup() {
createCanvas(800, 800);
colorMode(HSB, 360, 100, 100, 100);
// 建立粒子
for (let i = 0; i < 150; i++) {
particles.push(new DeepSeaParticle());
}
// 建立海流曲線
for (let y = 100; y < height; y += 80) {
curves.push(new FlowCurve(y));
}
}
function draw() {
// 半透明背景(軌跡淡出效果)
fill(215, 85, 5, 15);
noStroke();
rect(0, 0, width, height);
// 海流
for (let c of curves) {
c.update();
c.display();
}
// 粒子
for (let p of particles) {
p.update();
p.display();
}
// 暈影
drawVignette();
}
function drawVignette() {
noFill();
for (let i = 0; i < 40; i++) {
let alpha = map(i, 0, 40, 0, 30);
stroke(220, 90, 3, alpha);
strokeWeight(width * 0.02);
let offset = i width 0.015;
rect(-offset, -offset, width + offset 2, height + offset 2);
}
}
第四階段:風格融合與迭代
不要追求一模一樣
這是最重要的心態。你不是在「複製」AI 的圖,而是在「翻譯」。p5.js 有它自己的語言——動態、互動、演算法之美。讓最終作品有自己的生命。
加入 AI 圖沒有的元素
- 互動:滑鼠影響粒子的行為
- 時間:隨著時間推移,色彩或構圖緩慢演變
- 隨機性:每次重新整理頁面都是一幅新作品
- 聲音:如果搭配音樂,可以做音頻反應式視覺
function mouseMoved() {
// 滑鼠附近的粒子被吸引
for (let p of particles) {
let d = dist(mouseX, mouseY, p.x, p.y);
if (d < 150) {
let angle = atan2(mouseY - p.y, mouseX - p.x);
p.vx += cos(angle) * 0.05;
p.vy += sin(angle) * 0.05;
}
}
}
多張參考圖的融合
有時候最好的結果來自融合多張參考圖的元素:
- 從圖 A 取色彩方案
- 從圖 B 取空間結構
- 從圖 C 取紋理風格
- 加入你自己的動態設計
這種跨圖融合是程式化重現的獨特優勢——在傳統繪畫中很難做到如此靈活的元素組合。
從 AI 圖提取色彩的實用方法
用 Python 分析色彩
from PIL import Image
from collections import Counter
import numpy as np
def extract_palette(image_path, num_colors=8):
"""從圖片中提取主要色彩"""
img = Image.open(image_path)
img = img.resize((100, 100)) # 縮小以加速
# 量化色彩
img_quantized = img.quantize(colors=num_colors)
palette = img_quantized.getpalette()
colors = []
for i in range(num_colors):
r, g, b = palette[i3:(i+1)3]
colors.append((r, g, b))
return colors
# 使用
palette = extract_palette('ai_reference.png')
for i, (r, g, b) in enumerate(palette):
print(f"Color {i}: rgb({r}, {g}, {b}) -> #{r:02x}{g:02x}{b:02x}")
用瀏覽器開發者工具
更簡單的方法:把 AI 生成的圖片在瀏覽器中打開,用 ColorZilla 等取色器擴充套件直接取色。
思考:AI 圖像生成 vs 程式藝術
這兩者之間有一個有趣的張力:
- AI 生成:結果導向,快速,視覺品質高,但你對過程沒有控制
- 程式藝術:過程導向,較慢,但你理解每一個像素為什麼在那裡
把兩者結合,你得到的是:用 AI 探索視覺方向,用程式賦予它生命和邏輯。AI 是地圖,程式碼是旅程。
我認為這是一個非常有前景的創作方法。它不是偷懶——分析一張圖片並用演算法重建它,其實需要相當深入的視覺理解力和程式能力。
小結
從 Stable Diffusion 到 p5.js 的風格遷移,本質上是一種「跨媒介翻譯」。就像把一首詩從中文翻譯成英文——你不是在逐字對應,而是在捕捉精神和意境。
這個過程中,你會發現自己對視覺的理解變得更深——你開始能用語言精確描述一個色彩漸層的特質、一組粒子的動態行為、一條曲線的情緒暗示。這種分析能力,反過來也會提升你從零開始創作的能力。
延伸閱讀:
- Generative Design (Hartmut Bohnacker 等) — 生成式設計的經典教科書
- Tyler Hobbs 的文章 “Working with Color in Generative Art” — 程式藝術的色彩理論
- Stable Diffusion 的 prompt 工程指南 — 更有效地生成參考圖
- OpenProcessing 的 “Inspired By” 作品集 — 看看其他人如何從參考圖出發創作