色彩繽紛的幾何形狀不斷輻合旋轉,細細的線條卻像是毒刺一樣。動態改變形狀一下密密麻麻一下稀疏鬆散,多變的風貌讓人甜蜜陶醉卻又像是陷阱般危險的感覺!
本文是【p5.js 程式創作直播】210731 Sweet Trap 甜蜜陷阱 的直播影片筆記,大家如果想要和老闆一起 chill 度過寫程式的時光,可以打開影片開啟這趟心流之旅,或者…繼續往下看!
這次直播是用 openprocessing 網頁平台來撰寫,打開網頁就可以開始 coding 創作了!成品在這裡。
直播時老闆聊到了設計的作品被剽竊的故事,但也因為這樣被 Art Blocks 平台看見。現在正是NFT藝術品百花齊放的時候,大家在這支影片中可以了解到生成式藝術迷人的地方,甚至開始創作自己的 NFT 。來吧,這次的作品運用到不少關於角度的概念,讓我們一起建立一個秩序又隨機的世界!
這次直播筆記會帶大家學會
- 將三角函數的概念運用在極座標,透過計算角度來畫出花瓣狀的軌跡
- 旋轉與移動座標系,簡單定位軌跡中的每個點
- 利用存取滑鼠的座標,自由變化圖形的樣貌
- 計算角度簡單繪製出三角形狀
- 存下自己喜歡的色票並隨機呈現顏色,每一次播放都會產生不同顏色組合
- 運用noise()製造出有規律的隨機
會使用到的 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 的方形(如果要畫正方形的話,即寬度=高度)。
- triangle(x1, y1, x2, y2, x3, y3):以三個頂點座標繪製出三角形。
- text(str, x, y):在(x,y)座標呈現出文字str
- rotate(angle): 將座標系依照該角度旋轉
- translate(x, y): 將座標系移到(x,y) 上
- push(): 儲存目前畫筆設定的狀態
- pop(): 恢復畫筆在push()時儲存的狀態,與push()合併使用
- random(): 沒有傳參數時,會返回一個0~1之間的隨機浮點數。
- noise(x,[y],[z]): 產生0~1之間的浮點數。傳入的x,y,z代表座標,會在一、二、三維的Perlin noise噪聲空間取出對應該座標在0~1之間的值。這個方法會使得相近的座標取到的值也相近,比較有連續性,不會像random()每次取值都是完全隨機的。有興趣的同學也可以延伸閱讀相關資訊:2D Noise – Perlin Noise and p5.js Tutorial
- map(value, start1, stop1, start2, stop2, [withinBounds]):會回傳某個位於start1~stop1範圍的值如果對應到start2~stop2範圍中是多少。最後一個參數的意義可以參考文件描述。
- pow(n,e):計算n的e次方
- blendMode(mode):讓圖形相互以不同的方式疊加色彩,有各種模式可以選擇,例如:DARKEST、LIGHTEST、DIFFERENCE等。
- image(img, x, y, [width], [height]):以img材質在x,y座標畫出圖片。
- pixelDensity(val):增加像素的密度,預設像素的密度是與螢幕相同。
跟著老闆開始動手做
1. 簡單的起手式
在 openprocessing 網頁右上角可以Create a Sketch,會來到一個已有預設程式碼的新頁面。從這裡開始我們來認識setup()、draw()與mouseX()、mouseY()。
- setup(): 可以視為程式環境的初始化,在每次按下撥放鍵開始執行時,會呼叫 setup() 裡的程式碼一次。
- createCanvas(width, height):創建畫布,參數中分別設定寬跟高(單位是px)。也可以直接寫(windowWidth, windowHeight),會自動判斷螢幕的寬高變為滿版畫布。
- background(colorCode):設定背景顏色,依照 p5.js 文件說明傳入不同的色碼參數表示方式,這邊寫的 100 是代表 0(黑)~255(白) 之間的灰色值 100。
- draw(): 在不按停止播放的狀況下,會不斷重複執行在 draw() 裡面的程式碼,要繪製的內容主要會寫在這裡。
- ellipse(posX, posY, width, height):在 (posX, posY) 上繪製寬高 (width, height) 的橢圓形,如果 (posX, posY) 帶入 (mouseX,mouseY) ,表示取滑鼠的座標當作繪製圓圈的位置。
function setup() { createCanvas(windowWidth, windowHeight); background(100); } function draw() { ellipse(mouseX, mouseY, 20, 20); }
2. 繪製sin波形
這次老闆從自己日常紀錄的創作靈感筆記中,選擇創作類似花的圖案,可以用 sin 波來實踐-想想 sin 波的形狀是不是很像一片片的花瓣?正式的說法是,我們將在極座標(0~360度)上畫出 sin波 ,下圖的 θ 是從 0~360 度,可以看到不同的算式真的會讓 sin 波變成花瓣呢!
我們就先從畫出一個正常的 sin 波開始吧!老闆喜歡在畫布上再畫一個黑色矩形當作背景,這個可以寫在setup()中畫一次就好。接著在 draw() 裡透過 for 迴圈,讓 x 由左到右,每隔 20 就畫一個白色的圓點來描繪 sin 波波形。以下是 API 的相關參數意義。
- fill(colorCode):設定接下來要填入形狀的顏色,色碼0為黑色,色碼255為白色。
- rect(0,0,width,height):width,height是兩個可以方便取用的變數,儲存曾在createCanvas(w,h)中設定的寬高值。矩形的繪製會以(0,0)為左上角頂點,往右為寬、往下為高畫出與畫布一樣大的矩形。
- noStroke():設定接下來畫出的圖形沒有邊框。
- translate(0,height/2):座標系是從整個畫布的左上角為原點(0,0),往右方x越大,往下方y越大,我們運用translate()把座標系的原點改到(0,height/2),接下來座標的計算都可以重新依這個新原點為準。
- frameCount:從程式開始執行畫面不斷更新的次數,其實也就是draw()反覆執行的次數,所以frameCount是以固定的速度增加其數值。
- ellipse(x,y, 50):在(x,y)畫出寬高皆為50的圓形。
y = sin(x) 可以繪製出 sin 波形,如果我們想要調整波形的樣貌,可以進一步運用不同的參數來調整!在這裡如果把它表達成 y=sin(x/a+b)*c 來思考,會發現 a 越大波長越長;而 b 如果是個變動的數字,程式反覆執行時,就會讓波上的點開始垂直動起來(不然會是靜止的)。老闆即運用 frameCount/100 來當作垂直運動的速度,大家可以試試看如果將 frameCount 除以10、50 會有什麼不同?另外由於 sin() 只會給出 -1~1 之間的值,因此可以乘以一個倍數 c 來控制 y 的高度,在這裡用的是 height/5。大家可以在 y=sin(x/a+b)*c 中試驗不同 的a、b、c 參數來創作你喜歡的 sin 波模樣喔!
function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) } function draw() { fill(255) noStroke() translate(0,height/2) for(var x=0;x<width;x+=20){ let y=sin(x/10+frameCount/100)*height/5 ellipse(x,y, 50) } }
如果我們想要綜觀不同的參數設定會讓 sin 波長得如何不同,這時候可以好好運用滑鼠座標 mouseX、 mouseY 啦!老闆這邊想要觀察的是圓點取樣的多寡還有波的長短,因此利用 mouseX 由小到大的值對應為圓點取樣的間隔, mouseY 的大小則對應著波長的長短,並分別由變數 span、freq 把對應的值儲存下來。大家可以試驗滑鼠在不同的位置是如何影響波的樣貌?你也會發現很有趣的是當取樣的點(由 mouseX 決定)由多至少時,本身波長很短也會變得像波長很長的波,甚至看似多條 sin 波複合。
- map(mouseX,0,width,0,100,true):將原先 mouseX 的值本來從 0~width 大小,對應到 1~100 之間,最後的 true 是當 mouseX 的值超出 0~width,也嚴格限制值落在 1~100 之間。
function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) } function draw() { fill(0) rect(0,0,width,height) fill(255) noStroke() translate(0,height/2) let span = map(mouseX,0,width,1,100,true) let freq = map(mouseY,0,height,5,100,true) for(var x=0;x<width;x+=span){ let y=sin(x/freq+frameCount/100)*height/5 ellipse(x,y, 5) } }
3. 把 sin 波轉到極座標上
為了要讓 sin 波變成花狀,我們要運用極座標,把 x 當成 0~360 度,sin(x) 的值當成長度。首先,先把座標系的原點改移到畫布中央 (width/2,height/2) 。接著很有趣的是,老闆不直接算出圓點的位置,而是再度移動座標系:讓座標系旋轉 x 度數,再移動整個座標系讓原點移至 (sin(x),0) ,因此每一個圓形只要繪製在原點 (0,0) 上就好了!
座標系移動過後都要讓它回到原位再做下一次的移動,所以移動前都先用 push() 儲存目前的設定。每次旋轉+移動完座標系後,再透過 pop() 恢復原廠設定,下一次就又會從原先設定的狀態也就是座標系原點畫布中央開始!
- push()、pop():前者存下當前的畫筆設定、後者恢復 push() 時儲存的設定。通常我們會把想要大動特動的畫筆設定寫在 push() 與 pop() 之間,執行完想繪製的東西後就能夠恢復成原本冷靜的狀態。
- rotate(x/width*2*PI):座標系的旋轉。根據設定的 angleMode,可以填入弧度或是角度,為了避免搞混,我們使用在裡面填入弧度 PI。當 x 在 0~width 之間,x/width*2*PI 就是從 0~360 度的範圍。
function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) ellipse(0,10, 15) } function draw() { //畫背景 fill(0) rect(0,0,width,height) //畫圓圈 fill(255) noStroke() translate(width/2,height/2) //將原點設定到畫面中央 rect(0,0,50,50) //畫個矩形確認座標系原點是否移到畫布中央 let span = map(mouseX,0,width,1,100,true) let freq = map(mouseY,0,height,5,100,true) for(var x=0;x<width;x+=span){ push() rotate(x/width*2*PI) //把座標系旋轉到0-360度之間 let y=sin(x/freq+frameCount/100)*height/2 //藉由height/2讓波幅是畫面的一半高 translate(y,0) //把旋轉過座標系在X軸上移動y個距離 ellipse(0,0,10) pop() } }
4. 來幫圖形上色吧
上色時老闆喜歡運用 coolors 這個網站挑選喜歡的配色,可以用空白鍵隨選5個顏色的搭配,也可以鎖住喜歡的顏色、繼續點空白鍵直到找到五個最喜歡的顏色搭配為止。每個顏色條裡也有一些提供調整的選擇。小撇步是當你決定好時,可以複製上方的代表顏色的字碼回到程式世界喔!
老闆想嘗試看看不同的視覺效果,將原先的圓形改為方形。接著指定一個變數 colors 來儲存這串字碼,並用程式將一個個色碼分開後,將每個色碼前面加上「#」成為完整的表示,例如:#1be7ff,#6eeb83。
為了讓每個方形輪流上不同的顏色,採用取餘數的方式:colors[int(x%colors.length)],可以讓餘數落在 0~colors.length-1,對應到 colors 陣列裡的各個色碼,在這裡外面包了一層 int() 是因為有時候j avascript 餘數運算出來是浮點數,因此要讓它強制取整。
//指定一個色票陣列 var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a) function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) } function draw() { //畫背景 fill(0) rect(0,0,width,height) //畫圈圈 fill(255) noStroke() translate(width/2,height/2) rect(0,0,50,50) let span = map(mouseX,0,width,1,100,true) let freq = map(mouseY,0,height,5,100,true) for(var x=0;x<width;x+=span){ push() fill(colors[int(x%colors.length)]) //選取色票陣列裡的特定顏色 rotate(x/width*2*PI) let y=sin(x/freq+frameCount/100)*height/2 translate(y,0) rect(0,0,50) pop() } }
記得中途若是做到喜歡的圖樣,可以自訂範圍截圖存取(mac:command+shift+4、window:win+shift+s),如果要將程式碼階段性保存起來,在 openprocessing 右上角有樹枝狀的按鈕 fork,就可以再複製一個出來繼續往下做喔!
5. 用圓形、方形、三角形來豐富
x 是我們畫每個點的依據,現在如果要讓每個位置可以分別呈現圓形、方形、三角形可以怎麼做呢?老闆是運用 x 除以3(代表 3 種形狀的餘數)與 if 條件式來實現,藉由餘數 0、1、2 分別對應到繪製不同的形狀。但在這裡還有一個關於繪製正三角形的挑戰:如果直接去計算 triangle(x1, y1, x2, y2, x3, y3) 的每一點座標是有些困難的,於是我們用三角函數的方式來計算。看著下圖我們可以看到透過角度 0、120、240 度,可以取得頂點的 x、y 座標 (r*cos(θ),r*sin(θ))。
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a) function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) } //將製作三角形定義成一支function可以隨時在draw()裡呼叫取用 function myTriangle(x,y,r){ //function可以設定想定義的參數 push() translate(x,y) let points=[] //存放三角形的頂點座標 for(var i=0;i<3;i++){ let rr =r let angle=i*120 let xx = rr*cos(angle/360*2*PI) //將角度數值轉換為角度 let yy = rr*sin(angle/360*2*PI) points.push(xx,yy) //將各頂點座標依序放入points陣列 } triangle(...points) //用ES6語法...展開points從一陣列變成個別的6個值 pop() } function draw() { //畫背景 fill(0) rect(0,0,width,height) //畫圈圈 fill(255) noStroke() translate(width/2,height/2) rect(0,0,50,50) let span = map(mouseX,0,width,1,100,true) let freq = map(mouseY,0,height,5,100,true) for(var x=0;x<width;x+=span){ push() fill(colors[int(x%colors.length)]) rotate(x/width*2*PI) //把座標系旋轉到0-360度之間 let y=sin(x/freq+frameCount/100)*height/3 translate(y,0) //把旋轉過座標系在X軸上移動y個距離 let shapeId = int(x)%3 if(shapeId == 0){ rect(0,0,50) } if(shapeId == 1){ ellipse(0,0,50) } if(shapeId == 2){ myTriangle(0,0,50) } pop() } }
6. 妝點-陰影、材質
再來老闆使出自己愛用的方法,給予圖樣更豐富的變化。一開始嘗試陰影效果,有兩種陰影製作的方式可以選擇,除了陰影的顏色要設定外,分別結合陰影模糊程度的設定、陰影偏離物體多少。
- drawingContext:HTML5 Canvas的功能可以用這個API取得。
- drawingContext.shadowBlur:設定陰影模糊的程度
- drawingContext.shadowColor:設定陰影的顏色
- drawingContext.shadowOffsetX:設定陰影偏離物體多少x距離
- drawingContext.shadowOffsetY:設定陰影偏離物體多少y距離
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a) function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) ellipse(0,10, 15) //選擇一 drawingContext.shadowBlur=5 drawingContext.shadowColor = color(0,100)//透明度0-255 //選擇二,drawingContext.shadowBlur很當時可以使用 drawingContext.shadowColor = color(0,100)//透明度0-255 drawingContext.shadowOffsetX = 10 drawingContext.shadowOffsetY = 10 }
如果想要加入材質感,可以學習製作一塊材質圖樣,再把材質圖樣疊加到畫布中。在這裡老闆設計的是噪點感的材質,噪點由許多深淺不一的灰階值組成。在 p5.js 裡可以透過指定一個變數製作出空白圖樣範圍,把圖樣像素化後可以用 for 迴圈指定每一個像素要畫什麼顏色。在這裡顏色的設定利用了 noise() 產生較有規律的 0~1 數值、random([a,b,c]) 決定 noise() 值放大的倍率來設定顏色的透明度。大家也可以試試在 noise() 傳入不同的參數、random() 陣列裡設定不同的倍率來製作不同的噪點感。製作好材質後可以選擇特定的疊加方法繪製出圖片。
- createGraphics(width,height):設定一塊圖樣,傳入想要的寬高大小。
- loadPixels():將圖樣的像素傳到 pixels[] 陣列,後續才可以讀取或者寫入想要的圖樣。
- updatePixels():在設定完每一個像素的顏色後,可以用這個 api 更新成為新圖樣。
- color(gray, [alpha]):第一個參數代表 0~255 的灰階值,第二個參數代表透明度。
- noise(x,[y],[z]): 根據傳入的座標產生 0~1 之間浮點數,傳入的座標值越相近,產生出的浮點數會較有規律,不會變動很大。
- random([array]):如果沒有特別傳入參數,random() 會返回0~1之間的浮點數,如果有寫明一個陣列,則每次會隨機在陣列裡挑選一個元素返回。
- image(img, x, y, [width], [height]):在特定座標繪製出圖片。
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a) let overallTexture function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) //選擇二,drawingContext.shadowBlur很當時可以使用 drawingContext.shadowColor = color(0,100)//透明度0-255 drawingContext.shadowOffsetX = 10 drawingContext.shadowOffsetY = 10 //製作噪點材質 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(150,noise(i/10,i*o/300)*random([50,100,200]))) //每一個像素指定特定的顏色 //如果將random的值改小材質就不會太黑太明顯 } } overAllTexture.updatePixels() } function draw() { //畫背景 fill(0) rect(0,0,width,height) push() //畫圈圈 fill(255) noStroke() translate(width/2,height/2) rect(0,0,50,50) //製作三角形的函式 function myTriangle(x,y,r){ let points=[] for(var i=0;i<3;i++){ let rr =r let angle=i*120 let xx = rr*cos(angle/360*2*PI) let yy = rr*sin(angle/360*2*PI) points.push(xx,yy) } triangle(...points) } let span = map(mouseX,0,width,1,100,true) let freq = map(mouseY,0,height,5,100,true) for(var x=0;x<width;x+=span){ push() fill(colors[int(x%colors.length)]) rotate(x/width*2*PI) let y=sin(x/freq+frameCount/100)*height/2 anslate(y,0) let shapeId = int(x)%3 if(shapeId == 0){ rect(0,0,50) } if(shapeId == 1){ ellipse(0,0,50) } if(shapeId == 2){ myTriangle(0,0,50) } pop() } pop() //將噪點材質疊加到畫布上 push() blendMode(MULTIPLY) image(overAllTexture,0,0) // pop() }
7. 讓圖形大小變化與自轉、長出刺與小圓點
為了讓整個互動的畫面更豐富有變化,老闆運用sin() 來設定形狀的大小。此外,形狀們除了不斷輻合到畫面中央外,也用rotate() 讓它開始自轉,並且形狀上、周圍加上一些裝飾:看起來像是刺的長短不一的線條、修改利用前面製作三角形的函式讓每個形狀的周圍環繞三個小圓形。
for(var x=0;x<width;x+=span){ push() fill(colors[int(x%colors.length)]) rotate(x/width*2*PI) let y=sin(x/freq+frameCount/100)*height/3 translate(y,0) let shapeId = int(x)%3 let rr= sin(x)*80 //讓每個圖形大小變化 rotate(frameCount/50) //讓每個圖形自轉 if(shapeId == 0){ rect(0,0,rr) } if(shapeId == 1){ ellipse(0,0,rr) } if(shapeId == 2){ myTriangle(0,0,rr) } //畫上刺 strokeWeight(3) stroke(255) line(0,0,-rr,-rr) //線條與圖形用的是同一個座標系設定 //畫環繞的圓形 for(var i=0;i<3;i++){ noStroke() let rr =50 let angle=i*120 let xx = rr*cos(angle/360*2*PI) let yy = rr*sin(angle/360*2*PI) ellipse(xx,yy,5) } pop() }
8. 製作網格背景
再來我們要來製作現代感的網格背景,因此在座標系設定到畫面中央後,我們新增一段程式碼,設定線條的顏色並分別畫上水平線條與垂直線條。這裡老闆運用了取餘數,讓線條每 5 條就增強它的粗度與變得更明顯(調整透明度),這邊也用到了一些 javascript 的數學與邏輯表示方式。
- abs():取絕對值
- boolean?a:b:如果前面的變數 boolean 值是 true,就返回 a 值;是 false,就返回 b 值
translate(width/2,height/2) //將原點設定到畫面中央 //畫網格線 stroke(255,100) for(let xx=-width/2;xx<width/2;xx+=40){ let isSpan = (abs(xx/20)%5==0) strokeWeight(isSpan?3:1) stroke(255,20+isSpan?200:0) line(xx,-height/2,xx,height/2) } for(let yy=-height/2;yy<height/2;yy+=40){ let isSpan = (abs(yy/20)%5==0) strokeWeight(isSpan?3:1) stroke(255,20+isSpan?200:0) line(-width/2,yy,width/2,yy) } noStroke()
9. 隨機選取顏色子集合、印出文字
為了讓圖樣在程式每次開始執行時都可以選取不同的顏色來繪製,老闆運用隨機的概念,讓每個在顏色陣列裡的色碼,會透過機率的方式決定會不會被選到。為了避免所有顏色都未能被選入,也預先儲存一些絕對會畫上去的顏色。
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a) var useColors = ['#000','#fff'] //真正用於著色的陣列,可以預先填入一些顏色 let overallTexture function setup() { createCanvas(1000, 1000); background(100); fill(0) rect(0,0,width,height) ellipse(0,10, 15) //繪製陰影 drawingContext.shadowColor = color(0,100)//透明度0-255 drawingContext.shadowOffsetX = 10 drawingContext.shadowOffsetY = 10 //製作噪點材質 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(150,noise(i/10,i*o/300)*random([50,100,200]))) //每一個像素指定特定的顏色 //如果將random的值改小材質就不會太黑太明顯 } } overAllTexture.updatePixels() //顏色子集合 colors = colors.concat(colors) //隨機條件若很嚴格可以藉由讓顏色陣列複製自己,增加顏色被選到的機率 randomSeed(Date.now()) //讓隨機依據變動的數字(如用當下的時間)會更隨機 colors.forEach(clr=>{ //對於顏色陣列裡的每個顏色(設定變數clr)會逐一的執行{}裡的指令 if(random()<0.25){ //當random()的值小於某數時才會執行 useColors.push(clr) //執行將某色存入useColors陣列 } }) }
別忘了要將填色的部分改選用useColors陣列喔!
function draw(){ ... for(var x=0;x<width;x+=span){ push() fill(useColors[int(x%useColors.length)]) rotate(x/width*2*PI) let y=sin(x/freq+frameCount/100)*height/3 translate(y,0) let shapeId = int(x)%3 pop() } }
再來可以在畫面上以文字呈現一些參數是如何變化,讓作品看起來很有科幻系統的感覺。為了讓形狀都會有陰影但文字不會,將原本在 setup() 關於陰影的設定搬到 draw(),但在要繪製文字之前將陰影設定取消。這邊很有趣的是,老闆還繪製出了填色矩形記錄每次圖樣是由哪幾個顏色構成。
- text(str, x, y):在 (x,y) 座標呈現出文字
function draw() { drawingContext.shadowColor = color(0,200)//透明度0-255 drawingContext.shadowOffsetX = 10 drawingContext.shadowOffsetY = 10 //畫背景 fill(0) rect(0,0,width,height) push() // blendMode(SCREEN) //畫圈圈 fill(255) noStroke() translate(width/2,height/2) //將原點設定到畫面中央 //畫網格線 stroke(255,100) for(let xx=-width/2;xx<width/2;xx+=40){ // let isSpan = (abs(xx/20)%5==0) strokeWeight(isSpan?3:1) stroke(255,20+isSpan?200:0) line(xx,-height/2,xx,height/2) } for(let yy=-height/2;yy<height/2;yy+=40){ // let isSpan = (abs(yy/20)%5==0) strokeWeight(isSpan?3:1) stroke(255,20+isSpan?200:0) line(-width/2,yy,width/2,yy) } noStroke() //畫形狀 let span = map(mouseX,0,width,1,100,true) let freq = map(mouseY,0,height,1,100,true) let curveFactor = noise(frameCount/1000)*3+5 for(var x=0;x<width;x+=span){ push() fill(useColors[int(x%useColors.length)]) rotate(x/width*2*PI) let y=sin(x/freq+frameCount/100)*height/2 translate(y,0) //把旋轉過的X軸上移y個距離 let shapeId = int(x)%3 let rr=(pow(noise(x),2)+pow(sin(x),1.2))*100 //製作大小不一的形狀 rotate(frameCount/50)//自轉 if(shapeId == 0){ rect(0,0,rr) } if(shapeId == 1){ ellipse(0,0,rr) } if(shapeId == 2){ myTriangle(0,0,rr) } strokeWeight(3) stroke(255) line(0,0,-rr,-rr) //環繞的小圓形 for(var i=0;i<3;i++){ noStroke() let rr =50 // let cirR =10 *sin(x) let cirR =10 let angle=i*120+frameCount/100+x*curveFactor//? let xx = rr*cos(angle/360*2*PI) //將角度數值轉換為角度 let yy = rr*sin(angle/360*2*PI) ellipse(xx,yy,cirR) } pop() } pop() //為了寫文字取消陰影 drawingContext.shadowColor = color(0,200)//透明度0-255 drawingContext.shadowOffsetX = 0 drawingContext.shadowOffsetY = 0 push() for(var colorId = 0;colorId<useColors.length;colorId++){ fill(useColors[colorId]) strokeWeight(2) rect(colorId*40+40,height-210,30,30) //注意這裡的座標系原點是以左上 //角(0,0)計算,每個方形間隔40,寬高30 } fill(255) //字體設定白色 textSize(24) textStyle(BOLD) text("TIME: "+frameCount+"fp",50,height-130) text("SPAN: "+span.toFixed(2)+"\"",50,height-90) //這裡值得注意為了要顯示” //需要在前面加一條\方便程式辨識喔 text("FREQ: "+span.toFixed(2)+"Hz",50,height-50) pop() push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
10. 最後一點小調整!
為了讓很多東西不要只隨著 sin 變化,減少單調以及增加更多的韻律,例如小圓形原本只會跟著大圓形、方形、三角形一起同週期旋轉,為了讓它有自己的旋轉,加上了 frameCount/100,再透過加上 x*a(a 代表一個設定的倍數),讓每個位置上的三個小圓形都有不同的偏轉角度,看起來就像是扭轉纏繞的模樣。另外,利用 noise() 讓本來只會隨著 sin(x) 值規律變大變小的形狀可以增加一點隨機的變化,合併使用 pow() 次方的相乘讓值更極端。
我們可以在 setup() 設定一開始滑鼠的位置來規範一開始執行程式時就出現想要的圖樣,最後方便儲存圖片可使用 mousePressed() 偵測滑鼠點按事件的發生並以 save() 存下圖片。
- pow(n,e):n的e次方。
- save():存取當前畫面。
var colors = "1be7ff-6eeb83-e4ff1a-ffb800-ff5714-DB4D6D".split("-").map(a=>"#"+a) var useColors =["#000","#fff"] let overAllTexture function mousePressed(){ //偵測滑鼠點按 save() //儲存畫面 } function setup() { colors = colors.concat(colors) createCanvas(1000,1000); background(100); fill(0) rect(0,0,width,height) pixelDensity(2) //增加像素密度 // drawingContext.shadowBlur=5 randomSeed(Date.now()) mouseX = random(1,width/10) //設定一開始的滑鼠座標 mouseY = random(1,height/2) //設定一開始的滑鼠座標 colors.forEach(clr=>{ if (random()<0.25){ useColors.push(clr) } }) overAllTexture=createGraphics(width,height) overAllTexture.loadPixels() // noprotect // noStroke() for(var i=0;i<width+50;i++){ for(var o=0;o<height+50;o++){ overAllTexture.set(i,o,color(150,noise(i/10,i*o/300)*random([0,0,0,80,200]))) //可以透過在陣列裡複製多一點某個值讓它被隨選到的機率增加 } } overAllTexture.updatePixels() } function myTriangle(x,y,r){ push() translate(x,y) let points = [] for(var i=0;i<3;i++){ let rr = r let angle =i*120 let xx = rr* cos(angle/360*2*PI) let yy = rr* sin(angle/360*2*PI) points.push(xx,yy) } triangle(...points) pop() } function draw() { drawingContext.shadowColor=color(0,200) drawingContext.shadowOffsetX=10 drawingContext.shadowOffsetY=10 // print(mouseX,mouseY) //畫背景 fill("#000") rect(0,0,width,height) // push() // fill(0,0.1) // rect(0,0,width,height) // pop() push() // blendMode(SCREEN) //畫圈圈 fill(255) noStroke() //translate to center translate(width/2,height/2) stroke(255,100) for(let xx=-width/2;xx<width/2;xx+=40){ let isSpan = (abs(xx/20)%5==0?150:0) strokeWeight(isSpan?2:1) stroke(255,20+ isSpan?100:0) line(xx,-height/2,xx,height/2) } for(let yy=-height/2;yy<height/2;yy+=40){ let isSpan = (abs(yy/20)%5==0?150:0) strokeWeight(isSpan?2:1) stroke(255,20+ isSpan?100:0) line(-width/2,yy,width/2,yy) } noStroke() // rect(0,0,50,50) let span = map(mouseX,0,width,1,10,true) // print(span) let freq = map(mouseY,0,height,1,100,true) let curveFactor = noise(frameCount/1000)*3+5 //小圓形扭轉的程度 for(var x=0;x<width;x+=span){ push() fill(useColors[int(x%useColors.length)]) rotate(x/width*2*PI) let y = sin(x/freq+frameCount/100)*height/2 translate(y,0) let shapeId = int(x)%3 let rr = ( pow(noise(x),2)+ pow(sin(x),1.2))*80 //讓形狀大小變化度更大 rotate(frameCount/50) if (shapeId==0){ rect(0,0,rr) } if (shapeId==1){ ellipse(0,0,rr) } if (shapeId==2){ myTriangle(0,0,rr) } strokeWeight(3) stroke(255) line(0,0,-rr,-rr) for(var i=0;i<3;i++){ noStroke() let rr = 50 let cirR = 10 let angle =i*120+frameCount/100 + x*curveFactor //製造三個小圓形第二層旋轉、不同位置的三個小圓形偏轉不同角度 let xx = rr* cos(angle/360*2*PI) let yy = rr* sin(angle/360*2*PI) ellipse(xx,yy,cirR) } // ellipse(0,0,50) pop() } pop() //把陰影取消掉 drawingContext.shadowColor=color(0,200) drawingContext.shadowOffsetX=0 drawingContext.shadowOffsetY=0 push() textSize(24) textStyle(BOLD); for(var colorId =0;colorId<useColors.length;colorId++){ fill(useColors[colorId]) strokeWeight(2) rect(colorId*40+40,height-210,30,30) } fill(255) text("TIME: "+frameCount+ "fp", 50,height-130) text("SPAN: "+span.toFixed(2) + "\"", 50,height-90) text("FREQ: "+freq.toFixed(2) + "Hz", 50,height-50) pop() push() blendMode(MULTIPLY) image(overAllTexture,0,0) pop() }
老闆來結語
再次附上這次範例的成品<甜蜜陷阱>讓大家在開發時參考。這次的創作是從一個點子開始慢慢精修,一邊做一邊調整,讓我們快速回顧一下甜蜜陷阱的創作過程:
- 了解 openprocessing 創作的起手式 – setup() 與 draw()
- 運用滑鼠座標來動態改變sin波的樣貌
- 運用旋轉與移動座標系來繪製花狀波形
- 上色與變化形狀
- 加入陰影、噪點材質
- 調整形狀大小
- 自轉、毒刺與環繞的小圓形
- 加上網格背景與文字
- 最後的調整修飾
這部影片結合了許多好用的數學概念與 API,讓我們可以邏輯化的選取或製作特定效果。在寫程式時也可以善用註解,比較好區塊化地理解與管理每一段程式影響了畫面哪些部分。大家會發現老闆在過程中會不斷微調參數值或是回頭修改使用的 API 試驗不同的效果,這也是創作磨人卻有趣的地方,大家一起探索與試驗吧!
如果你喜歡老闆的教學,《互動藝術程式創作入門》課程中也有手把手的實作引導。寫程式製作生成藝術世紀是一趟需要精準又沿路充滿驚喜的旅程,需要腦瓜裡有彈性的空間-細心規劃,但也放膽試驗、歡迎意外!
此篇直播筆記由幫手 C 協助整理