這次的直播內容主要有四個部分:
- 透過繪製扭動的線條來呈現海葵觸鬚的樣子 (步驟一~步驟四)
- 加上滑鼠的互動以及視覺上的裝飾,包含了顏色以及材質,還有在外層加框框,形成了類似畫框的效果 (步驟五~步驟七)
- 加上細的觸鬚增加細節(步驟八)
- 海葵底部做收斂,形成一叢海葵,以及最後的修飾(步驟九~步驟十)
主要會用到的 API :
- push() / pop() 保存與還原畫布的狀態
- 透過 noise() 來產生隨機的,這與 random 的概念不一樣。在範例中會時常使用 noise,所以建議先稍微理解 noise 的概念
- 用 loadPixels() 來製作材質
- map() 轉換數值
還是新手?想要快速上手p5.js請來看p5.js 快速上手:互動網頁教學
Part 1 海葵
透過四個步驟,藉由 curveVertex
來畫線條,並藉由隨機移動線條來形成多條海葵觸鬚
單一條線
首先以畫面的中心為基準點,由下而上畫出一點一點所連成的線條。這邊因為我們將基準的位子轉換了translate(0,height)
,因此在畫線的的時候,注意是由下往上畫,vertex(xx,-i)
中的 i 要加上負號
function setup() { createCanvas(800, 800); background(0); xx = width/2 } var xx function draw() { translate(0,height) stroke(255) beginShape() strokeWeight(50) for(var i=0; i<500; i++){ vertex(xx,-i) } endShape() }
左右搖擺
畫好線後,我們為了讓整條線的每一個點隨機分布在不同位子上,同時整條線左右搖擺,所以在 noise 中放入了每個一個點(i)及時間(frameCount)的因子,並存在 deltaX 變數中。 這麼要注意一下,我們為了讓畫出來的圖形不要重疊在一起,所以這裡要在加上 background(0)
,讓每一次的背景都重新刷新
function draw() { translate(0,height) stroke(255) background(0) // 更新 background beginShape() strokeWeight(50) for(var i=0; i<500; i++){ let deltaX = noise(i/400, frameCount/100) * 200 // 加上 noise vertex(xx + deltaX,-i) } endShape() }
固定底部
現在看上去的狀態是整條線都要搖擺,但是我們希望它的底部是相對固定的,所以加上 let deltaFactor = map(i, 0, 50, 0, 1 , true)
,因為只要限制底部,所以這邊只將 0~50 的點進行轉換,並將 deltaFactor 與 noise 相乘。在 noise 部分,由於它所產生出的數值是 0~1,為了讓它是左右搖擺的,所以在減去 0.5,讓產生出來的值範圍在 -0.5 ~ 0.5 之間。這時候看上去還有些怪怪的,原因是因為系統預設填了白色的顏色,這邊要加上 noFill()
來取消系統的預設填色
function draw() { translate(0,height) stroke(255) background(0) noFill() //取消系統填色 beginShape() strokeWeight(50) for(var i=0; i<500; i++){ let deltaFactor = map(i, 0, 50, 0, 1 , true) let deltaX = deltaFactor * (noise(i/400, frameCount/100)-0.5) * 200 vertex(xx + deltaX,-i) } endShape() }
多個線條
這時候我們可以將剛剛的程式碼包成 anemone()
函式。接著就可以使用 for 迴圈來讓它一次產生多條擺動的線條,為了使每一條線條扭動的方式不一樣,因此加入一個變數 rid 至 function 中,並且放到 noise 位子。 另外為了將擺動變得更加平滑一些,這邊將 vertex(xx + deltaX,-i)
更改為 curveVertex(xx + deltaX,-i*2)
。
function setup() { createCanvas(800, 800); background(0); } function anemone(xx,rid){ beginShape() strokeWeight(80) for(var i=0; i<300; i++){ let deltaFactor = map(i, 0, 50, 0, 1 , true) let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300 curveVertex(xx + deltaX,-i*2) } endShape() } function draw() { translate(0,height) stroke(255) background(0) noFill() for(var i =0;i<20;i++){ anemone(i*200, i) } }
Part 2 互動與裝飾
現在畫好的基本的圖形後,接著就來上顏色、材質以及外框
加上顏色
首先,先建立顏色的清單,使用 forEach 去跑每一個顏色,並在 anemone()
中加入 clr 這個參數,讓用 stroke(clr)
來指定顏色,這時候就有一叢叢具有三種顏色的的海葵了,接著就要使用混和模式來控制顏色的疊加。這裡使用 blendMode(SCREEN)
來控制混和的效果,不過這邊有個問題是,這會讓原本 background 的失效,而解決的辦法就是在畫 backgound 前加上原本預計的顏色疊加模式 blendMode(BLEND)
。 這邊除了設定顏色外,也讓線條的粗度(strokeWeight)以及高度(hh)都加上 noise,讓整體更加有變化。
function anemone(xx,rid,clr){ beginShape() strokeWeight(noise(rid,5000)*180) // 使用 noise 加入 rid,加入變化性 let hh = noise(xx,rid,1000)*500 + random(2) // 高度也使用 noise 加入 rid,加入變化性 stroke(clr) for(var i=0; i<hh; i++){ let deltaFactor = map(i, 0, 50, 0, 1 , true) let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300 curveVertex(xx + deltaX,-i*2) } endShape() } function draw() { translate(0,height) stroke(255) blendMode(BLEND) // 設定回系統預設的疊加模式 background(0) noFill() let clrs = ['red', 'green', 'blue'] //建立顏色清單 blendMode(SCREEN) //設定疊加模式 for(var i =0;i<10;i++){ clrs.forEach((clr,clrId)=>{ anemone(i*100, i+clrId/2, clr) }) } }
- 變化多端的顏色 (Option,想讓海葵顏色更豐富可以參考)
滑鼠控制
設定會根據高度影響的 mouseFactor 以及滑鼠左右移動變化量的 mouseDelta。設定好後再加到 curveVertex()
中 x 的位置上,這樣的效果可能讓左右的搖擺是根據滑鼠的移動方向。而這裡還另外加上 mouseDirectionFactor
,讓滑鼠的左右移動與海葵搖擺的關係上更加自然。
function anemone(xx,rid,clr){ beginShape() strokeWeight(noise(rid,5000)*200) let hh = noise(xx,rid,1000)*500 stroke(clr) for(var i=0; i<hh; i++){ let deltaFactor = map(i, 0, 50, 0, 1 , true) let mouseFactor = map(i,0,500,0,1)*log(hh)/10 let mouseDirectFactor = noise(frameCount/50)-0.5 let mouseDelta = map(mouseX,0, width, -500, 500)*mouseDirectFactor let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300 curveVertex(xx + deltaX +mouseDelta*mouseFactor,-i*2) } endShape() }
加上材質
先在全域的地方定義材質的變數 overAllTexture,並在 setup() 定義材質的樣式。定義好材質後,就可以使用混和模式將材質疊到畫面上。不過由於一開始有移動畫面的中心點translate(0,height)
,為了讓材質上到正確的位子上,不受這裡的位移影響,所以必須把上材質前畫海葵的部分也用push()
和 pop()
包起來
let overAllTexture //定義材質 function setup() { createCanvas(800, 800); background(0); //設定材質 overAllTexture=createGraphics(width,height) overAllTexture.loadPixels() for(var i=0;i<width;i++){ for(var o=0;o<height;o++){ overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,50,100]))) } } overAllTexture.updatePixels() } function anemone(xx,rid,clr){...} function draw() { push() translate(0,height) stroke(255) blendMode(BLEND) background(0) noFill() let clrs = ['red', 'green', 'blue'] blendMode(SCREEN) for(var i =0;i<10;i++){ clrs.forEach((clr,clrId)=>{ anemone(i*100, i+clrId/2, clr) }) } pop() // 使用混和模式疊上材質 push() blendMode(MULTIPLY) image(overAllTexture,0 ,0) pop() }
加框
設定混和的模式 blendMode(BLEND)
,將矩形疊加在上形成外框。這裡要注意記得將原本生成海葵的地方所設定的 background(0)
取消,不然顏色會被蓋過去,這樣框的效果就出不來了。
function draw() { //加上外框 push() fill(0) noStroke() blendMode(BLEND) rect(0,0,width, height) pop() // push() translate(0,height) stroke(255) blendMode(BLEND) // background(0) 取消這裡的 background,不然會把顏色蓋過去 noFill() let clrs = colors blendMode(SCREEN) for(var i =0;i<10;i++){ clrs.forEach((clr,clrId)=>{ anemone(i*100, i+clrId/2, clr) }) } pop() push() blendMode(MULTIPLY) image(overAllTexture,0 ,0) pop() }
Part 3 細海葵
接下來要來加上相對比較細小的海葵,這裡可以直接複製我們前面已經利用 for 迴圈所產生出的海葵。在這裡因為我們想要海葵能夠相對長的比較細而且也比較長,所以這邊再多增加兩個參數在後面。接著再 anemone()
中新增變數名稱,分別為控制寬度的 thinkness 長度的 length,並帶到 function 中去做使用
//粗線條 for(var i =0;i<10;i++){ clrs.forEach((clr,clrId)=>{ anemone(i*100, i+clrId/2, clr) }) } //細線條 for(var i =0;i<5;i++){ clrs.forEach((clr,clrId)=>{ anemone(i*100, i+clrId/2 + 50, clr, 0.05, 1.2) }) }
function anemone(xx,rid,clr, thinkness=1, length=1){ //新增參數 thinkness=1, length=1 beginShape() strokeWeight(noise(rid,5000)*150*thinkness) // 乘上 thinkness let hh = noise(xx,rid,1000)*height/2 + random(2) stroke(clr) for(var i=0; i<hh; i++){ let deltaFactor = map(i, 0, 50, 0, 1 , true) let mouseFactor = map(i,0,400,0,1)*log(hh)/10 let mouseDirectFactor = noise(frameCount/50)-0.5 let mouseDelta = map(mouseX,0, width, -500, 500) * mouseDirectFactor let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300 curveVertex(xx + deltaX +mouseDelta*mouseFactor,-i*2* length) // 乘上 length } endShape() }
細線上加入球球
畫好了細的線條後,我們想要在細的線條的上的最後一個點上加上一個小球,所以這裡必須新增的兩個變數(lastX, lastY
)去紀錄每一個點,接著由 curveVertex(lastX,lastY)
畫出來線條來。當離開 for 迴圈時,代表已經畫完整條線了,並且此時變數lastX, lastY
所存的是最後的一個點的位子,這時在 for 迴圈外面在根據剛才紀錄去畫上圓形。而在小圈圈的繪製上,在這裡加上 nosie,讓小圈圈可以隨機的出現在細線尾端。
function anemone(xx,rid,clr, thinkness=1, length=1){ beginShape() strokeWeight(noise(rid,5000, frameCount/1000)*150*thinkness) let hh = noise(xx,rid,1000+frameCount/100)*height*0.6 + random(2) stroke(clr) let lastX, lastY for(var i=0; i<hh; i+=2){ let deltaFactor = map(i, 0, 50, 0, 1 , true) let mouseFactor = map(i,0,400,0,1)*log(hh)/10 let mouseDirectFactor = noise(frameCount/50)-0.5 let mouseDelta = map(mouseX,0, width, -500, 500) * mouseDirectFactor let deltaX = deltaFactor * (noise(i/400, frameCount/100 + mouseY/100,rid)-0.5)*300 lastX = xx + deltaX +mouseDelta*mouseFactor lastY = -i*2* length curveVertex(lastX,lastY) } endShape() if(thinkness!=1 && noise(frameCount/1000,rid)<0.8){ //新增 noise ellipse(lastX,lastY-10,6,6) } }
Part 4 一叢海葵
目前海揆線條是由下而上往上生長,現在我們希望它是由中間往外擴散,變成一叢的感覺。所以為了 改變 X 的位子,使用 log 去取值,產生 ratio,再由中心點 width/2
向外向上去畫出一個指數型弧線的線條。這邊直接看 ratio 這個變數一眼看上去有些複雜,不過一開始也是由簡單直覺的方向去嘗試的,像是先試試 let ratio = map(i,0,500,0,1,true)
,這會讓圖形直接變成一個倒三角形,形成一個線性的變化,接著才嘗試 log
,使圖形呈現指數的圖形。實際 log 所對應數值所畫出來的圖形可以參考這裡。可以看的出來在一開始 y 軸的數值上升的很快,接著快速的趨近於緩和,這所對應的ratio
數值也會是如此
let ratio = map(log(i),0,noise(frameCount/100, mouseX/100)*3+5,0,1,true) curveVertex(lerp(width/2,lastX,ratio),lastY)
最後的微調
最後,在一些小地方進行調整。
- 將粗度與高度都加入時間上(frameCount)上的變因
strokeWeight(noise(rid,5000, frameCount/1000)*150*thinkness) let hh = noise(xx,rid,1000+frameCount/100)*height/2 + random(2)
- 滑鼠上下移動會影響左右的搖擺,所以把 deltaX 這個變數上新增 mouseY 在 noise 之中
let deltaX = deltaFactor * (noise(i/400, frameCount/100 + mouseY/100,rid)-0.5) * 300
- 由於在顏色上有點過曝,所以加上透明度的效果,並且加上 noise,試試看讓他有忽暗忽亮的效果
for(var i =0;i<10;i++){ clrs.forEach((clr,clrId)=>{ let useColor = color(clr) useColor.setAlpha(150 + noise(frameCount/100,i)*10) anemone(i*100, i+clrId/2, useColor) }) }
結語
回顧整個範例,可以發現我們在不少的地方都使用了 noise 來產生隨機,進而產生了一些動態感。常常與 noise 搭配的有隨時間變化的 frameCount 以及滑鼠移動的變化量 mouseY。在 noise 的使用上有時也並非就直接就使用它,而是當我們覺得某個屬性,像是海葵都高度,向外擴張的程度想要更有隨機動態感的時候再加上去的
在每一個點的繪製上為了製造的左右晃動以及跟滑鼠互動,所以定義了不少的變數,其中還常常用到了 map 以及 noise,這裡面的數字沒有一定的絕對數值,這都是在創作過程中慢慢地去嘗試出來的。若是覺得有些程式碼看起來太過複雜,建議可以先將 noise 改回一般的常數,這樣閱讀起來比較簡單也容易思考。
還意猶未盡?看看這一篇【p5.js創作教學】電波電路Wave Circuit – 來做個逼哩逼哩送訊號的電路吧!的教學讓你功力再進階。
如果你因此對互動藝術程式創作產生興趣,歡迎加入老闆開的 Hahow 課程互動藝術程式創作入門,讓老闆跟你分享不同的創作!
互動藝術程式創作入門是為了不會程式的人設計的課程,課程中會帶你看看不一樣的作品,並從基礎引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓大家覺得寫 code 不是這麼難的事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠做出和與眾不同的創作。