夜晚抬頭望向天空,黑夜中有著數不盡的星星,讓人不禁讚嘆太空之美。今天老闆就要化身為太空人,觀察一顆由冷暖色調所搭配而成的星球。
一開始會先使用函式的方式來建構藍色粒子雲,隨後加上 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()
裡面,讓粒子可以隨時間不斷變化繪製位子。
老闆覺得目前粒子在分布上似乎不太均勻,所以打算改用極座標來做,使用極座標來設定位置的方式在其他作品裡也曾使用過,可以參考 【p5.js創作教學】來畫一個瘋狂的龍捲風吧!(直播筆記) 這篇文章。
為了讓上一個與下一個粒子的分布上不要呈現完全隨機跳動,所以這裡老闆使用了 noise 來設定粒子的距離 xx
、角度 ang
以及半徑大小rr
。
而在設定上,第一眼看上去會覺得「這一串程式碼好長喔」,但其實拆解下來,可以看到老闆只用了幾個參數就產生隨機,分別是代表粒子序號的 i
、系統變量(也就是計算程式啟動以來的幀數 frameCount
),以及當滑鼠水平移動時也會產生影響的mouseX
。
而其他的常數則是老闆慢慢嘗試去調整出來的,可以理解成是每一個常數所影響占比,所除的數字越大,該變數所影響的量越小。你們也可以自己改變數字,試出最喜歡的樣子。
這裡要特別注意的是,老闆希望整個粒子雲可以呈現中間密外圍疏,所以在 xx
這個設定距離的參數上的尾端又多乘上一個 noise。此外,在星球顏色的變化上也改為使用 noise,會顯得更加地平順,不會一閃一閃的。
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) }
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()
新增好材質後,接下來要來調整一些粒子跟線條的細節,目前所繪製的球體有 200個,這裡我們改為 120個,看起來比較不擁擠,另外粒子雲發散的距離從400改為300,縮小其範圍。這時候會發現,旁邊的弧形線條都擠在最裡面的位置了,所以在 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()
接下來就來畫一些橫軸及縱軸的尺規,來讓畫面上扭曲沒有固定的形體可以透過 2D 固定的線條搭配。這裡繪製出的是每隔五格會有一條相對長一點的線條,這樣看去上會更加有刻度感。
/* 加上尺規 */ 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
來繪製圍繞在粒子雲四周的弧形、軌跡,增添細節。 - 視覺上的調整,除了加入材質外,也新增了尺規與座標,這樣固定的物件,與中心的動態物件做為對比,視覺上在穩定與動態之間達到平衡。
這就是我們用p5.js寫出來的簡單的小作品啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。
相同的繪製原理還能應用在甚麼作品上呢?若對p5.js寫成的互動藝術程式創作有興趣,歡迎加入老闆開的Hahow互動藝術程式創作入門課程,與另外將近兩千位同學一起創作吧!
此篇直播筆記由幫手 阮柏燁 協助整理