直播 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/直播/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Mon, 04 Apr 2022 13:51:56 +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 直播 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/直播/ 32 32 【p5.js創作教學】鳥與電線桿 https://creativecoding.in/2022/03/24/p5-js-birds-on-poles/ Thu, 24 Mar 2022 02:41:00 +0000 https://creativecoding.in/?p=2056 利用p5.js創作一點都不難!跟著老闆直播,利用簡單的random、class和參數概念,完成童趣的都市景觀《鳥與電線桿》。

這篇文章 【p5.js創作教學】鳥與電線桿 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
《鳥與電線桿》作品
《鳥與電線桿》作品

今天我們要來做的作品《鳥與電線桿》,可以分為三個項目,分別是鳥、電線桿以及顏色的設計。上圖的完成品,不管是鳥或是電線桿,它們看上去都有規律性,但是彼此之間又同時帶有一些差異性,像是每一隻鳥的大小以及所站的位置等,而電線桿則因為前後粗度不一樣以及每條左右的高度不同而形成交錯感。

透過此次互動藝術創作教學,你會學到

  • 如何透過建立 class 的方式,讓我們可以一次新增多個相同的物件
  • 透過參數設定,讓每個所產生的物件有不一樣的形體變化
  • 透過將背景顏色參數化的設定,製作出不同作品的效果

在開始製作電線桿之前你該知道的p5.js與 原生 js 小技巧

  • class 與 constructor : 要建立物件就必須使用到 class,一般來說在 class 中會搭配另一個關鍵字 constructor,這代表建構子,有了它就可以初始化一個新的物件。
  • lerp : 用於計算出兩個數值之間的相對比例的值,在本範例中用於繪製電線桿的線以及鳥的位置上,還有在後續製作漸層背景顏色時也會使用到相同的概念。
  • push() / pop() 保存與還原畫布的狀態(可以參考老闆互動藝術程式創作入門的課程筆記:章節 7 進階繪圖 – 畫布操作與編織複雜圖形 (pop/push 的圖解)
  • Object.assign : 原生 js,用於物件的值指定給另一個物件。
  • push: 原生 js,將生成的電線桿物件放進陣列當中。

❗由於這次的範例中老闆有使用到物件的概念,所以在檔案上分為兩個檔案 mysketchtab2,在每段程式碼的最上方會標註這是屬於哪一個檔案的程式,提供同學們參考。

一、建立第一組電線桿

第一步驟是要畫一組電線桿,而電線桿可以細分為兩個物件,分別是電線桿的柱子以及連接兩根柱子之間的線,下面先從連接的曲線開始做起。

  1. 繪製弧線

在畫曲線前,首先定義兩邊電線桿的位置,分別是 leftPointrightPoint,接著在這兩位置之間切成十等分,並且需要知道每一等分的位置點。舉例來說,如果電線桿的兩個位置分別是 x1 = 0X2 = 100,切成十等分就會得到一組座標為 [0, 10, 20, 30, ...,90, 100],這裡就要使用到 lerp ,它一共需要三個參數 (數字A,數字B,插值量),這可以依照插值量的大小來得到介於數字A與數字B比例的值,像是 [0, 100, 0.3] 就會是 30,而 [0, 100, 0.6] 則是 60,要注意的是插值量的值必須介於0.0 與1.0 之間。

有了各別點的位置後,接著就要讓每個點的位置移動一點位置,並將其連接起來形成弧線的效果,要做出向下弧線的效果可以加上 sin 值,透過觀察 sin 波可以發現從 0 到 π 的弧形是向下的,而 π 到 2π 的弧形是向上的,所以在 sin值中要取 0 到 π。

2. 加上電線桿

畫好曲線後,桿子相對比較簡單,就在兩邊的位置以 rect() 畫上寬 10,高度為 500 的長方形。在這裡可以將剛剛在曲線上用於辨識點位置的圓形註解起來,留下一條乾淨的弧線。

3. 加上滑鼠互動

建置好電線桿後,這裡可以來嘗試加上一點小小的互動,讓電線桿的左側位置是根據滑鼠的位置來決定,這樣一來在移動滑鼠時,電線桿的寬度也會跟著改變囉 ! 不過這裡要記得在前面加上 background(255),讓背景不斷更新,不然就會變成許多電線桿疊在一起的畫面。

// mysketch
function setup() {
  createCanvas(1000, 1000);
  background(255);
}

function draw() {
  // 3. 重置背景
  background(255)
  let leftPoint = createVector(50, 350)
  let rightPoint = createVector(800, 350)
  // 3. 加上滑鼠互動
  rightPoint.x = mouseX

  stroke(0)
  strokeWeight(5)
  noFill(0)
  // 1. 加上弧線
  beginShape()
  for (var i = 0; i <= 10; i++) {
    let midPoint = p5.Vector.lerp(leftPoint, rightPoint, i / 10)
    midPoint.y += sin(i / 10 * PI) * 60
    //鳥大概的位置
    //ellipse(midPoint.x, midPoint.y, 30)
    vertex(midPoint.x, midPoint.y)
  }
  endShape()

  // 2. 加上桿子
  fill(0)
  rect(leftPoint.x, leftPoint.y, 10, 500)
  rect(rightPoint.x, rightPoint.y, 10, 500)
}

二、建立物件形式

接著將電線桿包裝成 class,class就像是一種模板一樣,當把模板設定好後,我們就可以快速生成同樣的物件。

首先在 openprocessing 上面開一個新的 tab,在裡面開始撰寫 class。在openprocessing 中,不須要使用 import 的方式來引入其他的檔案,它就會自動幫我們引入,非常地方便。

1. 建立 constructor

在初始化時會使用到關鍵字 constructor ,並且帶入參數 args ,這可以讓我們設定每一個物件所要指定的特定值。而避免在建立物件時,有些屬性是沒有被傳入的,所以需要建立一個預設的數值 def。在 Object.assign(def, args) 中,如果使用者有特別設定,就會把使用者引入的參數 args 蓋到預設值 def 上,而 Object.assign(this, def) 則是在將客製化後的設定值蓋到這個物件本體 this 上。

  1. 建立 draw() 與 update()

每一個物件中會需要 draw() 與 update() 來繪製圖形,這裡將剛剛繪製第一個電線桿的相關內容複製到 draw 中,並在前後分別加入 push, pop ,避免每次畫完一個電線桿,畫布狀態會被影響。

// tab 2
class BirdBar {
  // 1. 建立 constructor
  constructor(args) {
    let def = {
      leftPoint: createVector(50, 350),
      rightPoint: createVector(50, 350),
    }
    Object.assign(def, args)
    Object.assign(this, def)
  }
  // 2. 建立 draw() 與 update()
  draw() {
    // 要用 push() pop() 包起來
    push()
      stroke(0)
      strokeWeight(5)
      noFill(0)
      beginShape()
      for (var i = 0; i <= 10; i++) {
        let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / 10)
        midPoint.y += sin(i / 10 * PI) * 60
        // ellipse(midPoint.x, midPoint.y, 30)
        vertex(midPoint.x, midPoint.y)
      }
      endShape()

      fill(0)
      rect(this.leftPoint.x, this.leftPoint.y, 10, 500)
      rect(this.rightPoint.x, this.rightPoint.y, 10, 500)
    pop()
  }
  update() {
  }
}

三、在主程式新增物件

在設定好模板後,就可以來到主程式來建立物件。由於是要一次建立好幾個電線桿,所以首先建立存放電線桿的陣列 birdBars,接著使用迴圈的方式建立五個電線桿,在這裡設定每個電線桿的左右間格為 400,而每個電線桿左側的起點位置為則是 -300 + 400 * i,可以注意到與 i 相乘的常數同為 400,與電線桿兩側的距離一樣,這樣才會形成所有的電線桿相連在一起的效果。

設定好參數後,再來在主程式的 draw 裡面透過 forEach 的方式呼叫每一個物件中的 draw 與 update,將圖形繪製出來。

// mysketch
let birdBars = []
function setup() {
  createCanvas(1000, 1000);
  background(0);
  for (var i = 0; i < 5; i++) {
    let startX = -300 + 400 * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, 350),
      rightPoint: createVector(startX + 400, 350),
    }))
  }
}

function draw() {
  fill(200)
  rect(0, 0, width, height)

  let leftPoint = createVector(50, 350)
  let rightPoint = createVector(800, 350)
  rightPoint.x = mouseX

  birdBars.forEach(bar => {
    bar.update()
    bar.draw()
  })
}
《鳥與電線桿》第三步驟:完成一組電線桿與電線
《鳥與電線桿》第三步驟:完成一組電線桿與電線

四、鳥兒站在電線桿上

透過物件的方式完成了電線桿後,再來要加上鳥的形狀。這個步驟不困難,還記得在第一步驟的時候就已經畫出每一個點的圓形樣式了,在這裡只需要稍微做修改即可。

為了要讓鳥看起來是站在電線桿上的樣子,所以 y 的位置要向上移動高度 30 的一半,也就是 15,另外鳥也不太會站在電線桿上,所以要設定條件,如果是電線桿頭尾的話,不會繪製鳥兒。

// tab 2    
  draw() {
    push()
      stroke(0)
      strokeWeight(5)
      noFill(0)
      beginShape()
      for (var i = 0; i <= 10; i++) {
        let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / 10)
        midPoint.y += sin(i / 10 * PI) * 60
        // 其實就是剛剛所畫的橢圓,但是鳥不會站在桿子上,所以要扣除掉頭尾
        if (i != 0 && i != 10) {
          ellipse(midPoint.x, midPoint.y - 15, 20, 30)
        }
        vertex(midPoint.x, midPoint.y)
      }
      endShape()

      fill(0)
      rect(this.leftPoint.x, this.leftPoint.y, 10, 500)
      rect(this.rightPoint.x, this.rightPoint.y, 10, 500)
    pop()
  }
《鳥與電線桿》第四步驟:確認鳥的位置
《鳥與電線桿》第四步驟:確認鳥的位置

五、為鳥添上顏色

老闆這裡一樣透過配色網站 coolors 來找尋配色的靈感,將網址中色票的部分複製進來,透過字串的處理後,變成可使用的顏色形式。在填色上老闆先以取於餘數的方式,讓五個顏色以規律方式依序呈現在鳥的身上,不過這時候會發現填色後的效果有點怪怪的,原因在於後面我們有把線條連起來,為了讓填色的效果是正常的,需要在這裡要加上 push()和pop() 將繪製鳥的地方包起來。

//tab 2
// 新增顏色
let colors = "0c090d-e01a4f-f15946-f9c22e-53b3cb".split("-").map(a => "#" + a)

class BirdBar {
  constructor(args) {
    ...
  }

  draw() {
    push()
      stroke(0)
      strokeWeight(5)
      noFill(0)
      beginShape()
      for (var i = 0; i <= 10; i++) {
        let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / 10)
        midPoint.y += sin(i / 10 * PI) * 60
        if (i != 0 && i != 10) {
          // 指定隨機顏色,並且使用 push() pop() 包起來
          push()
            noStroke()
            fill(colors[i % 5])
            ellipse(midPoint.x, midPoint.y - 15, 20, 30)
          pop()
        }

        vertex(midPoint.x, midPoint.y)
      }
      endShape()

      fill(0)
      rect(this.leftPoint.x, this.leftPoint.y, 10, 500)
      rect(this.rightPoint.x, this.rightPoint.y, 10, 500)
    pop()
  }
  update() {
  }
}
// mysketch
function draw() {
  // 更改背景顏色
  fill("#679dbf")
  rect(0, 0, width, height)
}
《鳥與電線桿》第五步驟:彩色的鳥隻
《鳥與電線桿》第五步驟:彩色的鳥隻

目前畫面上所看到的是依序排列的顏色,為了讓它有點變化,就讓顏色隨機呈現。說到隨機,你可能會想到使用 random,不過這裡無法使用的原因在於 draw 會一直不斷地執行,所以每一幀都會重新產生一組隨機的顏色,以至於畫面中的顏色是會不斷變化的。

在這裡我們要呼叫另一個與隨機也有相關的函式 noise,在 noise中,只要輸入一個固定的數值,那它出來的也會是一個固定的數值,不會變動,這樣一來就不會隨機跳動了。不過也因為這樣的特性,它看上去又再次呈現規律的樣子,原因是其中的 i 值都是固定的從 1~9,所以每一組電線桿的排列的顏色組合會是一樣的,在這裡可以透過在屬性上新增 randomId: random(100000),並加到 noise 當中,讓每一次所輸入的數值都是隨機不同的,這樣一來所輸出的數值也會形成隨機。

//tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  // 新增 randomId 增加顏色的隨機性
  randomId: random(100000)
}
//tab 2
if (i != 0 && i != 10) {
  push()
    noStroke()
    // fill(colors[i%5])
    // 以 noise 的方式來隨機取顏色,記得要加上 this.randomId
    let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex])
    ellipse(midPoint.x, midPoint.y - 15, 20, 30)
  pop()
}
《鳥與電線桿》第五步驟:鳥的隨機顏色
《鳥與電線桿》第五步驟:鳥的隨機顏色

六、修正電線桿位置

現在的接點有點怪怪的,因為 rectangle 是針對左上角來做繪製的,所以這裡要調整一下電線桿的位置,要將它往左移動自己一半的寬度。

另外也將電線桿的高度與寬度改以屬性的方式呈現,後續在新增其他組電線桿的時候會比較方便一些。

//tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  // 將電線桿也同樣變成屬性
  barSize: createVector(10, 500)
}
//tab 2
// 減去電線桿寬度一半的長度 this.barSize.x / 2
rect(this.leftPoint.x - this.barSize.x / 2, this.leftPoint.y, this.barSize.x, this.barSize.y)
rect(this.rightPoint.x - this.barSize.x / 2, this.rightPoint.y, this.barSize.x, this.barSize.y)
《鳥與電線桿》第六步驟:修正電線桿位置
《鳥與電線桿》第六步驟:修正電線桿位置

七、做出不同組的電線桿

目前一組的電線桿已完成,接下來新增其他組的電線桿,老闆希望每一組的電線桿的寬度以及彼此之間的前後距離是不同,下面我們分成兩個步驟來實作。

  1. 電線桿不同的寬度

在第三步驟時,我們將電線桿彼此的寬度設定為 400,而現在則設定為一個隨機的寬度,使用 random 隨機產生 300 至 500 的數值並存至變數 barDist。

在先前的 startX 其值為 -300 + 400 * i,-300 所代表的為整排電線桿的起始位置,而 400 則代表每根電線桿的距離,i 表示是第幾根電線桿,所以這裡要將代表距離的 400 取代為 barDist。

// mysketch
function setup() {
  createCanvas(1000, 1000);
  background(0);
  // 新增變數  barDist
  let barDist = random(300, 500)
  for (var i = 0; i < 5; i++) {
    let startX = -300 + barDist * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, 350),
      rightPoint: createVector(startX + barDist, 350),
    }))
  }
}

2. 前後層距離不同

在前一個步驟設定寬度時是改變 X 的值,而要設定前後位置則是要改變 Y 的值,在外層再新增一層迴圈,並設定 startY = o * 100 + 200,這表示第一層的 Y 位置為 200,而每一層所間隔的距離為 100。

// mysketch
function setup() {
  createCanvas(1000, 1000);
  background(0);
  for (var o = 0; o < 4; o++) {
    let barDist = random(300, 500)
    // 垂直距離不一樣,所以新增 Y 的距離
    let startY = o * 100 + 200
    for (var i = 0; i < 5; i++) {
      let startX = -300 + barDist * i
      birdBars.push(new BirdBar({
        leftPoint: createVector(startX, startY),
        rightPoint: createVector(startX + barDist, startY),
      }))
    }
  }
}
《鳥與電線桿》第七步驟:複製多組鳥與電線桿,豐富畫面
《鳥與電線桿》第七步驟:複製多組鳥與電線桿,豐富畫面

八、製作漸層背景

在 p5.js 中沒有可以直接繪製漸層的方式,不過我們可以透過好幾個方形來達成。先設定漸層兩邊的顏色,並命名為 startColorendColor,這裡我們製作的是垂直方向的漸層,所以在外層 for 迴圈的數值範圍是 0~height,並以每 30 為單位繪製一個方形。

在顏色設定上使用了 lerpColor,它與一開始在畫電線桿所使用到的 lerp 概念上是一樣的,都是前兩個參數是給值 A 與 B ,而第三個參數則是給予一個插值,即可找到介於 A 與 B 之間插值比例的顏色。

// mysketch
// 新增漸層顏色
let startColor = color("#aaa")
let endColor = color("#ddd")

noStroke()
for (let y = 0; y < height; y += 30) {
  let currentColor = lerpColor(startColor, endColor, y / height)
  fill(currentColor)
  rect(0, y, width, 30)
}
《鳥與電線桿》第八步驟:製作漸層背景
《鳥與電線桿》第八步驟:製作漸層背景

九、鳥

完成了背景之後,要精細製作主角「鳥」,這個步驟一共分為五個小節,先後針對了鳥的外觀、數量以及動態的呈現去做調整。

  1. 將身體上下拆分

老闆首先將鳥的身體從原本的橢圓形拆分成兩個半圓形,不過在重新繪製之前,為了方便圖形的定位,所以在畫每個點的位置之前,先將原點移動至先前繪製鳥身體中心的位置。

我們可以用 arc 來繪製半圓形,一共給定六個參數,最後兩個參數是指定角度的範圍,上半圓的範圍是 0~PI,而下半圓的範圍則是 PI~2 * PI,而因為剛剛已經 translate 座標位置了,在中心點的設定上皆為 (0, 0)。

//tab 2
if (i != 0 && i != 10) {
  push()
    // 移動中心點
    translate(midPoint.x, midPoint.y - 15)
    noStroke()
    // fill(colors[i%5])

    // 原本用橢圓畫,改為使用兩個半圓形畫
    let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex])
    arc(0, 0, 20, 30, 0, PI)
    // 下半身建立另外一個隨機的顏色
    let colorIndex2 = int(3 + noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex2])
    arc(0, 0, 20, 30, PI, 2 * PI)
  pop()
}
《鳥與電線桿》第九步驟:調整鳥的身軀
《鳥與電線桿》第九步驟:調整鳥的身軀

2. 設定鳥的隨機大小

在鳥的屬性值中新增 birdSize,代表鳥的大小,接著將與鳥身體大小有關的都改使用參數的方式呈現,第一個是中心點的位移,每隻鳥所要向上位移的量為自己身高的一半,所以要減去 this.birdSize.y / 2,而在繪製半圓形的身體中則改以 this.birdSize.xthis.birdSize.y 表示。

當上述都設定好後,在產生出鳥的物件時,就可以以 random 的方式產生出不同高度與寬度的鳥囉。

//tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  barSize: createVector(5, 500),
  // 新增 birdSize 屬性
  birdSize: createVector(20, 30)
}
//tab 2
if (i != 0 && i != 10) {
  push()
    // 中心點 Y 軸移動改為參數
    translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
    noStroke()
    // fill(colors[i%5])

    let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex])
    // 身體大小改為參數
    arc(0, 0, this.birdSize.x, this.birdSize.y, 0, PI)
    let colorIndex2 = int(3 + noise(i, this.randomId) * 20) % colors.length
    fill(colors[colorIndex2])
    // 身體大小改為參數
    arc(0, 0, this.birdSize.x, this.birdSize.y, PI, 2 * PI)
  pop()
}
// mysketch
birdBars.push(new BirdBar({
  leftPoint: createVector(startX, startY),
  rightPoint: createVector(startX + barDist, startY),
  // 在設定好鳥大小的屬性後,便可以隨機指定鳥的大小
  birdSize: createVector(random(20, 20), random(20, 40))
}))
《鳥與電線桿》第九步驟:設定鳥的隨機大小
《鳥與電線桿》第九步驟:設定鳥的隨機大小

3. 畫鳥嘴

在畫鳥嘴前,一樣再新增一組隨機的顏色。老闆以三角形的方式來呈現鳥嘴的樣式,它一共需要六個參數,分別是三個點的 x 與 y 位置。

// tab 2
let colorIndex3 = int(2 + noise(i, this.randomId) * 20) % colors.length
fill(colors[colorIndex3])
translate(5, 0)
triangle(0, 0, 0, -10, 10, -5)

4. 改變鳥的數量與密集程度

先前在每條電線上是以規律的方式站了十隻鳥,在這裡要將鳥的數量改為變數的方式呈現,好讓鳥在數量的設定上會比較方便。建立一個計算鳥數量的變數 nodeCount,並且將與鳥數量的數值替換成變數。 另外在繪製鳥的條件下,除了頭與尾不畫鳥之外,老闆也設定了只有當 i 是 3 或 5 的倍數時才畫鳥,這樣一來會讓畫面更有錯落的感覺。

// tab 2
// 新增  nodeCount,並更換將常數都更換為變數 (總共4個地方要替換)
let nodeCount = 30
for (var i = 0; i <= nodeCount; i++) {
  let midPoint = p5.Vector.lerp(this.leftPoint, this.rightPoint, i / nodeCount)
  midPoint.y += sin(i / nodeCount * PI) * 70
  // 取 5 跟 3 的餘數,這樣會比較有錯落的感覺
  if (i != 0 && i != nodeCount && (i % 5 == 0 || i % 3 == 0)) {
    push()
      translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
      noStroke()
      // fill(colors[i%5])
      let colorIndex = int(noise(i, this.randomId) * 20) % colors.length
      fill(colors[colorIndex])
      arc(0, 0, this.birdSize.x, this.birdSize.y, 0, PI)

      let colorIndex2 = int(3 + noise(i, this.randomId) * 20) % colors.length
      fill(colors[colorIndex2])
      arc(0, 0, this.birdSize.x, this.birdSize.y, PI, 2 * PI)

      let colorIndex3 = int(2 + noise(i, this.randomId) * 20) % colors.length
      fill(colors[colorIndex3])
      translate(5, 0)
      triangle(0, 0, 0, -10, 10, -5)
    pop()
  }

  vertex(midPoint.x, midPoint.y)
}
《鳥與電線桿》第九步驟:加上鳥嘴
《鳥與電線桿》第九步驟:加上鳥嘴

5. 鳥動態跳起

到這裡鳥的設定上差不多了,老闆這裡加上translate,並以 noise 的方式讓鳥以隨機的方式向上跳動。

// tab 2
translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
translate(0, noise(i + frameCount / 50) * 50 - 30)
《鳥與電線桿》第九步驟:隨機跳動的鳥嘴
《鳥與電線桿》第九步驟:隨機跳動的鳥嘴

十、調整電桿感外觀

  1. 提升電線桿質感

在開始調整之前,老闆先將剛剛所製作鳥跳動的效果註解起來,視覺上比較不會影響到我們的調整過程。首先將電線桿的寬度與線條的粗度 ( strokeWeight(1) )縮小之外,也將電線桿的間距都拉大,讓看上去的質感更加提升。

// tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  // 將電線桿寬度調小
  barSize: createVector(3, 500),
  birdSize: createVector(20, 30)
}
// mysketch
let startY = o * 200 + 50
《鳥與電線桿》第十步驟:調整電線桿
《鳥與電線桿》第十步驟:調整電線桿

2. 新增多條電線

老闆在找實體電線桿參考圖的時候,注意到每根電線桿實際上彼此之間是有多條電線的,所以老闆決定再多增加兩條電線,可以直接複製第一組的程式後貼上,再微調參數。在決定上下高度的參數 y 上,第二組的電線桿左側加上 50,右側加上 100,而第三組則是左側增加 120 ,右側加上 50,這樣子形成高低與交錯的效果。

// mysketch
for (var i = 0; i < 5; i++) {
  let startX = -300 + barDist * i
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY),
    rightPoint: createVector(startX + barDist, startY),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  // 新增其他組別的電線桿,左右兩測的高度不一樣,由低到高
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 50),
    rightPoint: createVector(startX + barDist, startY + 50 + 50),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  // 新增其他組別的電線桿,左右兩測的高度不一樣,由高到低
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 120),
    rightPoint: createVector(startX + barDist, startY + 50),
    birdSize: createVector(random(10, 20), random(30, 40))
  }))
}
《鳥與電線桿》第十步驟:增加電線
《鳥與電線桿》第十步驟:增加電線

十一、新增與調整顏色

  1. 多彩的鳥

接著要來替剛剛所增加的鳥填上不同的顏色。老闆一樣至配色網站 coolors 找合適的配色組合。接著在屬性上新增屬性 colors ,預設值設定為黑色。設定好後就可以在生成鳥的物件時候,以 random 隨機選取三組顏色方式來讓鳥有不同的色彩組合。

// tab 2
let colors1 = "0c090d-e01a4f-f15946-f9c22e-53b3cb".split("-").map(a => "#" + a)
let colors2 = "447604-6cc551-9ffcdf-52ad9c-47624f".split("-").map(a => "#" + a)
let colors3 = "173753-6daedb-2892d7-1b4353-1d70a2-fff-222".split("-").map(a => "#" + a)

let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  barSize: createVector(4, 500),
  birdSize: createVector(20, 30),
  // 新增預設顏色
  colors: ["#000"]
}
// mysketch
for (var i = 0; i < 5; i++) {
  let startX = -300 + barDist * i
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY),
    rightPoint: createVector(startX + barDist, startY),
    // random 三組顏色,下面兩個也是一樣
    colors: random([colors1, colors2, colors3]),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 50),
    rightPoint: createVector(startX + barDist, startY + 50 + 50),
    colors: random([colors1, colors2, colors3]),
    birdSize: createVector(random(10, 20), random(30, 30))
  }))
  birdBars.push(new BirdBar({
    leftPoint: createVector(startX, startY + 120),
    rightPoint: createVector(startX + barDist, startY + 50),
    colors: random([colors1, colors2, colors3]),
    birdSize: createVector(random(10, 20), random(30, 40))
  }))
}
《鳥與電線桿》第十一步驟:改變鳥的顏色
《鳥與電線桿》第十一步驟:改變鳥的顏色

2. 黑暗模式

這裡老闆來嘗試看看如果是背景黑色,而電線桿是白色的話效果如何,結果效果意外地不錯。電線桿的部分新增電線顏色的屬性,並且分別加在電線桿與電線上。

// mysketch
// 將背景改為黑色
let startColor = color("#000")
let endColor = color("#333")
// tab 2
let def = {
  leftPoint: createVector(50, 350),
  rightPoint: createVector(50, 350),
  randomId: random(100000),
  barSize: createVector(4, 500),
  birdSize: createVector(20, 30),
  colors: ["#000"],
  // 新增電線桿顏色
  barColor: color(255)
}

// 更改電線顏色
stroke(this.barColor)
strokeWeight(1)
noFill(0)
beginShape()
let nodeCount = 30
for (var i = 0; i <= nodeCount; i++) {...}


// 更改電線桿顏色
fill(this.barColor)
rect(this.leftPoint.x - this.barSize.x / 2, this.leftPoint.y, this.barSize.x, this.barSize.y)
rect(this.rightPoint.x - this.barSize.x / 2, this.rightPoint.y, this.barSize.x, this.barSize.y)
《鳥與電線桿》第十一步驟:調整背景顏色
《鳥與電線桿》第十一步驟:調整背景顏色

十二、調整鳥的細節

  1. 跳動方式

將之前鳥跳動的設定打開,由於現在在 noise 的參數中只給第幾個 i 以及時間參數 frameCount,所以會發現當它跨不同組的時侯,上下擺動的頻率會是一樣的,這裡可以使用到之前為了製造隨機顏色所創建 randomId,將它加到 noise 之中。

// tab 2
translate(0, noise(i + frameCount / 50, this.randomId) * 50 - 30)

2. 改變鳥嘴方向

老闆在這裡有曾嘗試過替鳥加上眼睛,不過效果不是很好,所以後來就拿掉了。除了眼睛外,老闆這裡調整了嘴巴的位置,讓它以置中的方式呈現。

// tab 2
translate(0, -5)
triangle(-5, 0, 0, 8, 5, 0)

3. 調整鳥的密集程度

現在看上去鳥的數量上有點多,所以將取餘數的數值加大,降低鳥的密集程度。

// tab 2
if (i != 0 && i != nodeCount && (i % 6 == 0 || i % 4 == 0)) {...}
《鳥與電線桿》第十二步驟:調整鳥隻的細節

十三、調整電線桿顏色與背景色

為了不讓畫面上看起來那麼的單調,老闆從兩個地方著手,第一是改變每一組電線桿的顏色,以隨機的方式指定顏色,另一個則是指定背景顏色,讓作品的主體可以呈現出來。

// mysketch
//  2. 調整背景顏色
background(255);
for (var o = 0; o < 4; o++) {
  let barDist = random(300, 500)
  let startY = o * 200 + 50
  for (var i = 0; i < 5; i++) {
    let startX = -300 + barDist * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY),
      rightPoint: createVector(startX + barDist, startY),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      // 1. 隨機取電線桿的顏色
      barColor: random(colors1)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 50),
      rightPoint: createVector(startX + barDist, startY + 50 + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      // 隨機取電線桿的顏色
      barColor: random(colors2)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 120),
      rightPoint: createVector(startX + barDist, startY + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 40))
    }))
  }
}
《鳥與電線桿》第十三步驟:調整電線桿及背景顏色
《鳥與電線桿》第十三步驟:調整電線桿及背景顏色

十四、多種情境

接下來要做的是如何用同一組演算法,來做出不同作品的效果。首先在 setup 中定義出幾種不同的情境,像是早上、中午、晚上,接著在 draw 裡面設定每種情境之下的背景漸層顏色,這樣一來當我們每次按下重新整理時,就可以隨機看到不同色系的作品了。

// mysketch
function setup() {
  // 定義 currentTime,需要再 setup 裡面,是因為要在這裡才能夠使用 random
  currentTime = random(["day", "night", "evening"])
}

function draw() {
  // 根據不同的狀態設定不同的顏色
  let startColor = color("#000")
  let endColor = color("#333")

  if (currentTime == "day") {
    startColor = color("#92a0a5")
    endColor = color("#eee")
  }

  if (currentTime == "evening") {
    startColor = color("#ffca59")
    endColor = color("#f43030")
  }
}
《鳥與電線桿》第十四步驟:早中晚的不同背景顏色

十五、滑鼠互動

除了讓鳥隨機的跳動之外,這裡增加當滑鼠移動到鳥身上後會跳起的效果,使用計算距離的 dist,計算出鳥的位置與滑鼠之間的距離小於一定距離時,便會向上移動。

translate(midPoint.x, midPoint.y - this.birdSize.y / 2)
translate(0, noise(i + frameCount / 50, this.randomId) * 50 - 30)
// 當滑鼠的距離跟鳥很接近時,會向上移動
if (midPoint.dist(createVector(mouseX, mouseY)) < 100) {
  translate(0, sin(i + frameCount / 30) * 30 - 15)
}
《鳥與電線桿》第十五步驟:增加滑鼠互動效果
《鳥與電線桿》第十五步驟:增加滑鼠互動效果

十六、電線桿粗細

最後在電線桿的粗細上做一點調整,讓前面的電線桿比較粗、後面的電線桿比較細。決定電線桿前後的參數是 o,一共有五層,所以在桿子的寬度上可以用常數乘上 o 的方式,讓越下面的電線桿越粗,製造出層次感。

for (var o = 0; o < 4; o++) {
  let barDist = random(300, 500)
  let startY = o * 200 + 50
  for (var i = 0; i < 5; i++) {
    let startX = -300 + barDist * i
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY),
      rightPoint: createVector(startX + barDist, startY),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      barColor: random(colors1),
      // 讓前面的電線桿比較租
      barSize: createVector(4 + o * 5, 500)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 50),
      rightPoint: createVector(startX + barDist, startY + 50 + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 30)),
      barColor: random(colors2),
      barSize: createVector(4 + o * 5, 500)
    }))
    birdBars.push(new BirdBar({
      leftPoint: createVector(startX, startY + 120),
      rightPoint: createVector(startX + barDist, startY + 50),
      colors: random([colors1, colors2, colors3]),
      birdSize: createVector(random(10, 20), random(30, 40)),
      barSize: createVector(4 + o * 5, 500)
    }))
  }
}
《鳥與電線桿》完成圖
《鳥與電線桿》完成圖

總結

回顧一下這次的鳥與電線桿的製作過程

  1. 建立建立第一組電線桿,使用 lerp 方式製作弧形的電線
  2. 新增一個 tab 檔案,用於建立 class,並在主程式 sketch 中透過 class 建立物件
  3. 在電線桿上每一個點的位子 midPoint 上劃出鳥的外型並且上色
  4. 以 for 迴圈的方式建立四組電線桿
  5. 使用一格格色塊的方式製作出背景漸層的效果
  6. 在鳥的外觀、數量以及動態的呈現上做調整
  7. 修正電線桿的間距以及數量
  8. 對鳥的部分細節做調整
  9. 改變電線桿與背景顏色
  10. 針對不同情境設定不同的背景顏色
  11. 調整電線桿粗度,產生前後層次感

以上就是我們用p5.js寫出來的作品啦!相同的繪製原理還能應用在甚麼作品上呢?

延伸閱讀:
用p5.js玩創作,讓小機器人動起來!哲宇的互動藝術體驗(直播筆記)
【p5.js創作教學】Aqua Planet 水色星球 – 來製作發光碰撞的行星吧!(直播筆記)
【p5.js創作教學】Quantum Unstable 量子不穩定 – 發光糾纏的量子系統

若對互動藝術程式創作有興趣,歡迎加入老闆開的Hahow課程互動藝術程式創作入門,與另外將近兩千位同學一起創作吧!

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

墨雨設計banner

這篇文章 【p5.js創作教學】鳥與電線桿 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【互動網頁程式教學】用 GSAP 製作直播互動動態效果 https://creativecoding.in/2022/03/10/gsap-livestream-webpage/ Thu, 10 Mar 2022 05:20:00 +0000 https://creativecoding.in/?p=1684 本篇教學帶大家使用 vue.js, vue-cli, gsap 製作模擬 facebook 手機版的直播畫面,連結視訊鏡頭中使用者的畫面,並加上留言區塊以及可以點擊的表情符號等功能。跟著老闆一起,動態網頁製作好簡單。

這篇文章 【互動網頁程式教學】用 GSAP 製作直播互動動態效果 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
用 GSAP 製作直播互動動態效果成品
用 GSAP 製作直播互動動態效果成品

本文翻自【互動網頁程式教學】用 GSAP 製作直播互動動態效果,若是對文章內容有疑問,或是想要老闆手把手帶你飛,都可以觀看影片跟著動手做,也附上這次成品

這次要帶大家使用 vue.js, vue-cli, gsap 來模擬 facebook 手機版的直播畫面,首先將視訊鏡頭中使用者的畫面做為直播畫面,下半部留言區塊點擊表情符號後,表情符號加上彈幕的動畫效果。輸入訊息後,訊息會經由轉場動畫出現在畫面中,使用者也能點選刪除留言來看到訊息的離場動畫。

製作表情符號進出場的動畫會使用到 gsap ,gsap 是由 greenSock所開發,常被用來取代以前的 flash,提供許多製作動畫的套件,包含這次會使用到的 tweenMax 及 timelineMax,由於是透過 js 所撰寫,動畫呈現有更大的自由度。但要注意個人和商用部分功能是免費的,若需引入專案時要多留意。範例中,也會帶大家使用 vue transition 提供的兩種模式,來觸發表情符號與留言的進出場效果。

這次直播筆記會帶大家學會以下內容:

  • 認識 vue 中的 ref, $refs
  • 使用原生 js 載入視訊影像
  • 使用 gsap 製作表情符號過場動畫
  • 使用 vue 中的 transition 製作過場動畫

事前準備

開發環境

老闆在這次專案改使用 CodeSandbox進行開發,關於環境和其他套件的設定,同學可以參考老闆的成品

CodeSandbox 比起之前老闆示範時常用的Codepen來說,功能較完整一些,除了提供大家建立 project、安裝需要使用的 library 之外,也能在上面跑 npm 的 package 設定。製作大型專案需要測試時,老闆習慣會使用 CodeSandbox ,但小缺點就是不支援 emmet(註:輸入簡化碼後會自動產生完整HTML & CSS程式碼,加快程式碼輸入,也降低手誤機率),撰寫程式碼較不方便一些。

透過 new sandbox 創建新的專案,選擇 Vue( vue2 的 cli)後,可以看到左邊有 files 欄,包含專案所有資料夾及檔案,這次專案只會在 App.vue 這支檔案中開發,同學們不用被資料夾結構嚇到。

打開 App.vue 檔案之後可以發現有三個區塊分別為:

  • <template>:撰寫 html,改使用 pug
  • <script>:撰寫 vue 及 js
  • <style>:撰寫 css,改使用 scss

首先,將使用不到的元件 HelloWorld 相關的敘述全部拔除,準備好基本的結構後,可以看到右邊的畫面只剩下一個 vue 的 logo。

//將HTML撰寫語言改成pug
<template lang="pug">
#app
  img(alt="Vue logo", src="./assets/logo.png", width="25%")
</template>

<script>
export default {
  name: "App"
};
</script>

//將撰寫語言改成scss
<style lang="scss">
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

接著,讓我們來安裝這次會使用的套件。畫面的左欄有 Dependencies,因為我們在創建專案時選擇 vue,可以看到 codeSandbox 已經幫我們裝了兩個套件。我們只需要再將 gsap 載入即可,在放大鏡區塊(Add Dependency)打入 gsap 並選擇安裝,就大功告成。

在Code Sandbox裡加入gsap套件

接下來會使用到的 API:

這次專案會使用以下的內容,這邊先重點整理給大家,不清楚的地方,可以透過後面跟著老闆操作,或是觀看相關文件,了解每個 api 使用時機。

Vue

  • script
    • el:資料要綁定的區塊
    • data:vue 要綁定的資料放置處
    • mounted:vue 的生命週期, el 被掛載之後會執行裡面的程式碼
    • methods:使用到的 function 放置處
    • computed:計算屬性,會因為 data 內的值改變,而跟著變動
    • $refs:可以搭配 ref 屬性來取得 DOM 元件 (延伸參考資料)
// javascript
var vm = new Vue({
  el: '#app',
  data: {
    text: 'Hello World',
    texts: ['H', 'i']
  },
  mounted () {...},
  methods: {
    changeText () { 
      this.$refs.input.focus()
    }
  },
  computed: {
    showText () { return ...}
  }
})
  • template:畫面部分會使用到以下內容
    • {{text}}:將資料綁定到畫面中顯示
    • v-model:將資料綁定到畫面中顯示或修改
    • v-for:讓陣列資料重複產生 dom,可以搭配索引值綁定
    • :key:可以搭配 v-for 使用,提供 vue 識別每個 dom 是不同的,在傳入 v-for 的陣列中,key 要是獨特的值,避免識別上出錯。
    • @click=””:當點擊目標物會觸發傳入 click 的內容
    • ref:可以在程式碼中搭配 $refs 取得 DOM 元件(延伸參考資料)
// html
#app
	p {{text}}
	input (v-model="text ref="input")
	p(v-model="showText")
	div(:class="")
	div(v-for="(item, idx) in texts", :key="idx") {{item}}
	button(@click="changeText()")
  • vue – transition-group:vue 提供給在 dom 要被加入、移除或更新時的動態效果,使用方法會在後面實做中解說。若想要參閱官方說明文件可點此閱讀
  • js:Math.random():會產出一個大於等於 0、小於 1 之間的隨機小數。若想要參閱更詳細的說明文件可點此閱讀

gsap

  • tweenMax:針對指定的 DOM 在動畫時間內執行動畫
TweenMax.to(執行動畫的DOM, 動畫時間, {
  y: 200, // 位移 200 px
  rotate: 360, // 旋轉 360 度
  delay: 3, // 3 秒後才執行動畫
  repeat: 2, // 會重複執行兩次
  yoyo: true // 會倒帶後再執行一次
});
  • timeLineMax:可讓動畫多段依序進行 ,使用 to 去接後續要播放的動畫
let tl = new TimelineMax() // 新增 tl 變數
tl.to(this.$refs.logo, 1, { // 使用 this.$refs 去取得 dom
  y: 200,
  rotate: 360
}).to(this.$refs.logo, 1, {
  scale: 2
})

js – getUserMedia

提供瀏覽器獲得使用者影像,navigator 會詢問瀏覽器有沒有影片可以使用,找到之後將其放到 video tag 中,要記得提供瀏覽器取用麥克風或錄影機的權限。(延伸參考資料)

var constraints = { audio: true, video: { width: 1280, height: 720 } };
navigator.mediaDevices
  .getUserMedia(constraints)
  .then((mediaStream) => {
    var video = this.$refs.myVideo;
    video.srcObject = mediaStream; // 將 video 指定到指定的 DOM 中
    video.onloadedmetadata = function (e) {
      video.play();
    };
  })
  .catch(function (err) { // 出錯時的處裡
    console.log(err.name + ": " + err.message); 
  });

跟著老闆開始動手做

操作一段與多段動畫

我們在環境準備階段已經把 gsap 裝到專案中,首先我們使用 vue 的 logo 來練習 gsap 製作動畫方式。gsap 內有很多個製作動畫的方式,老闆帶大家操作兩種型式的動畫,分別為 tweenMax, timelineMax 兩種。

讓 logo 動起來之前,先介紹兩種方式讓 gsap 抓到 logo 這個 dom 元件。

  • html 賦予 id,使用 #logo 讓 gsap 取得 dom
  • html 賦予 ref,使用 $refs 讓 gsap 取得 dom

我們想要讓 logo 一秒內下滑,並旋轉,這邊會使用到 gsap 的 TweenMax,所以我們將它 import 到專案中,並在 vue 的 mounted 階段操作動畫,mounted 是 vue 的生命週期,會在 vue app 載入後執行裡面的動畫,寫法及參數如下。gsap 有許多的動畫值可以操作,建議同學們不用死背,需要時去查文件即可。

<template lang="pug">
#app
  img#logo(alt="Vue logo", 
           src="./assets/logo.png", 
           width="25%")
</template>

<script>
import { TweenMax } from "gsap";
export default {
  name: "App",
  mounted() {
    TweenMax.to("#logo", 1, {
      y: 200, // 位移 200 px
      rotate: 360, // 旋轉 360 度
      delay: 3, // 3 秒後才執行動畫
      repeat: 2, // 會重複執行兩次
      yoyo: true // 會倒帶後再執行一次
    });
  },
};
</script>
使用TweenMax.to做出旋轉下滑再倒帶的動畫
使用TweenMax.to做出旋轉下滑再倒帶的動畫

完成一段式的動畫後,會發現 TweenMax.to() 無法滿足多段式的動畫需求。如果我們希望動畫是多段小動畫依序進行,那要一直寫許多 TweenMax.to 並加上 delay 嗎?

其實,gsap 有另一個功能 TimelineMax 就可以達成我們的需求,使用 TimeLineMax 時,要注意需要先新增 new TimelineMax 的變數,使用的方式是第一段動畫完成後,使用 to 去接後續要播放的動畫,傳入的參數與 TweenMax 一樣。使用方法如下:

我們前面提到有兩種方式可以取得 dom 元件,這邊改使用 vue 所提供的 ref 及 $refs 去取得要執行動畫的 dom 元件。

<template lang="pug">
#app
  img(ref="logo", alt="Vue logo", src="./assets/logo.png", width="25%") 
  //- img 多加 ref 屬性
</template>

<script>
import { TweenMax, TimelineMax } from "gsap";
export default {
  name: "App",
  mounted() {
    let tl = new TimelineMax() // 新增 tl 變數
    tl.to(this.$refs.logo, 1, { // 使用 this.$refs 去取得 dom
      y: 200,
      rotate: 360
    }).to(this.$refs.logo, 1, {
      scale: 2
    })
  },
};
</script>
使用TimelineMax做出旋轉下滑再放大的兩段動畫
使用TimelineMax做出旋轉下滑再放大的兩段動畫

直播畫面與 live 標籤

接著來處理畫面,會處理的內容分別為:模擬手機直播畫面的樣式切版、使用 video 視訊畫面做為直播影片、利用 ref 來取得 video 的位置、 live 動畫效果與時間計數器。

  • 模擬手機畫面:只需要針對畫面樣式進行調整,在幫 live 區塊做定位時,記得在 #app 多加上 position: relative,否則預設會以 body 做為參考。
  • 使用 video 作為直播影片:在畫面上準備待會要放置 video 的 dom,並在裡面放上一個 video tag ,這邊可以對 video tag 使用 muted 屬性,待會的影像就會是靜音的狀態。接著在 mounted 中使用 getUserMedia 來獲得影像。vue 初始化時,會先建立一個空的 video DOM,到了 mounted (vue app 載入之後)階段,navigator會詢問瀏覽器有沒有影片可以使用,找到之後將其放到 video tag 中,要記得提供瀏覽器取用麥克風或錄影機的權限。
  • 這邊我們也練習前面提到的 ref ,來取得 video tag,MDN 上面提供的範例是使用 function,因為 function 有自己的 scope,無法在函式內部使用 this 取得 vue 本身。有兩種解法,在外面宣告 _this 變數,或是用 es6 的 arrow function。
  • live 動畫效果:要幫 live 字樣加上呼吸燈的亮暗亮暗效果,除了前面有練習過的 repeat, yoyo 屬性外,也會使用到 gsap 中的 easing api,可以選擇自己喜歡的時間曲線後,在專案中引入。
  • 時間計數器:在 mounted 中使用 setInterval 來進行每秒都會增加時間的值,利用這個值換算成需要的格式。透過 computed 來回傳需要的字串,computed 的使用時機為「已經知道資料是什麼,基於原本的值去加工後回傳,不會影響到原本的資料」。也利用 padStart 將時間中的時分秒三個資料都能是2位數,在最後回傳結果字串時,利用 es6 的頓號`來組裝字串。
<template lang="pug">
#app
  .liveLabel
    .red(ref="liveTag") LIVE //LIVE小標
    .counter {{timeLabel}} //時間計數器
  .videoContainer
    video(ref="myVideo", autoplay="true", muted)
</template>

<script>
import { TweenMax, Power0 } from "gsap";
export default {
  name: "App",
  data() {
    return {
      time: 0,
    };
  },
  computed: {
    timeLabel() {
      let sec = this.time % 60;
      let min = Math.floor(this.time / 60) % 60;
      let hour = Math.floor(this.time / 3600) % 24;
      let pd = (num) => (num + "").padStart(2, "0"); // padStart api, 不足長度字串在前面補上0
      return `${pd(hour)}:${pd(min)}:${pd(sec)}`;
    },
  },
  mounted() {
  // 每秒執行一次增加 time 的值
    setInterval(() => {
      this.time++;
    }, 1000);
  // Live 呼吸燈
    TweenMax.to(this.$refs.liveTag, 1, {
      css: {
        backgroundColor: "rgba(255, 0, 0, 0.3)",
      },
      ease: Power0.easeNone,
      repeat: -1,
      yoyo: true,
    });

  // 影片串流
    var constraints = { audio: true, video: { width: 1280, height: 720 } };
    navigator.mediaDevices
      .getUserMedia(constraints)
      .then((mediaStream) => { // 改成 arrow function, this就不會抓到內部而是外層的元件
        var video = this.$refs.myVideo;
        video.srcObject = mediaStream;
        video.onloadedmetadata = function (e) {
          video.play();
        };
      })
      .catch(function (err) {
        console.log(err.name + ": " + err.message);
      });
  },
};
</script>

<style lang="scss">
html,
body {
  background-color: #333;
  display: flex;
  justify-content: center;
  align-items: center;
}
#app {
  position: relative;
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  width: 390px;
  height: 744px;
  background-color: white;
}
.videoContainer {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 450px;
  overflow: hidden;
  video {
    height: 100%;
  }
}
.liveLabel {
  position: absolute;
  color: #fff;
  display: flex;
  left: 50%;
  top: 30px;
  transform: translateX(-50%);
  .red {
    padding: 5px 10px;
    background-color: red;
    font-weight: 900;
  }
  .counter {
    padding: 5px 10px;
    background-color: rgba(black, 0.6);
  }
}
</style>
順利將直播影片置入網頁中
順利將直播影片置入網頁中

表情符號功能

接下來我們要來做表情符號清單與點擊表情符號後的效果。

  • 表情符號清單:新增一個變數記錄所有表情符號,結合 split 就能將表情字串轉換成陣列,調整樣式後,將選單 #emojiToolBar 放置在畫面的右下方。
  • 表情符號被點擊後動畫效果:清單中的表情符號被點擊後,使用 css 的類別選擇器 :active 來改變 transition 做為被點擊的動態回饋。我們也在清單中的每顆表情符號使用 vue 的語法 @click,當表情符號有點擊事件時,會觸發 addEmoji 函式,同時將被點擊的表情做為參數傳入函式中。
  • 記錄有哪些表情符號被點擊:當使用者點擊表情符號後,我們需要記錄有什麼表情符號被觸發,才有辦法去跑對應的動畫,所以在 data 中我們新增一個變數 currentEmojiList ,當清單中的符號被按壓後,會將新的表情符號 push 到陣列裡。
  • nextTick 確保資料已更新(延伸參考資料):因為 vue 不是即時更新,資料更新和畫面更新有時間差,所以在更新資料後,馬上去抓新的 dom 會失敗,改使用 nextTick 確定資料更新完畢才跑後續的程式碼。
  • tweenMax 初始化設定:使用了 gsap.set() 這個 api,可以針對準備進場的動畫做初始設定,老闆希望表情剛進場時能從小變到大,所以我們在 set 中新增一個 scale: 0.2。
  • 表情符號進出場動畫:期望的動畫流程為,按壓表情符號後,先往上飄並慢慢放大到定點,往左飄變小並離場,因為每個表情符號都要兩段式的動畫,這時就可以使用前面提到的 TimelineMax 來達成效果。若是有超出畫面則被隱藏,只要透過 css 去對 #app 做 overflow: hidden 即可。
  • 加上隨機數值:完成前面幾點,目前的動畫會有點死板,為了讓動畫更自然,我們讓每個表情起始點不同,上移的距離也不同,製造出交錯的表情符號動畫。分別在 set 內新增一個 x 的值,隨機從0~-100 中挑一個數並加上20;也讓每個表情符號上移的 y 位置不同 ,所以在第一段動畫的終點,讓 y 的值組合不同的 random數。
  • 時間函數:大致功能都完成後,希望兩段動畫能再自然一點,所以為兩段動畫都加上速度曲線的值 ease,大家也可以參考相關文件,動手試試不同種的速度效果。
<template lang="pug">
#app
  ...
  .contentArea
    ul.floatingEmojiList
      li.floatingEmoji(
        v-for="(emoji, emojiId) in currentEmojiList",
        :class="`emoji_${emojiId}`"
      ) {{ emoji }}
  ul#emojiToolBar
    li.emojiBtn(v-for="emoji in emojis", @click="addEmoji(emoji)") {{ emoji }}
</template>

<script>
import { TweenMax, TimelineMax, Power0, Power1, Power4 } from "gsap";

const emojiList = "👍,🎉,😂,😯,😢,😡";

export default {
  name: "App",
  data() {
    return {
      time: 0,
      emojis: emojiList.split(","),
      currentEmojiList: [],
    };
  },
  computed: {
    ...
  },
  mounted() {
    ...
  },
  methods: {
    addEmoji(emoji) {
      this.currentEmojiList.push(emoji);
      let tl = new TimelineMax();
      this.$nextTick(() => {
        let _id = `.emoji_${this.currentEmojiList.length - 1}`;
        tl.set(_id, {
          scale: 0.2,
          x: Math.random() * -100 + 20,
        })
          .to(_id, 1, {
            y: -200 + Math.random() * -100,
            scale: 1,
            ease: Power4.easeOut,
          })
          .to(_id, 3, {
            x: -500,
            scale: 0.6,
            ease: Power1.easeIn,
          });
      });
    },
  },
};
</script>

<style>
...
#app {
  ...
  overflow: hidden;
}
...
#emojiToolBar {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  display: flex;
  list-style: none;
  .emojiBtn {
    font-size: 40px;
    width: 50px;
    cursor: pointer;
    transition: 0.5s;
    &:active {
      transition: 0s;
      transform: scale(0.8);
    }
  }
}
.contentArea {
  position: relative;
}
.floatingEmojiList {
  list-style: none;
  .floatingEmoji {
    position: absolute;
    right: 50px;
    top: 50px;
    font-size: 50px;
  }
}
</style>

改使用 transition 元件製作表情符號動畫

接下來我們來使用 vue 中 transition-group 元件改寫表情符號進場的過程。vue 提供了 transition 與 transition-group 兩種元件,讓元件在特定的時間點觸發指定的 function 或加上特定的 class 名稱(詳細請參考延伸資料)。transition 與 transition-group 的差別在於,如果只有一個元件會改變使用前者,這個專案是用在由 for 產出的 li 元件們上,所以使用後者。接著就可以把原本在 addEmoji 裡的程式碼搬到 enter 中。

此時,也可以拔掉 nextTick ,因為在 transition-group 上的屬性 v-on:enter 會在確定資料更新才觸發進場,就不用再使用 nextTick 去監聽元件是否生成。要注意的是,如果有使用 v-for ,記得要補上 key 值。

<template lang="pug">
#app
  ...
  .contentArea
    ul.floatingEmojiList
      transition-group(v-on:enter="enter") //子元件進場時會觸發 enter 函式
        li.floatingEmoji(
          v-for="(emoji, emojiId) in currentEmojiList",
          :key="emojiId", // 補上 key
          :class="`emoji_${emojiId}`"
        ) {{ emoji }}
  ul#emojiToolBar
    li.emojiBtn(v-for="emoji in emojis", @click="addEmoji(emoji)") {{ emoji }}
</template>

<script>
import { TweenMax, TimelineMax, Power0, Power1, Power4 } from "gsap";

const emojiList = "👍,🎉,😂,😯,😢,😡";

export default {
  ...
  mounted() {
   ...
  },
  methods: {
    enter(el) { // 動畫進場時觸發的動畫
      let tl = new TimelineMax();
      tl.set(el, {
        scale: 0.2,
        x: Math.random() * -100 + 20,
      })
        .to(el, 1, {
          y: -200 + Math.random() * -100,
          scale: 1,
          ease: Power4.easeOut,
        })
        .to(el, 3, {
          x: -500,
          scale: 0.8,
          ease: Power1.easeIn,
        });
    },
    addEmoji(emoji) { // 將動畫內容搬到 enter 函式中
      this.currentEmojiList.push(emoji);
    },
  },
};
</script>

留言區塊

製作送出留言的功能,分別有以下項目需要完成:

  • 準備假資料:先準備單筆資料的格式,分別有頭像顏色、發言人、內容。
  • 輸入框及送出按鈕:這次只是模擬訊息送出的狀態,機制會是使用者輸入留言,成功送出訊息時,將這個訊息加到 comments 中,並將輸入框清空。若是輸入框為空的,則使用預設的內容送出。
  • 預設訊息轉成 json 字串格式再轉回來:要多做這層處理,是因為預設留言 message 是物件,直接賦值的話會是傳參考,需要透過這種方式,創造一個全新的物件。
  • 調整表情符號 bar 樣式:將表情工具的寬度改為 100%,加上透明背景。
<template lang="pug">
#app
  ...
  .contentArea
    input(v-model="message")
    button(@click="addMessage") Add Comment
    .comments(v-for="(comment, commentId) in comments", :key="commentId")
      .head(:style="{ backgroundColor: comment.color }")
      .content
        .name {{ comment.name }}
        .sentence {{ comment.content }}
</template>

<script>
import { TweenMax, TimelineMax, Power0, Power1, Power4 } from "gsap";

const emojiList = "👍,🎉,😂,😯,😢,😡";

let message = {
  color: "#333",
  name: "Lorem ipsum",
  content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
};

export default {
  name: "App",
  data() {
    return {
      ...
      comments: [],
      message: "",
    };
  },
  computed: {...},
  mounted() {...},
  methods: {
    addMessage() {
      const newMessage = JSON.parse(JSON.stringify(message)); // 創造一個全新的物件
      if (this.message !== "") {
        newMessage.content = this.message;
        this.message = "";
      }
      this.comments.push(newMessage);
    }
    ...
  },
};
</script>

<style lang="scss">
#emojiToolBar {
  position: absolute;
  right: 0;
  bottom: 0;
  margin: 0;
  padding: 5px;
  display: flex;
  justify-content: flex-end;
  list-style: none;
  width: 100%;
  background-color: rgba(#fff, 0.8);
	...
}
...
.contentArea {
	position: relative;
  list-style: none;
  padding-left: 0px;
  margin-left: 10px;

  .comments {
    display: flex;
    list-style: none;
    padding: 5px;
    font-size: 15px;

    .head {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      margin-top: 10px;
      margin-right: 20px;
      margin-left: 10px;
      flex-shrink: 0;
    }
    .content {
      text-align: left;
      .name {
        font-weight: 900;
      }
    }
  }
}
</style>
加上留言功能以及表情符號清單,越來越像直播的頁面了
加上留言功能以及表情符號清單,越來越像直播的頁面了

新增/刪除訊息

最後我們利用新增訊息功能,來練習 transition,首先因為每筆訊息都是用 v-for 跑出來,所以我們要用 transition-group。

  • 使用 name 來幫訊息加上動畫:前面的表情符號我們是用 v-on:enter ,當元件被監聽到加入畫面中時,觸發 enter 函式。這邊改使用 name 來觸發(延伸閱讀了解Transition),動態加上 class , vue 總共提供六個時間點,讓使用者為他們加上進場或離場動畫,同學可以去觀察 vue 在 dom 上做了什麼事。
  • 調整對應時間點的動畫樣式:大家可以觀察當我們使用 name 來製作動畫後,vue 會在特定時間幫我們在對應的元件上新增 class。利用這些 class 我們就可以來製作過場動畫。要注意動畫的權重如果太小,有些效果無法順利觸發。
  • 刪除訊息:既然完成了新增訊息,刪除訊息也能快速完成,老闆希望保留訊息的完整性,所以這邊調整成,當使用者點擊移除訊息的按鈕,只會在這則訊息的物件上新增一個 delete: true 的值,搭配 v-if 就能將這則訊息隱藏。
<template lang="pug">
#app
  ...
  .contentArea
    input(v-model="message")
    button(@click="addMessage") Add Comment
    transition-group(name="fade") // 改使用 name 製作動畫
      .comments(
        v-for="(comment, commentId) in comments",
        :key="commentId",
        v-if="comment.delete != true" // 當delete 的值不為 true 時,隱藏訊息
      )
        .head(:style="{ backgroundColor: comment.color }")
        .content
          .name {{ comment.name }}
          .sentence {{ comment.content }}
        button(@click="removeComment(comment)") - // 點擊後觸發 removeComment 函式
    ul.floatingEmojiList
      transition-group(v-on:enter="enter") // 當元件進入時,觸發 enter 函式
        li.floatingEmoji(
          v-for="(emoji, emojiId) in currentEmojiList",
          :key="emojiId",
          :class="`emoji_${emojiId}`"
        ) {{ emoji }}

  ul#emojiToolBar
    li.emojiBtn(v-for="emoji in emojis", @click="addEmoji(emoji)") {{ emoji }}
</template>

<script>
import { TweenMax, TimelineMax, Power0, Power1, Power4 } from "gsap";

const emojiList = "👍,🎉,😂,😯,😢,😡";

let message = {
  color: "#333",
  name: "Lorem ipsum",
  content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
};

export default {
  name: "App",
  data() {
    return {
      time: 0,
      emojis: emojiList.split(","),
      currentEmojiList: [],
      comments: [],
      message: "",
    };
  },
  computed: {
    timeLabel() {
      let sec = this.time % 60;
      let min = Math.floor(this.time / 60) % 60;
      let hour = Math.floor(this.time / 3600) % 60;
      let pd = (num) => (num + "").padStart(2, "0");
      return `${pd(hour)}:${pd(min)}:${pd(sec)}`;
    },
  },
  mounted() {...},
  methods: {
    removeComment(comment) {
      comment.delete = true;
    },
    addMessage() {
      console.log("hi");
      const newMessage = JSON.parse(JSON.stringify(message));
      if (this.message !== "") {
        newMessage.content = this.message;
        this.message = "";
      }
      this.comments.push(newMessage);
    },
    ...
  },
};
</script>

<style lang="scss">
...
.contentArea {

  .comments {
    ...
    &.fade-enter-active, // 利用 transition name 做進出場動畫
    &.fade-leave-active {
      transition: all 0.5s;
    }
    &.fade-enter,
    &.fade-leave-to {
      opacity: 0;
      transform: translateY(10px);
    }
    ...
  }
}
</style>
增加刪除留言的功能
增加刪除留言的功能

老闆來結語

這邊再提供一次範例的成果,讓大家在實作時參考,也帶大家快速回顧一次製作流程:

  1. 使用 codeSandbox 來開發專案,安裝 vue-cli, gsap 後,整理預設提供的檔案。
  2. 結合 vue 的 ref, $refs 來取得元件。
  3. 透過 gsap 中的 tweemMax, timelineMax 來製作一段或多段式的動畫。
  4. 利用原生 js 的影片串流模擬直播畫面,並加上 live 與時間計數器的效果。
  5. 了解 vue 提供的 nextTick 能夠確保資料更新後才進行畫面渲染。
  6. 製作表情符號工具欄,在使用者點擊後,能使用 timelineMax 製作表情符號動畫,結合 random 的 api 讓表情動畫更加自然。
  7. 使用 vue transition-group 來做為表情符號與新增刪除留言的進出場動畫。

這次利用 fb 的直播畫面做為目標,帶大家練習 gsap 製作動畫的方式,大家也可以挑戰自己,看看線上有哪些產品或網站有使用到動畫,想辦法使用 gsap 來實現,做為刻意練習的目標。萬事起頭難,一個作品不可能一步到位,大家在開發時,可以先將最終目標拆分成不同階段任務,從一開始的雛型慢慢開發出每個區塊,最後組裝在一起,就會十分有成就感啦!

跟著老闆上課去 👉 動態互動網頁程式入門(HTML/CSS/JS) 👉 動畫互動網頁特效入門(JS/CANVAS)

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

墨雨設計banner

這篇文章 【互動網頁程式教學】用 GSAP 製作直播互動動態效果 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>