夜晚抬頭望向天空,黑夜中有著數不盡的星星,讓人不禁讚嘆太空之美。今天老闆就要化身為太空人,觀察一顆由冷暖色調所搭配而成的星球。
一開始會先使用函式的方式來建構藍色粒子雲,隨後加上 noise 來形成動態效果,並加上橘色系的粒子來做搭配。建構好粒子後,便在粒子四周增加弧形的線條,圍繞著粒子雲,最後則是繪製尺標,讓整體作品看起來更有正在觀測的感覺。

透過此次互動藝術創作教學,你會學到
- 學習函式的使用方式,如何透過參數的傳遞,來指定粒子雲的位置與半徑大小範圍
- 如何應用極座標的概念,來繪製粒子的運動位置,並且使用 noise 的技巧讓粒子隨機移動
- 透過不同色彩疊加的模式,來達成每項物件所期待產生的視覺效果
在開始製作之前你該知道的p5.js小技巧
- 極座標 來繪製粒子位置
- 透過 noise() 產生隨機的效果,比起一般常用的 random 來說會更自然、和諧一點。在範例中會時常使用 noise,所以建議在開始之前點進去先稍微理解 noise 的概念
- drawingContext 繪製陰影
- push() / pop() 保存與還原畫布的狀態(可以參考老闆互動藝術程式創作入門的課程筆記:章節 7 進階繪圖 – 畫布操作與編織複雜圖形 (pop/push 的圖解))
- blendMode() 設定圖層的疊加模式
就讓老闆帶你們一步步完成吧

1. 建立粒子系統
首先,先從建立最核心的粒子開始,老闆建立了planet(),並指定了三個參數,由前到後分別是 X 位置、Y 位置以及距離中心點位置的距離 r,可以注意到這邊的 r 在函式中給定了預設的30,這代表當我們呼叫函式的時候,如果第三個數字是沒有給定的,那 r 就會預設成 30,而目前呼叫的函式是有給定值 40 的。
在放置粒子的座標上,老闆將座標移動至畫布的中間,以中間向四周繪製大小、顏色不一的藍色粒子。顏色的部分,老闆到 https://coolors.co/ 網站選取想要的顏色,並複製網址後段色碼的區域,再將文字處理成可使用的色碼形式。
ellipse 的參數前兩項是位置,第三個是半徑,所以 ellipse(random(r), random(r), rr) 所代表的是「以畫布的中心點起,隨機在距離0到40之間的位置上,繪製半徑介於0到20之間的圓」。
//給定顏色
var colorsBlue = "04080f-507dbc-a1c6ea-bbd1ea-dae3e5".split("-").map(a=>"#"+a)
// 建立 planet 函式來繪製粒子
function planet(x,y, r=30){
push()
translate(x,y)
for(var i=0;i<30;i++){
// 隨機給定顏色
let cc= random(colorsBlue)
fill(cc)
// 繪製陰影
drawingContext.shadowColor = color(cc)
drawingContext.shadowBlur =30
// 設定半徑範圍,並繪製粒子
let rr = random(0,20)
ellipse(random(r), random(r), rr)
}
pop()
}
function setup() {
createCanvas(800, 800)
background(0)
// 混和模式設定成 SCREEN
blendMode(SCREEN)
planet(width/2, height/2, 40)
}
function draw() {
// ellipse(mouseX, mouseY, 20, 20);
}

2. 用 noise 建立連續變化
首先,先將繪製粒子的 planet() 移動至 draw() 裡面,讓粒子可以隨時間不斷變化繪製位子。
老闆覺得目前粒子在分布上似乎不太均勻,所以打算改用極座標來做,使用極座標來設定位置的方式在其他作品裡也曾使用過,可以參考 var colorsBlue = "04080f-507dbc-a1c6ea-bbd1ea-dae3e5".split("-").map(a=>"#"+a)
function planet(x,y,r=30){
push()
translate(x,y)
for(var i=0;i<200;i++){
// 顏色改為連續變化,這樣才不會閃閃的
let cc = color(colorsBlue[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length])
fill(cc)
drawingContext.shadowColor = color(cc)
drawingContext.shadowBlur =30
// 以極座標方式畫圓形位置
// xx 乘上 noise,就會產生中間有比較多粒子的效果
let xx = noise(i*2,frameCount/100+mouseX/500)*r*noise(i)*2
let ang = noise(i,frameCount/800+mouseX/1000,500)*10*PI
let rr = noise(i,500,frameCount/50+mouseX/500)*40
ellipse(xx*cos(ang),xx*sin(ang),rr)
}
pop()
}
function setup() {
createCanvas(800, 800)
background(100)
// planet(width/2, height/2, 100)
}
function draw() {
blendMode(BLEND)
// 加上外框
fill('#0f1954')
rect(0,0,width,height)
blendMode(SCREEN)
planet(width/2, height/2, 400)
}
目前的畫面有些單調,所以想加上一些偏橘色粒子。由於都是粒子,增加的程式碼就直接寫在 除了不同顏色大小的調整外,在粒子半徑大小 這裡要來繪製類似魔法陣的線條,連接方式是將每一個粒子的位置相連接起來。 為了可以記錄上一個粒子與現在粒子的位置來做連接,必須要新增變數 然而,將所有點與之間都連接起來,線條太多了,所以老闆增加了 除了魔法陣外,老闆也繪製了弧形的線條,原本想要使用 在藍色粒子上,由於陰影有一點太不亮了,所以把陰影跟粒子分開來調整,另外增加了 做到一個階段,有雛型的樣子後,老闆習慣為畫面加上材質,新增材質共有三大步驟,分別是命名材質、設定材質、疊加材質。 新增好材質後,接下來要來調整一些粒子跟線條的細節,目前所繪製的球體有 200個,這裡我們改為 120個,看起來比較不擁擠,另外粒子雲發散的距離從400改為300,縮小其範圍。這時候會發現,旁邊的弧形線條都擠在最裡面的位置了,所以在 調整到這裡,老闆覺得中間的粒子與線條差不多了,所以開始嘗試往背景的著手。這裡老闆使用 老闆希望那些橘色的小粒子可以多一些稜角,以搭配中心圓形的球,所以將橘色小粒子從 接下來就來畫一些橫軸及縱軸的尺規,來讓畫面上扭曲沒有固定的形體可以透過 2D 固定的線條搭配。這裡繪製出的是每隔五格會有一條相對長一點的線條,這樣看去上會更加有刻度感。 最後老闆本來想要外面加上一整個外框來框住星球,有一種正在觀測星球的感覺,但感覺有些太死板,所以嘗試了一些半圓形或是扇形的形狀,最終決定在右下角加上五個弧狀的扇形作為點綴,並與前面繪製弧型一樣,使用了 我們總結一下這次的水色星球小品,製作過程可以分為三大部分: 這就是我們用p5.js寫出來的簡單的小作品啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。 相同的繪製原理還能應用在甚麼作品上呢?若對p5.js寫成的互動藝術程式創作有興趣,歡迎加入老闆開的Hahow互動藝術程式創作入門課程,與另外將近兩千位同學一起創作吧! 此篇直播筆記由幫手 阮柏燁 協助整理
3. 加上暖色粒子與調整粒子大小
planet 函式中,而且我們可以複製原本藍色粒子的片段去做修改,讓整體視覺變成冷暖色交替。但是如果只有更改顏色、粒子大小太相近的話,看上去有點喧賓奪主,所以稍微調整成藍色大橘色小,將橘色粒子的半徑大小改為 rr/2。rr 的設定上,老闆為了使離中心越遠的粒子顯得越小,加上了開平方 sqrt 。/* 加上暖色色票 */
var colorsRed = "fe7f2d-fcca46-a1c181-619b8a-333".split("-").map(a=>"#"+a)
function planet(x,y,r=30){
push()
translate(x,y)
for(var i=0;i<200;i++){
let cc = color(colorsBlue[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length])
fill(cc)
drawingContext.shadowColor = color(cc)
drawingContext.shadowBlur =20
let xx = noise(i*2,frameCount/100+mouseX/500)*r*noise(i)*2
let ang = noise(i,frameCount/800+mouseX/1000,500)*10*PI
/* 加上 sqrt,讓靠近中心的粒子較大,越遠的粒子較小 */
let rr = noise(i,500,frameCount/50+mouseY/500)*30*(15/(sqrt(xx)+1))
ellipse(xx*cos(ang),xx*sin(ang),rr)
/* 加上橘球 */
let cc2 = colorsRed[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length]
fill(cc2)
drawingContext.shadowColor = color(cc2)
drawingContext.shadowBlur =10
ellipse(xx*cos(ang*2),xx*sin(ang*2),rr/2)
}
pop()
}

4. 加上線條與修正藍色粒子
lastX、lastR、 lastAng 來記錄上一個粒子的位置,在函式中記得也要更新此次粒子的位置,來作為下一個粒子的參考位置。random()<0.1 的條件,使線條出現的頻率不會那麼高。line 一段一段繪製,但是發現這樣實在有些麻煩,所以就改以使用 arc 的方式來畫。與線條一樣,透過加上random() 的方式,讓畫面呈現一閃一閃的效果 。shadowCC 來設定陰影。function planet(x,y,r=30){
push()
translate(x,y)
/* 1. 設定變數做紀錄,以便於繪製點與點的連線 */
let lastX, lastR, lastAng
for(var i=0;i<200;i++){
/* 2. 將藍色球跟其陰影分開設定透明度 */
let cc = color(colorsBlue[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length])
cc.setAlpha(150)
fill(cc)
let shadowCC = color(cc)
shadowCC.setAlpha(255)
drawingContext.shadowColor = shadowCC;
drawingContext.shadowBlur =20;
let xx = noise(i*2,frameCount/100+mouseX/500)*r*noise(i)*2
let ang = noise(i,frameCount/800+mouseX/1000,500)*10*PI
let rr = noise(i,500,frameCount/50+mouseY/500)*30*(15/(sqrt(xx)+1))
ellipse(xx*cos(ang),xx*sin(ang),rr)
let cc2 = colorsRed[int(noise(frameCount/10,i)*colorsBlue.length)%colorsBlue.length]
fill(cc2)
drawingContext.shadowColor = color(cc2);
drawingContext.shadowBlur =10;
ellipse(xx*cos(ang*2),xx*sin(ang*2),rr/2)
/* 3. 新增點與點連線 */
if (lastX && random()<0.1){
push()
stroke(255,50)
line(xx*cos(ang),xx*sin(ang),lastX*cos(lastAng),lastX*sin(lastAng))
pop()
}
/* 4. 新增外圍弧形線條 */
if (random()<0.5){
push()
stroke(255,50)
noFill()
arc(0,0,xx,xx,ang,ang+PI)
pop()
}
/* 5. 更新變數 */
lastX=xx
lastR=rr
lastAng=ang
}
pop()
}

5. 調整平衡,加上材質
在全域中命名材質變數 let overAllTexture
在 setup() 中設定材質的樣式overAllTexture=createGraphics(width,height)
overAllTexture.loadPixels()
// noStroke()
for(var i=0;i<width+50;i++){
for(var o=0;o<height+50;o++){
overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,10,20])))
}
}
overAllTexture.updatePixels()
在 draw() 中改變與色塊的混合模式push()
blendMode(MULTIPLY)
image(overAllTexture,0,0)
pop()
arc 的參數上都乘上 2 ,讓弧形向外擴長,同時也加上橘色色系在線條上,讓整個作品看起來更加一致。還有一點細節是老闆在此加上了 noise,這使線條多了動態感。 Pow 畫成由內而外,根據距離中心點的不同,疊加了好幾層不同透明度的圓,形成了一圈一圈的圓形尺規,雖說是尺規,但不是規規矩矩一圈一圈等距的圓形,這邊可以先觀察下圖兩者的差異。
Pow 其實就是以前高中數學所學過的指數,必須有兩個參數 Pow(底數, 次方),而老闆所使用的次方數是小於 1 的,這樣所產生的效果是,數值越小時,轉換過後所產生的差異會越大。舉例子來說,pow(1000,0.9) – pow(900,0.9) = 46 < pow(200,0.9) – pow(100,0.9) = 46,所以仔細觀察可以看到,越內圈的間距是大於外圈之間的間距,而這樣小細節,讓所疊加的圓並非死板板的一格一格,而是更加產生更加靈活一點的漸層感。/*1. 上下的半徑壓扁,並將顏色修改為跟橘球一樣 */
if (random()<0.5){
push()
stroke(cc2)
noFill()
arc(0,0,xx*2,xx*2,ang*2,ang*2+noise(i,frameCount/200))
pop()
}
function draw() {
blendMode(BLEND)
fill(30, 35, 86, 50)
rect(0,0,width,height)
blendMode(SCREEN)
/*2. 修正半徑範圍 */
planet(width/2, height/2, 300)
push()
blendMode(MULTIPLY)
image(overAllTexture,0,0)
pop()
/*3. 加上圓形尺規 */
stroke(255,50)
noFill()
blendMode(MULTIPLY)
for(var i=0;i<width;i+=100){
fill(150,map(i,width/2,width,0,20))
ellipse(width/2,height/2,pow(i,0.9)*3,pow(i,0.9)*3)
}
}

6. 加上漸層陰影與刻度
ellipse 改為 rect ,並且跟著軌跡去做旋轉,才不會那麼地死板。//ellipse(xx*cos(ang*2),xx*sin(ang*2),rr/2)
/*將橘色粒子的球體改為方形 */
push()
rectMode(CENTER)
translate(xx*cos(ang*2),xx*sin(ang*2))
rotate(ang*2)
rotate(i)
rect(0,0,sqrt(rr)*sin(frameCount/2+i)*2)
pop()
/* 加上尺規 */
blendMode(BLEND)
stroke(colorsRed[1])
push()
for(var i=0;i<width;i+=10){
line(i,40,i,45+(i/10%5==0?15:0))
line(40,i,45+(i/10%5==0?15:0),i)
point(i,i)
}
pop()
arc 來畫,並且使用了 noise 來增加隨機動態感。/*右下角加上點綴型外框 */
noFill()
strokeWeight(5)
drawingContext.shadowColor = colorsRed[1];
drawingContext.shadowBlur =30;
for(var i=0;i<5;i++){
let aa = noise(i,frameCount/10)*2
arc(width/2,height/2,width-100-i*20,height-100-i*20,aa,aa + noise(i,frameCount/10)/2)
}
drawingContext.shadowBlur =0;
strokeWeight(2)

結語
noise,亦或是根據離中心不同距離而有不同的粒子大小所增加的 sqrt 等。arc 來繪製圍繞在粒子雲四周的弧形、軌跡,增添細節。




