loadPixels() 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/loadpixels/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Thu, 03 Jun 2021 06:23:31 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.2.2 https://creativecoding.in/wp-content/uploads/2022/03/cropped-cct-logo-icon-2-32x32.png loadPixels() 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/loadpixels/ 32 32 【p5.js創作教學】電波電路Wave Circuit – 來做個逼哩逼哩送訊號的電路吧!(直播筆記) https://creativecoding.in/2021/04/16/p5-js%e6%95%99%e5%ad%b8%ef%bc%8d%e9%9b%bb%e6%b3%a2%e9%9b%bb%e8%b7%afwave-circuit-%e4%be%86%e5%81%9a%e5%80%8b%e9%80%bc%e5%93%a9%e9%80%bc%e5%93%a9%e9%80%81%e8%a8%8a%e8%99%9f%e7%9a%84%e9%9b%bb%e8%b7%af/ Fri, 16 Apr 2021 02:33:00 +0000 https://creativecoding.in/?p=502 互動藝術程式教學簡介 這次的直播要來製作具有逼哩逼哩效果的電路,當中可以分為三部分內容,分別為背景的格線、連接的線條以及細節上的裝飾,在創作過程中會各自去調整,有可能調整了線條後,再去修改背景,而後又…

這篇文章 【p5.js創作教學】電波電路Wave Circuit – 來做個逼哩逼哩送訊號的電路吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
互動藝術程式教學簡介

這次的直播要來製作具有逼哩逼哩效果的電路,當中可以分為三部分內容,分別為背景的格線連接的線條以及細節上的裝飾,在創作過程中會各自去調整,有可能調整了線條後,再去修改背景,而後又再回去修改線條,透過來來回回的在細節上修正來完成最終的作品。

https://imgur.com/qkrP9yb.gifhttps://imgur.com/Ydt29fT.gif

主要會用到的 API :

還是新手?想要快速上手p5.js請來看p5.js 快速上手:互動網頁教學

1. 建立網格點點

一開始我們這邊先用雙層的 for 迴圈來建立垂直與水平間隔上都是 50 的格線

let ww, hh
function setup() {
  createCanvas(800, 800);
  background(100);
	
  ww = int(width/50)
  hh = int(height/50)
	
}

function draw() {
  for(let i=0; i<ww; i++){
    for(let o=0; o<hh; o++){
      push()
        translate(i*50,o*50)
        rect(0,0,10,10)
      pop()
    }
  }
}
建立網格點點

2. 隨機連彩色的線

  • colors 設定: 在顏色上,雖然可以直接使用指定內建的顏色來使用,但這邊我們使用外部的選色工具來協助。首先先連到 coolors 這個配色的網站,當開始使用的時候,可以發現上面的網址列會呈現像是https://coolors.co/ed6a5a-f4f1bb-9bc1bc-5ca4a9-e6ebe0 ,仔細發現這串網址後面的字串就是顏色的代碼,這時只要將後面的顏色字串複製下來,接著經過字串的處理後就可以變成讓使用的顏色了。
  • 標點建立與連線,分為二個部分
    • 首先是先 Array.from 來建立 50 個隨機的位置點的陣列,如果我們將它列印出來的話,會產生出像是 [start: {x: 13, y:8}, end {x: 3, y:5}] 的陣列
    • 接下來用 forEach 來將 links 這個陣列的每一個元素來去提取出來。不過這裡有一個小問題,就是現在 links 所存的數字是表示方格上的第幾個點,而非實際的位子,所以這邊要使用 getPos() 來將位子的點轉換成實際的位子。舉個例子來說,像是原本點是 (3,5),而這個點在畫布上的實際位子是 (150, 250)。在位子轉換後,就使用 line(st.x, st.y,ed.x, ed.y) 將線條連接起來。

這邊可以建議大家稍微停下來思考一下透過 Array.from() 來建立陣列的細節以及 links.forEach() 提取陣列來做操作的架構,因為在後續我們會增加一些屬性,像是波的波型,亦或是波的振幅都是一樣透過在 Array.from() 中進行屬性的設定,接著在 links.forEach() 中進行實際的繪製。對了,這裡除了畫線條之外,也在後面加上一個黑色的方塊,來讓我們很像電路板的作品跟最後面的背景來做區別

let links = []
let ww, hh
let colors = "fff-437f97-849324-ffb30f-fd151b".split("-").map(a=>"#"+a)
function getPos(gridIndex){
  return createVector(gridIndex.x*50,gridIndex.y*50)
}

function setup() {
  createCanvas(800, 800);
  background(100);
	
  ww = int(width/50)
  hh = int(height/50)
	
  links = Array.from({length: 50}, (d,i) => ({
    start: createVector(int(random(ww)), int(random(hh))),
    end: createVector(int(random(ww)), int(random(hh))),
    color: random(colors)
  }))
	
  print(links)
}

function draw() {
  //黑底
  fill(0)
  rectMode(CORNER)
  rect(0,0,width,height)
  stroke(255)
  strokeWeight(3)
  rectMode(CENTER)
	
  // 網格
  for(let i=0; i<ww; i++){
    for(let o=0; o<hh; o++){
      push()
        translate(i*50,o*50)
        rect(0,0,10,10)
      pop()
    }
  }
	
  stroke(255)
  strokeWeight(5)
	
  //線條
  links.forEach(link=>{
    stroke(link.color)
    let st = getPos(link.start)
    let ed = getPos(link.end)
    line(st.x, st.y,
      ed.x, ed.y)
  })
  noStroke()
}

3. 將線條加上光暈/特定點放大

目前線條看上去有點太硬了,感覺死板板的,所以這裡透過加上模糊的效果來讓整體上看起來柔和一些,使用 drawingContext 來去設定線條模糊的顏色 (當然是跟實體直線的線條顏色一樣) 以及模糊的程度。設定完後就把它加在繪製線條的位子之前

除了調整線條外,我們也調整一下背後的點點。如果點點在垂直以及水平的方向都是五的倍數的話,就將那個點設定的更加明顯一些,這樣子背景看上去就稍微比較有韻律感了。

// 網格
for(let i=0; i<ww; i++){
  for(let o=0; o<hh; o++){
    push()
      translate(i*50,o*50)
      // 這裡透過去看看是不是五的倍數後,也去更動了 rect 的參數數值
      let ww = (i%5==0 && o%5==0) ? 10 : 3
      rect(0,0,ww)
    pop()
  }
}

stroke(255)
strokeWeight(5)

//線條
links.forEach(link=>{
  // 使線條有光暈
  drawingContext.shadowColor = color(link.color);
  drawingContext.shadowBlur = 30;
	
  stroke(link.color)
  let st = getPos(link.start)
  let ed = getPos(link.end)
  line(st.x, st.y,
    ed.x, ed.y)
})

4. 移動的 Siri 波型

這裡要持續的去改變線條,將原本的直線變成波型,同時讓它動起來,看起來更活潑一些。

在波的繪製上,首先要先找出兩個重要的數字來協助我們,一個是距離,另一個則是角度。在距離上是使用 dist() 來取得,而角度稍微比較複雜一些些,必須要先將終點減去起點得到向量後,再用 heading() 來取得角度。這裡有個要注意的是,heading() 它的單位是 radians 弧度,並非一般我們認知的 360度 / 90 度的單位。

有了距離以及角度後,就可以開始畫波浪囉。一開始先將畫筆移動到起始點的位子,並根據所得出的角度旋轉面向終點的位子,接著就開始透過 for 迴圈一小段一小段來畫出現線條了。這裡有個地方要注意的是,由於每次畫線條的時候,都會透過 translate(st.x, st.y) 去移動畫筆,而間接影響到畫布的設定,所以這邊要記得用 push() / pop () 給包起來。

let st = getPos(link.start)
let ed = getPos(link.end)

// 透過一條條短短的區段去畫線

// rr 計算起點與終點的位子 
let rr = st.dist(ed)
// 計算夾角
let ang = ed.copy().sub(st).heading()

// 因為每一次都必須要 translate 到畫線的起點,所以這裡我們要用 Push()/Pop() 包起來
push()
  translate(st.x, st.y)
  rotate(ang)
  beginShape()
  for(var i=0; i<rr; i+=2){
    vertex(i, sin(i/5)*5)
  }
  endShape()
pop()

接著要使靜態的東西動起來,最常使用的小技巧就是 – 加入 frameCount ,因為它是系統上會隨著改變的系統變數,但是全部都只加上 frameCount 的話,這樣就會全部都線條一起同個頻率一起扭動,這樣的畫面有點不太自然,所以加上了 freq 屬性,並且放到 vertex() ,這樣線條就會隨著自己的頻率各自搖擺囉。

// 新增屬性 freq
links = Array.from({length: 50}, (d,i) => ({
  start: createVector(int(random(ww)), int(random(hh))),
  end: createVector(int(random(ww)), int(random(hh))),
  freq: random(1,50),
  color: random(colors)
}))


// 加上 frameCount 與 link.freq 來畫線條,產生動態
push()
  translate(st.x, st.y)
  rotate(ang)
  beginShape()
  for(var i=0; i<rr; i+=2){
    vertex(i, sin((i+frameCount)/link.freq)*5)
  }
  endShape()
pop()

再來就是要浪波看起來就像是 Siri 般,兩邊頭尾的地方相對於中間的地方振福較小,所以增加了變數 ratio 後,再相乘到 vertex 的第二個參數中。一開始你看到可能會想,這個變數看上去也太複雜了吧,不過實際上老闆在製作過程中也是嘗試了非常多不同的組合跟方式才慢慢試出來的。大家也可以玩玩看不同的效果,找到你最喜歡的樣貌。

for(var i=0; i<rr; i+=2){
  // 新增 ratio 後,乘到  vertex,使線條的頭跟尾會的波會變比較小
  let ratio = 5*(rr/2-abs(i-rr/2))/rr
  vertex(i, sin((i+frameCount)/link.freq)*5*ratio)
 }
https://imgur.com/1QmW3pB.gif

5. 外觀調整

目前波的部分處理到一個段落了,接下來要來調整背景,以及替它加上材質。

雖然說先前已經有對後面的點點進行設定了,但是看上去似乎有點不太明顯,所以除了大小,在顏色上也要有所區別。這裡先透過 (i%5==0 && o%5==0) 判定同時垂直與水平都是5倍數的點才是 isGridPoint ,接著在下面設定如果是 isGridPoint 的話,顏色會比較深,反之其餘會比較淡。

// backgroung grid 
for(let i=0; i<ww; i++){
  for(let o=0; o<hh; o++){
    push()
      translate(i*50,o*50)
      // 跟 ww 的概念一樣,判定同時垂直與水平都是5倍數的點才是 isGridPoint
      let isGridPoint = (i%5==0 && o%5==0)
      // 這裡透過去看看是不是五的倍數後,變去更動了 rect 的參數數值
      let ww = (i%5==0 && o%5==0) ? 10 : 3
      // 是 isGridPoint 的話,顏色會比較深,反之其餘會比較淡
      stroke(isGridPoint?255:100)
      rect(0,0,ww)
    pop()
  }
}

在加材質上,可以分為三大區塊,分別為定義材質、設定材質以及使用材質,這三個都是設定在不同地方,這是要特別注意的地方。

// 定義材質,在全域的位子
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,50,100])))
  }
}
overAllTexture.updatePixels()

// 使用材質,在 draw 裡面,而且通常是放在最尾巴的地方
// 這邊要注意的是要加上 push 以 pop
push()
  blendMode(MULTIPLY)
  image(overAllTexture,0,0)
pop()
https://imgur.com/93CmVWa.gif

6. 加入不同種類的波形

除了現在的 sin 波外,這裡再來多加上一個方波,這個算是波裡面的一個**屬性,**所以與前面提到的一樣,當想要再多加上一個功能需要屬性時,就加在 Array.from() 中。這裡多加上 type 屬性,並且設定兩種波型,分別是 sin 波以及方波。

設定好了之後,就是到下面 vertex(i, sin((i+frameCount)/link.freq)*5) 的位子來畫波,可以觀察在影響波的形狀是透過設定 y 的數值,現在由於要來畫不同形狀的波,所以這裡把 y 位子的地方抽出來,設定名為 yy變數,接著根據不同的波型去設定 yy 的數值,最後在尾端再將yy 帶入 vertex()中。

links = Array.from({length: 50}, (d,i) => {
  let start = createVector(int(random(ww)), int(random(hh)))
  let end: createVector(int(random(ww)), int(random(hh)))
  return {
    start, end,
    freq: random(1,50),
    color: random(colors),
    type: random(['sine','square'])
  }
})
beginShape()
for(var i=0; i<rr; i+=2){
  let ratio = 5*(rr/2-abs(i-rr/2))/rr
  // yy 抽出來變成變數
  let yy 
  if (link.type=="square"){
    yy=(i+frameCount + mouseY)%100<50?1:-1
  }else{
    yy = sin((i+frameCount + mouseX)/link.freq)
  }
  vertex(i, yy*5*ratio)
}
endShape()
https://imgur.com/jE3bCPn.gif

7. 加入起始/結束點

加入起點與終點的部分不難,要注意的是,這段是要加在 links.forEach()={} 之中。

🔔 在這裡有個蠻重要的地方要提醒大家,由於 canvas 在開發的時候其實蠻吃效能的,所以可以嘗試將效能先關起來,另外也可以將線條的數量暫時調小,從原本的 50 調整成 30,這樣電腦才不會使負荷那麼大。

push()
  strokeWeight(5)
  ellipse(st.x,st.y,20,20)
  ellipse(ed.x,ed.y,20,20)
pop()
https://imgur.com/2XvRtCW.gif

到目前為止,作品上架構都已經完成得差不多了,接下來就是一些細節上的調整,讓整體更加具有科技感

8. 格線與方格

這裡要替背景加上格線,形成九宮格,而這九宮格的線條跟 isGridPoint 一樣,是以五的倍數去做繪製的。而方格部分則是將其旋轉後,製作出一個向外有如呼吸般的方格。

  • 九宮格: 在畫九宮格上,可以細分為畫直線與橫線。這裡比較直觀的地方是畫橫線時是在第一層控制左右寬度的 for 迴圈,而畫橫線時,則是在第二層控制垂直的 for 迴圈中,但有個要注意的是,在第二層中不僅僅限制 o%5==0 ,還多加了 i==0 ,原因在於,如果不加上的話,直線會因為上一層的 for 迴圈關係,而重複畫了好幾次。
  • 方格: 在方格的三個設計 – 位子標示、旋轉以及呼吸燈的效果,製作上都不難,但這邊的前後位子順序相當重要,如果把旋轉擺放到最前面的話,這樣一來就會連同字也一個旋轉了,要避免這樣的狀況,除了可以用最簡單的方式就是把位子標示往前移,或是使用 push() / pop() 來保存與還原畫布的狀態。
// 網格
for(let i=0; i<ww; i++){
  // 畫上橫線
  if (i%5==0){
    push()
      strokeWeight(1)
      stroke(255,100)
      noFill()
      line(0,i*50,(ww-1)*50,i*50)
    pop()
  }
  for(let o=0; o<hh; o++){
    push()
      translate(i*50,o*50)
      let isGridPoint = (i%5==0 && o%5==0)
      let ww = (i%5==0 && o%5==0) ? 10 : 3
      stroke(isGridPoint?255:100)
	//加上位子標示
	if (isGridPoint){
	  push()
	  noStroke()
	  fill(255)
	  textStyle(BOLD)
	  textSize(15)
	  text("("+i+","+o+")",15,10)
	  pop()
	}
		
        // 是 isGridPoint 的話,旋轉
        if(isGridPoint) rotate(PI/4)
        rect(0,0,ww)

        // 是 isGridPoint 的話,向外如呼吸般擴散的方格
        if(isGridPoint){
          noFill()
          stroke(255,100)
          strokeWeight(1)
          rect(0,0,ww*(3+sin((frameCount)/10 + i + o)))
        }
    pop()

    // 畫上直線
    if (i==0 && o%5==0){
      push()
      strokeWeight(1)
      stroke(255,100)
      line(o*50,0,o*50,(hh-1)*50)
      pop()
    }
  }
}
https://imgur.com/142cgOw.gif

9. 加入裝飾用的文字

既然前面都加上格點位子,那再來替線條加上各自的標示,顯示著是甚麼類型的波形以及編號,雖然這不太起眼,人們不一定會去細看,但是當畫面資訊量很多,而且很整齊的時候,那個裝飾加分的效果就出來了。

與先前一樣,要加上屬性就到上面的 links = Array.from({}) 來做設定,由於文字上要做一點加工,所以 type 拉到上面去做宣告,接著在 label 上設定顯示線條的種類與編號。設定好後,就去下面地方寫 label 的樣式,這裡一樣要注意文字放的位子,這裡可以回頭看看第四章移動的 Siri 波型畫上線條的架構, label text 的位子要被放在移動到線條起始點的位子以及旋轉方向之間。

links = Array.from({length: 50}, (d,i) => {
  let start = createVector(int(random(ww)), int(random(hh)))
  let end: createVector(int(random(ww)), int(random(hh)))
  let type = random(['sine','square'])
  return {
    start, end,
    freq: random(1,50),
    color: random(colors),
    label: type + " #"+i,
  }
})
// label text 
push()
  rotate(PI/8)
  rect(20,-5,2,2)
  noStroke()
  fill(255,180)
  text(link.label,15,10)
pop()
// 因為每一次都必須要 translate 到畫線的起點,所以這裡我們要用 Push()/Pop() 包起來
push()
  translate(st.x, st.y)

 // label text 放的位子在這
  rotate(ang)
  beginShape()
  for(var i=0; i<rr; i+=2){
    vertex(i, sin(i/5)*5)
  }
  endShape()
pop()
https://imgur.com/wXY6xuj.gif

這是目前整體看上去的樣子,看上去稍微顯得有點雜亂,線條上比較偏向各自發展,所以接下來會針對波的長度與方向來修正。

10. 調整波的方向以及種類

在這裡有三個地方要調整,分別是波的行走方向、波形的種類以及改變波的振福

  • 波的行走方向 : 為了限制距離及方向,要先新增 randomDelta,接著結束點的位子取決於起點加上 randomDelta 的數值。
  • 波形的種類: 原本波的設定上是 random(['sine','square']) ,但老闆覺得多一點 sin 波比較好看,所以多增加了 sin 波的數量。
  • 波的振福: 新增波的屬性 amp,設定好後在下方乘上 yy 的數值,這裡要寫成 yy*=link.amp 或是 yy = yy * link.amp 都可以
links = Array.from({length: 50}, (d,i) => {
  let start = createVector(int(random(ww)), int(random(hh)))
  let randomDelta = random([-2,2,-5,5,-10,10])
  // 結束點的位子取決於起點加上 randomDelta 的數值
  let end = start.copy().add(createVector(random(randomDelta),random(randomDelta)))
  // 把 sine 與 square 的比例調整為 3比1
  let type = random(['sine','sine','sine','square'])
  return {
    start, end,
    freq: random(1,50),
    color: random(colors),
    label: type + " #"+i,
    // 新增振幅
    amp: random(1,3)*random(),
  }
})
beginShape()
for(var i=0; i<rr; i+=2){
  let ratio = 5*(rr/2-abs(i-rr/2))/rr
  let yy 
  if (link.type=="square"){
    yy=(i+frameCount + mouseY)%100<50?1:-1
  }else{
    yy=sin((i+frameCount + mouseX)/link.freq)
  }
  // 乘上振幅
  yy*=link.amp
  vertex(i, yy*5*ratio)
}
endShape()

11. 增加逼哩逼哩閃爍效果

這次的創作主題是與電有關,想像是與電有關的閃電或是電腦機台運作的時候,都會一閃一閃的,這邊就要來製造這樣的效果。因此要在繪製線條的外面再包一層 if ,限制在特定的情況之下才會顯示線條,而在 if 裡面所帶的參數有跟時間相關 frameCount,也有自行設定隨機的變數 activeMod ,以及 forEach 中的編號 linkId 。為了要能夠存取 linkId ,記得要設定在 forEach 的第二個參數。

links= Array.from({length: 50},(d,i)=>{
  let start = createVector(int(random(ww)),int(random(hh)))
  let randomDelta = random([-2,2,-5,5,-10,10])
  let end = start.copy().add(createVector(random(randomDelta),random(randomDelta)))
  let type = random(['sine','sine','sine','square'])
  return {
    start,	end,
    freq: random(0,50)*random(),
    color: random(colors),
    amp: random(1,3)*random(),
    type,
    label: type + " #"+i,
    activeMod: random(50,100)
  }
})
if((frameCount+linkId*30)%100<link.activeMod && random()>0.01){
  beginShape()
  for(var i=0; i<rr; i+=2){
    let ratio = 5*(rr/2-abs(i-rr/2))/rr
    let yy = sin((i+frameCount + mouseX)/link.freq)
    if (link.type=="square"){
      yy=(i+frameCount + mouseY)%100<50?1:-1
    }
      yy*=link.amp
      vertex(i, yy*5*ratio)
    }
  endShape()
}
// 原本的 forEach 只有一個參數
links.forEach((link)=>{}

// 加入第二個表示 index 的參數
links.forEach((link,linkId)=>{}
https://imgur.com/Oom6CBH.gif

12. 加上文字

最後做一點修飾,加上在科幻電影裡面會出現白底黑字的效果在畫面上的左上方。在畫白底的時候可以透過 textWidth() 來幫助我們動態的依據文字來計算出長度。

// 加上白底黑字
push()
  translate(width-50,50)
  rotate(PI/2)
  textSize(14)

  fill(255)
  let tx1 = " System ?: " + frameCount/10
  let tx2 = " Active Count: " + links.filter(link=>link.active).length
  rectMode(CORNER)
  rect(8,-5,textWidth(tx1)+5,15)
  rect(8,15,textWidth(tx2)+5,15)
  fill(0)
  text(tx1,10,5)
  text(tx2,10,25)
pop()
https://imgur.com/4xW6wGJ.gif

結語

起初在創作上老闆其實也沒有打算想要做逼哩逼哩的效果,一開始也僅僅想是嘗試看看將線條隨意連連看隨呈現什麼樣的效果。是到後半段去讓背景的網格點更加明顯,以及加上逼哩逼哩的效果才看起來有電波訊號得感覺,與最後文字的畫龍點睛才讓它讓整體變得更有科幻感。

https://imgur.com/qH5eQ1U.gif

還意猶未盡?看看這一篇製作色散海葵的教學讓你功力再進階。

如果你因此對互動藝術程式創作產生興趣,歡迎加入老闆開的 Hahow 課程互動藝術程式創作入門,讓老闆跟你分享不同的創作!

互動藝術程式創作入門是為了不會程式的人設計的課程,課程中會帶你看看不一樣的作品,並從基礎引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓大家覺得寫 code 不是這麼難的事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠做出和與眾不同的創作。

此篇直播筆記由幫手 阮柏燁 協助整理

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 【p5.js創作教學】電波電路Wave Circuit – 來做個逼哩逼哩送訊號的電路吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js 創作教學】 色散海葵(直播筆記) https://creativecoding.in/2021/01/25/p5-js-%e5%89%b5%e4%bd%9c%e6%95%99%e5%ad%b8-%e8%89%b2%e6%95%a3%e6%b5%b7%e8%91%b5/ Sun, 24 Jan 2021 17:08:55 +0000 https://creativecoding.in/?p=492 這次的直播內容主要有四個部分: 透過繪製扭動的線條來呈現海葵觸鬚的樣子 (步驟一~步驟四) 加上滑鼠的互動以及視覺上的裝飾,包含了顏色以及材質,還有在外層加框框,形成了類似畫框的效果 (步驟五~步驟七…

這篇文章 【p5.js 創作教學】 色散海葵(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>

這次的直播內容主要有四個部分:

  • 透過繪製扭動的線條來呈現海葵觸鬚的樣子 (步驟一~步驟四)
  • 加上滑鼠的互動以及視覺上的裝飾,包含了顏色以及材質,還有在外層加框框,形成了類似畫框的效果 (步驟五~步驟七)
  • 加上細的觸鬚增加細節(步驟八)
  • 海葵底部做收斂,形成一叢海葵,以及最後的修飾(步驟九~步驟十)

主要會用到的 API :

還是新手?想要快速上手p5.js請來看p5.js 快速上手:互動網頁教學

Part 1 海葵

透過四個步驟,藉由 curveVertex 來畫線條,並藉由隨機移動線條來形成多條海葵觸鬚

單一條線

首先以畫面的中心為基準點,由下而上畫出一點一點所連成的線條。這邊因為我們將基準的位子轉換了translate(0,height),因此在畫線的的時候,注意是由下往上畫,vertex(xx,-i) 中的 i 要加上負號

function setup() {
	createCanvas(800, 800);
	background(0);
	
	xx = width/2
}
var xx 
function draw() {
	translate(0,height)
	stroke(255)
	
	beginShape()
	strokeWeight(50)
	for(var i=0; i<500; i++){
		vertex(xx,-i)
	}
	endShape()
}
https://i.imgur.com/kTnQOnN.png

左右搖擺

畫好線後,我們為了讓整條線的每一個點隨機分布在不同位子上,同時整條線左右搖擺,所以在 noise 中放入了每個一個點(i)及時間(frameCount)的因子,並存在 deltaX 變數中。 這麼要注意一下,我們為了讓畫出來的圖形不要重疊在一起,所以這裡要在加上 background(0),讓每一次的背景都重新刷新

function draw() {
	translate(0,height)
	stroke(255)
	
	background(0) // 更新 background
	
	beginShape()
	strokeWeight(50)
	for(var i=0; i<500; i++){
		let deltaX = noise(i/400, frameCount/100) * 200 // 加上 noise
		vertex(xx + deltaX,-i)
	}
	endShape()
}

https://i.imgur.com/93JP5pa.gif

固定底部

現在看上去的狀態是整條線都要搖擺,但是我們希望它的底部是相對固定的,所以加上 let deltaFactor = map(i, 0, 50, 0, 1 , true),因為只要限制底部,所以這邊只將 0~50 的點進行轉換,並將 deltaFactornoise 相乘。在 noise 部分,由於它所產生出的數值是 0~1,為了讓它是左右搖擺的,所以在減去 0.5,讓產生出來的值範圍在 -0.5 ~ 0.5 之間。這時候看上去還有些怪怪的,原因是因為系統預設填了白色的顏色,這邊要加上 noFill() 來取消系統的預設填色

function draw() {
	translate(0,height)
	stroke(255)
	
	background(0)
	noFill()  //取消系統填色
	beginShape()
	strokeWeight(50)
	for(var i=0; i<500; i++){
		let deltaFactor = map(i, 0, 50, 0, 1 , true)
		let deltaX = deltaFactor * (noise(i/400, frameCount/100)-0.5) * 200
		vertex(xx + deltaX,-i)
	}
	endShape()
}

https://i.imgur.com/Uc0Dnog.gif

多個線條

這時候我們可以將剛剛的程式碼包成 anemone() 函式。接著就可以使用 for 迴圈來讓它一次產生多條擺動的線條,為了使每一條線條扭動的方式不一樣,因此加入一個變數 ridfunction 中,並且放到 noise 位子。 另外為了將擺動變得更加平滑一些,這邊將 vertex(xx + deltaX,-i) 更改為 curveVertex(xx + deltaX,-i*2)

function setup() {
	createCanvas(800, 800);
	background(0);
}

function anemone(xx,rid){
	beginShape()
	strokeWeight(80)
	for(var i=0; i<300; i++){
		let deltaFactor = map(i, 0, 50, 0, 1 , true)
		let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300
		curveVertex(xx + deltaX,-i*2)
	}
	endShape()
}

function draw() {
	translate(0,height)
	stroke(255)
	
	background(0)
	noFill()
	for(var i =0;i<20;i++){
		anemone(i*200, i)
	}
}

https://i.imgur.com/fsIIkTf.gif

Part 2 互動與裝飾

現在畫好的基本的圖形後,接著就來上顏色、材質以及外框

加上顏色

首先,先建立顏色的清單,使用 forEach 去跑每一個顏色,並在 anemone() 中加入 clr 這個參數,讓用 stroke(clr) 來指定顏色,這時候就有一叢叢具有三種顏色的的海葵了,接著就要使用混和模式來控制顏色的疊加。這裡使用 blendMode(SCREEN) 來控制混和的效果,不過這邊有個問題是,這會讓原本 background 的失效,而解決的辦法就是在畫 backgound 前加上原本預計的顏色疊加模式 blendMode(BLEND)。 這邊除了設定顏色外,也讓線條的粗度(strokeWeight)以及高度(hh)都加上 noise,讓整體更加有變化。

function anemone(xx,rid,clr){
	beginShape()
	strokeWeight(noise(rid,5000)*180) // 使用 noise 加入 rid,加入變化性
	let hh = noise(xx,rid,1000)*500 + random(2) // 高度也使用 noise 加入 rid,加入變化性
	stroke(clr)
	for(var i=0; i<hh; i++){
		let deltaFactor = map(i, 0, 50, 0, 1 , true)
		let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300
		curveVertex(xx + deltaX,-i*2)
	}
	endShape()
}

function draw() {
	translate(0,height)
	stroke(255)
	
	blendMode(BLEND) // 設定回系統預設的疊加模式
	background(0)
	noFill()
	
	let clrs = ['red', 'green', 'blue']  //建立顏色清單
	blendMode(SCREEN) //設定疊加模式
	for(var i =0;i<10;i++){
		clrs.forEach((clr,clrId)=>{
			anemone(i*100, i+clrId/2, clr)
		})		
	}
}

https://i.imgur.com/yOKzWU0.gif
  • 變化多端的顏色 (Option,想讓海葵顏色更豐富可以參考)

滑鼠控制

設定會根據高度影響的 mouseFactor 以及滑鼠左右移動變化量的 mouseDelta。設定好後再加到 curveVertex() 中 x 的位置上,這樣的效果可能讓左右的搖擺是根據滑鼠的移動方向。而這裡還另外加上 mouseDirectionFactor,讓滑鼠的左右移動與海葵搖擺的關係上更加自然。

function anemone(xx,rid,clr){
	beginShape()
	strokeWeight(noise(rid,5000)*200)
	let hh = noise(xx,rid,1000)*500
	stroke(clr)
	for(var i=0; i<hh; i++){
		let deltaFactor = map(i, 0, 50, 0, 1 , true)
		

		let mouseFactor = map(i,0,500,0,1)*log(hh)/10 
        let mouseDirectFactor = noise(frameCount/50)-0.5
		let mouseDelta = map(mouseX,0, width, -500, 500)*mouseDirectFactor
		let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300 
		curveVertex(xx + deltaX +mouseDelta*mouseFactor,-i*2) 
	}
	endShape()
}

加上材質

先在全域的地方定義材質的變數 overAllTexture,並在 setup() 定義材質的樣式。定義好材質後,就可以使用混和模式將材質疊到畫面上。不過由於一開始有移動畫面的中心點translate(0,height),為了讓材質上到正確的位子上,不受這裡的位移影響,所以必須把上材質前畫海葵的部分也用push()pop() 包起來

let overAllTexture   //定義材質

function setup() {
	createCanvas(800, 800);
	background(0);
	
	//設定材質
	overAllTexture=createGraphics(width,height)

	overAllTexture.loadPixels()
	for(var i=0;i<width;i++){
			for(var o=0;o<height;o++){
					overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,50,100])))
			}
	}
	overAllTexture.updatePixels()
}

function anemone(xx,rid,clr){...}

function draw() {
	push()
		translate(0,height)
		stroke(255)

		blendMode(BLEND)
		background(0)
		noFill()

		let clrs = ['red', 'green', 'blue']
		blendMode(SCREEN)
		for(var i =0;i<10;i++){
			clrs.forEach((clr,clrId)=>{
				anemone(i*100, i+clrId/2, clr)
			})		
		}
	pop()
	
	// 使用混和模式疊上材質
	push()
		blendMode(MULTIPLY)
		image(overAllTexture,0 ,0)
	pop()
}
https://i.imgur.com/WM3O1uj.gif

加框

設定混和的模式 blendMode(BLEND),將矩形疊加在上形成外框。這裡要注意記得將原本生成海葵的地方所設定的 background(0) 取消,不然顏色會被蓋過去,這樣框的效果就出不來了。

function draw() {
    //加上外框
	push()
		fill(0)
		noStroke()
	    blendMode(BLEND)
		rect(0,0,width, height)
	pop()
    //
	
	push()
		translate(0,height)
		stroke(255)
		blendMode(BLEND)
		// background(0) 取消這裡的 background,不然會把顏色蓋過去
		noFill()

		let clrs = colors
		blendMode(SCREEN)
		for(var i =0;i<10;i++){
			clrs.forEach((clr,clrId)=>{
				anemone(i*100, i+clrId/2, clr)
			})		
		}
	pop()
	
	
	push()
		blendMode(MULTIPLY)
		image(overAllTexture,0 ,0)
	pop()
}

Part 3 細海葵

接下來要來加上相對比較細小的海葵,這裡可以直接複製我們前面已經利用 for 迴圈所產生出的海葵。在這裡因為我們想要海葵能夠相對長的比較細而且也比較長,所以這邊再多增加兩個參數在後面。接著再 anemone() 中新增變數名稱,分別為控制寬度的 thinkness 長度的 length,並帶到 function 中去做使用

//粗線條
for(var i =0;i<10;i++){
    clrs.forEach((clr,clrId)=>{
        anemone(i*100, i+clrId/2, clr)
    })		
}

//細線條
for(var i =0;i<5;i++){
    clrs.forEach((clr,clrId)=>{
        anemone(i*100, i+clrId/2 + 50, clr, 0.05, 1.2)
    })		
}
function anemone(xx,rid,clr, thinkness=1, length=1){ //新增參數 thinkness=1, length=1
	beginShape()
	strokeWeight(noise(rid,5000)*150*thinkness) // 乘上 thinkness
	let hh = noise(xx,rid,1000)*height/2 + random(2)
	stroke(clr)
	for(var i=0; i<hh; i++){
		let deltaFactor = map(i, 0, 50, 0, 1 , true)
		let mouseFactor = map(i,0,400,0,1)*log(hh)/10
		let mouseDirectFactor = noise(frameCount/50)-0.5
		let mouseDelta = map(mouseX,0, width, -500, 500) * mouseDirectFactor

		let deltaX = deltaFactor * (noise(i/400, frameCount/100,rid)-0.5) * 300
		curveVertex(xx + deltaX +mouseDelta*mouseFactor,-i*2* length) // 乘上 length
	}
	endShape()
}

細線上加入球球

畫好了細的線條後,我們想要在細的線條的上的最後一個點上加上一個小球,所以這裡必須新增的兩個變數(lastX, lastY)去紀錄每一個點,接著由 curveVertex(lastX,lastY) 畫出來線條來。當離開 for 迴圈時,代表已經畫完整條線了,並且此時變數lastX, lastY所存的是最後的一個點的位子,這時在 for 迴圈外面在根據剛才紀錄去畫上圓形。而在小圈圈的繪製上,在這裡加上 nosie,讓小圈圈可以隨機的出現在細線尾端。

function anemone(xx,rid,clr, thinkness=1, length=1){
	beginShape()
	strokeWeight(noise(rid,5000, frameCount/1000)*150*thinkness)
	let hh = noise(xx,rid,1000+frameCount/100)*height*0.6 + random(2)
	stroke(clr)
	
	let lastX, lastY
	for(var i=0; i<hh; i+=2){
		let deltaFactor = map(i, 0, 50, 0, 1 , true)
		let mouseFactor = map(i,0,400,0,1)*log(hh)/10
		let mouseDirectFactor = noise(frameCount/50)-0.5
		let mouseDelta = map(mouseX,0, width, -500, 500) * mouseDirectFactor
		let deltaX = deltaFactor * (noise(i/400, frameCount/100 + mouseY/100,rid)-0.5)*300
		
		lastX = xx + deltaX +mouseDelta*mouseFactor
		lastY = -i*2* length
		curveVertex(lastX,lastY)
	}
	endShape()
	
    if(thinkness!=1 && noise(frameCount/1000,rid)<0.8){ //新增 noise
        ellipse(lastX,lastY-10,6,6)
    }
}

Part 4 一叢海葵

目前海揆線條是由下而上往上生長,現在我們希望它是由中間往外擴散,變成一叢的感覺。所以為了 改變 X 的位子,使用 log 去取值,產生 ratio,再由中心點 width/2 向外向上去畫出一個指數型弧線的線條。這邊直接看 ratio 這個變數一眼看上去有些複雜,不過一開始也是由簡單直覺的方向去嘗試的,像是先試試 let ratio = map(i,0,500,0,1,true),這會讓圖形直接變成一個倒三角形,形成一個線性的變化,接著才嘗試 log,使圖形呈現指數的圖形。實際 log 所對應數值所畫出來的圖形可以參考這裡。可以看的出來在一開始 y 軸的數值上升的很快,接著快速的趨近於緩和,這所對應的ratio 數值也會是如此

let ratio = map(log(i),0,noise(frameCount/100, mouseX/100)*3+5,0,1,true)
curveVertex(lerp(width/2,lastX,ratio),lastY)

最後的微調

最後,在一些小地方進行調整。

  • 將粗度與高度都加入時間上(frameCount)上的變因
strokeWeight(noise(rid,5000, frameCount/1000)*150*thinkness)
let hh = noise(xx,rid,1000+frameCount/100)*height/2 + random(2)
  • 滑鼠上下移動會影響左右的搖擺,所以把 deltaX 這個變數上新增 mouseY 在 noise 之中
let deltaX = deltaFactor * (noise(i/400, frameCount/100 + mouseY/100,rid)-0.5) * 300
  • 由於在顏色上有點過曝,所以加上透明度的效果,並且加上 noise,試試看讓他有忽暗忽亮的效果
for(var i =0;i<10;i++){
    clrs.forEach((clr,clrId)=>{
        let useColor = color(clr)
        useColor.setAlpha(150 + noise(frameCount/100,i)*10)
        anemone(i*100, i+clrId/2, useColor)
    })		
}

結語

回顧整個範例,可以發現我們在不少的地方都使用了 noise 來產生隨機,進而產生了一些動態感。常常與 noise 搭配的有隨時間變化的 frameCount 以及滑鼠移動的變化量 mouseY。在 noise 的使用上有時也並非就直接就使用它,而是當我們覺得某個屬性,像是海葵都高度,向外擴張的程度想要更有隨機動態感的時候再加上去的

在每一個點的繪製上為了製造的左右晃動以及跟滑鼠互動,所以定義了不少的變數,其中還常常用到了 map 以及 noise,這裡面的數字沒有一定的絕對數值,這都是在創作過程中慢慢地去嘗試出來的。若是覺得有些程式碼看起來太過複雜,建議可以先將 noise 改回一般的常數,這樣閱讀起來比較簡單也容易思考。

還意猶未盡?看看這一篇【p5.js創作教學】電波電路Wave Circuit – 來做個逼哩逼哩送訊號的電路吧!的教學讓你功力再進階。

如果你因此對互動藝術程式創作產生興趣,歡迎加入老闆開的 Hahow 課程互動藝術程式創作入門,讓老闆跟你分享不同的創作!

互動藝術程式創作入門是為了不會程式的人設計的課程,課程中會帶你看看不一樣的作品,並從基礎引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓大家覺得寫 code 不是這麼難的事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠做出和與眾不同的創作。

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 【p5.js 創作教學】 色散海葵(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>