【p5.js創作教學】Sweet Trap 甜蜜陷阱

色彩繽紛的幾何形狀不斷輻合旋轉,細細的線條卻像是毒刺一樣。動態改變形狀一下密密麻麻一下稀疏鬆散,多變的風貌讓人甜蜜陶醉卻又像是陷阱般危險的感覺!

<甜蜜陷阱>成品圖
<甜蜜陷阱>成品圖

本文是【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 波
變成花瓣的 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 波
動態的 sin 波

如果我們想要綜觀不同的參數設定會讓 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)
  }
}
偵測滑鼠位置控制 sin 波疏密
偵測滑鼠位置控制 sin 波疏密

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 協助整理

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

PHP Code Snippets Powered By : XYZScripts.com