身為台灣人可能沒親眼看過龍捲風,但從新聞或影片畫面裡大概它知道長甚麼樣子,威力又有多強大。

今天我們要利用p5.js來完成瘋狂的龍捲風捲起乳牛與房屋的一張動態圖片,在最一開始老闆當然是先嘗試著刻劃龍捲風的外型,接著分階段完成被龍捲風捲起的物體:乳牛以及穀倉,最後進行美術上的修改,包含了背景的顏色、加上材質以及調整龍捲風的線條等,最後加上滑鼠的互動來增加整體作品的豐富度。
透過此次互動藝術創作教學,你會學到
- 以幾何圖形繪製龍捲風,其中涵蓋了線條的調整,使用透明的填色讓龍捲風有雲霧感
- 如何應用極座標的概念,使用三角函數的觀念來繪製不規則飛行的物體的運動軌跡
- 繪製漸層背景的技巧,以及透過旋轉來增加背景的動態感
想要更了解三角函數,歡迎閱讀這兩篇複習: 來用可怕的三角函數做網頁吧! – Part 1 衛星繞月球(直播筆記) 來用可怕的三角函數做網頁吧! -Part 2科幻時鐘(直播筆記)
在開始製作龍捲風之前,你該知道的p5.js小技巧
- 極座標 來繪製運動軌跡
- 透過 noise() 來產生隨機的,這與 random 的概念不一樣。在範例中會時常使用 noise,所以建議在開始之前點進去先稍微理解 noise 的概念
- drawingContext 繪製陰影
- push() / pop() 保存與還原畫布的狀態(可以參考老闆互動藝術程式創作入門的課程筆記:章節 7 進階繪圖 – 畫布操作與編織複雜圖形 (pop/push 的圖解))
就讓老闆帶你們一步步完成吧!

1. 畫龍捲風
讓我們一起先來思考龍捲風看上去的樣子,它其實就是一層層圓圈,由下面的一個小圓圈開始慢慢地往上,圓圈變得越來越大。一開始我們先嘗試由上而下劃出一排圈圈,暫時不考慮大小的變化排列。
function setup() {
createCanvas(800, 800);
background(100);
}
function draw() {
translate(width/2, 0)
for(var i=0; i<height; i+=2){
let nn = noise(i/10, frameCount/10)
ellipse(0, i, nn*100)
}
}

下一步要讓圈圈有大小區分的排列,我們必須由 “nn” 這個變數上來調整,在後面加上 “i” 這個變數,這樣一來,nn 的數字大小也會相對應地跟著從小到大。
function setup() {
createCanvas(800, 800)
background(100)
}
function draw() {
translate(width/2, 0)
for(var i=0; i<height; i+=2){
let nn = noise(i/10, frameCount/10) + i/10
ellipse(0, i, nn*10, nn*3)
}
}

然而這樣上面小、下面大的樣式並不是我們所要的,所以在 nn 變數中的 i 要改成 “height-i”,這樣才能讓數字由大到小排列,所畫出的樣子才會像是龍捲風的模樣。除了大小排列外,我們也順便將圖形刪除填色,留下僅有線條的樣子。
我們使用 nn 這個變數,可以發現它在一開始的時候有加上 noise 的效果,這是一個可以產生出隨機的變數,有了它,龍捲風看上去更增添動態感。
function setup() {
createCanvas(800, 800)
background(100)
}
function draw() {
fill(0)
rect(0,0,width,height)
translate(width/2, 0)
stroke(255)
noFill()
for(var i=0; i<height; i+=3){
strokeWeight(random(2))
let nn = noise(i/10, frameCount/100)*20+(height-i)/9
ellipse(0, i-100, nn*8, nn*3)
}
}

2. 加上被捲飛的物體
一開始我們必須思考物體被龍捲風捲起時的運動軌跡,先不用繪製乳牛、車子這些我們最後真正所要呈現的物體,先以簡單的方塊完成軌跡之後,最後再替換掉即可。
在物體位置的繪製上,我們要使用極座標的方式表示,極座標需給定半徑r,以及角度 θ,有了這兩個參數,那一個點在平面座標上就可以表示為 [rcosθ,rsinθ]。老闆使用 ang 表示角度, r 來表示半徑。設定好後,就使用 translate 來進行偏移。現在的原點位於整張圖中間最上方的位置,為了讓物體平均分散在整個龍捲風的周圍,所以在 y 座標中,會再加上 o*50,來讓物體是由上而下來進行分布的。
// 龍捲風
for(var i=0; i<height; i+=3){
strokeWeight(random(2))
let nn = noise(i/10, frameCount/100)*20+(height-i)/9
ellipse(0, i-100, nn*8, nn*3)
}
// 飛行物體
for(var o=0;o<10;o++){
let ang = o+frameCount/10
let nn = noise(i/10,frameCount/100)*20+(height-i)/9
let r = nn*10
fill('blue')
push()
translate(cos(ang)*r,sin(ang)*r+o*50)
rect(0,0,50,50)
pop()
}

現在看上去,物體仍旋轉得很制式,這是因為每一個物體的角度是直接根據它是第幾個來去設定它的角度的。為了增加隨機性,我們增加一個 rot 變數,並且透過 noise 增加隨機性,設定好再使用rotate做旋轉。
// 飛行物體
for(var o=0;o<10;o++){
let ang = o*5+frameCount/50
let nn = noise(i/10,frameCount/100)*20+(height-i)/9
let r = nn*(30+noise(o,frameCount/50)*5)
let rot = noise(o,frameCount/100)*10
fill('blue')
push()
translate(cos(ang)*r,sin(ang)*r+o*50)
rotate(rot)
fill('white')
rect(0,0,50,50)
pop()
}

3. 畫牛
設定好物體的旋轉方式後,我們來將方塊換成牛隻。在牛的繪製上可大致分成頭部、身體以及腳。為了使牛看起來精緻可愛,所以這裡大多都是由基本形狀如圓形、方形等等所構成,在這裡就是要考驗大家一筆一筆繪製、調整的耐心了。 這裡比較要注意的是,為了讓牛看起來比較生動些,我們將腳加上一些動畫,不管是前腳還是後腳,老闆都讓它左右邊的腳相差大約一個 PI 的角度左右,腳擺動起來才不會那麼地生硬。
另外,由於現在物體都是被龍捲風吹起來亂亂飛的,有點看不清楚到底有沒有把牛畫正,所以這裡會在 draw() 加入 drawCow(50, height-50),讓下方有一個固定的牛隻方便參考。
function drawCow(x,y){
push()
//身體
translate(x,y)
fill(255)
noStroke()
rect(0,0,50,25,5)
fill(0)
rect(5,0,12,15,5)
rect(25,10,10,10,5)
rect(45,5,5,10,5)
// 後腳
fill(255)
push()
translate(3,20)
push()
rotate(sin(frameCount/35))
rect(0,0,5,20,5)
pop()
push()
translate(5,0)
rotate(sin(frameCount/20+PI))
rect(0,0,5,20,5)
pop()
pop()
// 前腳
push()
translate(40,20)
push()
rotate(sin(frameCount/35))
rect(0,0,5,20,5)
pop()
push()
translate(5,0)
rotate(sin(frameCount/20+PI))
rect(0,0,5,20,5)
pop()
pop()
//頭部
translate(-22,-5)
rect(0,0,20,25,5)
fill(255, 179, 160)
rect(0,15,20,10,5)
fill(0,60)
ellipse(6,20,6)
ellipse(14,20,6)
fill(0)
ellipse(5,8,3)
ellipse(15,8,3)
fill(247, 216, 150)
rect(0,-8,3,10,10)
rect(16,-8,3,10,10)
pop()
}
// 飛行物體
for(var o=0;o<10;o++){
let ang = o*5+frameCount/50
let nn = noise(i/10,frameCount/100)*20+(height-i)/9
let r = nn*(30+noise(o,frameCount/50)*5)
let rot = noise(o,frameCount/100)*10
fill('blue')
push()
translate(cos(ang)*r,sin(ang)*r+o*50)
rotate(rot)
drawCow(0,0)
// fill('white')
// rect(0,0,50,50)
pop()
}
drawCow(50, height-50)

4. 調整龍捲風視覺 + 天空上色
第四部分一開始先將天空調整成偏向綠色外,其餘都是調整龍捲風的視覺,調整內容有以下幾點:
- 加上陰影
不管是龍捲風或是飛行的牛隻,都在繪製之前加上陰影,先是指定顏色,接著再去設定其偏移的量
drawingContext.shadowColor = color(0,30); drawingContext.shadowOffsetY = 0 drawingContext.shadowOffsetX = 0
- 以 arc 繪製龍捲風線條
原本繪製的方式是使用ellipse形成一個完整的圓,我們改以 arc 角度來繪製,使用sin來設定龍捲風的起始位置後,再放入 arc 中,這樣子這樣子看起來更像是旋風。
- 左右移動
身為一個龍捲風,左右搖擺起來是一定要的,所以在 translate 的 x 位置再加上cos(frameCount/10)*10,讓整個龍捲風搖擺起來。
- 線條與填色
將構成龍捲風線條註解起來,並且新增填色fill,讓龍捲風以一個雲霧的感覺呈現
上述幾點調整的位置都是 draw() 中,修改後程式碼如下
function draw() {
fill("#9BC1BC")
noStroke()
rect(0,0,width,height)
translate(width/2 + cos(frameCount/10)*10,0)
// stroke(255,100) // 拿掉 stroke
noFill()
// 龍捲風
drawingContext.shadowColor = color(0,30);
drawingContext.shadowOffsetY = 0
drawingContext.shadowOffsetX = 0
for(var i=0; i<height; i+=4){
strokeWeight(random(1)+2)
fill(255,30) // 加上填色
let nn = noise(i/10, frameCount/100)*20+(height-i)/9
let stAng = sin(i+frameCount/2)
arc(0,i-100,nn*8,nn*3,stAng,stAng+PI*1.5)
}
// 飛行物體
noStroke()
drawingContext.shadowColor = color(0,30);
drawingContext.shadowOffsetY = 2
drawingContext.shadowOffsetX = 2
for(var o=0;o<10;o++){
let ang = o*5+frameCount/50
let nn = noise(i/10,frameCount/100)*20+(height-i)/9
let r = nn*(30+noise(o,frameCount/50)*5)
let rot = noise(o,frameCount/100)*10
fill('blue')
push()
translate(cos(ang)*r,sin(ang)*r+o*50)
rotate(rot)
drawCow(0,0)
pop()
}
drawCow(50, height-50)
}

5. 漸層天空
我們要把原本單色系背景改為漸層天空,先將原本的背景註解起來。在繪製漸層背景上,首先先指定一個稍微偏綠色的藍,接著每隔一層線條就加上數值五的藍色,使其越來越偏向藍色,但是單純只有漸層似乎還是有點單調,畢竟此次的主題是龍捲風,所以老闆同時也加上不規則的旋轉,使整體更加有一種不受控制的感覺。
// 漸層背景
let cc = color("#6AD5CB")
push()
rectMode(CENTER)
for(var i=0;i<height;i+=100){
push()
cc.setBlue(cc._getBlue()+5)
fill(cc)
rotate(random(-1,1)/10)
rect(width/2,i,width*1.2,100)
pop()
}
pop()
// 原本的背景
// fill("#9BC1BC")
// noStroke()
// rect(0,0,width,height)

6. 加上材質
為了增加一點繪畫感,我們可以為整個畫面新增材質疊加。共有三大步驟,分別是命名材質、設定材質、疊加材質。
- 命名材質
在全域的區域命名材質變數let overAllTexture - 設定材質
在setup()中設定材質的樣式
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,30,50])))
}
}
overAllTexture.updatePixels()
- 疊加材質
在 setup() 中改變與色塊的混合模式
push()
blendMode(MULTIPLY)
image(overAllTexture,0,0)
pop()

加上材質後,畫面上黑色一條一條的痕跡更明顯了,我們稍微將它修正成沒有線條,並且改變透明度。
push()
let cc = color("#6AD5CB")
cc.setAlpha(100) // 設定透明度
noStroke() // 設定沒有線條
push()
rectMode(CENTER)
for(var i=0;i<height;i+=100){
push()
cc.setBlue(cc._getBlue()+5)
fill(cc)
rotate(random(-1,1)/10)
rect(width/2,i,width*1.2,100)
pop()
}
pop()
// fill("#9BC1BC")
// noStroke()
// rect(0,0,width,height)

7. 加入穀倉
在畫穀倉上也沒有太高深的技巧,與繪製牛一樣,像個手工業一樣慢慢一筆一畫描繪出來。不過有一個比較少看到的用法是 shearX ,它可以將物體沿著X軸歪斜指定角度,用來協助繪製梯形的屋頂。 另外老闆在生成這些穀倉時,並沒有再多寫一個迴圈,而是直接用餘數的方式去決定要生成牛還是穀倉。原先放在左下角當成繪製參考的穀倉也沒有拿掉,因為老闆後來覺得將單一穀倉就這樣固定在那裏也不錯。
function drawHouse(x,y){
push()
translate(x,y)
// rect(0,0,100,50)
fill(199, 77, 93)
rect(20,0,80,40)
fill(219, 77, 93)
rect(0,0,80,40)
fill(255,50)
rect(0,0,80,6)
stroke(255,100)
strokeWeight(2)
noFill()
rect(40,10,20,10)
rect(40,20,20,10)
rect(40,10,10,20)
noStroke()
//屋頂
shearX(PI/12)
fill(68, 61, 63)
rect(20,0,80,-20)
shearX(-PI/6)
fill(58, 51, 53)
rect(0,0,80,-20)
pop()
}
// drawCow(0,0)
if (o%6!=0){
drawCow(0,0)
}else{
drawHouse(0,0)
}

8. 修飾細節跟最後收尾
translate 加上與滑鼠 mouseX 做互動
// translate(width/2 + cos(frameCount/10)*10,0) translate(width/2 + cos(frameCount/20 + mouseX/50)*50,0) // 加上 mouse
龍捲風的細線條隨機出現
for(var i=0; i<height; i+=4){
strokeWeight(random(1)+2)
fill(255,10) // 加上填色
// 隨機出現線條
if (random()<0.1){
stroke(255)
}else{
noStroke()
}
let nn = noise(i/10, frameCount/100)*20+(height-i)/9
let stAng = sin(i+frameCount/2)
arc(0,i-100,nn*8,nn*3,stAng,stAng+PI*1.5)
}

結語
我們總結一下這次的龍捲風小品,製作過程可以分為三大部分:
- 龍捲風的繪製,思考龍捲風的模樣以及它具有什麼特性,我們可以用什麼圖形以及線條去模擬,從最一開始使用圓形來構成龍捲風的架構,慢慢地調整細節,讓它更加接近真實的模樣。
- 被捲起的物體 – 乳牛與穀倉兩者所使用技巧都是用基本的圖形建構而成。另外使用極座標表示被捲起的軌跡也是相當值得學習的技巧。
- 視覺上的調整,加入材質以及漸層背景等應用,以及滑鼠的簡單互動讓作品更加完整。
這就是我們用p5.js寫出來的簡單的小作品啦!相同的繪製原理還能應用在甚麼作品上呢?你們也可以參考老闆這件作品的OpenProcessing頁面,一起來切磋吧!
若對互動藝術程式創作有興趣,歡迎加入老闆開的Hahow課程互動藝術程式創作入門,與其他兩千位同學一起創作吧!




