基礎 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/category/tutorial/basics/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Wed, 05 Jul 2023 04:48:10 +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/category/tutorial/basics/ 32 32 p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇) https://creativecoding.in/2022/12/26/p5js-workshop-clab-2/ Mon, 26 Dec 2022 03:12:37 +0000 https://creativecoding.in/?p=3435 老闆在第一屆「Processing 臺灣國際社群日」活動中,受邀為設計工作坊擔任講師,針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享。分別為第一天的基礎練習與第二天的生程式藝術實作,本篇文章內容為第二天的實作練習,希望能讓同學能學習到以基礎的方式,將創意想像以自己的技術實現!

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
臺灣當代文化實驗場 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 頻道),當然還有想要進一步正式開始創作,歡迎加入 互動藝術程式創作入門課程 開始學習!本次分享就到這邊結束啦,歡迎各位一起踏入程式與藝術交織的世界!

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

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(下篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
p5 js互動藝術程式創作 – 初階應用實戰教學!(上篇) https://creativecoding.in/2022/12/26/p5js-workshop-clab-1/ Mon, 26 Dec 2022 03:10:20 +0000 https://creativecoding.in/?p=3403 老闆在第一屆「Processing 臺灣國際社群日」活動中,受邀為設計工作坊擔任講師,針對 p5.js互動藝術程式創作入門的主題進行為期兩天的分享。分別為第一天的基礎練習與第二天的生程式藝術實作,本篇文章內容為第一天的基礎練習,希望能讓同學能學習到以基礎的方式,將創意想像以自己的技術實現!

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(上篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

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

什麼是 Creative Coding ?

老闆以自我介紹作為開場,從大學的電機工程背景到至紐約就讀新媒體藝術相關研究所,而開始了一連串生成式藝術的創作。對於什麼是 Creative Coding,老闆說自己也是在不太理解的狀況下開始的。是用程式創作藝術嗎?那到底程式創作出來的藝術,最終呈現出來的效果又會是如何呢?這些想必是初接觸生成式藝術的人都會提出的問題,因此老闆提供不同的案例分享,作為答覆的參考。
以目前最常看到的例子來說,視覺上,像是原先平面的設計海報以動態的方式呈現增加趣味性,或是模擬傳統媒材(如水墨)等等,藉由制定規則,讓每一次不同互動產生出的作品都有不一樣的呈現,這使每件藝術作品都是獨一無二的,並且在參與度提升的情況下,成為對互動者更特別的藝術存在。還有在互動網頁上,像是老闆成立的墨雨設計工作室,就有許多應用相關技術的案例,例如與台灣啤酒合作的網頁。或是現在有很多網頁,當使用者將滑鼠向下移動要瀏覽整個畫面時,也會有不同的動畫呈現,整個瀏覽體驗就像是一個完整的時間軸。

老闆作品 Slient Sky 參考範例(連結
台灣啤酒合作案例(連結

在執行設計創作時,老闆也提出自己建立規則的範例供大家參考,以下方作品為例,從中心散開的圓心,其規則並不複雜。先從中心設定隨機數量生成的粒子,粒子會向外成長擴散,並且在成長的過程中變化成不同的顏色,以及施加不同的外力讓粒子扭曲變形。綜合以上這四項變化,就會得出每一次都是不同作品的互動創作。

視覺平面的傳達外,像是在美術館裡的互動式藝術,或是在街角呈現的大型裝置都是可以用 p5.js 執行的。

p5.js 是什麼?

Processing 為一種開源式語言,最初的目的是希望能以視覺化的方式幫助非程式設計師學習程式語言,而後演變成在電子藝術或是互動式設計上都能發現其應用。

Processing 網站首頁(截圖自 Processing網站首頁 )

p5.js 即是將 Processing 以 Javascript 語言做應用,使其可在網路瀏覽器上進行直接創作,免費及開放資料庫的特性,降低了學習門檻,也吸引更多人進入創意程式設計 Creative Coding 的領域。

p5.js 網站首頁(截圖自 p5.js網站首頁 )

此外還有像是 MaxMSP,以 Creative Coding 應用在音樂的視覺化效果呈現。其撰寫時與 p5.js 的不同在於,p5.js 一般會以程式碼顯示,但在 MaxMsp 是直接以視覺的圖案的方式做呈現,將語言命令可視化成一塊一塊的圖形,再依位置的安排去創造不一樣的效果,串接出不同的流程,進行音樂的創作。

MaxMSP 網站首頁(取自 MaxMSP 網站首頁)

或是 Sonic Pi,一個使用文字創作音樂的程式設計環境。

Sonic Pi 網站首頁(取自 Sonic Pi 網站首頁 )

Touch Designer 也屬於視覺為基礎的語言,採用圖形化的介面讓使用者創作,實現多媒體特效。

Touch Designer 網站首頁(取自 Touch Designer 網站首頁 )

不同的創作環境,都說明 Creative Coding 不論在視覺上,或是聲音上都有豐富的創作應用。而 p5.js 代表的就是更全面的環境,整合了從3D 到聲音都有涵蓋。有同學提到,既然 p5.js 與 processing 的語法幾乎相容,那還有什麼狀況下選擇 processing 比較好呢?老闆回覆是,其實 processing 在過去的優勢現在幾乎在 p5.js 上都能進行,建議剛入門的同學以好操作上手的 p5.js 開始創作入門,這樣到後面如果真的因為 pocessing 可能在效能上有相對好一些,要回 processing 上進行創作,入門時間也不會過長。歸類以下 p5.js 的優勢,簡化了語法、減少了無程式背景的創作者的入門門檻,以及多項資料庫可使用或是簡易自行建立。

課程開始

首先,登入進 Openprocessing ,直接點擊連結便能開始以創作,或是也可以追蹤老闆及不同的創作者,參考不同的創作概念。進入個人創作頁面後,便能看到 「Create a Sketch」,每建立一個 Sketch 都代表一個新的創作實驗場域。點進去創作介面會先看到基本程式裡分成兩個部分, setupdraw 兩種不同的規則。前者代表在畫布上的設定,一次性的準備動作,後者代表不斷在畫布上重複著畫上新東西的動作。在創作同時,我們也可以在右方欄位做 layout 設定,讓撰寫程式同時在同一版面看到作品呈現。

一、顏色置換示範

推薦使用 Chrome 瀏覽器的擴充功能「 ColorZilla 」,可以挑選想要的顏色並顯示出色票。當有了色票後,便可以依上色元素不同,在 background() 或是 fill() 做更換。

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

function draw() {
  circle(mouseX, mouseY, 20);
}

二、繪製圖形、圖形大小與顏色變更示範(筆刷變換)

基本變化像是圖形中不填色 noFill()、去掉圖形外框 noStroke(),或是 strokwWeight() 控制線框粗細。另外,當你要暫停一行程式的功能,請按下 ctrl 鍵與 /,就能看到反灰並且暫停功能的程式碼。

基本圖形控制可以至 p5.js 資料庫中做多方搜尋,以本次範例橢圓形 ellipse 為主,有設定位置是特別指定或是以滑鼠 mouseX、mouseY 代替,與形狀的長寬大小設定。

進入到顏色階段,針對圖形總共有兩個部份可以做編輯,分別是圖形內的顏色以及圖形線條的顏色。fill() 提供圖形內的顏色,stroke()則負責圖形線條的顏色,在顏色選取上,能使用顏色色票、顏色名稱(請記得要加上雙引號,例如:”white”)以及 rgb 顏色(CSS Colors 網站)。如果想要使顏色變化根據互動而有更多元的呈現,可以將 rgb 色彩數值的其中一項更改成滑鼠位置,如此一來顏色就會根據滑鼠位置變化。

滑鼠效果除移動中會有變化外,在 if 與 else 間進行條件判斷,就能在不同情況中呈現不同的效果,下方是根據滑鼠點按之間的效果呈現。

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)
  
  if (mouseIsPressed){
    ellipse(mouseX, mouseY, 100,100);
  } else {
    stroke(255)
    noFill()
    rect(mouseX,mouseY,100,100)
  }
}

如果要增加顏色多樣性,可以使用 random() 設定需要的顏色數值,讓不同的顏色隨機出現。也另外補充個撰寫程式小技巧,可以在選擇多組相同數字時,使用 ctrl 鍵加上 D 進行多重選取減少時間。

上述是由變換位置去進行筆刷的變更,那藉由時間來變更的部分,可以使用 frameCount() 編輯。以示範案例中的橢圓形舉例來說,假使要讓橢圓形的 Y 軸隨著時間來進行筆刷大小的變化,在相對應的 Y 軸編輯位置放上 frameCount 就可以執行,下方範例:

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)

  if (mouseIsPressed){
    ellipse(mouseX, mouseY, 200,frameCount);
  } else {
    stroke(255)
    noFill()
    rectMode(CENTER)
    rect(mouseX,mouseY,100,100)
  }
}

講解到這邊後,老闆給線上參與的同學進行五分鐘的線上練習,題目為:請設計出一個隨機大小為 0 到 50 的圓形筆刷。與此同時,也開放同學在製作時有任何問題能當場詢問,以下問題歡迎參考。

【問題一】 在 p5.js 裡面是否有質感筆刷可以應用或是製作呢?像是油漆筆刷般的效果?

答:有許多方式可以處理,目前先以比較便捷的方式做示範。

先至網路上找尋相關筆刷的圖樣,比如此次是製作油漆類型,那就搜尋相近的筆刷圖案,先下載圖片。

下載其中自己比較喜歡的圖片後,再至 openprocessing 的編輯頁面上傳圖片檔案。便可以開 使進行質感操作啦!

首先,需要先使用 var 宣告與 preload function 預載入,設置質感 texture 的出現,再設置筆刷 image 進行應用,程式與呈現效果如下:

var paintTexture

function preload(){
  paintTexture =loadImage("20190806101953yop172.png")
}

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

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)

  if (mouseIsPressed){
    image(paintTexture,mouseX,mouseY)
  } else {
    stroke(255)
    noFill()
    rectMode(CENTER)
    rect(mouseX,mouseY,100,100)
  }
}

【問題二】 如何分享在 openprocessing 上創作的作品呢?

答:將作品儲存後,可以使用網址直接分享,或是 openprocessing 作品頁面右上方的分享功能都可以使用喔。

接下來進到線上練習的分享時間!

經過五分鐘的練習後,同學應用上述老闆教學說明到的不同應用進行創作。老闆也針對這些分享作品提供不同的建議,或是告訴其他同學這些不同創作是怎麼製作的。例如,有作品使用透明度進行不同編輯,在這邊透明度的應用分為兩種:第一種為圖案顏色的透明度,此類型是在圖案填色 fill() 中,除了 rgb 填寫外,再增加一個代表透明度的數字,變成 rgba 模式,例如 fill(255,255,255,10),代表顏色具有百分之十的透明度;另一種情況為圖形筆刷在背景留下軌跡與否,在 background() 編輯時,設定透明度,讓背景每秒覆蓋的顏色不會完全覆蓋,如此一來就能留下上一秒的軌跡。

三、變數解說教學

進入應用變數的環節,老闆以設計橢圓形為範例說明。如何快速的將兩個不同的,寬與高等比例放大呢?我們需要設置一個櫃子來存放將這些即將運用到的變數,在需要的時候就可以快速抓取使用。運用 let 設置存放變數,舉例來說 let r=50,後續在 draw 裡面,使用到 r 時程式就會帶入 50 這個數字。

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

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

function draw() {
  noStroke()
  fill(mouseX/4,mouseY/4,200)
  print(mouseX/4)

  if (mouseIsPressed){
    let r =random(50,200)
    ellipse(mouseX, mouseY, r,r);
    image(paintTexture,mouseX,mouseY)
  } else {
    stroke(255)
    noFill()
    rectMode(CENTER)
    rect(mouseX,mouseY,100,100)
  }
}

四、如何繪製形狀

經由上述的基礎設定、顏色選填、變數教學與基本圖形設定解說後,接下來進行繪製形狀的教學。形狀除了先前提到的圓形或是方形外,還有線條、3/4圓甚至是自己建立的幾何形狀,其實都能應用函數的變化去繪製出。老闆先以線條舉例,函數式為 line(x1,y1,x2,y2),分別代表連成線條的不同點,而線條粗細是以 strokeWeight() 調整。那該如何得知座標呢?可以先透過設定文字,顯示出滑鼠在座標上移動的位置。

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

function draw() {
  background(255)
  strokeWeight(10)
  line(0,0,500,500)
  textSize(50)
  text(mouseX + "," + mouseY,mouseX,mouseY)
}

老闆也提到如何將圖案融合文字,呈現對話框般的資訊。先設定隨機出現的長方形,並且將方角設定成圓角,使其更具有對話框的設定。並且運用變數設定,使文字與對話框在相同的設定下隨機出現。

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

function draw() {
  // background(255)
  strokeWeight(2)
  let x = random(width)
      y = random(height)
  rect(x,y,200,50,20)
  text("HELLO",x,y+40)
}

或者使用特定條件呈現更多層次的畫面,像是加上 if()else() 的二元判斷變化,以及使用變數讓不同文字隨機出現在不同的對話框中。執行的同時,老闆也會分享一些在寫程式的快捷鍵和小撇步,假使不想只選取單一文字,而是同行多個文字選取,可以根據游標位置,按下 alt 和 shift 再向右或左按下鍵盤左右鍵。或是因為撰寫項目太多,想要進行分類時,可以將無效文字標註在程式中,以防後續搞混或忘記。另外,建立好形狀繪製的基礎後,我們也可以控制出現的頻率。其中一種作法是使用  frameRate() 來編輯。

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

function draw() {
  strokeWeight(2)
  let x = random(width)
      y = random(height)

  if(random()<0.5){
    fill(0)
    rect(x,y,200,50,20)
    textSize(30)
    fill(255)
    text(random(["HELLO","TAIWAN","CREATIVE","CODING"]),x+20,y+40)
  } else {
    // 指定文字大小
    textSize(30)
    // 白底黑字
    let myText = random(["CLAB","TAIWAN","當代文化實驗場"])
    let w=textWidth(myText)
    // 對話框
    fill(255)
    rect(x,y,200,50,20)
    fill(0)
    // 文字
    text(myText,x+20,y+40)
  }
}

瞭解圖形與文字的隨機變化後,繼續進行隨機自行繪製形狀的產生,從三角形隨機顏色變化為例,以及自行繪製的圖形示範。

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

function draw() {
  strokeWeight(random(5))
  line(0,0,width/2,height/2)
  fill(random(0,255),random(0,255),200)
  triangle(0,0,width/2,height/2,mouseX,mouseY)
}

在隨機變化中,老闆習慣設定顏色要在哪種色調或色系間做變化進行設計,建議以這樣的方式進行才比較不易出現奇怪的顏色配置問題。

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

function draw() {
  strokeWeight(random(5))
  line(0,0,width/2,height/2)
  fill(random(50,100),random(100,150),200)
  triangle(0,0,width/2,height/2,mouseX,mouseY)
}

再來是自行繪製的圖形解說,老闆以王冠繪製示範。多邊形製作需要使用到 beginShape() 代表起始位置、 endShape() 代表終點位置,以及 vertex() 說明在起始與終點間會經過哪些點。

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

function draw() {
  background(255)
  // strokeWeight(random(5))
  strokeWeight(50)
  line(0,0,width/2,height/2)
  // line(width/2,height/2,mouseX,mouseY)
  // ellipse(mouseX, mouseY, 20,20);
  fill(random(50,100),random(100,150),200)
  // triangle(0,0,width/2,height/2,mouseX,mouseY)
  fill("#ffcc00")
  beginShape()
    vertex(200,200)
    vertex(200,600)
    vertex(750,600)
    vertex(750,200)
    vertex(600,350)
    vertex(470,200)
    vertex(350,350)
  endShape(CLOSE)
  // 繪製座標文字
  fill("red")
  textSize(50)
  text(mouseX+","+mouseY,mouseX,mouseY)
  // circle(mouseX, mouseY, 20);
}

或者是互動式的顏色應用,將王冠與背景顏色或是新增圖形都設定成會依據滑鼠位置的改變進行更動。使用預設函數改變線條粗細、圖案顏色或是模式等等,去繪製圓形、方形,甚至是多邊形,提供作者與互動者不同的作品溝通模式。像是在皇冠上,老闆進行顏色、大小和特別圖形繪製,主要是根據 mouseXmouseY 的設定。

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

function draw() {
  background(mouseX/4,100,50)
  strokeWeight(20)
  line(0,0,width/2,height/2)
  fill(random(50,100),random(100,150),200)
  fill(255,mouseX/4,0)
  beginShape()
    vertex(200,200)
    vertex(200,600)
    vertex(750,600)
    vertex(750,200)
    vertex(600,350)
    vertex(470+mouseX/10,200)
    vertex(350,350)
  endShape(CLOSE)
  fill(255)
  circle(325,480,100)
  circle(600,480,100+mouseY/10)
  fill(0)
  circle(325,480,30)
  circle(600,480,30)
  line(355,550,500,520+mouseX/30)
  // 繪製座標文字
  fill(255)
  textSize(50)
  text(mouseX + "," + mouseY, mouseX, mouseY)
}

說明完後,又進到同學們自行練習的時間啦!

那也一樣,老闆趁這時解答同學問題。其中有一位同學詢問到霓虹效果該如何呈現,老闆拿出先前做過類似效果的作品說明示範(作品連結)。主要使用 shadow() 的疊色模式編輯,總體上分為兩個步驟,一個是指定形狀中的顏色,另一個是周圍光暈的 dawingContext() 代表周圍光暈的顏色及顯現的程度。老闆以王冠作品進行實際操作, 將王冠周圍的光暈,從最原始的單一色調設定,進階到隨滑鼠移動改變顏色,到最後是點按滑鼠鍵進行光暈顏色變更,多樣化的創作方式供同學參考。

var lightColor

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

function draw() {
  background(mouseX/4,100,50)
  strokeWeight(20)
  line(0,0,width/2,height/2)
  fill(random(50,100),random(100,150),200)

  // 畫王冠
  fill(255,mouseX/4,0)
  drawingContext.shadowColor = lightColor;
  drawingContext.shadowBlur =30;
  beginShape()
    vertex(200,200)
    vertex(200,600)
    vertex(750,600)
    vertex(750,200)
    vertex(600,350)
    vertex(470+mouseX/10,200)
    vertex(350,350)
  endShape(CLOSE)

  // 畫眼睛
  fill(255)
  circle(325,480,100)
  circle(600,480,100+mouseY/10)
  fill(0)
  circle(325,480,30)
  circle(600,480,30)
  line(355,550,500,520+mouseX/30)

  // 繪製座標文字
  fill(255)
  textSize(50)
  text(mouseX + "," + mouseY, mouseX, mouseY)
  // circle(mouseX, mouseY, 20);
}

function mousePressed(){
  lightColor = color(
    random([
      "#F2C400",
      "#F9C784",
      "#8AE1FC"
    ])
  )
}

後續特別應用,例如想讓 frameCount() 的變化是限制在特定範圍內,可以使用 constrain() 限制 frameCount() 隨機大小的範圍,或是使用 sincos 設計。 sin 是指一到負一之間,譬如 sin(frameCount)*50+200,就可以得知是設定在 負五十加兩百到五十加兩百間。或是,如果要設定由左到右的數字大小,使用 map 設定初始範圍與限制範圍進行有意識的創作編輯。

結語

主要實作教學結束後,老闆也傳授了一些編輯使用上的常用事項。例如,openprocessing 封面設定是可以藉由上方編輯按鈕,進行特定封面圖案設定,如下圖圖示,在此頁面按下右上方 edit 編輯即可操作。

也有同學問到,mouse 的座標是否可以連結到外部感應器的點位?可以開啟編輯頁面左方需連結感應的部分,但如何應用就需要後面再多做分享。或是,想要將 p5.js 的效果呈現在網頁上,能如何操作呢?老闆說明,最快的方式是使用 iframe,先至分享區域做壓縮檔下載、直接使用 emded code 程式碼崁入,或是使用 GitHub 將程式碼轉換成 index 檔案後,到 setting 的 GitHub Pages 上傳,並且再選擇 main 之後存檔,就能藉由網址看到自己已 p5.js 創作的互動程式以網頁的方式呈現。上述精彩的課程教學以及同學們多樣化的提問,就是說明互動藝術程式創作還有好多應用面向可以進行探討,與繼續發掘它不同的樣貌。閱讀到這邊,相信你也對互動藝術程式抱持著更高的興趣了吧!就讓我們接續收看下半場的精彩解說吧!

但,如果你已經迫不急待要開始進行創作,加入 互動藝術程式創作入門課程 開始進行深度學習吧!還有不要忘了 追蹤老闆 Twitter 和訂閱 老闆,來點寇汀吧。Boss, CODING please. Youtube頻道隨時補充新媒體藝術的養份,讓我們一起探索這多元的世界!

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

這篇文章 p5 js互動藝術程式創作 – 初階應用實戰教學!(上篇) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
圓圓圈圈:利用迴圈呈現重覆的美 https://creativecoding.in/2022/07/28/cc-ch5-circle/ Thu, 28 Jul 2022 11:32:00 +0000 https://creativecoding.in/?p=2951 「重複」,在藝術表現或日常中很常見的概念,在Creative Coding的領域內更是大量被使用的表現手法,跟大家分享如何在程式創作內加入「重複」概念及實作。

這篇文章 圓圓圈圈:利用迴圈呈現重覆的美 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
在進入正題之前,先請大家觀察下方的圖片。花磚雖然不是台灣傳統建築的「專屬記憶」,但飄洋過海以後落地生根,逐漸在這裡發展出自己的特色。在單一磚片上設計出花紋,透過大量且重複地排列,形成獨特的空間氛圍,也連結成我們小時候的回憶。

「重複」──在藝術表現或日常中很常見的概念,或存在於大自然的晶體結構中、或存在於阿嬤家的廁所廚房內。以音樂而言,在同一首曲子中常可聽到反覆出現的旋律;以文學而言,詩歌中也常出現反覆的句子或單字等;以視覺而言,經由單一的圖案或形體,上下左右不斷的重複予人單純、規律的感受。

在Creative Coding的領域內重複更是大量被使用的表現手法,像是改變物件大小、方向、時間、頻率,或是重複粒子、文字、符號、紋理、以及動作等,善用重複可以讓畫面看起來有整體感。今天就要跟大家分享如何在程式創作內加入重複的概念,以及如何用利用「迴圈」快速製造相同的物件。

各式各樣的汽水罐整齊排列,形成一個奇妙的汽水秩序世界。 
Soda Fantasy @ Che-Yu wu
各式各樣的汽水罐整齊排列,形成一個奇妙的汽水秩序世界。
Soda Fantasy @ Che-Yu wu

目標介紹

  1. 瞭解程式中達成重複的「for 迴圈」概念與常見的應用方式
  2. 瞭解如何在重複中套用規則與創造變化
  3. 嘗試不同的重複概念應用與變化
  4. 結合變數、使用者操作創作重複的作品

迴圈可以重複執行同一組程式碼,幫我們處理不斷重複的事情。 for 迴圈 – 需要以程式碼執行次數作為迴圈的條件。

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

// 舉例
for(var i=0 ; i < 3 ; i++){
  console.log(i)
}

根據上面的概念,如果我們要在畫布上每 100px水平間隔畫一個圓且重複三次,可以有下面兩種寫法:

// 寫法一:手動複製貼上=人工迴圈
ellipse(0,100,30,30)
  ellipse(100,100,30,30) // 相較於上面,右移動了 100 px
  ellipse(200,100,30,30) // 相較於上面,又右移動了 100 px

// 寫法二:使用程式語言的 for 迴圈
for(var i=0; i<3 ; i++){
  ellipse(100*i, 100, 30, 30)  // 計數變數 i 只要小於 3 時都會做這件事,直到不滿足條件則停下並跳出該程式區塊
}

俗話說的好,能交給別人做的事就不要自己做,善用迴圈可以幫助我們快速處理類似的事件。除了單層的for迴圈以外還有巢狀個for迴圈結構:

for(var i=0; i<2; i++){
  for(var j=0; j<2; j++){    // 在每一個 i 中,都會這層迴圈完再跳往下一個 i
    ellipse(50*i, 50*j, 40)  // 分別於 (50*i, 0) 和 (50*i, 50) 畫圓,畫完之後再往下一個 i 繼續重複
  }
}

在上述的例子中,可以把巢狀迴圈想像成時鐘的分針跟秒針,秒針要跑完一圈,分針才會前進一格。同樣地,外層迴圈i要等內層迴圈j全部跑完才會再+1,所以當j是0和1的時候,分別在(50i, 0) 和 (50i, 50) 的地方畫圓,接著j又會從0開始印,直到外層迴圈結束。(延伸閱讀:[JS] 迴圈筆記

以上了解迴圈的概念後,我們試著用迴圈來創作屬於自己的作品吧。由於Ju編最近去了一個有趣的音樂表演活動Rangeselector,由台灣另類電子搖滾樂團眠腦主演,串連影像、空間動態感測技術所編製的新型態live session,除了隨著節奏跳動的幾何粒子等動態畫面外,觀眾也可以透過移動位置來和展場的視覺產生互動。聽完這場很不一樣的live現場後,覺得或許也可以將自己喜歡的音樂當成主題,來進行Creative Coding的創作。

這次的靈感來源為台灣樂團The Fur. – Friday Love,復古的曲風配上輕快活潑的歌詞,行版的節奏讓人忍不住想跳起舞來,就像一顆又一顆的彩球。藉著這首甜蜜的歌曲,來個類專輯封面的創作吧!

首先我們挑選一組具有復古感的色票,這邊選用的以粉色、紅色系為主,配上米白和橘咖等輔色。既然是專輯封面,那歌名是一大重點,為了凸顯歌名我們試試看在Illustrator製作標準字並輸出,所以在規劃作品草稿時可分為靜態的標準字圖片和動態生成的彩球兩個部分。

復古的配色常以暗濁的暖色調為主,明度和純度都比較低。

如何在OpenProcessing導入圖片呢?我們可以把圖片上傳到第三方平台Imgur,然後把圖片當作背景來使用。

接著來準備下方空白處的彩球,定義一個陣列colors把挑選好的色票放進去,再準備一個circles的空陣列。我們要做一組4*4共16顆的彩球,這邊定義5個變數x, y, d, num, col,分別代表彩球在x軸和y軸的產生位置、彩球的大小、個數和顏色,利用巢狀迴圈的結構產生共16顆:

function setup() {
  createCanvas(600,600);
    let seg = 4;
    let w = width / seg;
    for (let i = 0; i < seg; i++) {
      for (let j = 0; j < seg; j++) {
        let x = i * w + w / 2; //x軸產生位置
        let y = j * w + w / 2; //y軸產生位置
        let d = random(0.5, 1) * w; //彩球大小
        let num = int(random(1, 4)); //彩球個數
        let col = random(colors); //彩球顏色
        for (let k = 0; k < num; k++) {
          circles.push({
          x: x,
          y: y,
          d: d * 0.7,
          c: col
        });
        movers.push(new Mover(x, y, d * 0.5));
      }
    }
  }
}

把準備好的彩球畫出來,注意圖層的順序必須畫在background上方,為了避免跟畫面上的其他元素重疊,位置和大小也要稍微調整一下。

function draw() {
  background(img);
  translate(width / 2, height / 2); // 定位在畫布中間
  scale(0.5); //範圍大小
  translate(-width / 2, -height / 4); // 定位在畫布偏下

  for (let c of circles) {
    fill(c.c); // 用變數c的顏色來填充
    circle(c.x, c.y, c.d); // 畫在變數x,y的位置、變數d的大小
  }
  noStroke();
}

現在我們有一顆又一顆的基本彩球了,看起來是不是很像糖果呢?為了替畫面增加一些動態性,我們在彩球旁邊增加幾顆移動的小彩球,這邊需要分為三種function:定義小彩球參數的constructor、畫小彩球的show、移動小彩球的move。在OpenProcessing的語法庫中,有一些常見的數學常數可以做使用,比如下方用到的PI,可以很快速地引入使用。

let movers = [];

class Mover {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.cs = this.r * 0.4; //第一種大小 
    this.cs0 = this.r * 0.4; //第二種大小
    this.t = random(100);
    this.off = 0;
    this.tStep = random(0.01, 0.05);
    this.ang = random(PI); 
    this.aStep = random(-1, 1) * 0.01;
    this.col1 = random(colors); //第一種顏色
    this.col2 = random(colors); //第二種顏色
    while (this.col1 == this.col2) {
      this.col1 = random(colors);
    }
  }

  show() {
    push();
    translate(this.x, this.y);
    rotate(this.ang); //旋轉的角度
    stroke(255); //加上白色邊框以凸顯小球
    fill(this.col1); //
    if (this.cs0 * 0.15 < this.cs) {
      fill(this.col2); 
      circle(this.off, 0, this.cs);
    } //錯開大球和小球的顏色
    pop();
  }

  move() {
    this.off = map(sin(this.t), -1, 1, -1, 1) * this.r; //彩球正面的角度
    this.cs = map(cos(this.t), -1, 1, this.cs0, 0); //彩球背面的角度
    this.t += this.tStep; //彩球的速度 
    this.ang += this.aStep; //彩球的圓周速率
  }
}

記得在 setup 的地方把 movers 推入 Mover,這樣我們就有移動的小彩球囉!

movers.push(new Mover(x, y, d * 0.5));

這樣就完成啦,希望大家喜歡這次的分享唷,一起試試看如何利用迴圈創作吧!

成品請往這邊走 👉🏻 https://openprocessing.org/sketch/1560267

歡迎加入互動藝術程式創作入門(Creative Coding)線上課程,課程中你可以認識程式與互動藝術產業應用,開啟對工程跟設計的想像,學會使用 p5.js 開發互動介面,整合繪圖、音訊、視訊、文字、3D、互動與機器創作完整的作品,並將創作輸出應用在個人品牌或網站、主視覺或海報,甚至互動裝置、遊戲與教材製作等場景,讓你對進修的資源與路線更有方向。

有興趣的朋友歡迎加入我們的臉書社團,第一時間接收活動報名消息,希望不久的將來,就能看到你跟大家分享你的生成式藝術創作囉!

此篇文章由 Jeudi Kuo 撰寫

這篇文章 圓圓圈圈:利用迴圈呈現重覆的美 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【互動網頁程式教學】活用GUI Object與繼承概念,完成Canvas物件導向的滑鼠拖曳互動 https://creativecoding.in/2022/06/16/gui-object-canvas/ Thu, 16 Jun 2022 02:19:00 +0000 https://creativecoding.in/?p=2879 利用GUI Object的概念,快速畫出多個物件,利用canvas物件導向概念加上事件偵測,讓滑鼠位置與物件互動,達成亮度的提示以及拖曳物件。

這篇文章 【互動網頁程式教學】活用GUI Object與繼承概念,完成Canvas物件導向的滑鼠拖曳互動 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
很多的讀者經常使用像是canvas這樣的函式庫,東西已經包裝成物件,可以輕鬆的拖曳、偵測點擊。這次的老闆週四寫程式,要來教大家在不使用函示庫的情況下,直接使用純繪圖的canvas生成物件,並偵測事件,來用滑鼠點選跟拖曳物件。

Canvas 物件導向與繼承,來做個土炮架構控制元件吧!(上)成品圖
Canvas 物件導向與繼承,來做個土炮架構控制元件吧!(上)成品圖

目標

這篇文章將會讓你學到:

  1. 向量的基礎知識
  2. 把純繪圖的canvas包成物件的架構
  3. 利用物件導向完成元件與滑鼠的基礎互動

架構

此次影片要實作圖形使用者介面(Graphical User Interface,本篇後以GUI稱之)作為小畫家的操作介面。GUI是採用圖形方式顯示的使用者介面,讓使用者可以使用滑鼠或相關設備操縱螢幕上的圖標或菜單選項,跟早期的電腦使用命令列介面相比,讓使用者在視覺上更容易接受。

實作GUI前,可以先了解一下物件設計的架構:

  1. 成品中的每個方塊都是一個GUI object
  2. 而所有的方塊可以形成一個GUI group
  3. 最後再交由GUI scene 畫出整個場景
GUI結構示意圖
GUI結構示意圖
GUI結構範例
GUI結構範例

了解向量

向量加法示意圖
向量加法示意圖

在畫布的操作中,位置是很重要的一環,位置通常由座標(通常是x,y)構成,物件的起始位置與終點位置,可以看作為「座標的變化量」,而「向量」正是可以輕鬆的表示出座標的變化。(小觀念:向量的定義為 方向的變化量

在程式碼中使用「向量」概念

先來做一個向右跑的小方塊作為使用向量的範例。我們把 HTML 的 Preprocessor 設為 Pug 再把 CSS 設為 Sass,在 HTML 中先畫一個 canvas#mycanvas。

使用「座標」畫出白色方塊物件並讓物件向右移動:

// JS setting
var ww,wh
var canvas = document.getElementById("mycanvas")
var ctx = canvas.getContext("2d")
function init(){
  ww = window.innerWidth
  wh = window.innerHeight
  canvas.width = ww
  canvas.height = wh
}
init()
window.addEventListener("resize",init)
 
//使用「座標」定義物件
var obj = {
  p:{
    x: 0,
    y:0
  },
  size:{
    x:100,
    y:100
  },
  v:{
    x:5,
    y:0
  }
}
function draw(){
  ctx.fillStyle="black" //把上一個畫面蓋掉
  ctx.fillRect(0,0,ww,wh) //把上一個畫面蓋掉
  ctx.fillStyle="white"  
  ctx.fillRect(obj.p.x,obj.p.y,obj.size.x,obj.size.y)
}
setInterval(draw,30)
 
function update(){
  obj.p.x += obj.v.x
  obj.p.y += obj.v.y
}
 
setInterval(update,30)

可以從這一段程式碼中,看到定義物件的位置時,需要個別設置x與y的變量,在 update 物件位置時,也需要個別對x與y做處理。

使用「向量」畫出物件及物件移動:

// 定義向量class,並加入向量的加(add)減(sub)乘(mul)運算
class Vec2{
  constructor(x,y){
    this.x=x
    this.y=y
  }
  add(v){
    return new Vec2(this.x+v.x,this.y+v.y)
  }
  mul(s){
    return new Vec2(this.x*s,this.y*s)
  }
  sub(v){
    return this.add(v.mul(-1))
  }
}
 
var obj = {
  p: new Vec2(0,0), // new 一個向量物件
  size:new Vec2(100,100), // new 一個向量物件
  v: new Vec2(5,0) // new 一個向量物件
}
function draw(){
  ctx.fillStyle="black"
  ctx.fillRect(0,0,ww,wh)
  ctx.fillStyle="white"  
  ctx.fillRect(obj.p.x,obj.p.y,obj.size.x,obj.size.y)
}
setInterval(draw,30)
 
function update(){
  obj.p = obj.p.add(obj.v) //使用向量概念中的「相加」
}

setInterval(update,30)

先實作出一個向量類 Vec2,後續使用 new Vec2 就可以使用向量。在後續需要大量生成物件時,可以比座標更容易生成大量物件。

小結:使用向量物件讓程式碼更簡潔,也讓位置設置變得更容易了。

實作

此次實作會用到老師預先製作好了template,內容包含了向量類 Vec2、畫布設置以及一些滑鼠的事件及記錄,可以參考這裏,需要的同學可以fork一份回自己的codePen再往下繼續做呦!

製作物件

一開始需要建立一個GUIObject 類,方便後續快速製造出大量的GUI object。另外再預先定義一個Scene類,並且在裡面增加 addChild(),把所有的GUIObject可以放在children 中,方便畫出。

class  GUIObject{
  constructor(args){
    let def={
      p: new Vec2(0,0),
      size: new Vec2(0,0)
    }
    Object.assign(def,args)
    Object.assign(this,def)
  }
  draw(){
    ctx.fillStyle="white"
    ctx.fillRect(this.p.x,this.p.y,this.size.x,this.size.y)
  }
}
 
class Scene{
  constructor(args){
    let def={
      children: []
    }
    Object.assign(def,args)
    Object.assign(this,def)
  }
  addChild(obj){
    this.children.push(obj)
  }
  draw(){
    this.children.forEach(obj=>{
      obj.draw()
    })
  }
}

接著可以利用剛剛定義好的GUIObject直接畫出一個長方形。

var rect = new GUIObject({ 
  p: new Vec2(30,30),
  size: new Vec2(100,30)
})
 
function draw(){
  ...
  rect.draw()
  ...
}
利用物件導向畫出一個方塊
利用物件導向畫出一個方塊

函式多載

現在要來做一點程式碼的優化。

函式多載是讓一個同名函式帶入的參數可以有不同類型跟不同數量。在函式內做類型判別跟數量判別,可以讓後續在呼叫函式的時候更容易。

現在scene的addChild只能放入一個物件,並且無法判斷進入的物件是不是scene期待的GUIObject。因此要在這邊修改addChild,讓user給什麼都可用,無論是給一個GUIObject、給多個GUIObject或給一個array都可以運行。

addChild(){
  if (arguments.length==1){ // 如果input的arguments只有一個
    if(arguments[0] instanceof GUIObject){ // 如果input的剛剛好是GUIObject
      this.children.push(arguments[0])
    }
    if(arguments[0] instanceof Array){ // 如果input的是array,這邊暫時不檢查裡面是否皆為GUI object
      this.children = this.children.concat(arguments[0])
    }
  }else{
    for(var i=0;i<arguments.length;i++){ // 如果input了很多個object
      this.children.push(arguments[i])
    }
  }
}

addChild得到的input為arguments, 可以看到程式碼中對arguments進行判斷與操作。我們在這邊繪製兩個長方形,讓Scene蒐集GUIObject後一起畫出。(後續會使用這個方式)

var scene = new Scene()
function init(){
  let rect = new GUIObject({
    p: new Vec2(30,30),
    size: new Vec2(100,30)
  })
  let rect2 = new GUIObject({
    p: new Vec2(130,30),
    size: new Vec2(200,200)
  })
  scene.addChild(rect,rect2) // 防呆裝置已啟動
}
function draw(){
  scene.draw()
}
利用修改後的function生成多個方塊
利用修改後的function生成多個方塊

物件與滑鼠互動

完成物件後,我們最後要加入滑鼠與物件的幾種互動。

(1) 滑鼠靠近方塊,改變顏色

這個小動態的目的是要讓user知道滑鼠有沒有放在物件上,先讓物件亮度降低,當物件與滑鼠位置重疊,便讓物件亮度上升:

a. 新增一個 isHovering 參數,如果是true就調整物件亮度

class GUIObject{ 
  constructor(args){
    let def={
      ...,
      isHovering: false // 新增一個 isHovering 參數,預設為false
    }
   ...
  }
  draw(){
    ctx.fillStyle="rgba(255,255,255,0.4)"
    if (this.isHovering){
      ctx.fillStyle="rgba(255,255,255,1)"
    }
    ...
  }
}

b. 要偵測滑鼠位置(在Scene 類加入事件偵測)

class Scene{ 
  constructor(args){
    let def={
      ...,
      el: null // 要知道是哪個document, 在new的時候指定
    }
    ...
    this.init()
  }
  init(){
    // 事件偵測
    this.el.addEventListener("mousemove",(evt)=>{
      let mousePos = new Vec2(evt.x,evt.y) // 拿滑鼠位置
      // 處理滑鼠移動
      this.children.forEach(obj=>{
        obj.handleMouseMove(mousePos) //傳入滑鼠位置
      })
    })
  }
}
 
// 在new的時候指定document
var scene = new Scene({
  el: document.querySelector("canvas")
})

c. 加入 handleMouseMove 來改變 isHovering 參數的值

class GUIObject{
  ...
  handleMouseMove(pos){
    let point1 = this.p,
        point2 = this.p.add(this.size)
    // 判斷滑鼠是否在物件範圍內
    if (pos.x>point1.x && pos.x < point2.x &&
      pos.y>point1.y && pos.y < point2.y){
      this.isHovering = true
    }else{
      this.isHovering = false
    }
  }
}
滑鼠靠近方塊,改變方塊顏色並改變鼠標圖示
滑鼠靠近方塊,改變方塊顏色並改變鼠標圖示

(2) 用滑鼠拖移方塊

當滑鼠位置與物體重疊時,點擊滑鼠右鍵,可以拖移物件:

a. 新增 isDraggable 參數來決定物件是否可以被拖移

class GUIObject{
  constructor(args){
    let def={
      ...
      isDraggable: false
    }
    ...
  }
 
// 在new GUIObject時要記得把 isDraggable設成true
function init(){
  let rect = new GUIObject({
    p: new Vec2(30,30),
    size: new Vec2(100,30),
    isDraggable: true
  })
}

b. 新增 InRange function 來判斷滑鼠位置是否在物件內 (可以重複使用)

class GUIObject{
  ...
  inRange(pos){
    let point1 = this.p,
        point2 = this.p.add(this.size)
    return pos.x>point1.x && pos.x < point2.x &&
           pos.y > point1.y && pos.y < point2.y
  }
  ...
}

c. 新增 dragging 參數來判斷物件是否正在被拖移中

class GUIObject{
  constructor(args){
    let def={
      ...
      isDraggable: false,
      dragging: false
    }
   ...
  }
}

d. 新增 handleMouseDown 及 handleMouseUp 來處理滑鼠按鍵的事件處理

class GUIObject{
  ...
  handleMouseMove(pos){
  if (this.inRange(pos) ){
    this.isHovering = true
  }else{
    this.isHovering = false
  }
  if (this.dragging){
    this.p = this.p.add(pos.sub(this.lastPos))
    this.lastPos = pos
  }
}
 
handleMouseDown(pos){
  if (this.inRange(pos)){
    this.lastPos = pos
    if (this.isDraggable){
      this.dragging = true
    }
  }
}
handleMouseUp(){
  this.lastPos = null
  this.dragging = false
}

e. 事件偵測加入滑鼠按鍵按下與鬆開的偵測

class Scene{
  ...
  init(){
    ...
    // 按下滑鼠按鍵
    this.el.addEventListener("mousedown",(evt)=>{
      let mousePos = new Vec2(evt.x,evt.y)
      this.children.forEach(obj=>{
        obj.handleMouseDown(mousePos)
      })
    })
    // 放開滑鼠按鍵
    this.el.addEventListener("mouseup",(evt)=>{
      let mousePos = new Vec2(evt.x,evt.y)
      this.children.forEach(obj=>{
        obj.handleMouseUp(mousePos)
      })
    })
 ...

f. 改變鼠標,當鼠標指到物件時,鼠標會從箭頭變成👉🏻

class Scene{
  constructor(args){
    let def={
      …
      flag: false
    }
  }
  ...
  update(){
    if(this.flag){
      this.el.style.cursor="pointer"
    }else{
      this.el.style.cursor="initial"
    }
  }
  init(){
    let flag = false
    this.el.addEventListener("mousemove",(evt)=>{
      this.flag = false
      let mousePos = new Vec2(evt.x,evt.y)
      this.children.forEach(obj=>{
        if(obj.handleMouseMove(mousePos)){
          this.flag = true
        }
      })
    })
  }
  ...
}
// 讓scene update起來
function update(){
  time++
  scene.update()
}
滑鼠拖移方塊位置成果圖
滑鼠拖移方塊位置成果圖

完成滑鼠與物件的基礎互動啦!

結語

以上就是老闆利用Canvas做滑鼠控制物件的網頁基礎互動,這次在前面講解概念的部分花了較多時間,來不及完成小畫家,希望對於剛開始接觸 Canvas 的你有一些幫助。

再次快速總結步驟:

  1. 利用GUI Object的概念,快速畫出多個物件
  2. 加上事件偵測,讓滑鼠位置與物件互動,達成亮度的提示
  3. 加上滑鼠按鍵的事件偵測,讓滑鼠拖移物件

看著框框跟隨著滑鼠的移動,真的很有成就感啊!

老闆的工商時間

想了解更多如何寫出漂亮清晰的網頁嗎?老闆在 Hahow 的教學課程 動畫互動網頁程式入門(HTML/CSS/JS) 用平易近人的語言,用簡單的方式帶你作出不簡單的網頁。已經有網頁程式基礎了嗎?進階課程 動畫互動網頁特效入門(JS/CANVAS) 能讓你紮實掌握 JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。

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

墨雨設計banner

這篇文章 【互動網頁程式教學】活用GUI Object與繼承概念,完成Canvas物件導向的滑鼠拖曳互動 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
五月社群聚(下):張文瀚教你用數學作畫 GLSL fragment shader https://creativecoding.in/2022/06/02/creative-coding-meetup-202205-changwenhan-shader/ Thu, 02 Jun 2022 02:21:00 +0000 https://creativecoding.in/?p=2837 第二次的 Creative Coding 社群聚,我們邀請到了聲音藝術家吳秉聖,以及 Team9 技術長、黑洞創造前端工程師及國立清華大學藝術學院的兼任教師張文瀚,下集由張文瀚介紹如何用數學作畫:fragment shader 的入門與應用。

這篇文章 五月社群聚(下):張文瀚教你用數學作畫 GLSL fragment shader 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
第二次的 Creative Coding 社群聚,上集由聲音藝術家、同時也是噪咖藝術的音樂統籌吳秉聖帶來聲音、光、裝置與創意程式的互動關係,本篇下集則是由張文瀚分享 fragment shader 的有趣創作思維。

張文瀚曾是清大藝術學院的兼任教師,教虛擬實境藝術,也是黑洞創造的前端工程師,自己開了一間 Team9 工作室擔任技術長,主要是在做遊戲,像是「文字遊戲」是只用文字做出來的一個解謎遊戲。

上一位講者吳秉聖分享的是如何用視覺化程式語言、軟體結合實體裝置,製作出互動的裝置作品(想閱讀上集的這邊請)。當大家談到 Creative Coding 通常都會直覺想到 p5.js 等等,叫電腦幫你畫出東西,慢慢組成漂亮的畫面,張文瀚介紹的則是 Creative Coding 的另一個面向、另一種 Creative Coding 的工具: GLSL 的 fragment shader,以及他跟 p5.js 相較之下的優劣勢。

如何叫電腦畫出一個圓?

先從圓的定義開始:「由距離特定座標 500px 以內的所有點集合而成的圖形」在這個畫面中的所有像素,若是離圓心 500px 以內的為黑色,不是的則維持白色。 在 p5.js 內甚至不用懂這個,只要跟 p5.js 說:「在畫布中央用黑色畫一個半徑為500的圓!」他就會幫你畫好了。

下一個問題,如何畫出一個圓,而圓內每個像素都是從黑到白的隨機顏色?

p5.js 畫不出來。

這就是 shader 派上用場的時候了!

像素等級的上色就交給 fragment shader (片段著色器),前提是要親自處理這些運算,了解數學、座標及空間的關係。 需要一些陣痛期才能轉換成 shader 的邏輯和思路,一旦上手之後就可以很順地做出細緻的作品。

為什麼會需要用到片段著色器?

如何用shader快速完成混合漸層效果

想像要做出如左圖這般混合漸層的效果,他的上色規則是甚麼?

對每個像素說,「你在越上面,就越綠;你在越右邊,就越紅。」 用程式替像素上色則變成:

for (var y=0; y < height; y++){
  for (var x=0; x < width; x++){
    var r = (x / width)*255;
    var g = (y / height)*255;
    var b = 0;
    pixels[y][x] = [r,g,b];
  }
}

如果一個畫布的長寬為 1920×1080,總共就有 2,073,600 個像素要著色,也就是為了要完成這一個漸層,這個迴圈程式碼總共要跑兩百萬次!如果只是畫一次,就算多花一點時間也沒關係,何況現在的硬體越來越強大,說久也其實不需要多久。

如果現在規則再加上一條「時間越推移,你就越藍」,而且要及時處理還能跑 60fps,1920 寬 x 1080 高 x 60 fps,每秒鐘得做1.2億次的著色指令!

CPU單執行緒,一次處理一件事
CPU單執行緒,一次處理一件事
CPU單執行緒,事情一多就塞車
CPU單執行緒,事情一多就塞車

像這樣的迴圈程式指令就像一個個的生產線,CPU的單執行緒邏輯,一次只能處理一件事情,東西一多,就會向右邊的圖一樣塞車。我們需要GPU圖形處理器的多執行緒邏輯,同時有無數跟管子並行處理。

Fragment Shader 片段著色器

片段著色器的著色工廠利用上千萬工人迅速完成圖案
片段著色器的著色工廠利用上千萬工人迅速完成圖案

片段著色器就像是一個著色工廠,用你制定好的統一規則,決定畫布裡每個像素的顏色,工廠裡的幾百萬個工人,每一個工人負責一個像素,只要工人依據規則(各種參數)對各自負責的像素著色,每人只要畫一筆,一幅畫就瞬間完成了。

優點:由於利用 GPU 並行處理,可以快速做出複雜的像素操作,某些複雜的數學運算可以直接使用硬體運算,跟電流一樣快速。

兩個最大的限制,第一個是盲視,每一個線程(工人)都是完全獨立的,無從得知其他線程的運算結果,更無法干預;第二個是無記憶,每一個線程只會知道此刻的狀況,不會記得上一刻的自己在做甚麼,每一幀都是完全獨立的。

回到原本的畫布問題,我們需要用 fragment shader ,對每個像素說:「你的紅與綠的程度是由你的 uv 所決定,而藍的程度則由時間推移所決定。」

uniform vec2 u_resolution;
uniform float u_time;

void main(){
  vec2 uv = gl_FragCoord.xy/u_resolution.xy;
  gl_FragColor = vec4(uv.x, uv.y, sin(u_time)*0.5*0.5, 1.);
}

你的每一個上色程度都是用 uv 決定,就像是每個像素的座標或是工人的名牌一樣,每個像素都有一個自己的 uv ,就可以指定它改變顏色。

Fragment shader 可以算是比較類似C語言的程式語言,撰寫著色器程式的思考模式要從單個像素的角度出發,而非畫布的整體。

以右邊的例子來說,就是對著每個像素說:

「你在畫布左半邊就是白色,右半邊就是黑色;但你跟滑鼠的距離若是在畫布大小 5% 內就是紅色。」

用 shader 寫出跟隨滑鼠移動的紅點
用 shader 寫出跟隨滑鼠移動的紅點
uniform vec2 u_resolution;
uniform vec2 u_mouse;

void main(){
  vec2 uv = gl_FragCoord.xy/u_resolution.xy;
  vec2 dist = uv - u_mouse.xy/u_resolution.xy;
  vec3 color = mix(
    vec3(step(uv.x, 0.5)),
    vec3(1,0,0),
    step(length(dist), 0.05)
  );
  gl_FragColor = vec4(color, 1.);
}

Shader 不可思議之最:空間折疊

若要在畫布上畫出一百個圓,最直覺的想法是用迴圈,但要畫的圓越多,指令要執行的次數越多,有沒有其他不用迴圈卻能達成同樣效果的方法?

利用簡單數學運算完成2x2的四個圓
利用簡單數學運算完成2×2的四個圓
利用簡單數學運算完成2x2的四個圓
利用簡單數學運算完成2×2的四個圓

在畫布中心畫一個圓形(上方左圖),並將水平與垂直線分別均分十等份,做 0.0 到 1.0 標號,接下來只需要一個數學運算 uv = fract(uv * 2.0 ); 取餘數的小數點(捨去整是數1),瞬間就能變成四個圓形(上方右圖),每一個方塊其實都是原本完整的 0.0 到 1.0 , 1.0 改成 0.0、1.2 變成 0.2,以此類推,x 和 y 都是相同邏輯。

我們只用了一個數學運算式便把一個圓形變成 2×2 四個,舉一反三,不費吹灰之力就可以變成 5×5、10×10、100×100 的畫面,無論在這個圖上渲染多少個圓,都不會消耗額外資源,僅僅改變數學運算裡的一個參數!效能的高低只要是看指令執行的多寡,像這個例子只有一行運算,完全不會有效能上的差異,連在手機上也可以跑,很多 shader 吃效能是因為畫面的細緻要求和大量的運算。

理論上,你可以擁有無線長寬、無限縮放的畫布,極限僅在於你的像素多寡而已。這個感覺像是你先看一個圓, zoom out 將攝影機往外拉,看到一百個,再往外拉,看到上萬個,用極少量的運算做到複雜的視覺效果。

但,如果只能在每個空間複製一模一樣的圖案,那就太單調了。讓每個小空間內都出現不同的變化需要另一個數學的運算:取 floor 把小數點去掉,進而得到他的 ID 身分證明,再依據該圓圈 ID 的 x 值做出圓形半徑改變。

live coding 一下:

首先在畫布中寫出一個圓,並製作出方才提到的空間折疊效果,然後再把圓的半徑拉出來指定,這個半徑會隨著 ID 的 x 值的不同而改變。如果把 uv 縮放得更誇張就可以看出差異,縮放 30 倍(下圖左)和 100 倍(下圖中)的效果已經明顯不同,可以利用這個方式製作許多不同的效果。若圓圈的大小還受 y 軸的影響,相乘會出現更不一樣的 pattern ;加上時間的參數,就變成了動態。

依據x值改變圓半徑,再縮放30倍
依據x值改變圓半徑,再縮放30倍
依據x值改變圓半徑,再縮放100倍
依據x值改變圓半徑,再縮放100倍
依據x值改變圓半徑,再加上y值參數
依據x值改變圓半徑,再加上y值參數

Shader 除了從全白的畫布開始畫之外,也可以當作濾鏡使用,一個範例是這個網站http://filters.pixijs.download/dev/demo/index.html,可以靠右側的控制器改變顏色、曝光、模糊、風格化等,這是 shader 實務上最常用到的應用方式。

Pixi JS 網站範例截圖
Pixi JS 網站範例截圖

如果想用 shader 創作,可以從哪個裡找到學習資源呢?

第一個是 Shadertoy 老牌的創作平台,使用純粹的 fragment shader 而無法結合其他語法,所有神人都在這裡,你在這裡會看到很多匪夷所思到無法想像是怎麼做出來的作品,尤其又以 3D 為大宗,主要也可能是因為 Ray Marching 這個技術特別流行,大家嘗試了很多在 2D 畫布中渲染 3D 物體。

另一個創作平台是大家更熟悉的 OpenProcessing,可以用 p5.js 結合 shader 作創作,也可以使用 vertex shader + fragment shader,可以自由上傳或讀取自己的素材,達到更多不同的效果,但要小心沒有 GLSL 的語法上色,因 WebGL 版本較舊,不支援部分語法,手機支援度也較差,有視覺顯示或是無法運算等問題。 因為可以結合 p5.js ,張文瀚近期也開始改在這個平台上創作和發表作品,也做了他的 shader template 可以直接 fork 一版來做你的創作。

張文瀚<Out of bounds>作品截圖
張文瀚<Out of bounds>作品截圖,https://openprocessing.org/sketch/1540595

有參加第一次社群聚的人看到這件作品應該不陌生,這件是張文瀚上次的共創主題「宇宙」的作品。後來張文瀚也製作了一篇如何從零開始做出這件作品的教學,歡迎大家參考! 看似複雜但程式碼其實很少,只有 37 行,即使是初學者也可以透過教學文章慢慢踏入 shader 領域。對張文瀚來說,撰寫 shader 的成就感最主要來自於「僅用少少的 code 跟簡單的數學運算就可以達到絢麗的視覺效果」。

Shader 學習資源

台灣目前沒有太多中文的 shader 學習資源,不過 The Book of Shaders 即是針對入門者打造的教學網站,有翻譯成簡體中文的版本,且可以即時做範例,是最推薦的學習管道。

The Art of Code:從初學實作到進階理論都有的 Youtube 頻道,也主要針對 shadertoy 上作品的實作,全英文

Inigo Quilez 頻道:Shader 界宗師,以前貌似在皮克斯工作,動畫裡用到大量的 shader, Youtube 頻道裡多數為進階理論和 live coding,全英文

Inigo Quilez 網站:更深奧的理論與實作文章,全英文

 Inigo Quilez 在 Shadertoy 上的頻道截圖
Inigo Quilez 在 Shadertoy 上的頻道截圖

讓大家看一下 Inigo Quilez 在 Shadertoy 上的頻道,乍看真的無法相信這些細緻五官的人、風景、蝸牛等等都是用一行行的程式碼製作而成的,對於也還算初學的張文瀚在研究程式碼的時候,真的看不懂任何一行 code ,但這些作品真真確確是透過這些加減和數學運算組合而成的。

張文瀚個人作品分享

互動動態網站<體感溫差>
互動動態網站<體感溫差>截圖

第一件是替藝術家展覽製作的互動動態網站<體感溫差>,核心概念是使用 shader 當作濾鏡製作出熱像儀的效果以及熱像軌跡,左上角顯示滑過軌跡上的顏色所對應的溫度。

字畫產生器成果截圖
字畫產生器成果截圖

第二件則是文字遊戲延伸出來的字畫產生器,用純文字組成畫面,也另外開發一個小工具,方便大家做宣傳圖和素材。這個網站可以非常即時地將影片、影像轉換成文字,顯示的內文、字型、模式等都可以自由調整。

其他的作品歡迎來張文瀚的 Shadertoy 頁面 以及 Open Processing 頁面 觀看玩耍。

張文瀚的個人網頁>>https://changwenhan.com/

五月社群聚共創主題:<山>

張文瀚<And As The Sun Goes Down>

是一個靜態 shader 作品,使用到先前提到的 Ray Marching 的技術,這個山坡是用數學式所描述出來的 3D 物體,讀一張影像素材,用素材上的像素決定他的突起程度,去掉這一個步驟的話就是很一般、很光滑的 mapping,由素材本身的複雜度去決定成品最終的樣貌。 因為本身並不擅長顏色搭配,通常一件作品就保守用兩到三個顏色決定色調。

Jennifer

Jennifer共創作品

自由接案 js 工程師,「山」這個主題想到Open Processing上水墨畫的山,齊柏林在淡水的看見台灣展覽,有很多台灣的照片,所以決定做台灣的地形,月世界。

從沒有真正創作過,一開始想到是用遞迴樹(recursive tree)的方法,從稜線開始畫,但線條會相互重疊,變得很不自然,不是山稜線該有的樣子,所以認為需要改用面而非線去畫;接著在搜尋其他更好的製作方法時,找到主打資料處理、數據視覺化的 Observable 平台,參考了其中網友發表模擬珊瑚碎形的作法(網頁連結),回頭調整作品。

哲宇<Mountain & water>

吳哲宇 <Mountain & Water>
吳哲宇 <Mountain & Water> https://openprocessing.org/sketch/1519323

使用 noise 做不同層次的疊加,有一個 x 從左到右,在 y 上疊很多不同的sin 及 cos;一個大的perlin noise再乘上一個波,增加疏密的變化;另外一個比較有趣的手法,山脈往下長的這個效果是使用 mainGraphics.image(mainGraphics,0,2) 把祝張圖往下畫,上面再多疊一條又一條的線,整體畫面就呈現一直往下長的動態效果。

2022年5月也是第二次的 Creative Coding 社群聚就在這邊到一個段落了,如果還沒有看過上集由聲音藝術家吳秉聖帶來聲音、光、裝置與創意程式的人趕快點擊閱讀,這次因為疫情飆升、台北又下大雨而沒有太多的人來現場交流實在可惜,希望下一次有機會能夠在現場見到喜愛用 Creative Coding 創作的你們!

還不知道如何開始踏入 Creative Coding 嗎?那 Hahow 課程 <互動藝術創作程式入門>再適合你不過了,快加入2000位同學的行列,一起學習互動藝術創作吧!

墨雨設計banner

整理編輯:Chia 編

這篇文章 五月社群聚(下):張文瀚教你用數學作畫 GLSL fragment shader 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【Vue.js入門】一小時學會 Vue.component,完成動態飯店房間清單 https://creativecoding.in/2022/05/17/vue-js-hotel-room-list/ Tue, 17 May 2022 02:51:00 +0000 https://creativecoding.in/?p=2593 我們會從 Vue.js 基本概念開始講起,非常適合剛開始接觸 Vue 的朋友們,接著我們用 Vue component 實作一個動態的飯店編輯頁面,你會學到:Vue.js 基本概念、Vue.js 語法指令及深入操作 Vue.component。

這篇文章 【Vue.js入門】一小時學會 Vue.component,完成動態飯店房間清單 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
假如今天我有很多不同的商品要賣,網站上有很多商品的細節需要編輯、計算,一個一個改實在太麻煩了,如果一個輸入錯誤,可能還會造成虧本,這樣可不行啊!因此我們今天練習用 Vue.js 做一個「牽一髮動全身」的動態編輯頁面,只要改一個地方,用到這筆資料的所有地方都會自動變更,想想真是太棒了!

這篇文章裡,我們會從 Vue.js 基本概念開始講起,非常適合剛開始接觸 Vue 的朋友們,接著我們用 Vue component 實作一個動態的飯店編輯頁面,跟著這篇文章,你會學到:

  1. Vue.js 基本概念:了解 Vue.js 是什麼、為什麼工程師要使用 Vue.js 操作資料。
  2. Vue.js 語法指令:學習如何使用 Vue.js 帶入資料、怎麼使用 v-bind、v-model 等 Vue 語法。
  3. 深入操作 Vue.component:用 Vue 元件實作一個能夠動態改變資料的編輯頁面,將 Vue 的基本語法與元件合併使用,透過元件的實際操作理解元件間的溝通傳遞。

我們會用飯店房間當作範例資料來練習,並在 Codepen 上實作,Codepen 是一個讓我們在編輯程式碼的同時能夠馬上看到結果的線上程式碼編輯器,註冊完就能夠使用了,那就讓我們開始吧!

如果想跟著影片一起動手做的話,請到這邊

Vue.js 基本概念

什麼是 Vue.js?Vue.js 其實是一套 JavaScript 的程式庫,負責把資料轉為網頁呈現。過去假如我們要寫一個飯店的網頁,我們要在 HTML 中一行行寫出飯店的房間標題、房型描述等等,假如要修改資料,就要在茫茫的 HTML 海中一筆筆修改;但是利用 Vue.js,我們不再需要一個個把資料寫死,而是可以先寫好一個模板,然後將裡面的資料用變數的方式代入。

例如,今天我們有一份自我介紹的模板:

「哈囉,我的名字是___,來自風非常大的地區___,喜歡___、___。」

我可以依據自己的個人資料來填空,這些填入的資料就是「變數」。Vue.js 中,變數可以不是單一的值,例如這裡的興趣以陣列(Array)的形式儲存,因此我們可以用索引(index)的方式來取出陣列裡的所有資料。

填空後的自我介紹就變成了:

「哈囉,我的名字是吳哲宇,來自風非常大的地區新竹,喜歡聽音樂、畫圖。」

但 Vue.js 的功能不止於此,它還有許多方便的功能。

v-for:迴圈,取出清單裡面所有資料

假設在「興趣」項目裡有一百筆資料,我們可以利用 v-for 抓取這一百筆資料,用一行 v-for=” hobby in hobbies” 自動重複標籤一百遍,列出這一百項資料。

  <ul>
    <li v-for="hobby in hobbies">
      {{ hobby }}
    </li>
  </ul>

// Vue.js 資料
  { 
    hobbies: ["聽音樂","畫圖"] 
  }

computed:前處理,先行運算

例如一個商品的價格是「原始價格x折扣數」,在 Vue.js 中,我可以不用每一次都自己運算,而是利用 computed 功能,創造「final-price 」這個變數,定義好最終價格等於原始價格乘以折扣數,這樣我們就可以直接使用 final-price 這個變數。

<h5> 折扣後的價格為 {{ final_price }} 元 </h5> 

// Vue.js 資料
  computed: {
    final_price: function() {
      return price * discount
    }
  }

v-bind:屬性綁定

在沒有使用 Vue.js 的時候,我們會使用 CSS 去改變網頁物件的顏色、框線或內容等,而 Vue 讓我們可以直接根據資料自動產生 CSS 帶入網頁。利用 v-bind:style 後面給予一個物件,物件裡則是一般 CSS 的寫法,Vue 就會幫我們自動產生 CSS 套在元件上,讓我們在資料裡就能夠定義或抽換元件的 CSS 樣式。

<div class="cover" v-bind:style="color_css"> 

// Vue.js 資料
  {
    color_css: {
      "background-image": "網址"
    }
  }

看到這裡,有沒有感覺得 Vue.js 好像一個個搬運工呢?是的,Vue.js 用起來就像是挖空格,我們規定好模板跟資料後,讓 Vue 物件依照要求把資料填空進去。所以使用 Vue.js 不可少的三元素就出現了:

  1. 模板:資料呈現的規則。
  2. 資料:要被填入的內容。
  3. Vue 物件:我們必須要新增一個 Vue 物件 ,讓它幫我們把資料呈現出來,也就是幫我們把資料搬到模板裡面的搬運工。

Vue 基本概念實作練習

看到這邊,是不是了解了 Vue 的基本概念了呢?我們用 Codepen 做一個小小的範例,讓我們更熟悉 Vue.js 吧。

首先,在這份範例中,我們會用到的是一份房間資料格式。這份資料的最外層用大括號(curly bracket)包起來,顯示它是一個物件,裡面記錄了房間的名稱、價格、設備等資料。

{
    "name": "經濟雙人房",
    "eng": "Economy Double Room",
    "price": 7000,
    "amount": 0,
    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
    "discount": 0.9,
    "equipment": {
      "wifi": false,
      "bathtub": true,
      "breakfast": true
    }
},

接下來我們在 Codepen 裡開啟一個 new pen,同時在設定裡將 HTML 選擇為 Pug,CSS 設定為 Sass,在 JS 的部分用 CDN 載入 Vue.js,存檔後我們的基礎設定就完成了。

Codepen裡的設定
Codepen裡的設定

我們先將剛才的房間資料存進 Javascript 裡,並且宣告資料名稱為 roomdata:

// JavaScript
  var roomdata = {
    "name": "經濟雙人房",
    "eng": "Economy Double Room",
    "price": 7000,
    "amount": 0,
    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
    "discount": 0.9,
    "equipment": {
      "wifi": false,
      "bathtub": true,
      "breakfast": true
    }
  };

接著在 HTML 裡寫入要呈現的內容

h1 房間的資料
  h2 名稱 {{ roomdata.name }}
  h2 價錢 {{ roomdata.price }}

現在,我們要在 JS 裡宣告宣告 Vue 實例,並且在 HTML 裡利用 #app 指定作用區域,這個作用區域表示我們要讓 Vue 在特定的區域檢查是否有要求代換的資料,如果我們把 #app 用原始 HTML 語法來看,代表的是:

<div id="app">

同時我們在 JS 裡也將宣告的 Vue 裡寫進入定義的 el 屬性,el (element) 代表的是作用的元件區域,值寫入 “#app”。

new Vue({
  el: "#app",
  data: {
    roomdata: {
      name: "經濟雙人房",
      eng: "Economy Double Room",
      price: 7000,
      amount: 0,
      cover: "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
      discount: 0.9,
      equipment: {
        wifi: false,
        bathtub: true,
        breakfast: true
      }
    }
  }
});

這樣,Vue 就能夠用變數自動代換指定的資料,接下來,我們練習讓 Vue 做計算。

computed:前處理、先行運算

將 HTML 中的最後的價錢指向變數 {{ final_price }} ,但我們的資料裡並沒有一筆資料名稱叫做「final_price」呀?沒關係,我們接著要自行定義它。

在 Vue 實例中,新增一個 computed (計算屬性),computed 的值必須是一個物件,裡面可以定義不同的運算式。我們現在需要的是 final_price,final_price 的值必須是一個函式 (function),在函式裡要求回傳「價格x折扣」。到這邊,{{ final_price }} 已經能夠幫我們回傳每間房間的價格了!

定義final_price算法
定義final_price算法

v-model:資料雙向綁定

下一個我們要練習的功能是 v-model,v-model 能夠幫我們做雙向的綁定,也就是當我們指定好 v-model 的兩端時,只要更動其中一處,另一端也會同步做更動。

觀看範例會更容易明白,我們在剛才寫好的 HTML 中增加兩行程式碼:

label 價錢
input(v-model="roomdata.price")

將 input 與資料中的 price 用 v-model 做綁定,對應的是 roomdata.price,這時畫面上便產生了 input 輸入框,框內就是 roomdata.price 的值 7000。這時如果我們改變了輸入值,資料就會同時做改變;反之如果改變了資料,輸入也會同步變化。

無論數值怎麼變,都可以套用折扣算式得到最終價錢
無論數值怎麼變,都可以套用折扣算式得到最終價錢

v-for 迴圈

接下來,我們用 v-for 取出清單內的所有資料,我們在 data 裡增加 rooms,值則是一個陣列,陣列裡包括 room1、room2、room3。

data: {
  rooms: [
    { name: "room1" }, 
    { name: "room2" }, 
    { name: "room3" }],
  ...
}

接著,我們在 HTML 裡用 v-for 把資料取出來。

h1 房間列表
  ul
    li(v-for="room in rooms") {{ room.name }}

在標籤 li 後面加上(v-for=”room in rooms”),這裡的 rooms 是 JavaScript 裡 data 中的 rooms,而 rooms 裡面的資料我們用 room 來命名(當然也可以換成其他名字),而我們指定好要以迴圈取出的資料範圍後,再以 {{ room.name }} 指定我們要取的是每一筆 room 的 name。

開始建置多間房間的列表
開始建置多間房間的列表

到這邊,我們練習了怎麼使用 Vue 物件呈現資料、也用了 computed、v-bind、v-for 做前運算、雙向綁定以及迴圈。附上完整的練習程式碼給大家參考。

// HTML
#app
  label 名稱
  input(v-model="roomdata.name")
  label 價錢
  input(v-model="roomdata.price")
  h1 房間的資料
    h2 名稱 {{ roomdata.name }}
    h2 價錢 {{ roomdata.price }}
    h2 最後的價錢 {{ final_price }}

  h1 房間列表
    ul
      li(v-for="room in rooms") {{ room.name }}

// JavaScript
new Vue({
  el: "#app",
  data: {
    rooms: [{ name: "room1" }, { name: "room2" }, { name: "room3" }],
    roomdata: {
      name: "經濟雙人房",
      eng: "Economy Double Room",
      price: 3000,
      amount: 0,
      cover: "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
      discount: 0.9,
      equipment: {
        wifi: false,
        bathtub: true,
        breakfast: true
      }
    }
  },
  computed: {
    final_price: function () {
      return this.roomdata.price * this.roomdata.discount;
    }
  }
});

接下來我們就要進入這次的主題: component 元件。

為什麼要使用 Vue 元件呢?因為我們在架設網站的時候,很有可能會面對一份非常龐大的資料!資料庫中可能有上百筆資料、有不同的邏輯、許多複雜的呈現規則等等,因此工程師們想出的解決辦法就是把龐大的資料拆成不同的 Vue 物件,每一個物件除了負責自己的資料外,也能夠繼承資料以及繼承方法。

「繼承資料」指的是能夠接收源數據的資料,例如飯店裡每一間房間都要打九折,我們可以在每間房間裡指定繼承源數據的折扣數 0.9,這樣就不用在每一間的資料中寫上一行「discount = 0.9」。

而「繼承方法」則好比開放權限,一個 Vue 物件除了能夠有自己指定的方法外,也能夠接收源數據規定好的方法,甚至能夠用接收到的方法回頭套用到源數據上。

接下來,我們就來一步步實作吧。

Vue Component 實作練習

我們開啟一個新的 Pen,並且一樣在設定裡將 HTML 選擇 Pug,CSS 設定為 Sass,同時引入 Bootstrap 的 CDN 方便我們之後做排版,在 JS 的部份載入 Vue.js,這樣基礎設定就完成了。接著將我們需要用到的資料複製到 JavaScript 裡,並且宣告資料的名稱為 rooms。

// JavaScript
	var rooms = [
	  {
	    "name": "經濟雙人房",
	    "eng": "Economy Double Room",
	    "price": 7000,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(1).jpg",
	    "discount": 0.9,
	    "equipment": {
	      "wifi": false,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "海景三人房",
	    "eng": "Sea view triple Room",
	    "price": 7800,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(2).jpg",
	    "discount": 0.8,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": false
	    }
	  },
	  {
	    "name": "典雅景觀房",
	    "eng": "Elegant landscape Room",
	    "price": 5400,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(3).jpg",
	    "discount": 0.85,
	    "equipment": {
	      "wifi": false,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "尊享豪華房",
	    "eng": "Exclusive Deluxe Room",
	    "price": 9800,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room%20(4).jpg",
	    "discount": 0.8,
	    "equipment": {
	      "wifi": true,
	      "bathtub": false,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "商務雙人房",
	    "eng": "Business Double Room",
	    "price": 5600,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (5).jpg",
	    "discount": 0.9,
	    "equipment": {
	      "wifi": true,
	      "bathtub": false,
	      "breakfast": false
	    }
	  },
	  {
	    "name": "溫泉雙人房",
	    "eng": "Hot spring double Room",
	    "price": 8400,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (6).jpg",
	    "discount": 0.6,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "總統套房",
	    "eng": "Presidential Suite",
	    "price": 23000,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (7).jpg",
	    "discount": 0.75,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": true
	    }
	  },
	  {
	    "name": "奢華四人房",
	    "eng": "Luxury four Room",
	    "price": 8500,
	    "amount": 0,
	    "cover": "http://bosscode.monoame.com/20170323_vue_comp/img/room (8).jpg",
	    "discount": 0.7,
	    "equipment": {
	      "wifi": true,
	      "bathtub": true,
	      "breakfast": false
	    }
	  }
	];

先看一下我們的目標設計稿和需求:

  1. 瀏覽區塊中:每個房型會以卡片的樣式呈現,內容包括房間的圖片、名稱、價格和設備等資料。
  2. 編輯區塊中:可以編輯飯店折扣數、服務費、各個房間的資料細節。
  3. 在編輯區塊中修改的內容會即時跟瀏覽區塊的資料做同步,只要調整一次折扣數或服務費,所有房間的資訊都能夠繼承變動後的數值自動做計算,也能夠針對單一項目做調整。
最終完成品
最終完成品

房型一覽區塊基本結構:利用 v-for 迴圈重複架構

我們先在 HTML 中將畫面右側的房型一覽基本架構寫出來,並利用 Bootstrap 做排版。

#app
  .container
    .row
      .col-sm-9
        .row
          .col-sm-4(v-for="myroom in rooms")
            h3 {{ room.name }}
            .info
              h5 {{ room.eng }}
使用boostrap排版
使用boostrap排版

定義 Vue.component 並繼承資料

接著,我們可以先把房型切成元件(component)。我們先在 HTML 中新增一個 template,並且給它一個 id 叫做 room。template 裡面就是我們的呈現規則,要注意的是 template 裡面需要一個總體物件,這是因為 template 裡面根源只能有一個物件,不能是複數的物件,因此我們用一個大的 div 標籤來包裹裡面的資料,這個 div 我們也給它一個 class=”room_col”。

#app
  .container
    .row
      .col-sm-9
        .row
          .col-sm-4(v-for="myroom in rooms")


template#room
  .room_col
    h3 {{ room.name }}
    .info
      h5 {{ room.eng }}

接下來,我們就要在 JS 裡新增一個 Vue 元件。新增的方式是 Vue.component()。括弧裡第一個變數是元件的名稱,我們定義它為 “room”,接著第二個變數我們給它一個物件 { },代表所有要初始化的設定。

第一個我們設定 template: “#app”,告訴元件呈現的規則是 HTML 裡的 template#room。接著我們設定 props (繼承),因為我們元件的資料是從源數據繼承的,我們將繼承的資料命名為 props: [“room_data”]。

Vue.component("room", {
  template: "#room",
  props: ["room_data"]
});

在 HTML 中呼叫繼承的資料

Vue 元件設定好了,我們要回到 HTML 裡使用剛剛設定的資料。在 .col-sm-4(v-for=”myroom in rooms”) 底下我們給予一個 room 標籤,然後使用 v-bind 跟 room_data 做綁定 (v-bind:room_data=”myroom”),同時因為我們在 Vue compoenent 中 props 定義的名稱是 “room_data“,因此在 HTML 裡 template 中我們要呼叫的就是 room_data 裡的 name 跟 eng。

#app
  .container
    .row
      .col-sm-9
        .row
          // 為了能夠區分對應的資料,這邊把原來的 room 更名成 myroom
          .col-sm-4(v-for="myroom in rooms") 
             room(v-bind:room_data="myroom") 

template#room
  .room_col
    h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
定義 Vue.component ,繼承並在HTML裡呼叫資料

接著,我們要來計算房間的價格。假設每一間房間有自己的定價和折扣數,另外整間飯店也有一個總體折扣數,我們的房價計算方式應該是:房價=定價x房間折扣x飯店折扣。為了讓大家能看清楚過程,我們先把每一個呼叫拆開寫出來。

飯店折扣我們在 vm 實例中先定義為 0.9。

var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    discount: 0.9    // 新增飯店折扣數
  }
});

定價x房間折扣我們已經知道怎麼寫了,也就是 {{ room_data.price }}*{{ room_data.discount }}。接著,我們要在元件的繼承屬性中增加折扣 props: [“room_data”, “hotel_discount”]。

Vue.component("room", {
  template: "#room",
  props: ["room_data", "hotel_discount"]  // 增加繼承 hotel_discount
});

接著,我們在 HTML 中,先前已經綁定 v-bind:room_data=”myroom” 的 room 標籤後面,綁定 v-bind:hotel_discount=”discount”。

#app
  .container
    .row
      .col-sm-9
        .row
          .col-sm-4(v-for="myroom in rooms")
            // v-bind:room_data 可以簡寫為 :room_data
            room(:room_data="myroom", :hotel_discount="discount")

最後,我們需要在 template 中呼叫 hotel_discount。

template#room
  .room_col
    h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
     h5 {{ room_data.price }}*{{ room_data.discount }}*{{ hotel_discount }} 

到這邊,房價的計算過程 定價x房間折扣x飯店折扣 已經可以正確地顯示出來了。

顯示房價的計算過程
顯示房價的計算過程

定義 computed 屬性做前運算

不過我們不能讓客人自己按計算機計算價錢,所以我們要讓 Vue 幫我們做運算。最單純的做法是我們把需要計算的過程寫在一個大括號裡:{{ room_data.price * room_data.discount * hotel_discount }},不過這樣顯然是一個很冗長的程式碼,因此我們定義一個 final_price 變數,再用 computed 幫我們做前運算。

這邊有一個小地方需要注意,如果運算後的結果是浮點數的話,Vue 並不會自動幫我們轉為整數,因此我們可以用 parseInt() 將計算結果先轉為整數後,再回傳。

// HTML
template#room
  .room_col
    h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
      h5 {{ final_price }}
	
// JavaScript
Vue.component("room", {
  template: "#room",
  props: ["room_data", "hotel_discount"],
  computed: {
    final_price: function () {
      return parseInt(
        this.room_data.price * this.room_data.discount * this.hotel_discount
      );
    }
  }
});

這邊大家容易混淆的是 this 到底指向哪裡?好消息是,在 Vue 元件中,this 永遠會指向元件本身。而我們在 computed 中定義的 final_price 也可以在元件中使用 this 呼叫來做其他的運算。

因為畫面設計稿上我們希望呈現出房間原價、總折數、以及折扣後的房價,因此我們可以在 computed 中定義我們需要的運算式,然後在 HTML 裡呼叫變數,同時我們增加 .cover 方便未來增加房間圖片。

/ HTML
template#room
  .room_col
    .cover
      h3 {{ room_data.name }}
    .info
      h5 {{ room_data.eng }}
      h5 {{ room_data.discount }}*{{ hotel_discount }} = {{ final_discount_show }}折
      h4 TWD
         {{ room_data.price }}
        .final_price {{ final_price }}


// JavaScript
Vue.component("room", {
  template: "#room",
  props: ["room_data", "hotel_discount"],
  computed: {
    final_discount: function () {
      return this.room_data.discount * this.hotel_discount;
    },
    final_discount_show: function () {
      return parseInt(this.final_discount * 100);
    },
    final_price: function () {
      return parseInt(this.room_data.price * this.final_discount);
    }
  }
});
呈現出房間原價、總折數、以及折扣後的房價
呈現出房間原價、總折數、以及折扣後的房價

增添 CSS 美化版面

我們可以調整 CSS 的樣式,讓畫面變得更美觀。除了調整元素的邊距、顏色、字體大小外,我們用了 position: relative / position: absolute(相對位置/絕對位置)以及浮動元素 float 做位置的排版,並且用偽類 pseudo class 增加價格最後的 $ 字號。

// CSS
$color_red: #DB4343 

*
  border: 1px solid #666    // 排版的時候為了方便觀看,我們增加邊框的樣式

.room_col 
  padding: 20px
  .cover
    height: 150px
    background-color: #eee
    position: relative
    h3
      position: absolute
      bottom: 10px
      font-size: 20px
      padding: 5px 15px
      background-color: #fff
  .info
    padding: 10px
    h5
      font-size: 12px
    .final_price
      float: right
      color: $color_red
      &:after
        content: "$"
調整CSS 美化版面
調整CSS 美化版面

computed 也能夠用來做 CSS 屬性運算

接下來,我們來做最好玩的部分,利用 v-bind 把房間圖片放進去。background-image 因為是一個 CSS 屬性,所以我們要讓 Vue 元件幫我們計算這個 CSS,我們在 computed 裡加上 bg_css 並且給它一個 function,在 function 裡計算完後再將結果回傳。CSS 中的背景圖片的語法是 background-image: “url(‘一段網址’)”,因此 function 裡也是如此,同時網址的部分呼叫繼承的資料 this.room_data.cover,最後別忘了要將結果回傳,回傳資料需要用大括弧 { } 包起來。

// JavaScript
  computed: {
    // ...
    bg_css: function () {       // 增加 background-image CSS 屬性
      return {
        "background-image": "url('" + this.room_data.cover + "')"
      };
    }
  }

接著,在 HTML 裡我們要利用 v-bind 操作 CSS,在 .cover 後指定 v-bind:style 並且指定 bg_css 物件。這時畫面上已經能載入圖片了。我們再使用 CSS 調整圖片的位置讓圖片,加上 background-size: cover 讓圖片縮放到封面大小、加上 background-position: center center 設定圖片初始位置為水平置中和垂直置中。

到這邊,輔助用的外框線我們就讓它功成身退吧。

// HTML
  .cover(v-bind:style="bg_css")

// CSS
  .cover
    height: 150px
    position: relative
    background-size: cover
    background-position: center center
載入圖片,位置都沒問題後將框線移除
載入圖片,位置都沒問題後將框線移除

最後我們要來做一點畫面上細緻度的調整。

首先,我們在將每一個房型的資料最外層的 div 標籤加上 class=”col-room”,並且給予內距 padding: 20px。避免混淆,將原來 template 最外層的 class 改成 “room-container”,加上陰影 box-shadow: 0px 0px 10px rgba(0,0,0,0.3)。這時,畫面上的房型一覽是不是很像用一張張卡片呈現,一目了然呢?

每張房型卡片都加上外框陰影
每張房型卡片都加上外框陰影

增加飯店資訊編輯區塊

到這裡為止,我們已經將設計稿上右側呈現的部分做好了,接著我們要來做左邊的編輯區塊。再看一眼我們的設計稿吧。

最終成品參考圖
最終成品參考圖

房型一覽的部分剛才我們已經用了 CSS grid 指定了 col-sm-9,因此,編輯區塊自然就是 col-sm-3 了(想了解更多格線佈局可參考:MDN)。我們為兩邊的畫面分別加上標題及分隔線,並且為畫面整體加上一點內距,讓視覺看起來比較美觀。

// HTML
#app
  .container
    .row
      .col-sm-3
        h1 飯店資料
        hr
      .col-sm-9
        h1 房間列表
        hr
        .row

// CSS
body
  padding: 20px
h1
  font-size: 30px

增加房間總折數編輯欄位

編輯區域第一個顯示的是飯店的總折數,我們加上 label 總折數,並且將 input 用 v-model 與 discount 做雙向綁定。

// HTML
#app
  .container
    .row
      .col-sm-3
        h1 飯店資料
        hr
        label 總折數
        // .form-control 是 Bootstrap 的標籤,我們用來美化視覺
        input.form-control(v-model="discount")   

// JavaScript
var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    discount: 0.9
  }
});

到這邊,厲害的事情發生了!我們在 input 中改變折扣數的話,可以看見房型一覽中的價格也會立即變化,這就是 Vue.js 厲害的地方!資料綁定的好處就在於一旦一方改變了,所有相依的資料也會同步改變。

編輯左側欄位,加上總折扣數
編輯左側欄位,加上總折扣數

增加服務費編輯欄位

接著我們在每一筆房間訂單加上服務費。我們先將要繼承的資料都定義好,這裡可以拆成三個步驟:

  1. 增加需要繼承的資料:我們先在 vm data 中加上 service_fee: 200。
  2. 在 HTML 中告訴元件 room 要繼承 service_fee,並且將元件中的服務費重新命名為 hotel_fee。
  3. 在元件 Vue.component 中的 prop 屬性中指定繼承 hotel_fee。

這樣我們就寫完了繼承的資料,接著我們要在計算中加上服務費,我們用 this.hotel_fee 呼叫。需要注意的是,透過繼承而來的資料,子元件會收到的是「純文字字串」,而不是外層元件的狀態內容,因此我們利用 JavaScript 強制轉型的特性,將 this.hotel_fee 乘上 1.0 轉為數字型別。

// HTML 
label 服務費
// 服務費用 v-model 來綁定
input.form-control(v-model="service_fee")
...
.col-sm-4.col-room(v-for="myroom in rooms")
  room(
    :room_data="myroom",
    :hotel_discount="discount",
    // 告訴元件 room 要繼承 service_fee,並且將元件中的服務費重新命名為 hotel_fee
    :hotel_fee="service_fee" 
  )

// JavaScript
// Vm 實例
var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    discount: 0.9,
    service_fee: 200,    // 增加服務費 200 元
  }
});

// Vue 元件
Vue.component("room", {
  template: "#room",
  // 增加繼承 hotel_fee
  props: ["room_data", "hotel_discount", "hotel_fee"],
  computed: {
    // 在 final_price 計算中加上服務費,需要乘上 1.0 是為了將 hotel_fee 的型別強制轉為數字
    final_price: function () {
      return (
        parseInt(this.room_data.price * this.final_discount) + this.hotel_fee * 1.0
      );
    },  
  }
});
加上服務費計算連動
加上服務費計算連動

增加各個房間編輯區塊

接著我們來製作各個房間的編輯區塊。首先增加標題 h1 房間編輯 及水平分隔線 hr 與上面的區塊做區分,接著我們增加一個 div 標籤並給予 room_edit 的 class,然後利用 v-for=”room in rooms” 做重複結構。room_edit 底下我們利用 input 跟 v-model 做雙向資料的綁定,分別綁定房間的名稱、價格、折扣數、英文名稱以及圖片網址。

// HTML
h1 房間編輯
hr
.room_edit(v-for="room in rooms")     // 利用 v-for 做重複結構
	h4 {{ room.name }}
	label 房間名稱
	input.form-control(v-model="room.name") 
                        // 利用 v-model 做雙向綁定
	label 價格
	input.form-control(v-model="room.price")
	label 折價
	input.form-control(v-model="room.discount")
	label 英文名稱
	input.form-control(v-model="room.eng")
	label 圖片網址
	input.form-control(v-model="room.cover")

當我們想要調整視覺的時候,隨時可以利用 Bootstrap 跟 CSS 做微調。例如我們給予每一個 input form-coontrol 的 class,讓它取得 Bootstrap 的預設樣式。

我們也能夠自行設定 CSS,例如將左側編輯區域的高度固定,超出的部分使用卷軸滾動的效果,也將每一個 room_edit 區塊利用 margin-top 做出間隔。

#app
  .container
    .row
      .col-sm-3.col-edit

//CSS
.col-edit
  height: 100vh
  overflow-y: scroll

.room_edit 
  margin-top:30px

到這邊,我們已經能在每一個房間的編輯區塊做個別房間的調整,例如將雙人房改為單人房、改變折扣數或價格等等。

增加各個房間編輯區塊
增加各個房間編輯區塊

增加房型的按鈕功能:v-on 事件處理

假如我們要新增一組房型的話怎麼辦呢?這時我們就可以用到 Vue 的 methods(方法)。我們在房間編輯區域最下面再增加一組 .room_edit + 新增房間,同時加上 @click 表示在點擊的時候要觸發指定的事件,這邊我們指定點擊時觸發 addroom 。觸發的事件我們則要寫進 Vue 物件裡,告訴它這個事件要做什麼。

// HTML
.room_edit(@click="addroom") + 新增房間

接著我們在 data 之後增加 methods,methods 的值是一個物件,因為裡面可以有許多不同的 method。這邊我們要增加的是 addroom。同時 addroom 的值是一個函式,我們希望 addroom 將資料推進 rooms 裡。要推的資料我們從上面房型資料中複製一組下來,並且將內容編輯一下。

現在,我們如果點擊「+ 增加房間」,Vue 就會幫我們增加一組房間的卡片,並且左邊也有對應的編輯區域,我們可以在編輯區直接修改房間資料,這樣是不是非常方便呢!

var vm = new Vue({
  el: "#app",
  data: {
    rooms: rooms,
    service_fee: 200,
    discount: 0.9
  },
  methods: {                    // 增加 methods,值是一個物件
    addroom: function () {      // 增加 addroom,值是一個 function
      this.rooms.push({         // 將資料推進 rooms 裡
        name: "新房間",
        eng: "new Room",
        price: 0,
        amount: 0,
        cover: "",
        discount: 0,
        equipment: {
          wifi: true,
          bathtub: true,
          breakfast: false
        }
      });
    }
  }
})
新增房間卡片並可以直接編輯資料
新增房間卡片並可以直接編輯資料

在房型卡片上顯示房間設備圖示,利用 v-if 做條件顯示

接下來我們要放入房間設備的小圖示,我們會用到 Font Awesome 這個好用的字型圖示工具,因此我們到 Setting 的 CSS 載入 font-awesome 的 CDN。接著,我們將房間介紹的模板 template#room 裡面加上一組 icons,icons 裡有三個圖示分別對應的是房間設備資料中的早餐、浴缸以及 wifi。

// HTML
  template#room
    // ...
    .info
      h5 {{ room_data.eng }}
        .icons   // 增加三個圖示並用一個 div 包起來
          span
            i.fa.fa-coffee
          span
            i.fa.fa-bath
          span
	      i.fa.fa-wifi

// JavaScript
    name: "經濟雙人房",
    // ...
    equipment: {       // 三個圖示要對應的是房間資料中 equipment 的三個屬性
      wifi: false,
      bathtub: true,
      breakfast: true
    }

我們希望當設備裡屬性是 true 時顯示圖示,而 false 時隱藏圖示,要怎麼做呢?這時候可以使用條件渲染 v-if,v-if 的意思是當條件為 true 時,瀏覽器便會幫我們渲染出來,如果是 false,瀏覽器則會忽略該元素。

// HTML
	.icons
		span(v-if="room_data.equipment.breakfast")
			i.fa.fa-coffee
		span(v-if="room_data.equipment.bathtub")
			i.fa.fa-bath
		span(v-if="room_data.equipment.wifi")
			i.fa.fa-wifi

這時,房間列表中,每個房間設備的對應圖示已經可以顯示出來了。

利用 v-if 做條件顯示,在房型卡片加上房間設備圖示
利用 v-if 做條件顯示,在房型卡片加上房間設備圖示

增加房間設備圖示的編輯欄位

接著,我們要在編輯區域中增加房間設備的區塊。一樣用 v-model 綁定 room.equipment 對應的設備,但我們要使用核取方塊(☑︎)來編輯設備的有無,因此在 input 加上 type=”checkbox”,並且加上 form-check-input 的 class 來套上 Bootstrap 樣式。最後我們加上一些 CSS 效果讓視覺美觀一點。

// HTML
	label 房間設備
      label 早餐
	  input.form-check-input(
          type="checkbox",
          v-model="room.equipment.breakfast"
        )
      label 浴缸
        input.form-check-input(
          type="checkbox",
          v-model="room.equipment.bathtub"
        )
      label wifi
        input.form-check-input(
          type="checkbox",
          v-model="room.equipment.wifi"
        )

// CSS
  .info
    // ...
    .icons                    // 增加 icons 的 CSS
      display: inline-block
      margin-left: 10px
      span
        margin-right: 5px
        opacity: 0.6
增加房間設備編輯欄位
增加房間設備編輯欄位

增加刪除功能

接著我們要增加刪除房型的功能,包括在編輯區域刪除某個房型,以及在瀏覽區域也能點擊刪除。我們先來看看編輯區域的刪除功能怎麼做吧。

我們先利用 Font Awesome 增加一個垃圾桶的圖示 i.fa.fa-trash,跟新增房間一樣,我們要加上點擊功能,所以加上 (@click=”delete_room(id)”)。delete_room 是一個 method,我們指定它要刪除指定的 id 房型。不過我們的資料中沒有 id,所以我們需要 Vue 幫我們在抓資料的時候幫我們把房型的 index 也抓出來,以 index 當作每個房型的 id,因此我們在 v-for 中加上 v-for=”(room, id) in rooms”。( v-for 語法可參考 Vue 官方文件。)

取得 id 後,別忘了在 vm 實例中增加這個 method,在 delete_room 中,我們呼叫 this.rooms 陣列,然後用 splice 刪除從指定的 index 中刪除 1 筆資料。最後,利用 CSS 讓游標滑過垃圾桶圖示的時候顯示可點擊圖示,優化一點使用者體驗,到這裡,編輯區塊的刪除功能就完成了!

// HTML
      // 在跑迴圈的時候讓 Vue 也幫我們取得 id (這邊的 id 就是 index)
	.room_edit(v-for="(room, id) in rooms")  
	  h4 {{ id + 1 }}{{ room.name }}
	  label 房間名稱
      i.fa.fa-trash.cursor_pointer(@click="delete_room(id)")

// CSS
	.cursor_pointer 
	  cursor: pointer

// JavaScript
	methods: {
	// ...
		delete_room: function (id) {
	      this.rooms.splice(id, 1);
	    }
	}
增加刪除房間的功能
增加刪除房間的功能

接著,我們來增加房型一覽中的刪除功能。

我們要讓每個房型卡片的右上角有個 ✘ 圖示,點擊圖示可以刪除,跟剛剛是不是很像呢?我們一步步完成它,先利用 Font Awesome 加入圖示,接著利用 CSS 的絕對定位調整位置,然後加上顏色變化。

// HTML
	template#room
	  .room_container
	    .cover(v-bind:style="bg_css")
	      h3 {{ room_data.name }}
	      i.fa.fa-times               // 增加圖示

// CSS
	i.fa.fa-times
      position: absolute
      top: 10px
      right: 10px
      color: white
      cursor: pointer
      transition: 0.5s
      &:hover
        color: $color_red

接著,我們讓 Vue 元件繼承 id 和 delete_room method,然後將刪除圖示也加上點擊刪除的功能 (@click=”delete_room(id)”),這樣,兩邊的刪除功能都完成囉!

// HTML
	.col-sm-9
    h1 房間列表
    hr
	    .row
                                     // 一樣讓 Vue 幫我們取得 id
          .col-sm-4.col-room(v-for="(myroom, id) in rooms") 
          room(
            :room_data="myroom",
            :id="id"               // 這邊的 id 會抓取迴圈跑出來的 id
            :hotel_discount="discount",
            :hotel_fee="service_fee",
            :delete_room="delete_room",    // 繼承的 delete_room
          )
	// ...

	template#room
	  .room_container
	    .cover(v-bind:style="bg_css")
	      h3 {{ room_data.name }}
	      i.fa.fa-times(@click="delete_room(id)")    // 加上點擊刪除功能


// JavaScript
	Vue.component("room", {
	  template: "#room",
	  props: ["room_data", "hotel_discount", "hotel_fee", "delete_room", "id"]   
		// 增加繼承 delete_room 以及 id
	}
房間卡片右上角的x刪除功能也完成了
房間卡片右上角的x刪除功能也完成了

收整房間編輯區塊

最後,左邊的編輯區塊看起來落落長的,我們把目前不需要用到的資料收整起來,只顯示要編輯的房間資料區塊就好了。

要收整所有房間到下拉式選單裡,我們增加一個 select 標籤,並且在 select 中設定 option 標籤,標籤我們要綁定房間的 id 以及顯示房間名稱,一樣用到的是 v-for,到這裡你是不是已經對 v-for 很熟悉了呢!因此我們在 option 後面加上 (v-for=”(r, id) in rooms”, :value=”id”) {{ r.name }}。這樣我們的下拉選單就做好了!

// HTML
h1 房間編輯
select.form-control(v-model="edit_id")
  option(v-for="(r, id) in rooms", :value="id") {{ r.name }}
收整左側房間編輯區塊
收整左側房間編輯區塊

接著,我們要讓編輯區塊在我們新增房間的時候,同時跳轉到新房間的編輯畫面。另外我們要把刪除房間按鈕綁定的 id 改成編輯中的房間 id 。

// HTML
	hr
	// 原來是 .room_edit(v-for="(room, id) in rooms")
	.room_edit(v-for="(room, id) in [rooms[edit_id]]")
	  h4 {{ room.name }}
        label 房間名稱
	//原來是 i.fa.fa-trash.cursor_pointer(@click="delete_room(id)") 
          i.fa.fa-trash.cursor_pointer(@click="delete_room(edit_id)")

/// JavaScript
  methods: {
    addroom: function () {
      this.rooms.push({
	  // ...
      });
      // 當 addrom 時,同時讓 edit_id = 最後一間房間
      this.edit_id = this.rooms.length - 1 },

最後,我們把「+ 新增房間」的功能按鈕位置調整一下,並且加上 Bootstrap 的 class 修改它的視覺效果,畫面是不是更美觀了呢。到這邊,我們的 Vue component 實作練習就大功告成囉!

// HTML
	h1 房間編輯
	  select.form-control(v-model="edit_id")
	    option(v-for="(r, id) in rooms", :value="id") {{ r.name }}
        // 移動「+ 新增房間」按鈕位置,並加上 Bootstrap class
	  button.btn.btn-secondary.room_edit(@click="addroom") + 新增房間 
調整編輯欄的視覺,完成此次練習
調整編輯欄的視覺,完成此次練習

總結

這次的練習真是段漫長的旅程啊,最後一起回顧一下我們完成了什麼吧:

  1. 學習 Vue.js 的基本概念及語法:我們認識了 Vue.js 是什麼,並且學習 v-for、v-bind 等 Vue.js 的基本指令。
  2. 實作練習 Vue.js 的基本語法:我們用一組簡單的飯店資料練習如何實際使用 v-model, computed 等 Vue 的指令來帶入資料,透過資料來驅動畫面。
  3. 實作完成一個動態飯店清單:我們利用 Vue 元件及 v-for 迴圈製作房型卡片,並且運用元件的繼承屬性以及繼承方法等特性,透過 v-model 做雙向綁定,讓資料能在一端修改時同步更動所有相依資料。

看見完成品你是不是也有滿滿的成就感呢?如果想看完整的程式碼,可以參考老闆的 Codepen。如果想跟 Vue 更熟悉,很推薦你實際看一下 Vue.js 的官方文件,透過一次次的查找資料跟練習,你也一定能將 Vue 用得像呼吸一樣自然!那我們下次見啦。 ₍₍ ◝( ゚∀ ゚ )◟⁾⁾


老闆的工商時間

想了解更多如何寫出漂亮清晰的網頁嗎?老闆在 Hahow 的教學課程 動畫互動網頁程式入門(HTML/CSS/JS) 用平易近人的語言,用簡單的方式帶你作出不簡單的網頁。已經有網頁程式基礎了嗎?進階課程 動畫互動網頁特效入門(JS/CANVAS) 能讓你紮實掌握 JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 【Vue.js入門】一小時學會 Vue.component,完成動態飯店房間清單 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【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 - 互動程式創作台灣站

]]>
【Canvas創作教學】畫個偵測敵人的動態雷達圖網頁 https://creativecoding.in/2021/09/23/canvas-creation-enemies-radar/ Thu, 23 Sep 2021 02:45:00 +0000 https://creativecoding.in/?p=1450 本次創作直播內容透過 Canvas 物件,將網頁當作畫布,繪製不同的圖形,完成模擬偵測敵人的雷達機介面;運用三角函數概念,以及模組化程式,簡單製作出動態的網頁,詳細的步驟解釋,無論有沒有基礎,都能輕鬆跟著說明完成,剩下的就由你自行發揮囉!

這篇文章 【Canvas創作教學】畫個偵測敵人的動態雷達圖網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
今天我們要來製作一個可以掃描出敵人的動態雷達圖,讓隱藏在地圖深處的隨機敵人現身。本次工具主要透過 Canvas 物件,將網頁當作畫布繪製各式不同的圖形,而在觀念上與上一次的時鐘是有些相似的,都運用了三角函數的概念,不過會有一些延伸的知識點,像是如何將相似的圖形以模組化的方式來撰寫,以及敵人在被掃略線掃到時,要如何顯現後再漸漸地消失。

透過此次教學,你會學到

  • 認識 Canvas,並將網頁在當作畫布一樣繪製線條、顏色與形狀
  • 學習觀察相似物件的特性,以模組化的方式呈現,減少程式碼的撰寫

認識常用的 Canvas 屬性

在 Canvas 中,每一次繪圖都是分為一個個區段的,在執行步驟上可分為三個步驟,這裡以像是操作機台白話的方式來做比擬。

  1. 按下繪圖開始樣式按鈕
  2. 設定所要畫的圖形,像是圖形或是線條,可以是單一或是多個都沒問題
  3. 確認上述設定圖形沒問題,開始畫圖,將圖形顯示在畫面上

上述三個步驟對應到 Canvas 的屬性分別是:

  1. 繪圖開始 : beginPath()
  2. 圖形設計 : 在圖形設計上可分為兩種,一種實心呈現色塊的圖形,另一種則是單一線條,或是以單一線條所構成的中空圖形,常見的畫圖方式有以下:
    • 畫正方形 rect(x 位置,y 位置,寬度,高度)
    • 畫弧形 arc(x 位置,y 位置,半徑,起始角度,終點角度)
    • 畫線條時會有兩個屬性搭配使用,分別是 moveTo(x 位置,y 位置) ,這僅會移動畫筆,但不畫線,而另一個則是 lineTo(x 位置,y 位置),這則是會以下筆的方式移動到特定位置。
    在圖形設定上也包含樣式,像是以顏色來說有 fillStyle 用於指定色塊,而 strokeStyle 則是指定線條顏色。
  3. 開始畫圖 : 這裡同樣在兩種不同的圖形有特別的指定方式,在色塊上是 fill(),而線條則是 stroke()

在正式進入本章的主題前,老闆找到了一個線上繪製數學圖形的網站,嘗試以不同的方式來說明極座標的概念。在先前的文章〈來用可怕的三角函數做網頁吧!Part 1Part 2〉說明了以極座標表示位置的方式,而在本次範例中,一樣會使用到極座標概念,想要回顧上次的教學內容,歡迎點選上面文章連結複習一下再開始!

在圖1-1中,看到網頁中的步驟三定義的變數 t,用於表示角度,而步驟四則顯示極座標 (cos(t), sin(t))的表示方式畫出圓點,當移動 t 的拉桿時,同時也代表角度正在改變,可以看到在畫面中的點以座標 (0, 0) 的位置為中心,在周圍以半徑為 1 單位的距離移動。

圖1-1 : 變數 t,用於表示角度
圖1-1

接著在圖 1-2 中的步驟五寫了一個定義圓的方程式,讓前面所提的圓心軌跡顯示出來。

而步驟六則定義了 r 也就是圓的半徑範圍,預設上 r 的半徑大小為 1 ,所以可以看到拖拉變數 t 也就是角度範圍時,圓點就在圓形軌跡上面移動,而當改變半徑 r 時,則會改變整個圓的大小。

圖1-2

為了讓圓點可以在圓的軌跡上,而不是固定在半徑為 1 的範圍中,所以在圖 1-3 的步驟四中,在 x 與 y 的座標位置都乘上了半徑 r,這樣一來,當 r 的大小有變化時,不僅圓的大小會改變,可以看到圓點距離中心點的位置也在改變。

在步驟四中所呈現的就是點在座標系統的呈現方式,其涵蓋了兩個變數,分別是距離中心點位置的半徑 r 以及角度 t

圖1-3
圖1-3

前置作業

設定 Code Pen

在 Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、Js 中引入 Jquery。

引入雲端字形

在此次範例會使用到外部字體來作為搭配,讓作品更好看。

  • 首先進入到 Google Fonts 中,搜尋 Abel 後進入頁面。
  • 在 Styles 區塊中的右側有加號 Select this style,點選後在右側會跑出視窗。
  • 開啟 Code Pen 中的 css 設定,將剛剛所選的連結貼在add another resource新增的欄位中,並點選儲存,這樣一來就可以使用了。

上述都設定好後,就要正式進入主題囉!

一、基礎版面

為了可以畫圖,所以需要一張畫布,那就是 Canvas,並指定 ID 為 #myCanvas。另外放置了訊息,分別是標題,以及一些訊息,這裡先暫時以 temp 作為代稱,這個在後面會更改為顯示掃到敵人時,敵人所在的角度與位置。

//HTML
canvas#myCanvas
.info
    h1 Boss, CODING Please
    p.message temp

接下來在樣式上做初始設定,我們希望是滿版的網頁,所以在長與寬都設置為 100%,而在預設上內距與外距會跑出來,但這些我們也不要,所以在 padding 與 margin 上設置為 0。

屬性 overflow 則是決定當物件超出原本的畫面時,該怎麼處理物件的顯示方式,這裡選擇 hidden,代表超出範圍的即隱藏起來。在字體上則是使用先前在 google font 所引入的 Abel。

//CSS
html, body
  //填滿視窗
  width: 100%
  height: 100%
  padding: 0
  margin: 0
  overflow: hidden
  font-family: Abel
步驟1-1設置基礎版面字樣
步驟1-1設置基礎版面字樣

現在畫面上仍看不到 canvas 的蹤影,所以指定背景顏色 #333 讓它顯示出來,所以發現它小小一個,但我們希望它撐滿整個版面,不過這個效果老闆選擇後續在 js 修改,這裡僅先調整訊息的位置。

訊息要放置在畫面的左下角,所以將它的定位改為絕對定位,並讓它離下方與左側各距離 50px。在字體的顏色上,敵人的訊息顏色是使用老闆特調的黃金色 rgb(185, 147, 98),標題的話則是白色,雖然標題暫時隱藏了,不過待會背景會設置為深色,就會看見標題了。而在這兩行字的間距上老闆希望可以距離近一些,所以將兩者 margin 都拿掉設置為 0。

//CSS
html, body
  //填滿視窗
  width: 100%
  height: 100%
  padding: 0
  margin: 0
  overflow: hidden
  font-family: Abel

canvas
  background-color: #333

.info
  position: absolute
  left: 50px
  bottom: 50px

h1
  color: white
  letter-spacing: 3px
  margin: 0

.message
  margin: 0
  color: rgb(185, 147, 98)
步驟1-2

二、基礎版面樣式設置

畫圖的第一步就是要取得畫布這個元素,在預設上我們有引入 Jquery,所以可以使用錢字號$的方式來抓取元素,所以這裡以錢字號加上在 html 中所設定 canvas 的 ID – #myCanvas,並且記得加上第零個的位置,這樣才會是 html 的元素。

接著處理這張畫布的渲染環境,由於是要在平面的範圍上作圖,所以使用 c.getContext("2d") 來存取的繪圖區域。

有了畫布後,要指定畫布的長寬,讓它可以撐滿整個畫面,所以創建兩個變數分別是 wwwh 來記錄畫面上的寬度與長度。另外,由於後續需要將主要的物件放置在畫面中央,所以也需要創建一個名為 center 的變數來記錄中心點的位置。

接著建立 getWindowSize()函數來指定畫布長寬與中心點,透過錢字號抓取 window 網頁元件,並取其視窗長度與高度的屬性 outerWidth()outerHeight(),放入到變數 ww, wh 中。有了長寬的數值後,就可以將它指定為畫布的長與寬了,而中心點的位置則是將兩者數值都除以 2。

設定好函數後,記得在下面呼叫一次剛剛所撰寫的函式,才會呈現所寫的效果。不過這裡有個問題,當我們拉動網頁視窗時,畫面並不會隨之更新,原因在於函式僅執行了一次,為了解決這個問題,所以需要加上 $(window).resize(getWindowSize) ,代表著當畫面有重新改變大小時,會重複執行一次 getWindowSize() 這個函式。

//JavaScript
var c = $("#myCanvas")[0];
var ctx = c.getContext("2d");
var ww, wh;
var center = { x: 0, y: 0 };

function getWindowSize() {
  //設定大小
  ww = $(window).outerWidth();
  wh = $(window).outerHeight();

  c.width = ww
  c.height = wh

  //重新設定中心點
  center = { x: ww / 2, y: wh / 2 };
}

getWindowSize();
//設定當網頁尺寸變動的時候要重新抓跟設定大小、中心
$(window).resize(getWindowSize);
步驟二:基礎版面畫製
步驟二:基礎版面畫製

三、繪製一個矩形

設定好畫布後,接著要嘗試在畫布上繪製圖形,這裡先來畫畫看一個正方形。建立一個名為 draw()的函式,裡面 ctx 也就剛剛所抓取的畫布名稱,而rect 則代表要繪製一個矩形,參數分別為 (x 起始位置,y 起始位置,寬度,長度)。為了讓它可以呈現出動態的效果,所以使用 setInterval(draw, 10),設定每十毫秒就執行一次 draw(),並且創建一個數值會向上遞增的變數 time 放到 x 位置中,這樣一來物件每十秒就會向前方移動一單位的距離。

//JavaScript
setInterval(draw, 10)
var time = 0;

function draw() {
  time += 1;
  ctx.rect(20 + time, 20, 150, 100);
  ctx.stroke();
}
步驟三:畫一個會跑的矩形
步驟三:畫一個會跑的矩形

但是這個時候會發現,舊的元素並沒有被清除掉。解決這個問題的方法為,在每一次繪圖的時候,也重新再一次指定背景。這邊可以注意到繪製填滿圖形與線條是不一樣的,若是要填滿圖形是 ctx.fill(),而繪製線條的話則是 ctx.stroke(),詳細的原理在第四步驟會做說明。

//JavaScript
function draw() {
  time += 1;

  ctx.fillStyle = "#fff"
  ctx.beginPath();
  ctx.rect(0, 0, 500, 500);
  ctx.fill();

  ctx.rect(0 + time, 0, 50, 50);
  ctx.stroke();
}
步驟三:每畫一個新的圖形前都要再蓋一次背景,才可以清除舊的元素
步驟三:每畫一個新的圖形前都要再蓋一次背景,才可以清除舊的元素

這樣子呈現的效果就沒有問題了,不過我們是要在整張畫布上作畫,當然需要再改變畫布的大小,為了確保可完整地覆蓋背景,所以設置一個很大的數值覆蓋在背景上,並將原本測試使用的矩型移除掉。

//JavaScript
function draw() {
  time += 1;

  //更新為整張畫布大小為黑色+放大
  ctx.fillStyle = "#111"
  ctx.beginPath();
  ctx.rect(-2000, -2000, 4000, 4000);
  ctx.fill();
}
步驟三:改變畫布大小
步驟三:改變畫布大小

這是現在畫面上所呈現的樣子,雖然看上去跟步驟二所呈現出的效果是一樣的,但是現在的這個背景會不斷更新,我們接下來將圖形繪製上去的時候,也才不會造成圖形疊加在一起的問題。

四、畫垂直線

背景設定好後,就要來繪圖囉,不過在開始之前,要先來說明在 canvas 中的座標系統:在canvas 中,當增加 y 數值的時候,會發現物體往畫面的下方移動,這是 canvas 預設的座標系統。而我們所熟悉的座標系統中,X 數值增加是向右,而 Y 數值增加則是向上,所以這裡要調整一下,在 sass 中改變 Y 軸的軸向。

//CSS
canvas
  background-color: #333
  transform: scaleY(-1)
//JavaScript
function getWindowSize() {
  ...
  center = { x: ww / 2, y: wh / 2 };
  ctx.restore();
  ctx.translate(center.x, center.y);
}

接下來就是要繪製 x 軸與 y 軸,線條是從左邊至右邊以及從下面至上面。在畫線上會需要使用到兩個指定,分別是 moveTolineTo,可以想像你手中現在拿著一支筆,moveTo代表手移動至該點的位置,但是不接觸紙張,而lineTo 則是從現在這個位置,將畫筆在紙上移動至 lineTo 所指定的位置上,所以繪製 x 軸上就是先移動至畫面的左側 (-ww / 2, 0) 的位置,接著移動到 (ww / 2, 0)的點上,而 y 軸也是相同的道理。

這邊為何會需要在最後加上 stroke() 呢 ? 原因在於,如果每下達一個指令時,渲染機制就馬上執行畫線的話,這樣是很吃效能的,所以系統是預設讓我們在新增完所有路徑的時候,再使用 stroke()將剛剛所指定的路徑繪製出,同樣的概念也適用於 fill()

//JavaScript
function draw() {
  time += 1;

  ctx.fillStyle = "#111"
  ctx.beginPath();
  ctx.rect(-2000, -2000, 4000, 4000);
  ctx.fill();

  // 畫座標軸
  ctx.strokeStyle = "rgba(255,255,255,0.5)";
  //x
  ctx.moveTo(-ww / 2, 0);
  ctx.lineTo(ww / 2, 0);
  //y
  ctx.moveTo(0, -wh / 2);
  ctx.lineTo(0, wh / 2);
  ctx.stroke();
}
步驟四:畫製X、Y軸
步驟四:畫製X、Y軸

五、利用極座標畫線條

建立好座標軸後,就要來開始挑戰本章的大魔王 – 動態掃略線。首先第一步是要以極座標來畫線,會需要兩個重要的數值,分別是線條的長度 r 以及線條的角度 deg。

線條要以中心點為起點,所以將畫筆移動圓點 (0, 0)位置,接著是移動至另一個端點,也就是( r * cos(角度), r * sin(角度) ),但是此時會發現畫面上所呈現的效果怎麼跟預想的不太一樣,看上去很明顯並非是 45 度。

//JavaScript
var color_gold = "185,147,98";
function draw() {
  ...
  // 以極座標方式繪製線條
  ctx.strokeStyle = "rgba(" + color_gold + ",1)";
  var r = 100;
  var deg = 45;
  ctx.moveTo(0, 0);
  ctx.lineTo(r * Math.cos(deg), r * Math.sin(deg));
  ctx.stroke();
}
步驟五:利用極座標畫線條
步驟五:利用極座標畫線條

原因在於口語上我們表達會是 90度、180度,但是實際上它的單位會是弧度,比如說 180 度相當於是角度 PI,其數值的大小為 3.14,而非 180,所以這裡需要進行單位上的轉換,建立一個 deg_to_pi 變數,並將 π 除上 180,接著我們在 console 裡面試試看呈現出的結果,可以看到角度乘上 deg_to_pi 時,就會是正確的徑度數值,這樣一來後續在定義角度的時候,我們就可以用熟悉的角度來去定義囉。

角度轉換為π (Pi)
角度轉換為π (Pi)

下面將定義的變數 deg_to_pi 與角度 deg 做相乘後,所呈現出就會是正確的徑度數值與角度。

//JavaScript
var color_gold = "185,147,98";
var deg_to_pi = Math.PI / 180;  //新增轉換定義

function draw() {
  ...
  // 以極座標方式繪製線條
  ctx.strokeStyle = "rgba(" + color_gold + ",1)";
  var r = 100;
  var deg = 45;
  ctx.moveTo(0, 0);
  ctx.lineTo(r * Math.cos(deg_to_pi * deg), r * Math.sin(deg_to_pi * deg));
  ctx.stroke();
}
步驟五:呈現出正確的45度角
步驟五:呈現出正確的45度角

做到這邊,確實有達成所希望的效果沒錯,不過如果每次要設定點的位置時,都需要寫一長串的話似乎有點麻煩,所以老闆這邊習慣寫一個可以計算點位置的函數,只需要傳入兩個參數,分別是長度以及角度後,就可以得到一個物件,裡面包含點的 x 位置與 y 位置。

除了更改點的呈現方式之外,這裡有個小地方要注意,就是會發現所有的線條,包含前面設定的 xy 軸都變成了金色,原因在於繪圖系統又重新將它們漆上了金色,為了可以與路徑的設定切分,需要加上一個另一個的 beginPath() 告知繪圖系統要建立一個新的路徑。

//JavaScript
var color_gold = "185,147,98";
var deg_to_pi = Math.PI / 180
// 1. 更改為 function 從極座標轉串成點
function Point(r, deg) {
  return {
    x: r * Math.cos(deg * deg_to_pi),
    y: r * Math.sin(deg * deg_to_pi)
  }
}

function draw() {
  ...
  ctx.strokeStyle = "rgba(" + color_gold + ",1)";
  var r = 100;
  var deg = 45;
  var newpoint = Point(r, deg)  // 2.以函數取得 newPoint,
  ctx.beginPath();  // 3. 為了避免前面的軸線再次重新被描一次而變成金色,所以這裡要加上 beginPath
  ctx.moveTo(0, 0);
  ctx.lineTo(newpoint.x, newpoint.y);
  ctx.stroke();
}

六、扇形掃描線

扇型的掃描線我們不使用 arc 來繪製,是因為無法達成透明度變化的效果,而要構成掃描線的樣式,可以透過每一個單位的線條或是三角形組合而成,這裡先以較為簡單的線條方式來繪製看看。

每次角度改變一度,就繪製一條線條,所以創建一個 for 迴圈,迴圈執行的次數 line_deg_len 也就是弧形的角度大小,在線條角度上以 time 這個變數的數值為基準減去迴圈中的 i 值,這樣就有 100 個連續相差 1 的角度數值,另外因為 time 是會隨時間變化的,連帶著這 100 個角度值也會不斷變化,進而生成了動態的旋轉扇形。

在透明度的設定上,則是在每一條線畫的時候就指定個別的透明度,不過由於透明度的值是 0~1,我們不能直接放 i 值,而是要放 i / line_deg_len

//JavaScript
function draw() {
  ...
  ctx.strokeStyle = "rgba(" + color_gold + ",1)";
  var r = 200;
  var deg = time;
  var newpoint = Point(r, deg)

  var line_deg_len = 100;  // 弧線的角度
  for (var i = 0; i < line_deg_len; i++) {
    var deg = (time - i)
    var newpoint = Point(r, deg)

    ctx.beginPath();
    ctx.strokeStyle = "rgba(" + color_gold + "," + (i / line_deg_len) + ")";
    ctx.moveTo(0, 0);
    ctx.lineTo(newpoint.x, newpoint.y);
    ctx.stroke();
  }
}
步驟六:以線條畫製扇形掃描圖像
步驟六:以線條畫製扇形掃描圖像

現在有一個漸層且會動態旋轉的扇形了,但是有兩個地方怪怪的需要調整:一個是它旋轉的方向反了,透明度較低的線條在前方;另一個則是在以線條的方式繪製下,會有明顯紋路的問題,所以接下來我們要改以畫三角形的方式來試試。

三角形相較於線條會複雜一些些,但是原理上是一樣的,只是線條是兩個點形成一條線,而三角形是三個點形成一個面,相較於線條的單一角度 time - i,還需要另一個相鄰的角度time - i - 1,並以這兩個角度來計算出每一個三角形的兩個頂點位置後,再將頂點與 (0, 0) 的位置連接起來。在最後要將原先用於線條的 stroke() 改為用於色塊的 fill()。這樣透過以小三角型色塊的方式呈現,相較於一條條的線條,在視覺上看起來會更加滑順。

接下來處理透明度的方向問題,由於 i 是由 0 開始的,以至於在一開始線條的透明度為 0,這裡有個小技巧就是以 1 減去原先設定的數值,這樣順序就會從由小至大變成由大至小了。

//JavaScript
function draw() {
  ...

  // 改用三角形畫圖
  var line_deg_len = 100;
  for (var i = 0; i < line_deg_len; i++) {
    // var deg = (time-i)
    // var newpoint = Point(r, deg)
    var deg1 = (time - i - 1)
    var deg2 = (time - i)

    var point1 = Point(r, deg1)
    var point2 = Point(r, deg2)
    var opacity = 1 - (i / line_deg_len)

    ctx.beginPath();
    ctx.fillStyle = "rgba(" + color_gold + "," + opacity + ")";
    ctx.moveTo(0, 0);
    ctx.lineTo(point1.x, point1.y);
    ctx.lineTo(point2.x, point2.y);
    // ctx.stroke();
    ctx.fill()
  }
}
步驟六:改為用三角形繪製掃描線,更為平滑,也改好方向
步驟六:改為用三角形繪製掃描線,更為平滑,也改好方向

七、敵人系統

在第七章敵人系統中是比較大的章節,因此將其拆分成四個小節來做說明,分別有:

  • 如何隨機地產生一組敵人
  • 如何將隨機的敵人擺放至畫面上
  • 如何判定掃略線掃到敵人,並且敵人會做出相對應的變化
  • 調整敵人樣式,使敵人看起來更完整

在開始製作敵人系統的這個章節前,要先來建立一個 Color 的函式,透過傳遞一個參數代表透明度來指定需要的顏色,後續也會比較方便。

//JavaScript
// 建立 Color 函數來做使用
function Point(r, deg){...}

function Color(opacity) {
  return "rgba(" + color_gold + "," + opacity + ")";
}

function draw(){...}

7-1 隨機產生敵人

首先創建一個長度為 10 的陣列,並且在裡面放入空的陣列,在圖A中可以看出 enemies 的值為一排空陣列。

接著透過 map 函數做陣列中元素的轉換,將原本空的陣列放入兩個值,分別為 x 與 y,其結果可在圖B中所見。而實際上我們需要創建的資訊一共有三個,分別是半經 r 、角度 deg 以及透明度 opacity,如圖C。

由於位置是隨機產生的,所以在半徑與角度上都是使用 Math.random() ,這裡值得一提的是,角度我們可以直接使用熟悉的 0~360 的角度系統,原因是在於我們使用先前已經寫好了 Point() 函數取得點的位置,而函數中也已經處理好角度轉換的問題了。

// 建立十個空物件的寫法 (圖A)
var enemies = Array(10).fill({})  

// 若建立十個物件,物件中 key 為 X、Y (圖B)
var enemies = Array(10).fill({}).map(
  function (obj) {
    return {
      x: 5,
      y: 5
    }
})

// 我們要繪製敵人則是需要建立十個空陣列,物件中 key 為半徑 r、角度 deg、透明度 opacity (圖C)
var enemies = Array(10).fill({}).map(
  function (obj) {
    return {
      r: Math.random() * 200,
      deg: Math.random() * 360,
      opacity: 1
    }
})
建立十個空物件的寫法 (圖A)
建立十個空物件的寫法 (圖A)
若建立十個物件,物件中 key 為 X、Y (圖B)
若建立十個物件,物件中 key 為 X、Y (圖B)
我們要繪製敵人則是需要建立十個空陣列,物件中 key 為半徑 r、角度 deg、透明度 opacity (圖C)
我們要繪製敵人則是需要建立十個空陣列,物件中 key 為半徑 r、角度 deg、透明度 opacity (圖C)

7-2 敵人在畫面上顯示

隨機產生出敵人後,接著就是將敵人依序顯示在畫面上。

除了使用 for 迴圈來去存取陣列中的物件外,另一種方式則是使用 forEach,這裡使用 obj 來當作每一個元素的代稱,在每一個 obj 中都有三個屬性可做存取,分別是半徑 r 以及角度 deg以及透明度 opacity。由於這裡是要計算出點的位置,所以僅需要前面兩個值,再放入之前所創建的 Point 函數,便可得到該點所在的確切位置 obj_point 了。

敵人先以圓形來做為表示,要畫圓的方式是使用 arc ,其語法為

arc (中心點 x 位置,中心點 y 位置,圓的半徑,起始角度,終點角度)

由於是要畫完整的圓,所以在角度上設定為 0 至 2π。

//JavaScript
function draw(){
  ...

  enemies.forEach(function (obj) {
    //本體
    ctx.fillStyle = Color(1);
    var obj_point = Point(obj.r, obj.deg);

    ctx.beginPath();
    ctx.arc(
      obj_point.x, obj_point.y,
      10, 0, 2 * Math.PI
    );
    ctx.fill();
  })
}
步驟7-2:以原點代表敵人隨機出現的位置
步驟7-2:以原點代表敵人隨機出現的位置

7-3 當線條角度與敵人一樣時,將敵人透明度變成 1

前面只是確認每一個點確實有被設定,而且都有顯示出來。接著要來實作掃略線的功能,當線條與敵人重疊時,敵人才會顯示,並在一段時間後再次消失。

首先要先定義掃略線,在前面的步驟中,定義了 time 這個變數,在每次執行一次 draw() 時,數值便會向上加 1,而且透過 time 定義角度後,將扇形掃略的效果所畫出來,我們要定義掃略線當下的位置,也是使用到 time 這個變數。

不過前面有提到 time 會隨著執行時間不斷地增加,但是角度的範圍僅限於 0~360,所以需要將 time 取 360 的餘數time % 360,讓數值維持在 0~360 之間。

有了掃略線 line_deg 的角度後,接著就是跟敵人的角度來做比較,當兩者的角度差小於一定數值的時候,就代表兩者有重複到了,在這裡老闆以兩者設為兩者的距離取絕對值後小於 1 ,若是符合條件,便將透明度變為 1。而在敵人出現後,要讓他漸漸消失,等待下一次被掃略後出現,只需要將其透明度乘上一個小於 1 即可,透明度的值便會漸漸從 1 趨近於 0。

//JavaScript
var enemies = Array(10).fill({}).map(
  function (obj) {
    return {
      r: Math.random() * 200,
      deg: Math.random() * 360,
      opacity: 0  //  1. 敵人透明度設為 0
    }
})


function draw() {
  var line_deg = time % 360;  // 2. 定義掃略線
		  
  enemies.forEach(function (obj) {
    //本體
    ctx.fillStyle = Color(obj.opacity);
    var obj_point = Point(obj.r, obj.deg);
		
    ctx.beginPath();
    ctx.arc(
      obj_point.x, obj_point.y,
      10, 0, 2 * Math.PI
    );
    ctx.fill();
		
    // 3. 判定掃略線與敵人位置
    if (Math.abs(obj.deg - line_deg) <= 1) {
      obj.opacity = 1;
    }
    obj.opacity *= 0.99
    })
}
步驟7-3:當掃描線的線條角度與敵人一樣時,將敵人透明度變成 1
步驟7-3:當掃描線的線條角度與敵人一樣時,將敵人透明度變成 1

7-4 修改敵人樣式

目前敵人的樣式上只有單個圓圈而已,顯得有些單調,我們想將敵人的符號加上叉叉,以及一個有動態向外擴展的圓圈。在開始之前,先將原先的圈圈大小做調整,將半徑為 10 大小縮小為 4。

叉叉為兩個線交疊而形成的,而線條的長短代表叉叉的大小。老闆先是定義了 x_size 這數值大小,這是繪製線條值所移動的距離,也代表叉叉的大小。兩條線分別是從左下至右上以及右下至左上,中心點的位置與圓圈同為 (obj_point.x obj_point.y),要移動至左下角時,在 X 座標與 Y 座標的值都是減去 x_size,而右上角的點則是都加上 x_size,其餘另外兩個點則以此類推。

步驟7-4:敵人樣式的叉叉解說
步驟7-4:敵人樣式的叉叉解說

接著來畫向外擴張的圈圈,這裡可以直接複製上面的開始畫圈圈的程式碼,並改成 strokeStyle 以線條的方式呈現。一個向外擴展的圈圈代表半徑的大小隨著時間在不斷變化的,在這裡當然是可以再寫一個變數來代表動態的半徑大小,但是其實我們已經有現成的變數可以使用,也就是透明度,所以不需要再額外寫一個。那就將半徑乘上 obj.opacity 吧,此時會發現圈圈反而是從外向內縮小,原因在於 obj.opacity 就是從 1 趨近於 0 漸漸越來越小的。

其解法就是將 1 除上 obj.opacity後,當透明度值越小,所得到相對應的數值會越大,這裡要特別注意的是,由於在除法中除上 0 是沒有意義的,所以需要加上一個極小的數值來避免掉這樣的情況。

// 圈圈由外向內
ctx.arc(point.x, point.y, 20*opacity
        , 0, 2 * Math.PI);

// 圈圈由內向外
ctx.arc(point.x, point.y, 20*(1/(obj.opacity + 0.001));
        , 0, 2 * Math.PI);

以下是改變了實心圓大小、加上叉叉以及向外擴展圈圈的完成程式碼

enemies.forEach(function (obj) {
  //本體
  ctx.fillStyle = Color(obj.opacity);
  var obj_point = Point(obj.r, obj.deg);

  ctx.beginPath();
  ctx.arc(
          obj_point.x, obj_point.y,
          4, 0, 2 * Math.PI  //  1. 半徑縮小為 4
  );
  ctx.fill();

  if (Math.abs(obj.deg - line_deg) <= 1) {
    obj.opacity = 1;
  }
  obj.opacity *= 0.99



  // 2. 畫叉叉   
  ctx.strokeStyle = Color(obj.opacity);
  var x_size = 6;
  ctx.lineWidth = 4;
  ctx.beginPath();
  ctx.moveTo(obj_point.x - x_size, obj_point.y - x_size);
  ctx.lineTo(obj_point.x + x_size, obj_point.y + x_size);
  ctx.moveTo(obj_point.x + x_size, obj_point.y - x_size);
  ctx.lineTo(obj_point.x - x_size, obj_point.y + x_size);
  ctx.stroke();

  // 3. 往外消失的圓線
  ctx.strokeStyle = Color(obj.opacity);   // 線條是strokeStyle
  ctx.lineWidth = 1;

  var point = Point(obj.r, obj.deg);
  var r = 20 * (1 / (obj.opacity + 0.001));

  ctx.beginPath();
  ctx.arc(point.x, point.y, r
          , 0, 2 * Math.PI);
  ctx.stroke(); // 線條是stroke()
}
步驟七:完成雷達掃描出現敵人的動態
步驟七:完成雷達掃描出現敵人的動態

八、修改左下角文字

在掃略線掃到敵人後,除了讓敵人顯現外,當然也要將敵人的位置標示出來。在判定掃略線與敵人碰觸到的判斷式中,用 jquery 的方式抓取左下角顯示文字的元素 .message,再填上要顯示的文字,分別是距離中心的半徑長度以及角度。在預設上系統會顯示非常多位數的小數點,這裡可以透過 toFixed(3) 來限制小數點所呈現的位數,括號內的數值即代表要呈現小數點幾位數,這樣一來讓視覺上比較好看一些。

if (Math.abs(obj.deg - line_deg) <= 1){
  obj.opacity=1;
  $(".message").text("Detected: "+obj.r.toFixed(3) +" at "+ obj.deg.toFixed(3) + "deg");
}

九、外圍刻度

在外圍的刻度上要先定義幾個四個變數來使用,分為是

split : 將圓形切分的份數。
feature : 每隔幾度要以比較長的線條呈現,就像家裡時鐘整點位置的線條會長一點。
tart_r : 距離中心點的起始位置。外圍刻度要為在掃略線的外圍,所以數值比掃略線的 200 再多一些。
len : 線條的長度。

在角度的要特別注意需要轉換,deg=(i/split)*360,這是因為在切分上只有切成了 120 份而非 360 份,所以 i 值並非代表角度,而是要先以i/split 判斷是第幾份,再將數值乘上 360 才會是正確的角度。後續則是將靠內側的點以及靠外側的點計算出來後,再相連就可以囉。

接著再使用判斷式 i % feature == 0,也就是當每隔 15 個單位時,將線條的長度以及粗度都設定得比其他線條的數值更大一些。

function draw(){
  ...

  ctx.strokeStyle=Color(1);
  var split=120;
  var feature=15;
  var start_r=230;
  var len=5;
		  
  for(var i=0;i<split;i++){
  ctx.beginPath();
  // 角度要轉換成 360
  var deg=(i/split)*360;
         
  // 如果在大刻度上就變粗變長
  if (i % feature == 0) {
    len = 10;
    ctx.lineWidth = 3;
  } else {
    len = 5;
    ctx.lineWidth = 1;
   }
		    
  // 轉換內側點以及外側點的位置
  var point1=Point(start_r,deg);
  var point2=Point(start_r+len,deg);
		    
  ctx.moveTo(point1.x,point1.y);
  ctx.lineTo(point2.x,point2.y);
		    
  ctx.stroke();
  }
}
步驟九:增加外圍刻度
步驟九:增加外圍刻度

十、畫龍點睛的線條

剩下還有三個不同的線條需要繪製,分別是最內側的虛線、與掃略線半徑相同的實線,以及最外圍由兩個超過四分之一弧形所組成的外框。在最後一個步驟中,要嘗試使用函式帶入參數的方式來一次繪製三種不同的線條,這也是本次範例中相當精彩的地方。

由於一個圓為 360 度,所以設定一個 1 到 360 的 迴圈,並且透過先前寫過的點位置的函數轉換,轉換成 point,再將這 360個連接在一起。其中從外部所傳進來的參數 r 所代表的為半徑的大小。

function draw(){
  ...

  function CondCircle(r) {
    ctx.beginPath();
    ctx.strokeStyle = Color(1);

    for (var i = 0; i <= 360; i++) {
      var point = Point(r, i);
      ctx.lineTo(point.x, point.y);
    }
    ctx.stroke();
  }
			
  CondCircle(300)
}!
步驟十:先畫個實線
步驟十:先畫個實線

以上是實線的呈現方式,那虛線呢 ? 來看看下圖吧 !

虛線繪製方法解說
虛線繪製方法解說

想像拿的一支畫筆,當從每一個點前往下一個點的位置時,也就是 i 至i+1,可以選擇這一次是要用 lineTo 下筆畫線,還是用 moveTo 不要畫線只要移動就好。上面線條是的虛與實是剛好以 1:1 的方式繪製,那如果是要以其他比例,像是下圖呢 ?

不同比例的需線畫製解說
不同比例的需線畫製解說

假設圖片中所呈現有畫線與無畫線的比例是 4:1 ,那就是抓成五等分,有其中四分要畫線,而一份不須畫線,而這可以使用取餘數的方式來完成,下面實作就以每 180 度為一個區塊,在每一個區塊中其中的 90 度以畫線,另外的 90 度則不會畫線。

function draw(){
  ...

  function CondCircle(r) {
    ctx.beginPath();
    ctx.lineWidth = 1;
    ctx.strokeStyle = Color(1);

    for (var i = 0; i <= 360; i++) {
      var point = Point(r, i);

      if (i % 180 < 90) {
        ctx.lineTo(point.x, point.y);
      } else {
        ctx.moveTo(point.x, point.y);
      }
    }
    ctx.stroke();
  }
			
  CondCircle(300)
}!
步驟十:完成虛線畫製
步驟十:完成虛線畫製

有了使用餘數來畫線的概念後,接著就是要來集大成的時候了。要將決定畫不畫線的地方也就是 i % 180 < 90 改為傳送一個函式進來的方式來進行判斷,這裡命名為 func_cond,另外增加一個可以設定線條粗度的變數lwidth

原本的四分之一弧形取餘數的數值不變,但而外加上了 time,使其可以產生出動態的效果。最內側的虛線則是每個區塊為三等份,其中一等分不畫線,所以回傳值為 (deg % 3) < 1,而與掃略相同的圓圈是實線,代表每一條線條都要連起來,在回傳值上都是 true

function draw(){
  ...

  function CondCircle(r, lwidth, func_cond) {
    ctx.beginPath();
    ctx.lineWidth = lwidth;
    ctx.strokeStyle = Color(1);
		
    for (var i = 0; i <= 360; i++) {
      var point = Point(r, i);
      if (func_cond(i)) {
        ctx.lineTo(point.x, point.y);
      } else {
        ctx.moveTo(point.x, point.y);
      }
    }
    ctx.stroke();
  }
		
  // CondCircle(300)
		
  // 最外圍的四分之一弧形
  CondCircle(300, 2, function (deg) {
    return ((deg + time / 10) % 180) < 90;
  });

  //  最內側的虛線
  CondCircle(100, 1, function (deg) {
    return (deg % 3) < 1;
  });

  // 與掃略線相同長度的實線
  CondCircle(200, 1, function (deg) {
    return true;
  });
}!
步驟十:產生動態虛線,並完成本次作品<偵測敵人的動態雷達圖網頁>
步驟十:產生動態虛線,並完成本次作品<偵測敵人的動態雷達圖網頁>

回顧

在本次範例中,可以發現一整個作品幾乎都是由 Canvas 所繪製出來的,就讓我們一起來回顧製作的流程吧

  1. 建構畫面上所需要的文字與本章主角 Canvas
  2. 抓取 Canvas 物件,並設定畫面長寬可隨頁面大小做更動
  3. 在 Canvas 繪製一個動態圖形,並調整底層背景樣式,讓所繪製圖形不會疊加在一起
  4. 正式開始繪製所需要要的圖形,從建立 X 軸與 Y 軸,再從單一的線條到以多個線條來形成掃描扇形
  5. 建立敵人系統是此次最重要的章節,隨機生成敵人的位置,並與掃略線做搭配,以及設定掃到敵人時所需要呈現的樣式
  6. 加上外圍刻度
  7. 以模組化的方式畫出中內外的線條三種不同的線條

透過這次範例讓我們見識到 Canvas 厲害之處,原來它真的就像畫筆一樣,可以畫出這麼美感與創意兼具的作品,想要看更多動態網頁、互動藝術作品,你可以加入老闆的動畫互動網頁程式入門 (HTML/CSS/JS)或是動畫互動網頁特效入門(JS/CANVAS),或來訂閱老闆 youtube 頻道吧!

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

墨雨設計banner

這篇文章 【Canvas創作教學】畫個偵測敵人的動態雷達圖網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
Vue.js入門:英文2000字互動遊戲網頁 https://creativecoding.in/2021/08/26/vue-js-%e8%8b%b1%e6%96%872000%e5%ad%97%e4%ba%92%e5%8b%95%e9%81%8a%e6%88%b2%e7%b6%b2%e9%a0%81/ Thu, 26 Aug 2021 01:38:00 +0000 https://creativecoding.in/?p=1397 你多久沒鍛鍊英文了呢?這次直播使用HTML(Pug)CSS(Sass)以及Javascript(Vue.js)來完成一個練習2000字英文單字的小互動遊戲網頁,難度適中,尤其適合剛開始學習Vue.js的新手,無論是跟著步驟逐步操作,或是聽老闆的影片都能快速完成。

這篇文章 Vue.js入門:英文2000字互動遊戲網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
英文2000字選擇題互動網頁完成圖
英文2000字選擇題互動網頁完成圖

這次直播的主題來自於老闆在2018年印象清華-物聯網科技藝術展中創作的展品〈英文8-2〉,其概念為十根代表清大不同學院的光柱,使用者只要掃描光柱上的QR Code就會跳出互動式的英文單字題目,只要答對題數越多、分數就越多,累積的分數便會即時地投射到光柱上,形成高高低低、動態交錯的有趣光景。

在這次直播中會來聊些這個專案內使用Vue的相關經驗,聊聊製作互動裝置藝術實作時整合的各種辛酸血淚史,以及如何快速地解析別人資料,利用Vue框架製作成幫你找回國中逝去英文能力的遊戲。

我們將以Code Pen做為本次實作的平台,這是一個可以在創作的當下即時看到程式碼運作狀況的線上程式碼編輯器,只要簡單註冊就可以使用囉!

如果想搭配直播影片一起實作,請往這邊走 👉🏻 https://www.youtube.com/watch?v=maFbo96YT8U

前期準備

在開始之前我們根據概念來進行規劃,想像一個英文單字的互動答題App需要哪些東西:

  1. 整理網路上現成的單字表,把資料變成符合我們條件的JSON格式物件,單字表必須同時具備英文、中文與詞性(今天借用的是109英文銜接教材2000單字
  2. 產生隨機的英文題目,並利用整理後的物件選出正確答案和其他類似的詞當作選項,並判斷答題者的正確與否
  3. 如果答題者正確,跟後端同步狀態累加分數

首先在Code Pen上開一個新的pen,將HTML的預處理器設定成Pug、CSS的預處理器設定成Sass、JS的CDN掛入Vue。

程式環境設定
程式環境設定

接著把單字表上的文字複製下來貼到Javascript中,var一個a,並且用ES6的頓號 ` 把文字包起來。

//Javascript
var a = `
A
able adj. 有能力的
about prep. 有關
above adv. 在上方
`

註:考慮篇幅關係這邊只貼上部分A字首的單字,實際資料請參考單字表。

我們快速分析一下他的架構組成,參考字首A的部分得知單字表主要可以分為:字首的段落開頭、英文、詞性、中文,也就是──只要是沒有英文單字的那一行就不會有「.」,如果說我們今天要把單字整理成一個一個的物件時,可以把每一行先分割出來、把含有「.」的留下,再分別拆解成英文、詞性、中文,這就是我們所需的資料。

1. 拆解單字表

利用語法split以空行來做分隔,再用語法filter把含有.的行過濾保留下來,利用list.lengthlist[n]在console查看過濾後的listlist2數量上的差異,代表我們的資料越來越乾淨了。利用語法map把原先陣列的一行一行轉化成一個一個,再存成另一個陣列,轉換的條件為用空格分割,console會發現list3裡面裝著一坨拉庫的[object Array](3)。再把list3拆分成wordcatatrans,分別對應英文、詞性、中文的物件。

//Javascript
var list = a.split("\n") //分割空行
var list2 = list.filter(item=>item.indexOf(".")!=-1) //過濾沒有.
var list3 = list2.map(item=>item.split(" ")) //單行轉單個
var list4 = list3.map(item=>({
  english: item[0],
  cata: item[1],
  trans: item[2]
})) //拆分成英文、詞性、中文

資料搬運小幫手Vue

Vue的特色在於資料雙向綁定,相較於jQuery需要選取物件、重新定義、再塞回去以直接操作 DOM 物件為主的方式,利用Vue的同步更新渲染資料可以幫助我們節省不少時間。Vue的寫法為在JS透過 new Vue建立作用範圍和待即時同步的資料,同時在HTML以{{}}包裹被更新的變數。以下為官方網站所舉的範例,Vue會將大括號{{}}的內容對應到message狀態,並且將之即時渲染至畫面上,也就是所指的「宣告式渲染」。

//HTML
<div id="app">
  {{ message }}
</div>
//Javascript
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

延伸閱讀: 「Vue.js 學習筆記 Day1」- 建立 Vue 應用程式 重新認識 Vue.js | Kuro Hsu

2. 實際運用Vue-單字小卡

運用Vue和剛剛整理好的單字表資料來試做一些英文小卡吧!取出list4中倒數20個單字用v-for迭代陣列中的物件,指定其資料種類並渲染在li的span,再給一些CSS的參數後就可以看到一張張排列整齊的你國中的惡夢單字小卡。透過Vue的幫忙,我們不用自己產生元件跟呈現,只需要確定資料是否正確即可。

//HTML
#app 
    h2 我的名字是{{name}}
    ul
      li(v-for="word in words")
        span {{word.english}} {{word.cata}} {{word.trans}} //指定word中的種類
//CSS
html,body
  background-color: #222

ul
  li
    background-color: #fff
    padding: 20px
    display: inline-block
    margin: 20px
    width: 200px
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4.slice(-20) //負號代表從後面數來的20個單字
  }
})
英文2000字選擇題互動網頁:步驟二,製作出單字卡
英文2000字選擇題互動網頁:步驟二,製作出單字卡

3. 製作答題選項

製作完單字小卡有沒有覺得長得很像我們的答題選項呢?在Vue中我們定義methods為操作不同 DOM 元素的方法,這邊需要綁定幾個動作:

  1. click DOM元素根據滑鼠點擊的動作,在console回傳所點擊的單字。
  2. getOptions挑選同詞性、同字首的隨機四個單字。用filter過濾word.cata == question.cata也就是詞性需與答案相同,過濾第二次word.english[0] == question.english[0]英文單字的字首(第0個字)需相同,過濾第三次word.english !== question.english確認答案不會等於題目。
  3. 使用sort把陣列的順序打亂:.sort((a,b)⇒a-b)是將大的往後排,但sort((a,b)=>Math.random())則是隨機取值,再扣掉比較函數0.5後成為真正隨機排序的陣列,加上.slice(0,4)限縮在一次只取四個單字。
  4. .sort前面加入第二個.slice()把原本的元素複製一份成新的陣列,避免影響到既有陣列的順序,雖然針對新的陣列動屬性依然會影響原先的物件,但兩個陣列的順序是分開的。
  5. 亂數打亂正確答案的位置,目前都是把正確答案推到最前面,取得result後用concat連接question這個陣列,再打亂一次排序,成為result2
//HTML
#app 
  h2 我的名字是{{name}}
  ul
    li(v-for="word in words", v-on:click="click(word.english)") //讓console顯示出滑鼠點擊到哪個英文單字
      span {{word.english}} {{word.cata}} {{word.trans}}
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(
      word => word.english !== question.english
      ).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let result2 = result.concat([question]).slice().sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})

註:concat只能做陣列與陣列的連接。

4. 建立一個出題目按鈕,產生新題目

methods新增pick(),在data中定義還沒開始之前question: null,用this存取本身的資料屬性,隨機選取陣列裡的其中一個字,記得因為index須為整數所以加上parseInt

//HTML
#app 
  button(@click="pick") 出題囉
  h2(v-if="question") {{ question.english }}
//CSS
html,body
  background-color: #222
  color: #fff
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    ...
    question: null
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
    },
    getOptions(question){
      ...
    }
  }
})
英文2000字選擇題互動網頁:步驟四,出題按鈕
英文2000字選擇題互動網頁:步驟四,出題按鈕

5. 建立選項

把單字的中文抓出來印成題目,同時也把選項抓出來存取,一開始會是空的陣列所以在data定義options: [],再在pick()中多加一行程式碼把產生的新題目裝回去。

//HTML
#app 
  button(@click="pick") 出題囉
  h2(v-if="question") {{ question.trans }}
    ul
      li(v-for="option in options") {{option.english}}
//CSS中要先把li的樣式暫時註解掉
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    ...
    question: null,
    options: []
  },
  methods:{
    click(word){
      ...
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
      this.options=this.getOptions(this.question)
    },
    ...
  }
})
英文2000字選擇題互動網頁:步驟五,建立選項
英文2000字選擇題互動網頁:步驟五,建立選項

6. 判斷答案正確與否

比較簡單的做法是在產生資料時同時附加他是否正確的資訊在其中,我們複製一份新的question避免影響原本的,在let result2前面加上let questionClone = JSON.parse(JSON.stringify(question)),再把帶有正確與否屬性的物件混到原有的選項中,把questionClone作為判斷的正確答案,而result2中原本的question也要記得替換成questionClone

methods新增check(option),點擊選項時如果正確,console印出correct、不正確則印出wrong,回答完後再重新出題this.pick()

//HTML
...
ul
  li(v-for="option in options",
     @click="check(option)") {{option.english}}
//Javascript
...
  methods:{
    click(word){
      console.log("click",word)
    },
    check(option){
      if (option.correct){
        console.log("correct")
      }else{
        console.log("wrong")
      }
      this.pick() //點選答案無論對錯都會換下一題
    },
    pick(){
      ...
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(
      word => word.english !== question.english
      ).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let questionClone = JSON.parse(JSON.stringify(question))
      questionClone.correct=true
      let result2 = result.concat([questionClone]).slice().sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})
英文2000字選擇題互動網頁:步驟六,console顯示出選擇了正確或錯誤答案
英文2000字選擇題互動網頁:步驟六,console顯示出選擇了正確或錯誤答案

7. 增加答題分數計算機制以及顯示正確或錯誤

要增加答題分數grade的計算機制,首先在data中定義grade: 0,在check(option)中如果答對了就加一分this.grade++,並在HTML中顯示。

//HTML
#app 
  h3 Score:{{grade}}
  ...
//Javascript
...
  data: {
    ...
    grade: 0
  },
  methods:{
    click(word){
      ...
    },
    check(option){
      if (option.correct){
        console.log("correct")
        this.grade++
      }else{
        console.log("wrong")
      }
      this.pick()
    },
    pick(){
      ...
    }
  }

目前答題正確與否只能靠分數是否有增加得知,要改成更直觀一點,點擊選項時如果正確,在題目右邊會印出correct並累加分數this.grade++、不正確則印出wrong,我們使用一個預設是空字串的變數status去儲存這個資訊,status設定過1秒後消失,回答完後再重新出題this.pick()

通常使用者在進到介面時題目已經出好了,答題後會自動更新,所以頁面剛載入時便自動執行一次pickmounted代表Vue已經準備好可以幫忙計算資料了,所以跟methodsdatael在同一層級。

//HTML
#app 
  .container
    .row
      .col-sm-12
        h3 Score:{{grade}}
        h2(v-if="question") Q: {{question.trans}}
          .status {{status}}
        ul 
          li(v-for="option in options", @click="check(option)") {{option.english}}
//Javascript
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4,
    question: null,
    options: [],
    status: "",
    grade: 0
  },
  mounted(){
    this.pick()
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    check(option){
      if (option.correct){
        this.status =("correct")
        this.grade++
      }else{
        this.status = ("wrong")
      }
      setTimeout(()=>{
        this.status=""
        this.pick()
      },1000)
      this.pick()
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
      this.options = this.getOptions(this.question)
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(word => word.english !== question.english).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let questionClone = JSON.parse(JSON.stringify(question))
      questionClone.correct = true
      let result2 = result.concat([questionClone]).sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})
英文2000字選擇題互動網頁:步驟七,增加答題對錯的顏色回饋

8. 設計畫面及加入動畫

完成骨幹後我們來稍微美化一下吧!在codepen設定CSS的地方引入Bootstrap和Animate.css,把DOM元素放到container內,讓版面豐富一些可以加入hover的滑鼠互動效果和答對或答錯時相對應的變色效果。

變色效果我們可以利用Vue的特性來製作──透過判斷式給予class,也就是判斷當前的status為correct或wrong,如果是correct則顯示綠色、wrong則顯示橘色,必須注意的是由於其他四個錯誤答案的status同時都會是wrong,所以要多下一個判斷點currentOption記錄只有點選的這個選項是wrong時才顯示橘色。在data定義currentOption: {}methodscheck(option)的加入條件this.currentOption = option

英文2000字選擇題互動網頁:步驟八,增加答題對錯的顏色回饋
英文2000字選擇題互動網頁:步驟八,增加答題對錯的顏色回饋

淡入的效果我們則透過Animate.css和Vue的key來製作,每次物件重新產生時都帶有新的english值,所以我們給予的key值也會不一樣,這樣他的animated.fadeIn效果就會重新被載入。

//HTML
#app 
  .container
    .row
      .col-sm-12
        h3 Score:{{grade}}
        h2.animated.fadeIn(v-if="question",:key="question.english") Q: {{question.trans}}
          .status {{status}}
        ul.animated.fadeIn(:key="question.english")
          li(v-for="option in options", @click="check(option)",:class="{correct: status=='correct'&& option.correct, error:status=='wrong' && currentOption.english==option.english}") {{option.english}}
//CSS
ul
  list-style: none
  padding: 0
  li
    padding: 10px
    margin-top: 20px
    border: 1px solid white
    cursor: pointer
    font-size: 30px
    transition: .5s
    &:hover
      background-color: rgba(white,0.1)
    &.correct
      background-color: #38d138
    &.error
      background-color: #ff7332
      
.status 
  float: right
//Javascript
...
var vm = new Vue({
  el: '#app',
  data: {
    name: "Frank",
    catas: ["a","b","c","d"],
    words: list4,
    question: null,
    currentOption: {}, //給予{}以防報錯
    options: [],
    status: "",
    grade: 0
  },
  mounted(){
    this.pick()
  },
  methods:{
    click(word){
      console.log("click",word)
    },
    check(option){
      this.currentOption = option //加入條件
      if (option.correct){
        this.status =("correct")
        this.grade++
      }else{
        this.status = ("wrong")
      }
      setTimeout(()=>{
        this.status=""
        this.pick()
      },1000)
    },
    pick(){
      this.question = this.words[parseInt(Math.random()*this.words.length)]
      this.options = this.getOptions(this.question)
    },
    getOptions(question){
      let result = this.words.filter(
      word => word.cata == question.cata).filter(
      word => word.english[0] == question.english[0]).filter(word => word.english !== question.english).slice().sort((a,b)=>Math.random()-0.5).slice(0,4)
      let questionClone = JSON.parse(JSON.stringify(question))
      questionClone.correct = true
      let result2 = result.concat([questionClone]).sort((a,b)=>Math.random()-0.5)
      return result2
    }
  }
})

以上就是這次的英文2000字即時互動小遊戲網頁的製作介紹拉,這次講解了許多Vue的基礎概念與用法,如果是剛開始接觸Vue的朋友很適合拿來小練身手唷!我們下次再見啦~

成品請參考這邊 👉🏻 https://codepen.io/frank890417/pen/RygVde

英文2000字選擇題互動網頁成果

重點回顧:

  1. 利用filter()、sort()、slice()整理與排序資料
  2. 宣告式渲染的使用方式,資料與function的對應關係
  3. 動態判斷class製作css animation效果

動畫互動網頁程式入門(HTML/CSS/JS)以簡單例子帶你入門網站的基礎架構及開發,用素材刻出簡單有趣又美觀的網頁和動畫,享受做出獨一無二的網頁所帶來的成就感,在職場上與設計師和工程師合作無間。

打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。長達3085分鐘,超過60個精緻範例與400張的投影片以上,以及四個加碼單元vue-cli、GSAP、D3、Three.js的投影片,成為hahow上最長的課程。

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

墨雨設計banner

這篇文章 Vue.js入門:英文2000字互動遊戲網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>