當初老闆一開始在做這個作品時,是沒有甚麼特定想法的,過程中其實也是蠻迂迴的。
一開始先以極座標的方式去定義點的位子,並用 curveVertex()
將點連成線。過程中不斷去調整 curveVertex()
中 X與Y的位置,到了步驟五,呈現出一顆中心原子核的時候才比較確定這次的主題,接下來就決定朝向物理以及科幻的風格去做,所以後面就加上了在四周圍繞的游離粒子、標示刻度的數字以及線條來包圍中間的量子完成作品。
透過此次互動藝術創作教學,你會學到
- 在製作 HUD 這類科幻類型主題時,可以使用哪些裝飾性的技巧
- 如何應用極座標的概念,來繪製線條,並透過旋轉角度速度、內外層圓圈、圓周上位子等不同的參數來調整出所希望的效果。
- 利用原有的素材 ( X與Y的位置),來產生出不一樣的物件效果 (游離粒子)
在開始製作之前你該知道的p5.js小技巧
- 極座標 來繪製粒子位置,並將點的位子連接起來形成曲線
- vertex() 有極座標位置後,以
vertex()
將點連成線,需要在畫線的前後加上beginShape()
與endShape()
- curveVertex() 與
vertex()
相同將點連成線,差異在於vertex()
所連成的線為直線,而curveVertex()
連成的為曲線 - 透過 noise() 產生隨機的效果,比起一般常用的
random()
來說會更自然、和諧一點。老闆的範例中會時常使用 noise,所以建議在開始之前點進去先稍微理解 noise 的概念 - drawingContext 繪製陰影
- push() / pop() 保存與還原畫布的狀態(可以參考老闆互動藝術程式創作入門的課程筆記:章節 7 進階繪圖 – 畫布操作與編織複雜圖形 (pop/push 的圖解))
- setAlpha() 可設定顏色的透明度
就讓老闆帶你們一步步完成吧!
1. 繪製圓形與波形
在最初的時候我們先由畫布中心向外畫出同心圓,來確認一下每一個圓大致上的位置。(程式碼中的1. 同心圓版本)
接下來就將原本繪製圓形的 ellipse
替換成以極座標 cos 與 sin 來作呈現,詳細的原理可參考 來用可怕的三角函數做網頁吧! – Part 1 。這樣我們就可以動態地指定半徑 – rr
的大小。(程式碼中的2. 改成 vertex,並以 cos 以及 sin 來指定點的位置)
另外,也可以更改 ang 的角度,展示出不同的波形,而途中會產生動態旋轉效果的原因是因為老闆在設定 ang 中,加入了系統變量(也就是計算程式啟動以來的幀數 frameCount
)。(程式碼中的3. 以 frameCount
來產生動態的角度變化 ang,隨時間製造出不同的波型)
這裡提供一個畫圓的示範連結,可以嘗試改變 cos 與 sin 裡面的 i
,以及後面所相乘的 50 ,透過修改去理解各個參數所代表的是甚麼含意。
function setup() { createCanvas(800, 800); } function draw() { background(230) translate(width/2, height/2) // 1. 同心圓版本 for(var i=0; i<width; i+=100){ ellipse(0, 0, width-i); } // 2. 改成 vertex,並以 cos 以及 sin 來指定點的位置 for(var i=0; i<width; i+=100){ beginShape() let rr = width-i; // ellipse(0, 0, width-i); for(var o=0; o<360; o+=3){ vertex(cos(o)*rr, sin(o)*rr) } endShape() } // 3. 以 frameCount 來產生動態的角度變化 ang,隨時間製造出不同的波型 for(var i=0; i<width; i+=100){ beginShape() let rr = width-i; // ellipse(0, 0, width-i); for(var o=0; o<360; o+=3){ let ang = o/1.2+frameCount; vertex(cos(ang)*rr, sin(ang)*rr) } endShape() } }
2. 加上顏色
在顏色的選擇上,老闆一樣使用配色網站 https://coolors.co/ 抓取隨機產生的配色,在選定好顏色後,可以直接複製上面的網址到程式中處理,變成可以使用的色票格式。
而在波形上老闆有做三個更動:
- 減少連接點的數量,360 個 → 100個。
- 在點與點的連接上,由
vertex
改為curveVertex
,差異在於curveVertex
在連接點與點之間會帶有弧形,會比較有滑順感,vertex
則是直線連接。 - 在 ang 角度的設定上,若是把係數拿掉,會長成
o / i + frameCount / o
,其中i
所代表的是由內而外的每一圈,當它被放在分母的時候,這也代表當越外圈(i
越小),o / i
也會越大,再參數相加後,ang
也跟著變大,這樣就達成了越外圈頻率越快,越是內圈頻率越慢的效果。
/*1. 設定顏色 */ let colors = "3d348b-7678ed-f7b801-f18701-f35b04".split("-").map(a=>"#"+a) function setup() { createCanvas(800, 800); } function draw() { background(0) translate(width/2, height/2) strokeWeight(2) noFill() for(var i=0; i<width; i+=100){ beginShape() let rr = (width-i)*0.8; // ellipse(0, 0, width-i); /*1. 設定顏色 */ stroke(colors[int(i/100)%5]) /*2. 改變波型數量: 360 -> 100 ; 改變角度(ang)設定 ; Vertex -> curveVertex */ for(var o=0; o<100; o+=3){ let ang = o/(1.2+i/2000)+frameCount/(40+o); curveVertex(cos(ang)*rr, sin(ang)*rr) } endShape() } }
3. 加上材質 調整波型大小
接著為了讓上面的波型有點分段,所以加上開始的角度 stAng
,在 stAng
中,老闆根據上該點是第幾圈 i
,以及是在圓周上哪個位置上 o
,來決定是哪個角度開始。
而在每一個點的位置上,老闆在 x 與 y 的座標上都加上 i
值,因為所有的點都同時加上一樣的正值,所以原本可以完整圓的畫面,現在看去只能顯示原本圖形的左上的部分,其餘的點並非消失,而是該點的位置已經超出畫面的範圍了。
/* 2. 加上 start angle */ for(var o=0; o<100; o+=3){ let stAng = frameCount/1000 + (o/2) + (i/2) let ang = stAng+o/(1.2+i/2000)+frameCount/(40+o); /* 3. 在 x 與 y 的位置上都加上 i */ curveVertex(i+cos(ang)*rr, i+sin(ang)*rr) }
下面是以只有一個圓的簡化版本,並且標示出位置點,如此一來比較容易思考點在位置上的變化。
除了調整波型外,老闆也在這裡加上材質,新增材質共有三大步驟,分別是命名材質、設定材質、疊加材質。
- 命名材質
在全域中命名材質變數let overAllTexture
/* 1. 加上材質 */ 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,40,80]))) } } overAllTexture.updatePixels()
- 疊加材質
在draw()
中改變與色塊的混合模式
push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop()
4. 調整縱向比例製造中心壓縮
這裡老闆先將原本上個步驟在 x 與 y 位置加上 i
的效果先拿掉。改以增加 x 參數中半徑的大小,在 rr
後加上了 i/2
,這會左右拉伸,製作出被上下壓縮的效果。而在 y 在位置上則是改加上 i/2
,產生出由上而下的,俯瞰的感覺。為什麼這裡不是一樣加上 i
呢 ? 這是因為這會使圖形超出畫面的範圍。
/*3. 線條變粗 */ strokeWeight(10) /*1. 將 X 變小 */ for(var o=0; o<100; o+=5){ let stAng = frameCount/1000 + (o/2) + (i/10) let ang = stAng+o/(3+i/5000)+frameCount/(30+o); curveVertex(cos(ang)*(rr+i/2), i/2+sin(ang)*rr) }
接著老闆替線條加上陰影,在陰影的設定上,有四個基本的參數,分別是顏色 (shadowColor
)、 X 的偏移量(shadowOffsetX
)、 Y 的偏移量(shadowOffsetY
)以及模糊的程度(shadowBlur
)。加上陰影是老闆在很多的創作時候會使用的手法,當這個陰影就是根據線條本身改變的時候,就會有拼貼的立體感出來。
function setup() { /*設定材質 */ /*2. 加上陰影 */ drawingContext.shadowColor = color(0,100) drawingContext.shadowOffsetY = 3 drawingContext.shadowOffsetX = 3 drawingContext.shadowBlur = 20 }
5. 移動中心點繞中心旋轉
再次調整波型呈現方式 !
這裡回歸到原始純粹的模樣,將 stAng
設定為 0,並且將原本加在 y 位置上的i/2
拿掉,這樣就像是一個圍繞的中心在旋轉的粒子球。
/*1. 回歸到最於原始,並且變成是繞中心點旋轉 */ let rr = (width-i)*0.5 ... for(var o=0; o<100; o+=5){ let stAng = 0 let ang = stAng+o/(3+i/5000)+frameCount/(30+o); curveVertex(cos(ang)*(rr+i/2), sin(ang)*(rr-i/10)) }
接著就把作品固定到中央,並且讓它慢慢地旋轉,此時突然發現這好像磁力線以及極光的感覺,所以就決定朝物理學的方向做,便是外圍先作圓形,在外面做一圈圓形,將原本裡面的線條做限制,並且將裡面的圓縮小且帶旋轉。
function draw() { /*4. 加上背景 */ fill(30,30,100,0.85) rect(0,0,width,height) push() /*1. 將球中心置於畫布中心 */ translate(width/2,height/2) /*3. 縮小並旋轉 */ scale(0.7) rotate(frameCount/500) stroke(255,150) /*2. 外圍加上一圈 */ noFill() strokeWeight(10) ellipse(0,0,900) for(var i=0; i<width; i+=100){ ... } pop() }
6. 加上發光效果跟調整材質
再加上圓形後,就開始覺得這個東西應該是會發光的,所以加上 Screen mode
,不過視覺上又會覺得顏色有點太多層,所以加上了 setAlpha
,以降低透明度的方式來去做調整,而且老闆還特別將線條與陰影分開做設定,可以看到陰影與線條的顏色是一樣的 (let cc = color(clr)
) ,只是透明度不同,透過這樣將 setAlpha 降低,畫面上就不會過曝,顏色看上去也就會比較明顯一點。
調整完色彩後,覺得可以再多加上一些變化,所以就再加上了 cos(ang*8+o/100)*rr/200
以及 sin(ang*8+o/100)*rr/200
,前面這串程式看上去很長,加上現在圖形還同時在旋轉,不太容易看出效果,所以這邊可以試著將程式簡化,用一開始所提供的同心圓範例去修改成簡單版本的(如下方示意圖)所呈現的效果是可以看到不只是完整的大波,上面另外也會有一些小的褶皺。
push() ... /*1. SCREEN mode */ blendMode(SCREEN) for(var i=0; i<width; i+=100){ beginShape() strokeWeight(8) let rr = (width-i)*0.5 /*2. 降低透明度,本體與陰影分開 */ let clr = color(colors[int(i/100)%5]) clr.setAlpha(150) stroke(clr) let cc = color(clr) cc.setAlpha(100) drawingContext.shadowColor =cc /*3. 加上小的波形 */ for(var o=0;o<150;o+=5){ let stAng =0 let ang = stAng+o/(3+i/5000)+frameCount/(30+o)+mouseY/100 curveVertex( cos(ang)*(rr+i/2)+ cos(ang*8+o/100)*rr/200 , sin(ang)*(rr-i/10) + sin(ang*8+o/100)*rr/200 ) } endShape() }
調到這裡其實這次的主題就明確了,就是物理量子力學的主題。
7. 加上外部裝飾
接下來,在做這種科幻型的主題時,老闆的慣用手法就是加上一些弧形或者是刻度。這裡老闆決定在四周畫上線條,有點像是準心的感覺,並且加上有一個似乎意義不明的數字,這樣一來就產生一種「好似有複雜的機制在後面運作」的感覺,為了避免顯示的數字太長,所以在數字上使用 toFixed(2)
來限制小數點後可顯示的位數為兩位數。另外在此星球旋轉上也加上了 mouseX
,讓滑鼠在移動時,也會進而影響星球旋轉的速度。
function draw() { push() /*1. 旋轉上加上滑鼠互動 */ translate(width/2,height/2) scale(0.73) let totalRotate = frameCount/500+mouseX/100 rotate(totalRotate) /*2. 加上四象的線條,像是準心 */ stroke(255) line(480,0,550,0) line(-480,0,-550,0) line(0,-480,0,-520) line(0,480,0,520) noStroke() fill(255) textSize(20) text(totalRotate.toFixed(2),500,30) pop() }
8. 加上游離粒子
這裡老闆想要加上一些游離粒子,而加上游離粒子的方式非常的特別,因為曲線看上去是一條線,但是實際上都是一個點一個點所連接起來的,所以老闆就想說那不如把 X 與 Y 拿來畫。
在將 X 與 Y 拿來使用前,先將畫曲線的點抽出來命名變數 let xx = cos(ang)*(rr+i/2)+ cos(ang*8+o/100)*rr/200
以及 let yy = sin(ang)*(rr-i/10) + sin(ang*8+o/100)*rr/200
,這樣在畫曲線 curveVertex(xx,yy)
與 ellipse(xx,yy,3)
粒子都會比較方便,程式碼也不會顯得太長。透過直接使用畫曲線的點去畫粒子,可以讓視覺上粒子與線條具有一致性的效果。
這裡有一點要注意,由於在粒子有使用 scale(1.4)
放大,為了不影響到其他物理,所以要用 push
與 pop
把它包起來。
for(var o=0;o<150;o+=5){ let stAng =0 let ang = stAng+o/(3+i/5000)+frameCount/(30+o)+mouseY/100 let xx = cos(ang)*(rr+i/2)+ cos(ang*8+o/100)*rr/200 let yy = sin(ang)*(rr-i/10) + sin(ang*8+o/100)*rr/200 curveVertex( xx ,yy ) /*1. 加上游離粒子 */ if (random()<0.1){ push() scale(1.4) ellipse(xx,yy,3) pop() } }
9. 加上裝飾
再來加一些一些裝飾性的物件,希望可以達到畫龍點睛的效果。
外圈還缺甚麼呢?一開始想要加像是光場或是力場的東西把它包住,但是又覺得似乎有些干擾,而且目前已經有一圈白線,所以改畫個在 HUD 裡面蠻常見的手法 – 虛線,畫弧形虛線的方式就是用小小的 arc 來畫,在畫面的下方。
到這裡差不多要做結尾了,這邊老闆在思考如何讓畫面變得更豐富,不是說硬將東西塞上去,而是能不能透過輔助的小物件,讓整件作品更有故事性。一開始老闆有想說加上像是折線圖或條圖,代表星球當下的狀態或速度,讓人可以知道這個星球當下的狀態。後來決定在整體的旋轉上面加上 noise,製造出就像是一個羅盤旋轉的感覺。
/* 1. rotate 加上 noise */ translate(width/2,height/2) scale(0.73) let totalRotate = frameCount/500+mouseX/100 + noise(frameCount/1000)*5 rotate(totalRotate) stroke(255,150) noFill() strokeWeight(4) ellipse(0,0,900) stroke(255) line(480,0,550,0) line(-480,0,-550,0) line(0,-480,0,-520) line(0,480,0,520) /* 2. 裝飾性橢圓 */ ellipse(0,550,50,20) ellipse(0,-550,50,20) ellipse(0,580,30,10) ellipse(0,-580,30,10) noStroke() fill(255) textSize(20) text(totalRotate.toFixed(2),500,30) noFill() blendMode(SCREEN) /* 3. 裝飾性線條 */ for(var i=0;i<200;i+=10){ stroke(255,50) arc(0,0,1200,1200,PI/200*i,PI/200*i+0.035) }
10. 加上原子核
既然作品現在看上去像是一個粒子,所以總該要有個原子核,而這個核心很不穩定,而周圍有個力場包圍住。為了製造出不穩定的感覺,所以在原子的位置以及大小,都加上了 noise 來產生出隨機的效果。
/* 1. 中間不穩定的圓心 */ fill(random()<0.3?colors[2]:255) ellipse(0 + (noise(frameCount/5,10)-0.5)*50, 0+ (noise(frameCount/3,1000)-0.5)*50, 30+ noise(frameCount/10)*40) noFill()
結語
我們總結一下這次的量子系統,製作過程可以分為三大部分:
- 中心帶有磁力線量子效果的圓心是此次作品的核心,老闆使用極座標的方式是先去定義每一個點的位置,再使用
curveVertex
以將點連接成一成線條。在極座標的表示上是 (cosθ * 半徑,sinθ * 半徑),在過程中不斷透過改變半徑比例的調整以及點的位置。 在這邊只要一個小小的變化,像是將半徑數值除以二,就會形成一個被壓縮的球體,建議大家可以一開始所提供畫圓的範例,多去嘗試改變其中不同的參數去觀察所對應會產生什麼效果,也因為是各個點都是相對獨立的,所以在位置的變化上可以有相對多的彈性,像是老闆在過程中就嘗試了加上stAng
以及ang
來試試看能不能有些有趣的變化,說不定你在嘗試的過程中就會發展出另一個和老闆完全不一樣的作品出來。 - 延續前面所說,線條實際上是由每一個點來定義,所以老闆就利用這樣的機會,透過定義好的點,加上一點變化形成四周的游離粒子;增加畫面的豐富度、疊加材質,做出與滑鼠X與Y位置的小互動。
- 想要製作 HUD 風格的作品可以加上單一顏色外框以及虛線線條,尤其可加上看似神秘的數值會更有效果。
這就是我們用p5.js寫出來的簡單的小作品啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。
相同的繪製原理還能應用在甚麼作品上呢?若對p5.js寫成的互動藝術程式創作有興趣,歡迎加入老闆開的Hahow互動藝術程式創作入門課程,與另外將近兩千位同學一起創作吧!
此篇直播筆記由幫手 阮柏燁 協助整理