p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇)

臺灣當代文化實驗場 C-LAB 在 2021 年 10 月 19 日到 10 月 24 日,邀請全球 Processing 使用者共同參與台灣第一屆「Processing 臺灣國際社群日」,活動中集結不同藝術家及設計師的觀點,帶領大眾以多方的視角,進入未來新媒體藝術的全新想像。
老闆在這一連串的活動中,除了擔任對談講者外(參考文章:創意程式設計:Processing/p5.js教學與趨勢觀察——王連晟、吳哲宇台美連線對談),也受邀為設計工作坊擔任講師,此次就是針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享,分別為第一天的基礎練習與第二天的生程式藝術實作,希望在兩天的時間內,讓同學能學習到生程式藝術創作的基礎,將自己的創意想像以技術實作呈現!在工作坊正式開始前,老闆提供下列素材讓學員進行課前準備,包含:p5.js 的簡短介紹與 Hahow 上的課程示範。
工作坊會從 p5.js 及工具介紹、p5.js 的開發入門、基本圖形繪製以及變數解說循序漸進。
本篇文章為第二天的生程式藝術實作,依據前一天的基礎練習再更進一階,影片總長兩個小時左右,那我們就事不宜遲開始進入工作坊啦!

上篇這邊走:

課程開始

首先,延續第一天的工作坊進度,老闆先大致瀏覽了昨日有進行創作練習的同學們的作品。有同學提出在創作時的疑問,例如,當設定圖形為筆刷時,雖然圖形是連續性的出現,但當滑鼠動作較快時,圖形就無法連續性的出現,這個問題能如何解決呢?

其實這個問題反向思考來說,就是要讓筆刷連在一起,所以老闆先設定 line(),將已經經過的滑鼠位置,與滑鼠正在進行的位置串起,形成如同在繪圖軟體中,筆刷繪製的效果。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  stroke(255)
  line(pmouseX, pmouseY, mouseX, pmouseY)
}

也可以再依據線條想要調整粗細、顏色變化,或是當滑鼠按下右鍵後再執行筆刷等等變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  stroke(255, frameCount%255, 100)
  strokeWeight(10)
  if(mouseIsPressed){
  line(pmouseX, pmouseY, mouseX, pmouseY)
}

第一階段 – 重複迴圈

短暫回顧完第一天的內容後,老闆進行第二天的主要內容的講解,包含以迴圈與互動、增加不同色彩或是複雜圖形的編輯做分享。先從迴圈與互動,建構創作規則上面開始。老闆以知名藝術家,草間彌生的作品說明建構創作規則的原因。草間彌生的知名創作,大多都建立在「觀察事物的形體」、「重複線條的位置」以及「大量的重複」等手法,去構成她的創作系統。或是莫內及梵谷,拆解其創作手法,就會發現使用連續性的線條和錯落的顏色,去重複堆疊出作品。而上述列出的方法,對程式創作來說,是低成本就能執行的技術,也因此我們能藉由這樣的方式去快速獲得一件具有「美術手法」的創作作品。以迴圈來說,可以設計出相同圖形但是不同排列,具有設計感或是抽象感的作品,像臺灣的傳統花磚,就是利用重複圖案,但是利用不同角度的鏡射,排列出具有韻律感的設計作品。

台灣傳統花磚(圖片來源
使用重複技巧的程式作品(作品連結

那我們也開始迴圈實作吧!

老闆先以最單純的圓圈重複做範例,在還沒學習到如何使用重複迴圈技法時,我們可能就是使用重複貼上同一組程式碼,再在位置上做些許變化執行。但這樣並不便利,也因此又能使用迴圈 for() 來解決此項問題,先來說明 for 的基本語法結構。

for (計數變數的起始狀態; 結束條件; 每次結束後變數如何變化) {
  概念相同,需要重複執行的事情
}

實際帶入數字撰寫示範說明:

function setup() {
  createCanvas(windowWidth, windowHeight);
   background(100);
}

function draw() {
  background(0)
  for(let i=0; i<10; i+=1){
    ellipse(width/2+i*50, height/2, 300, 300)
  }
}

在 i 這個變數下,根據前面設定的數字,起始到結束重複執行。

創作過程中,老闆也提供一些設計小技巧,像是顏色選擇。在創作中,顏色佔了很大的一部分,但當我們對於選色上想要有更快速的方式就是應用設計網站。老闆提供一個叫做 COOLORS 的網站給同學做參考,可以選擇自已想要的主要顏色,點按空白鍵就能產生多種配色選項。

COOLORS 網站首頁(網站連結

那要如何經由程式,去排列出不同顏色卻執行重複動作的流程呢?

老闆以陣列舉例,去執行顏色上的排列。在前一天的工作坊中,也有提到陣列以中括號執行即可。我們先列出需要的顏色,再將其存取起來。當形成一個完整陣列後,再套入重複圖形的顏色當中。

let colorList = ["#ffffff","#2e86ab","#d0cdd7","#ffa62b","#273469"]

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(255);
}

function draw() {
  background(0)
  noStroke()
  for(let i=0; i<50; i+=1){
    fill(colorList[i%5])
    circle(width/4+i*(mouseX/10), height/2, 300 -i*6)
  }
}

在程式創作的好處就是能夠快速地進行大量編輯,例如如果要替換配色,只要在陣列中變換色票即可,又或是,想要變化圖形的位置,我們可以從觀察程式中哪裡有重複進行編輯,以下示範:

先設置一個 function() ,將會重複使用到的程式包在裡面,再將設置的代表文字,套入 draw() 中,簡化重複的程式,增加多樣化編輯。

let colorList = ["#ffffff","#2e86ab","#d0cdd7","#ffa62b","#273469"]

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  drawCircles(random(width), random(height))
}

function drawCircles(posX, posY){
  for(let i=0; i<50; i+=1){
    fill(colorList[i%5])
    circle(posX+i*(mouseX/10), posY, 300 -i*6)
  }
}

不想制式的都從同一個顏色開始,可以在顏色上使用 random,讓顏色隨機出現,也需要 int 套入在設定前,讓出現的數字得以四捨五入為整數。

for(let i=0; i<count; i+=1){
  fill(colorList[int(i+random(10))%colorList.length])
  circle(posX+i*span, posY, 300 -i*(300/count))
}

增加了隨機的亂數,作品是否又多增加了不同的詩意呢?老闆在這邊提出了有趣的看法,他認為,程式雖然可大量重複的行為,但再加入無法預測的亂數後,出其不意的呈現,就會在視覺上趨向傳統所謂的藝術感。

迴圈教學到這邊,休息十分鐘讓同學提問以及練習。有同學提問,為何在此次練習中,老闆使用 let 而不是 var 做函數參數呢?老闆在這邊鼓勵大家,雖然在 javascipt 當中是以 var 做使用,但 let 會更加嚴謹。舉例說明,用在函數定義範圍時,let 就是在大括號的範圍內具有作用。但如果是 var 的話,是以 function 為範圍,所以定義較大,判斷上較不容易。在這十分鐘,老闆也持續的做小技巧教學。例如,要如何隨機對應不同顏色組合,做圖形上的顏色變化,我們必須先設定不同的 colorList 設定顏色組合,再依據需求變更。

延伸閱讀:使用 let / var / const 宣告變數的差異(鐵人賽:ES6 開始的新生活 let, const – 卡斯伯

let ColorList = ["#ffffff","#2e86ab","#d0cdd7","#ffa62b","#273469"]
let ColorList2 =["#c9cba3","#ffe1a8","#e26d5c","#723d46","#472d30"]

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  drawCircles(random(width), random(height), mouseX/5, 50, ColorList2)
}

function drawCircles(posX, posY, span, count=50){
  for(let i=0;i<count;i+=1){
    // 從清單中選取顏色
    let colorIndex = int(i+random(10))%ColorList2.length
    fill(ColorList2[colorIndex])
    circle(posX+i*span, posY, 300 -i*(300/count))
  }
}

可以依照上方陣列設定,變化要使用 ColorList 或是 ColorList2 執行效果。另外,設定畫布的大小可以讓畫面更接近畫作感覺。

再來進入到使用變數紀錄狀態,如同一個簡化邏輯的過程,就像是假如今天我們設置了三種不同筆刷效果供使用,要如何快速切換這些效果呢?最快的方式又是設定變數。設定筆刷 paintMode,並且設定筆刷在不同模式時的效果,像是隨機大小變化,就能使用 random 進行。

let colorList = "32373b-4a5859-f4d6cc-f4b860-c83e4d".split("-").map(clr=>"#"+clr)
let colorList2 = "0c090d-e01a4f-f15946-f9c22e-53b3cb".split("-").map(clr=>"#"+clr)
let colorList3 = "6622cc-a755c2-b07c9e-b59194-d2a1b8-fff".split("-").map(clr=>"#"+clr)
let colorList4 = "2f2d2e-41292c-792359-d72483-fd3e81-fff".split("-").map(clr=>"#"+clr)

let paintMode = 0

function setup() {
  createCanvas(1000,1000);
  background(0);
  paintMode = int(random(2))
  // print(colorList[2])
}

function draw() {
  // background(0)
  noStroke()
  if (paintMode==0){
    drawCircles(
      random(width),random(height),
      mouseX/5, 50, random([colorList, colorList2])
    )
  } else if (paintMode==1) {
    drawRects(
      random(width), random(height),
      mouseX/5, 50, random([colorList3, colorList4])
    )
  }
}

function mousePressed(){
  paintMode++
  paintMode = paintMode %2
}

function drawCircles(posX, posY, span, count=50, useColorList){
  for(let i=0; i<count; i+=1){
    let colorIndex = int(i+random(10) )%useColorList.length
    fill(useColorList[colorIndex])
    circle(
      posX+i*span, posY,
      300 -i*(300/count)
    )
  }
}

function drawRects(posX, posY, span, count=50, useColorList){
  for (let i=0; i<count; i+=1) {
    let colorIndex = int(i+random(10) )%useColorList.length
    fill(useColorList[colorIndex])
    rectMode(CENTER)
    rect(
      posX+i*span, posY,
      300 -i*(300/count)
    )
  }
}

應用重複的紋理與符號,改變物件的大小、方向或是出現的頻率,讓畫面的效果更加豐富。

第二階段 – 色彩

在 p5.js 裡常用的色彩系統有 RGB 與 HSB 兩種,RGB 是指顏色紅、綠與藍三種交疊後產生,HSB 是由色相、飽和度與亮度三種維度組成。應用 p5.js 的 Color 物件,並填上以像是一個數值、RGB 數值、填入顏色名稱、填入色票號碼或是宣告色彩模式後再填入色碼等多樣化模式都是得以進行的。了解基本設定後,就來進行實際操作示範吧!

先由圖形的顏色變化開始,假設要執行顏色漸變的圓形,可以先設定色彩系統 HSB,再依據可能使用滑鼠讓顏色變化的技巧,進而使顏色做漸進變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  colorMode(HSB)
  fill(mouseX%360, 100, 100)
  ellipse(mouseX, mouseY, 500, 500);
}

多方應用 random,使其在顏色或是圖形大小變化上,都能做一定範圍的控制。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  colorMode(HSB)
  noStroke()
  fill(random(0,50), random(50,100), 100)
  circle(mouseX, mouseY, random(300));
}

或者是控制在特定範圍內,即使是 random 的效果也能控制想要的方向,像是指定色相的呈現,顏色偏移在指定的範圍內。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  colorMode(HSB)
  noStroke()
  let startHue =random(0,150)
  for(let i=0; i<20; i++) {
    fill(startHue+i*5, random(50,100), 100)
    circle(mouseX+i*20, mouseY, 200-i*10);
  }
}

另一種常見的顏色使用功能為  blendMode(模式名稱),與 p5.js 當中的疊色模式十分相近, 帶入不同的模式時,疊色的效果就會不同,以 SCREEN 作範例,就是亮系的疊色效果,老闆調整變化速度與圓形顆數示範疊色效果,過程中也可以應由透明度的調整變化出不同的感覺。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  frameRate(5)
}

function draw() {
  colorMode(HSB)
  noStroke()
  let startHue = random(0, 150)
  blendMode(SCREEN)
  for(let i=0; i<1; i++){
    let currentStartHue = (startHue+mouseY)%360
    fill(currentStartHue+i*5, random(50,100), 100,1)
    circle(mouseX+i*100, mouseY, 200-i*10);
  }
}

或是設定變數,讓在不同位置圖形有指定的顏色,限制色相的範圍,使其在生成時顏色為固定效果,以及加上填塞與單純線條的選項,讓作品更多活潑變化性。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  // frameRate(5)
}

function draw() {
  colorMode(HSB)
  noStroke()
  let startHue = random(0, 150)
  blendMode(SCREEN)
  // blendMode(MULTIPLY)
  for(let i=0;i<1;i++){
    let xx = random(width) + i*100
        yy= random(height)
    let currentStartHue = (startHue+yy/3)%360
    if(random()<0.5){
      fill(currentStartHue+i*5, random(50,100), 100, 1)
      noStroke()
    } else {
      stroke(currentStartHue+i*5, random(50,100), 100, 1)
      noFill()
    }
    circle(xx, yy, random(100));
  }
}

但,當在使用重複多種圖案顯示時,有些圖案並沒有顯示在畫布上,這時可以應用 pushpop,將圖案分別開來指定顯示,或是設定 frameRate() 圖形的出現速度變化,控制一秒中出現幾次。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
  // frameRate(5)
}

function keyPressed(){
  if (key == "1") {
    frameRate(5)
  } else if (key == "2") {
    frameRate(30)
  } else if (key == "3") {
    frameRate(60)
  } else if (key == "4") {
    frameRate(120)
  }
}

function draw() {
  colorMode(RGB)
  background(0, 1)
  push()
    colorMode(HSB)
    noStroke()
    blendMode(SCREEN)
    let startHue =random(0,50)
    // blendMode(MULTIPLY)
    for(let i=0; i<1; i++){
      let xx = random(width)+i*100
          yy = random(height)
      let currentStartHue = (startHue+yy/3)%360
      if (random() < 0.5) {
        fill(currentStartHue+i*5, random(50,100), 100, 1)
        noStroke()
      } else {
        stroke(currentStartHue+i*5, random(50,100), 100, 1)
        noFill()
      }
      circle(xx,yy, random(50,500));
    }
  pop()
}

練習過程中,老闆也陸續回答同學的發問。

【問題一】請問 Openprocessing Editor 與 p5.js Editor 有所不同嗎?

回答:其實兩者的使用方式以及效能都是大同小異的,皆為提供給創作者即時顯示創作效果的地方。但老闆覺得在整體設計上,openprocessing 對於初學者來說較友善且容易上手,但每個人的感受不一,建議同學都能去嘗試看看自己比較適合哪種應用介面。

【問題二】請問 p5.js 可以做到如同 Team Lab 或是 梵谷光影展那樣的內容嗎?

回答:可以,但在 3D 的呈現上,因為函式庫的內容還沒有那麼完整,在建構呈現效果以及動畫上可能就需要再多加著墨。

【問題三】請問對於每行的分號加或不加有什麼心得或是建議嗎?

回答:老闆在撰寫時習慣不加,因為在 p5.js 撰寫時目前並不是必要的。但是如果同學們擔心的話,在網路上也都能找到自動幫你每行加分號的執行系統。

【問題四】請問 p5.js 有類似於 opencv 進行影像處理或是人臉辨識的進階 library 嗎?

回答:有的,但並不是官方的,名為 ml5.js。在臉部的輪廓或是 pixel 的呈現都是能夠抓到的。

臉部輪廓抓取 api 介面。(截圖自 ml5.js

老闆也有執行過類似的創作,抓取人體位置輪廓,以線條和原點呈現。(作品參考)也建議如果有更多函示庫應用的相關資訊想了解,可以觀看上一篇的影片介紹喔!(影片連結

第三階段 – 畫布操作

此階段以畫布操作,使同學在畫複雜圖形的時候,能以簡化的方式執行,快速且不需多樣計算,重點即是如何簡化繪製圖形的位置。

老闆先以要在畫布上畫多重圓形為舉例,比較先前執行與畫布操作後的差別。這邊應用到三角函數,因為需要使用三角函數去執行圖形的位置,而三角函數的取得需要使用到角度 degree() 功能去計算,今天以要得到一個在中心點圓形的45度角運行的圓形,以下為設定了角度、半徑與函數設定的程式。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  background(0)
  fill(255)
  let r = 200
  let ang =frameCount/100
  let x = r*cos(ang)
  let y = r*sin(ang)
  ellipse(width/2, height/2, 100)
  ellipse(width/2+x, height/2+y, 100)
}

此類的設定模式,會出現兩種問題。一,算式偏多,應該可以更加簡化。二,如果要畫多個圓形,就必須重複設定位置。對於這一系列的設定,老闆先解釋角度後說明如何應用,為何畫布的操作便是可以簡化此應用的方式。

我們在操作時,是為了要設定以一個圖形為中心,並依據此的某個特定角度,使用 translate() 進行其他圖形繪製。假設,今天要為中心的圖形,其實位在左上角,那我們移轉畫布就是讓左上角的圖形變成畫布的中央。(移轉畫布解說參考

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  background(0)
  fill(255)
  let r = 300
  let ang = frameCount/100
  let x = r*cos(ang)
  let y = r*sin(ang)
  translate(mouseX, mouseY)
  ellipse(0, 0, 100)
  ellipse(x, y, 400)
  ellipse(x, y, 300)
  ellipse(x, y, 200)
  ellipse(x, y, 100)
  fill(255, 0, 0)
  rect(200, 200, 50, 50)
}

此種移轉畫布的方式,讓圖形的總體位置相關參數,並不會跟程式邏輯混和在一起。偏移結束後,接下來進到畫布的旋轉。當設定為同種圖形在不同的位置重複出現,使用 translate()roatate() 繪製出多個圖形。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  fill(255)
  let r = 300
  let ang = frameCount/100
  translate(width/2, height/2)
  rotate(ang)
  translate(200, 0)
  rect(0, 0, 200, 200)
  fill(255, 0, 0)
  rect(200, 200, 50, 50)
}

嘗試多做點不同變化,像是旋轉的幅度以及圖形的大小隨著旋轉有所改變。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  push()
    fill(255)
    let r = 300-frameCount/2
    let ang = frameCount/100
    let currentScale = 1-frameCount/500
    translate(width/2, height/2)
    scale(currentScale)
    rotate(ang)
    translate(r, 0)
    rect(0, 0, 200, 200)
  pop()
}

在顏色排列上,除了 random 的隨機排列外,也能使用 noise() 執行持續性的變化。什麼是 noise?其代表著連續性的、可預測性的亂數。依據 perlin 噪聲圖上的不同位置,去影響每個設定,給相同點的時候,出來的結果會是一樣的,所以就可以嘗試將 random 更換成 noise 去觀察結果的差異。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  noStroke()
  push()
    colorMode(HSB)
    fill(noise(frameCount/50)*100,100,100)
    let r = 300-frameCount/2
    let ang = frameCount/20
    let currentScale = 1 - frameCount/500 + random(0.1, 0.5)
    translate(width/2, height/2)
    scale(currentScale)
    rotate(ang)
    translate(r, 0)
    rect(0, 0, 200, 200)
  pop()
}

將 noise 與 random 共同使用,讓顏色變化不死板,有多樣性的呈現。使用 hue 色相作為變數,去引導顏色為漸進的變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(0);
}

function draw() {
  push()
    colorMode(HSB)
    let useHue = (random(20)+noise(frameCount/50)*100 +frameCount/3)%360
    fill(useHue,100,100)
    let r = 300-frameCount/2
    let ang = frameCount/20
    let currentScale = 1 -frameCount/500 +random(0.1,0.2) +noise(frameCount/50)/2
    translate(width/2, height/2)
    scale(currentScale)
    rotate(ang)
    translate(r, 0)
    rect(0, 0, 200, 200)
  pop()
}

今日工作坊總結

在本次工作坊的下篇,我們總共學習到了三大項主題,包含迴圈的建構創作規則、變化色彩使用與留下痕跡,到最後的進階應用,畫布的變化。上述這些應用,老闆在做後一個示範中,將上述的功能都包含在裡面。由左到右畫一連串的長方形,這些動作是可以被累加的。先使用 translate 將圖形重複的執行,再設定變數以及出現的規則,將圖形依照滑鼠位置做變化。

function setup() {
  createCanvas(windowWidth, windowHeight);
  background(100);
}

function draw() {
  // ellipse(mouseX, mouseY, 20, 20);
  fill(random(255), 200, 200)
  translate(0, height/2)
  rectMode(CENTER)
  for(let i=0; i<50; i++){
    rotate(map(mouseY, 0, height, -0.5, 0.5))
    translate(50,0)
    scale(0.95)
    rect(0, 0, 500, 500)
  }
}

雖然快接近結束,同學們還是十分積極的詢問問題,像是,如果想針對資料進行統計的視覺化呈現,有什麼相關的 lib 或 sample 可以參考嗎?比如地區人口統計?針對此問題,老闆認為目前 p5.js 的函數庫在視覺化上的效果還不是最齊全的,可能會使用其他函數庫如 D3.js 來進行,但也還是可以參考先前的相關課程,針對 p5.js 在視覺化上的 api 應用教學,來嘗試進行(參考文章)。

最後,也不免俗的與同學分享老闆在互動藝術程式創作的其他課程,或是影片推廣給大家。包含 Creative Coding TW – 互動程式創作台灣站 的專業文章網站、社群軟體上的即時資訊分享 老闆 來點寇汀吧。 Boss,CODING please(臉書)、老闆來點寇汀吧 Boss, Coding Please (臉書)、老闆,來點寇汀吧。Boss, CODING please(Youtube 頻道),當然還有想要進一步正式開始創作,歡迎加入 互動藝術程式創作入門課程 開始學習!本次分享就到這邊結束啦,歡迎各位一起踏入程式與藝術交織的世界!

此篇直播筆記由幫手熊柑協助整理

分享
PHP Code Snippets Powered By : XYZScripts.com