本文翻自【Coding Vlog | p5.js】200514 Dreamy Bird 夢幻鳥 – 來做彩色又毛毛不知道是魚還是鳥的生物吧!若是想要老闆手把手帶你飛,可以跟著影片進行,這邊也附上成品歡迎大家一起動手做。
這一次分享的內容比較特別,是紀錄老闆創作的過程,起初只是想做金屬色的練習,調整不同屬性以及數值後,慢慢產生了生物的形體,而有了夢幻鳥的誕生。這個作品會利用線上的工具 openprocessing 來進行 p5.js 的創作。完成作品後會發現,其實使用到的 api 就只有那幾個,卻能創作出獨特又有趣的作品,大家了解 api 後,也能勇敢去嘗試調整,說不定會有更意想不到的作品產生。如果想要了解更詳細的製作流程和其他創作內容,可以去支持老闆的互動藝術程式創作課程哦!
這次直播筆記會帶大家學會以下內容:
- 利用 p5.js 進行創作互動作品
- 使用 noise 產生自然有序的隨機數
- 在作品中加入滑鼠互動,讓作品與觀賞者產生連結
事前準備
開發環境
開發會使用 openprocessing 線上撰寫程式碼,如果想知道較詳細的設定,可以到成品看老闆的開發環境設定。
- openprocessing:提供大家在網頁中直接使用 p5.js 進行開發,只要利用所提供的 api ,就能製作出有趣的效果。想要了解更多相關效果的開發,除了參考網站中其他的p5.js創作教學之外,也歡迎看看老闆的線上課程,跟老闆跟一起進入 processing 的世界。
會使用到的 API:
這次專案會使用以下的 api,先重點整理給大家,不清楚的地方,可以透過後面跟著老闆操作,了解每個 api 使用時機。
- createCanvas(width, height): 創建畫布,參數中分別傳入寬跟高。
- background(colorCode): 加上背景色,可依照文件傳入色碼參數。
- noStroke(): 取消繪製圖形的邊框。
- colorMode(): 定義顏色的方式,預設為 RGB 顏色,HSB 模式依序要填入的值則為(色相, 飽和度, 明度)。
- fill(): 選擇填入的顏色,依照 colorMode 選擇的填色模式填入對應的參數。
- ellipse(posX, posY, width, height): 在 (posX, posY) 上繪製一個寬高(width, height)的橢圓形。
- rect(x, y, width, height): 以 (x, y) 的位置為左上角的點,畫一個寬度 width 高度 height 的方形(如果要畫正方形的話,即寬度=高度)。
- random(): 沒有傳參數時,會返回一個隨機浮點數。
- noise(): 產生自然有序的隨機值,與 random 的概念不一樣。在範例中會使用到 noise,所以建議先稍微理解 noise 的概念,有興趣的同學也可以延伸閱讀相關資訊:2D Noise – Perlin Noise and p5.js Tutorial
- rotate(angle): 依照傳入的參數進行旋轉。
- translate(x, y): 將畫筆移到(x,y) 上。
- push(): 紀錄目前畫筆狀態。
- pop(): 恢復畫筆狀態。
- sin(): 正弦,將傳入的數值做為角度值,換算成 1~-1 的值。
- cos(): 餘弦,將傳入的數值做為角度值,換算成 1~-1 的值。
- atan2(y, x): 計算從指定點(y,x)到座標原點的角度。
跟著老闆開始動手做
1. 起手式
開啟新的 openprocessing > Create a Sketch,可以看到程式碼頁面已經有一段預設的程式碼,隨著滑鼠的移動,會沿路產生小球,理解這段程式碼後,接著只留下我們需要的部份。
- setup(): 可以視為環境初始化,只在開始執行的當下會呼叫一次,以下的程式碼使用了兩個 api
- createCanvas(width, height):創建畫布,參數中分別傳入寬跟高,如果直接寫螢幕的寬高(windowWidth, windowHeight),就會成為滿版的互動區塊。
- background(colorCode):加上背景色,可依照文件傳入色碼參數。
- draw(): 會依照時間不停地重跑裡面的程式碼,要製作互動的內容可以在這個 function 中呼叫。
- ellipse(posX, posY, width, height):在 (posX, posY) 上繪製一個寬高(width, height)的橢圓形。
function setup() { createCanvas(windowWidth, windowHeight); background(100); } function draw() { ellipse(mouseX, mouseY, 20, 20); }
2. 繪製基礎噪聲
在 draw 中,我們先調整顏色模式改成 HSB,後續與填色有關的 api 就會改成依序填入(色相, 飽和度, 明度),
- noStroke() 將每次繪製圖形的邊框取消掉,每個方塊間就不會有 stroke。
- colorMode():定義顏色的方式,預設為 RGB 顏色,HSB 模式依序要填入的值為(色相, 飽和度, 明度)。
接下來我們可以看到有兩個 for 迴圈,第一個 for 迴圈會每隔高度 20 ,再進行一次第二個 for 迴圈的內容,重新從左至右繪製一長串的方形。
- rotate(angle): 依照傳入的參數進行旋轉。
- fill():選擇填入的顏色,由於前面選擇了 HSB ,所以這邊要改使用 HSB 的方式填色。
- sin():將傳入的數值做為角度值,換算成 1~-1 的值。
- noise():躁聲,隨機序列生成器。跟 random 相比,可以利用多維的座標產生自然有序的序列,產出的值介於 0~1之間。
- rect(x, y, width):在 (x, y) 的位置畫一個寬度 width 的方形。
經過調整後,讓呈現的顏色有時偏白,有時飽和度不會那麼高。在 sin 或 noise 中代入的值,老闆會多除上一些數字,目的是為了讓呈現的顏色變化不要太快,但這沒有正確答案,同學可以在了解每個 api 的操作方式後,依照自己的經歷或感受,去嘗試自己喜歡的氛圍。
最後一步驟,老闆希望能讓每一個橫條看起來都不同進度,所以在每一條橫條繪製前,都旋轉一下,就完成繪製基礎噪聲階段了,產生類似彩虹的畫面。
function setup() { createCanvas(800, 800); background(100); } function draw() { colorMode(HSB) noStroke() for(var o = 0; o<height; o+=20) { rotate(PI/1000) for(var i = 0; i<width; i++) { fill(sin(i/100)*300, noise(i/50, o/1000)*100, sin(i/40,o/1000)*30+80) rect(i, o, 30) } } }
3. 依據噪聲橫列的影響色彩分佈跟變化
這個階段,老闆對每一行的波進行尺寸與填色的微調,也進行了波型的嘗試:
- 讓每一橫列產生偏移:填色位置加入 o ,隨著每一行 o 的值逐漸增加,使得每一橫列的波產生偏移。
- 讓波跟著時間動起來:填色位置加入時間因子 frameCount,讓整幅跟著時間的前進而動起來。
- 更豐富的顏色:希望每一行波使用了 o 之後,不是只有偏移效果,所以在 noise 中又加入了 noise。
- 降低波顏色變化速度:希望顏色變化的速度能更慢一點,針對 fill 內色相位置的值,除上更小的數字。
- 加上插畫材質:為了讓作品更有質感,所以我們為作品加上插畫材質,材質製作方式這邊不詳細介紹,同學可以將程式碼貼到對應的地方直接使用。
let overAllTexture function setup() { createCanvas(800, 800); background(0); // 插畫材質 overAllTexture=createGraphics(width,height) overAllTexture.loadPixels() 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,40,80]))) } } overAllTexture.updatePixels() } function draw() { colorMode(HSB) noStroke() for(var o = 0; o<height; o+=100) { for(var i = 0; i<width; i++) { fill( noise(i/400, o/400, noise(frameCount/150) + frameCount/50)*600%360, noise(i/50, o/1000, frameCount/100)*100, noise(i/40, o/1000, frameCount/100)*30+80 ) rect(i, o, 80) } } // 加上插畫材質 push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
4. 修飾幾何形狀與加入隨機大小
接下來,我們只會動到 draw 裡面的內容,老闆在這個階段做了以下嘗試:
- 貓毛效果:畫方塊的時候,利用變數 glitchAmount ,在 x, y 座標加上隨機偏移值。
- 隨著滑鼠位置改變的貓毛:將隨機偏移的值加上滑鼠的值,使作品與滑鼠產生了互動。
- 微調樣式,將外層的 for 迴圈 o 從 10 開始,讓作品與上下邊界的距離一樣
老闆也有嘗試加入隨機的黑線在波形中,產生類似現代藝術的感覺,但實際做出來的效果不好(下圖左),大家想要嘗試,可以將以下程式碼斜線的部分恢復。
let glitchAmount = 20 function draw() { colorMode(HSB) noStroke() glitchAmount = mouseX/10 // 隨著滑鼠變更數字的貓毛 for(var o = 10; o<height; o+=100) { for(var i = 0; i<width; i++) { fill( noise(i/400, o/400, noise(frameCount/150) + frameCount/50)*600%360, noise(i/90, o/1000, frameCount/100)*100, noise(i/80, o/1000, frameCount/100)*30+80 ) rect(// 繪製方塊時,繪製的座標結合隨機的值 i + random(-glitchAmount, glitchAmount), o + random(-glitchAmount, glitchAmount), 80 ) // if(noise(i, o, frameCount/100) < 0.1){ // push() // stroke(0) // strokeWeight(20) // rect(i, o, 80) // pop() // } } } push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
5. 使用三角函數繪製波型
接下來老闆想做出類似極光的效果,一系列的調整與操作後,慢慢地變成一塊一塊的物體往前移動中,過程中做了以下嘗試,大家也能跟著老闆一起嘗試:
- 由上到下、粗到細:極光從上到下粗到細,改變 rect 的第三個參數來實現改變波的大小
- 結合 sin 產生波形:使用 sin 來繪製方形所產生波形,比較像極光或海浪,
- 將波形結合時間因子 frameCount,讓波動起來
- 改變 rectMode 為 CENTER,讓波上下同時變大
let glitchAmount = 5 function draw() { colorMode(HSB) noStroke() glitchAmount = mouseX/10 rectMode(CENTER) // 調整繪製方形的模式 for(var o = 50; o<height; o+=100) { for(var i = 0; i<width; i++) { fill( noise(i/400, o/400, noise(frameCount/150) + frameCount/50)*600%360, noise(i/90, o/1000, frameCount/100)*100, noise(i/80, o/1000, frameCount/100)*30+80 ) rect( i + random(0, glitchAmount), o + random(-glitchAmount, glitchAmount), (sin(i/40 + frameCount/20+o*50)+1)*30+20 // 結合 sin 繪製波形,加上 frameCount 讓波能跟著時間動起來 ) } } push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
6. 加上眼睛與調整生物外觀
老闆認為一塊一塊向前的波形很像生物的身體,雖然還沒確定是魚還是鳥,但是老闆決定賦予每一個區塊眼睛。這階段老闆將一些值整理成變數,大家可以來回參照上一階段與這個階段的程式碼比較,在加上眼睛與生物外觀的調整過程做了以下嘗試:
- push 與 pop:因為老闆將眼睛位置統一記錄在 eyes 中,所以程式碼會先將所有生物的身體繪製完畢後,再繪製所有生物的眼睛,這邊就會需要把畫筆位置移到對的地方,所以使用了 translate。要注意的是,做畫筆的移動或是旋轉畫布時,我們會使用 push 將原本的狀態記錄著,當完成位置時再搭配 pop 去恢復原本畫筆的狀態。
- translate(x,y):將畫筆移動到 x, y 的位置
- 以波形的進度 (progAng) 作為眼球的位置:將每個完整波形的長度百分之 3 的位置存進陣列中,同學要記得使用餘數,因為隨著時間增加,frameCount 是一直增加的,利用 PI * 2 去處理餘數,就能取得每個波形進度。
- 生物的位置與滑鼠關聯:除了讓方塊的位置隨著時間去改變,這邊也做了滑鼠的互動,讓波形進度的值結合滑鼠位置。
- 繪製生物的方塊:生物的身體,是依不同時間點來決定出不同大小的方塊所組成,老闆將原本的方大小作為 progAng 變數的值,再由 hh 變數來組合使用 progAng。
- 區塊的大小更加生動:原本的區塊大小只是隨著滑鼠位置去變化,在 hh 的值中,除了利用 sin 之外,也加入 cos ,讓這個生物的外觀更有趣,產生毛邊金魚的感覺。
- 繪製眼睛:眼睛陣列(eyes)裡的物件,是所有符合條件的眼睛 x 座標,結合 ellipse,繪製眼白與眼珠。
let glitchAmount = 5 function draw() { colorMode(HSB) noStroke() glitchAmount = mouseX/100 rectMode(CENTER) for(var o = 50; o<height; o+=100) { let eyes = [] push() // 記錄當下初始畫筆的狀態 translate(0, o) // 移動畫筆到 (0, o) 的位置 for(var i = 0; i<width; i++) { push() // 再次紀錄當下畫筆狀態 translate(i,0) // 移動畫筆到(i, 0) 的位置 fill( noise(i/500, o/400, noise(frameCount/150) + frameCount/50)*600%360, noise(i/90, o/1000, frameCount/100)*100, noise(i/80, o/1000, frameCount/100)*30+80 ) let progAng = (i/40 + frameCount/20+o*50 + mouseY/100 + mouseX*noise(o)/100) % (PI*2) // 結合餘數計算,讓值介於 0~100 之間 let hh = (sin(progAng) + 1 + cos(progAng/2))*30 +20 // 結合波的進度作為每次繪製方塊的大小 rotate(sin(i/10)) rect( random(0, glitchAmount), random(-glitchAmount, glitchAmount), hh ) if( int(progAng/PI/2*100 ) == 2) { // 符合進度條件則儲存 x 座標 eyes.push(i) } pop() // 釋放畫筆位置 } eyes.forEach( eyeX => { // 將陣列內的物件全部拿出來繪製眼睛 fill('white') ellipse(eyeX, 0, 25) fill('#333') ellipse(eyeX, 0, 10) }) pop() // 釋放畫筆位置 } push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
7. 眼睛看向滑鼠、細調樣式
大致的生物形體告一段落後,除了細調樣式外,老闆也開始在作品中嘗試加入更多的互動性,例如讓眼睛看向滑鼠的位置,做了以下的操作:
- 讓眼球看向滑鼠位置:這邊需要先取得滑鼠與眼球的角度,再利用 cos, sin 讓眼球能擺放到對的位置。使用到了新的 api – atan2
- atan2(y2-y1, x2-x1):以弧度為單位,計算從指定的點 (y2,x2) 到 (y1,x1) 的角度,要注意這邊的 api 參數,第一個是 y 座標的計算,第二個才是 x 座標的計算。(https://p5js.org/reference/#/p5/atan2)
- 清掉雜訊:因為 p5 是不停的地繪製新的畫面,畫面出現了許多雜點,是因為沒有在每次繪製前,先將畫面清空,這邊只要在繪製前,在畫布上蓋上一個滿版的方形就能達成。需要注意的是,利用覆蓋滿版方塊來清除雜點時,由於我們前面使用的 rectMode(CENTER),除了調整繪製的座標外,也可以先改回使用 rectMode(CORNER),等清除畫面完成後,再繼續原本的程式碼。
- 微調生物的身體大小:微調的數值可以參考以下的程式碼,大家也可以嘗試看看不同的數值,看看會有什麼有趣的效果。
- 取消毛邊與滑鼠的互動:固定生物毛邊的程度。
- 調整背景色:老闆試著改變背景色,希望不要每個作品背景都是黑色。因為生物的顏色比較鮮豔,所以最後老闆挑了較深的顏色,來對比出作品的主角。
let glitchAmount = 5 function draw() { noStroke() // glitchAmount = mouseX/100 // 取消毛邊與滑鼠的互動 rectMode(CORNER) // 改變繪製方塊的模式 colorMode(RGB) // 使用 RGB 作為填色模式 fill(156, 104, 104, 200) // 每次重新繪製時加上底色 rect(0, 0, width, height) rectMode(CENTER) colorMode(HSB) for(var o = 50; o<height; o+=100) { let eyes = [] push() translate(0, o) for(var i = 0; i<width; i++) { push() translate(i,0) fill( noise(i/500, o/400, noise(frameCount/150) + frameCount/50)*600%360, noise(i/90, o/1000, frameCount/100)*100, noise(i/80, o/1000, frameCount/100)*30+80 ) let progAng = (i/40 + frameCount/20+o*50 + mouseY/100 + mouseX*noise(o)/100) % (PI*2) let hh = (sin(progAng) + cos(progAng/2) + cos(progAng/5)/3 +1)*30 // 微調毛邊樣式 rotate(sin(i/10)) rect( random(0, glitchAmount), random(-glitchAmount, glitchAmount), +hh ) if( int(progAng/PI/2*100 ) == 2) { eyes.push(i) } pop() } eyes.forEach( eyeX => { let mAng = atan2(mouseY - o, mouseX - eyeX) // 取得滑鼠與眼珠的相對位置 fill('white') ellipse(eyeX, 0, 25) fill('#333') ellipse(eyeX + cos(mAng)*5, sin(mAng)*5, 10) // 利用 cos, sin 將眼珠放置在對的位置 }) pop() } push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
8. 加入魚鰭與最後修飾
創作到尾聲,其實老闆還沒決定他是什麼樣的生物,看起來類似尖嘴巴的魚,為了讓作品更完整,在這裡我們賦予生物們魚鰭,並做最後的微調:
- 加上魚鰭:前面我們有記錄所有眼睛的位置,利用這個 for 迴圈,去繪製旋轉的三角形,讓它成為生物的鰭。這邊要記得使用 push 及 pop,不然會導致你下一次在繪製眼睛時出錯。
- 調整背景色:最後老闆選擇了深藍色的背景作為定調。
- 扭動的身體:繪製身體前的畫筆移動,在 y 參數的位置加上 sin ,可以繪製出魚移動時身體扭動的感覺。
let glitchAmount = 5 function draw() { noStroke() rectMode(CORNER) colorMode(RGB) fill(0, 0, 80, 180) rect(0, 0, width, height) rectMode(CENTER) colorMode(HSB) for(var o = 50; o<height; o+=100) { let eyes = [] push() translate(0, o) for(var i = 0; i<width; i++) { push() translate(i, sin(i/30)*20) // 利用畫筆的位移,讓鳥在往前時,身體也有了變化 fill( noise(i/500, o/400, noise(frameCount/150) + frameCount/50)*600%360, noise(i/90, o/1000, frameCount/100)*100, noise(i/80, o/1000, frameCount/100)*30+80 ) let progAng = (i/40 + frameCount/20+o*50 + mouseY/100 + mouseX*noise(o)/100) % (PI*2) let hh = (sin(progAng) + cos(progAng/2) + cos(progAng/5)/3 +1)*30 rotate(sin(i/10)) rect( random(0, glitchAmount), random(-glitchAmount, glitchAmount), +hh ) if( int(progAng/PI/2*100 ) == 2) { eyes.push(i) } pop() } eyes.forEach( eyeX => { let mAng = atan2(mouseY - o, mouseX - eyeX) fill('white') ellipse(eyeX, 0, 25) fill('#333') ellipse(eyeX + cos(mAng)*5, sin(mAng)*5, 10) push() // 繪製魚鰭時,記得使用 push, pop 來記錄與釋放畫筆狀態 stroke(0) noFill() translate(eyeX+50, 0) rotate(sin(eyeX/2+o/10)/2) // 畫筆進行旋轉,畫面會呈現魚鰭擺動的效果 triangle( 0, 0, 50, -20, 50, 20 ) pop() }) pop() } push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
老闆來結語
這次的創作一開始是老闆想要練習金屬色,一系列的調整與操作,最後才產出夢幻鳥這個作品,讓我們快速回顧一下夢幻鳥的創作過程:
- 了解 openprocessing 創作的起手式 – setup 與 draw
- 利用噪聲 noise 決定方塊的顏色
- 利用 for 迴圈的變數、噪聲與時間變數 frameCount,影響每一橫列的色彩分佈與變化
- 調整繪製方塊的形狀與大小
- 結合三角函數繪製出波形
- 為生物加上眼睛,並微調生物外觀
- 讓眼睛與滑鼠產生互動
- 加上魚鰭與最後修飾
萬事起頭難,一個作品不可能一步到位,將最終目標拆分成不同階段任務,從一開始的雛型慢慢開發出每個區塊,最後組裝在一起,也可以加上個人的創意去實現其他功能,讓作品更豐富。
由於這部影片比較特別,是紀錄老闆在練習與發想後,老闆回頭解說製作過程,所以中間會不停地去微調數值。創作的過程一定會有這種狀況發生,在創作時沒有所謂的正確答案,大家在了解工具之後,就勇敢地去嘗試吧!再附上這次範例的成品<夢幻鳥>,讓大家在開發時參考。
如果你喜歡老闆的教學,歡迎支持老闆,讓老闆在《互動藝術程式創作入門》課程中帶你一起學習。課程裡會帶你看看不一樣的作品,並引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓寫 code 不再是這麼困難的一件事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠在網頁上做出與眾不同的創作。
此篇直播筆記由幫手 H 協助整理