CSS 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/category/tutorial/css/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Mon, 11 Oct 2021 15:03:08 +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 CSS 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/category/tutorial/css/ 32 32 【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 - 互動程式創作台灣站

]]>
Vue.js入門:製作iOS風格的動態月曆與待辦清單網頁 https://creativecoding.in/2021/08/16/vue-js-ios%e9%a2%a8%e6%a0%bc%e5%8b%95%e6%85%8b%e6%9c%88%e6%9b%86%e8%88%87%e5%be%85%e8%be%a6%e6%b8%85%e5%96%ae%e7%b6%b2%e9%a0%81/ Mon, 16 Aug 2021 02:17:00 +0000 https://creativecoding.in/?p=1380 手機裡的動態月曆.只消一指就可以增加或是刪除行程,有想過要怎麼在網頁上實現嗎?老闆利用簡單的動態網頁範例和步驟解說,帶你一步步踏進Vue.js的世界。

這篇文章 Vue.js入門:製作iOS風格的動態月曆與待辦清單網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
使用 vue 製作 ios 動態月曆與待辦清單
使用 vue 製作 ios 動態月曆與待辦清單

本文翻自 [週四寫程式系列] – 來做 IOS 動態月曆結合待辦行程吧!直播影片,若是想要老闆手把手帶你飛可以跟著影片做,這邊也附上這次成品歡迎大家一起動腦動手做。

這次老闆要帶大家來做個 ios 動態月曆與待辦清單,本篇會提到行事曆與待辦清單的畫面切版,並利用這個專案來學習使用 vue 的資料與畫面綁定。

礙於直播時間,這次專案僅會實現以下功能:產出假的月份資料來展示、換算農曆、偏移天數、切換不同天觀看該天工作項目、新增工作、刪除工作、工作項目排序以及 vue 的進出場動畫。

完成這次專案後,大家也可以發想還有什麼功能可以加進來,例如月份時間的切換,不同類別的事件項目…等。如果想要了解更詳細的製作流程和其他創作內容,可以去支持老闆的網頁課程哦!

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

  • sass – mixin:可以將重複使用的 CSS 做成工具,減少重複 css 樣式的撰寫。
  • css – flex 排版
  • vue:資料與畫面綁定、生命週期 mounted

製作動態月曆網頁的事前準備

開發環境

開發使用 codepen 線上撰寫程式碼,大家可以先將環境設定成跟老闆一樣,如果想知道較詳細的設定,可以到成品這邊看老闆的設定。

  • html: Pug
  • css: reset
  • js: vue.js

會使用到的 API:

這次專案會使用以下的 api,先重點整理給大家,不清楚的地方,可以透過後面跟著老闆操作,了解每個 api 使用時機。

vue – javascript

  • el:資料要綁定的區塊
  • data:vue 要綁定的資料放置處
  • mounted: vue 的生命週期, el 被掛載之後會執行裡面的程式碼
  • methods:使用到的 function 放置處
  • computed:計算屬性,會因為 data 內的值改變,而跟著變動
// javascript
var vm = new Vue({
  el: '#app',
  data: {
    text: 'Hello World',
    texts: ['H', 'i']
  },
  mounted () {...},
  methods: {
    changeText () { ... }
  },
  computed: {
    showText () { return ...}
  }
})

vue – html:畫面部分會使用到以下內容

  • {{text}}:將資料綁定到畫面中顯示
  • v-model:將資料綁定到畫面中顯示或修改
  • :class:動態綁定屬性,也等於 v-bind:class=””,簡寫為 :class,也可以綁定其他屬性
  • v-for:讓陣列資料重複產生 dom,可以搭配索引值綁定
  • :key:可以搭配 v-for 使用,提供 vue 識別每個 dom 是不同的,在傳入 v-for 的陣列中,key 要是獨特的值,避免識別上出錯。
  • @click=””:當點擊目標物會觸發傳入 click 的內容
// html
#app
  p {{text}}
  input (v-model="text)
  p(v-model="showText")
  div(:class="")
  div(v-for="(item, idx) in texts", :key="idx") {{item}}
  button(@click="changeText()")

vue – transition-group:

vue 提供給在 dom 要被加入、移除或更新時的動態效果,使用方法會在後面實做中解說。若想要參閱官方說明文件可點此閱讀

Javascript

  • Math.random():會產出一個大於等於 0、小於 1 之間的隨機小數。若想要參閱更詳細的說明文件可點此閱讀
  • sort:對一個陣列的所有元素進行排序,並回傳此陣列,可以自訂規則函式做為參數傳入。若想要參閱說明文件可點此閱讀

Sass

mixin,可以將重複使用的 CSS 做成工具, 也能傳入參數、參數名稱以 $ 開頭。使用方法如下,後面實作中也會再帶大家操作一次。

//CSS
@mixin size($w, $h: $w)
  width: $w
  height: $h

css

這次專案會頻繁使用 flex 來排版,以下介紹常見的樣式,實際操作可以透過提供的遊戲及專案製作的過程了解。

  • display: flex:預設會將子層水平排列不斷行。
  • flex-wrap: nowrap | wrap | wrap-reverse 預設為 nowrap,超過寬度的子元素是否換行。
  • flex-direction: row | row-reverse | column | column-reverse 主軸線的方向,預設為橫向 row。
  • justify-content: 元素在主軸上排列的方式。
  • align-items: 元素在副軸上排列的方式。

下面舉例這段 css 會呈現的畫面,同學也可以嘗試看看換成不同數值,只看文件無法理解的話,可以透過遊戲學習 flex 的系列屬性,網路上有非常多的相關遊戲,這邊提供其中一款遊戲 FLEXBOX FROGGY大家可以挑戰一下!

css的flex相關程式碼說明圖
css的flex相關程式碼說明圖

跟著老闆開始動手做

1. 畫面切版

1-1 mixin

同學們在樣式開發中,肯定遇過重複撰寫相同程式碼的情況,如果有方法可以減少這種情況,除了加速開發外,也能讓程式碼更簡潔。Sass 中有一個工具 mixin,就能滿足我們的需求,不用一直重複造輪子,就可以在需要的位置引入,甚至可以傳入參數,傳入的參數要以 $ 符號為開頭,下面兩個 mixin 分別展示,有沒有傳入參數的差異,並教大家如何引用到 Sass 中。

第一個 mixin 工具為設定長寬,傳入的第一個參數會被 mixin 做為 width 的值,第二個參數則為 height,若是沒有傳入第二個參數,則會將第一個參數做為第二個參數傳入。

第二個 mixin 工具為使用 flex 將內容物垂直置中。

// CSS
@mixin size($w, $h: $w)
  width: $w
  height: $h

@mixin flex_center
  display: flex
  align-items: center
  justify-content: center
  flex-direction: column

.box
  +size(100px, 100px) // 也可寫為 +size(100px)
  +flex_center

1-2 vue 資料畫面綁定

雖然是畫面切版階段,但是老闆先帶大家認識 vue 畫面綁定資料的方式,讓大家不用在 html 中,用土法煉鋼的方式將星期分別輸入。

首先我們先創造一個 vue 實體,裡面的 el 值為資料要綁定的區塊,所以將 #app 填入, 這時html 中 id 為 app 的區塊就能進行畫面與資料綁定,data 內的值為資料,可以將要綁定或操作的資料放在此處。

要實現星期的內容能夠與畫面綁定,先在 data 中新增 key 值 tags,內容為每天的字串,在 html 中可以使用 v-for 將陣列的資料綁入,在 html 中可以看到 .tag 的位置後面有使用 v-for,tag 為每一個 tags 資料內的值,雙花括{{tag}}則可以將資料顯示在畫面中。這邊比較有趣的是,因為資料較單純,也可以將 tags 設定為字串去跑 v-for ,結果也會一樣。

// html
#app
  .phone
    .calender
      .head
        .tag(v-for="tag in tags") {{tag}}
      .body
        .daybox.active(v-for="i in 31")
          .infos
            .num {{i}}
            .lunar 初一
          .eventdot
// javascript
var vm = new Vue({
  el: "#app",
  data: {
    tags: ["日","一","二","三","四","五","六"]
    // tags: "日一二三四五六"
  }
})

1-3 畫面樣式切版

開始套資料前,先將畫面準備好,再來做資料與畫面綁定,利用前面的 mixin,將行事曆水平垂直置中在畫面中間,並將 .phone 設定長寬與背景色。.phone 中我們將行事曆分為兩層, .head 顯示日到六的標題, .body 則顯示各天日期、農曆與當天是否有工作項目。

要怎麼做到七個項目就會斷行呢?之前老闆有帶大家使用過 inline-block,應用在這邊會發現到了第六個項目,就會神奇地斷行,這是因為 inline-block 有一些預設樣式,導致父層寬度不夠,就把第七個項目往下推了。所以我們這邊改在父層使用 flex,flex 預設會將子元件併排在同一行,所以要斷行必須要加上 flex-wrap: wrap,當寬度不夠時,子元件就會換行。

至於 .daybox 中有 &.active 則是用來作為被選定時的樣式,該天被選定後,日期與農曆會被變成黑底白字,這邊我們先模擬此樣式,在後面資料綁定後,就能針對特定項目加上樣式。

//CSS
@mixin size($w, $h:$w)
  width: $w
  height: $h

@mixin flex_center
  display: flex
  justify-content: center
  align-items: center
  
html, body, #app
  +flex_center
  +size(100%)
  background-color: #333
  color: #555

.phone
  +size(360px, 560px)
  background-color: #fff

.head
    display: flex
    padding-top: 50px
    padding-bottom: 5px
    .tag
      width: calc(100% / 7)
      text-align: center
      font-size: 12px

.body
  display: flex
  flex-wrap: wrap
  .daybox
    +flex_center
    flex-direction: column
    width: calc(100% / 7)
    text-align: center
    padding: 5px 0px
    &.active
      .infos
        background-color: #222
        border-radius: 50%
        color: #fff
    .infos
      +size(40px)
      .num
        font-size: 20px
        padding-top: 5px
      .lunar
        font-size: 12px
    .eventdot
      +size(6px)
      background-color: #ddd
      border-radius: 50%
      margin-top: 5px
iOS風格動態月曆與待辦事項網頁:步驟一,畫面切版
iOS風格動態月曆與待辦事項網頁:步驟一,畫面切版

2. 選定日期

有了基本畫面,接著要將畫面綁定資料。前面我們天數是使用 v-for 直接針對數字 1~ 31 跑迴圈,這邊調整成在 mounted 時,創造 31 天的資料後存放到新的資料變數 days 中,代入到畫面。mounted 是 vue 生命週期的其中一個階段,會在元素被掛載且 $el(實體) 建立好的時候執行,可以理解成:當要放置內容的 DOM 產生時,執行 mounted 內的程式碼。

再來,我們要實現只會有一天被選定的功能,也就是只會有一天的 daybox 會有 active 的 class (黑色圓圈背景),使用者的互動會是點選不同天時,當天的樣式就會改變。

先改寫 v-for ,在前面的 day 改為 (day, day_idx),這個值就是 day 在 days 中的索引值。並在 data 新增一個 selected 值,記錄目前被觸發的為何者,接著使用 vue 中的語法 @click,當該 daybox 被按下時,將目前被選定的值改為被選定的 day_idx。結合前面學到的動態綁定 class,來將 active 的 class 賦予給 daybox ,也為 daybox 加上手指游標的樣式。就可以達成「當 selected 的值等於該天的 day_idx 時,就會為其加上 active 的 class」。

// HTML
...
.daybox(v-for="(day, day_idx) in days", @click="selected = day_idx", :class="day_idx === selected ? 'active' : ''")
  .infos
    .num {{day.number}}
    .lunar 初一
  .eventdot
// CSS
.daybox
  ...
  cursor: pointer
// javascript
...
data: {
  tags: ["日","一","二","三","四","五","六"],
  days: [],
  selected: 0 // 目前被選定的值
},
mounted() {
  for(var day=1; day<=31; day++){
    var new_day = {
      number: day
    }
    this.days.push(new_day);
  }
}
...
iOS風格動態月曆與待辦事項網頁:步驟二,選定日期
iOS風格動態月曆與待辦事項網頁:步驟二,選定日期

3. 農曆日期換算與偏移天數

接下來的動作,會使用到 vue 的 methods,可以將 methods 的功用理解成,將需要使用的 function 統一管理,需要使用的時候就能直接呼叫來觸發這些 function。

在農曆的日期換算的部分,雖然我們的月曆是假資料,為了仿真,讓這份月曆第一天的農曆,不是從初一開始,我們在 mounted 的一開始,新增一個 lunar 變數,做為農曆的偏移天數。每次 for 迴圈結束前,將 lunar 變數加一,使農曆一直往前推移,農曆數字轉化為國字的規則如下:

  • 如果傳入的 lunar 參數超過 30,用餘數換算成 0~29,確保農曆永遠從初一到三十中循環
  • lunar 小於等於 10,用”初”為開頭,並從國字字串取出特定位置的值組裝在一起
  • lunar 小於 20 時,用”十”為開頭,並從國字字串取出特定位置的值組裝在一起
  • lunar 等於 20,直接顯示二十
  • lunar 小於 30 時,用”廿”為開頭,並從國字字串取出特定位置的值組裝在一起
  • lunar 等於 30,直接顯示三十

我們也製造一個偏移天數 start_day 變數,讓這份月曆的 1 號,不一定從周日開始排序。利用 get_pen(d) 傳入一個偏移天數,讓第一天的 DOM 產生一個 margin-left 的 style。

// HTML
...
.daybox( v-for="(day, day_idx) in days", @click="selected = day_idx", :class="day_idx === selected ? 'active' : ''", :style="get_pan(day_idx)")
  .infos
    .num {{day.number}}
    .lunar {{lunar(day.lunar)}} //農曆換算
  .eventdot
// javascript
var vm = new Vue({
  data:{
    ...
    start_day: 2
  },
  mounted () {
    var lunar = 6; // 農曆偏移天數
    for(var day=1; day<=31; day++){
      var new_day = {
        number: day,
        lunar: lunar, // 傳入的每天資料新增一個紀錄農曆的數字
        events: [],
      }
      this.days.push(new_day);
      lunar++ // 下一天能拿到新的農曆值
    }
  },
  methods: {
    chinese_num(num) {
      var list = "十一二三四五六七八九";
      return list[num];
    },
    lunar(num) { // 換算農曆
      if (num > 30) num = num % 30;
      if (num <= 10) {
        return "初" + this.chinese_num(num % 10);
      } else if (num < 20) {
        return "十" + this.chinese_num(num % 10);
      } else if (num == 20) {
        return "二十";
      } else if (num < 30) {
        return "廿" + this.chinese_num(num % 10);
      } else if (num == 30) {
        return "三十";
      }
    },
    get_pan(id){ // 第一天的偏移位置
      if (id==0){
        return { "margin-left": "calc( "+this.start_day+" * 100% / 7)"};
      }
    },
  },
  ...
})
iOS風格動態月曆與待辦事項網頁:步驟三,農曆日期換算與偏移天數
iOS風格動態月曆與待辦事項網頁:步驟三,農曆日期換算與偏移天數

4. 產生每天工作項目

工作內容的資料會頻繁使用 js 原生語法 – Math.random() 來製作。Math.random() 在沒有輸入參數時,會隨機產生 0~1之間的小數。利用這點,可以拿來製作當天是否有工作、工作量、工作項目、時間、工作類型。

  1. 是否有工作:新增每天資料時,利用 random 產出的數字,達成機率性決定當天有沒有工作。若設定小於 0.4 ,則代表有 4/10 的機率當天會有工作,繼續跑下面產出工作內容的程式碼,否則 events 就是空陣列。目前階段有沒有工作只會決定該天有沒有小灰點,後面會介紹每天的工作清單如何製作。
  2. 工作量: Math.ranodm() * 3,會產出 0~3間的隨機小數,將產出的數值代入以下的 for 迴圈,就會隨機產出 0~3 項的工作。
  3. 工作項目、工作類型:將所有工作項目的陣列代入,並使用前面的產出的單日工作量的整數值作為 id ,去取得工作項目陣列中對應的工作名稱。這邊要注意,random 值除了要轉為整數外,也要等於工作項目陣列的長度,避免程式碼取到 undefined 的值。
  4. 時間:使用 Math.random() 產出時間字串。時間顯示的格式,老闆設定如下: 小時的區塊為 0~24 小時,中間組合 : 符號,分鐘的部分避免程式碼太過複雜,在前面先創造分鐘變數,利用 parseInt 讓分鐘的值只會有四種 0, 15, 30, 45。要注意的是,如果只有 0,我們希望呈現的會是兩個 0 ,所以在這邊用三元運算子判斷,如果 minute 的值為 0,則在前面多補一個 0。

此時,31 天的資料順利產生,利用 events 是否有內容,判斷要不要呈現小灰點,畫面上也能使用資料來動態綁定 class,當該天存在工作項目時,則為 eventdot 加上 has_eventclass。再開發時,同學也可以使用 codepen 中的 console 工具,檢查該天是不是有工作內容,方法為 vm.days[日期].events

// HTML
...
.body
  .daybox( v-for="(day, day_idx) in days", @click="selected = day_idx", :class="day_idx === selected ? 'active' : ''", :style="get_pan(day_idx)")
    .infos
      .num {{day.number}}
      .lunar {{lunar(day.lunar)}} // 農曆換算
    .eventdot(:class="{'has_event': day.events.length > 0}") // 當天是否有工作
// CSS
...
.eventdot
  +size(6px)
  background-color: #ddd
  border-radius: 50%
  margin-top: 5px
  opacity: 0 // 當天沒工作的樣式
  &.has_event
    opacity: 1 // 當天有工作的樣式
// javascript
...
mounted() {
  var lunar = 6;
  for (var day = 1; day <= 31; day++) {
    var new_day = {
      number: day,
      lunar: lunar,
      events: []
    };

  if (Math.random() < 0.4) { // 機率性決定當天有沒有工作
    var count = parseInt(Math.random() * 3); // 新增0~3項工作
      for (var o = 0; o < count; o++) {
        var minute = parseInt(Math.random() * 4) * 15; // 產出 0, 15, 30, 45
        new_day.events.push({
          title: ["整理房間丟垃圾", "出門參加活動", "打包行李"][parseInt(Math.random() * 3)], // 從工作陣列中,隨機取一個值
          time: parseInt(Math.random() * 24) + ":" + (minute == 0 ? "0" : "") + minute // 如果分鐘數為 0,則補一個 0 為開頭
        });
      }
    }
    this.days.push(new_day);
    lunar++;
  }
},

5. 待辦事項與項目排序

完成日曆的顯示後,要開始製作當天的工作項目清單 todo_list 。我們會使用 vue 的另外一個功能 computedcomputed 無法傳入參數,會因為 data 內資料變動,動態改變回傳的結果,也就是將資料代入後重新計算的屬性。當使用者選擇不同天,當天如果有工作內容,就會顯示該天的工作項目。

使用 data 內的 days 陣列與 selected ,能夠取得目前選擇的當天資料,這邊要注意的是,因為一開始 days 長度是 0,取當天資料會失敗,所以要多使用 if 判斷式,在資料還沒創造完畢前,避免回傳的值出錯。

取得當天資料後,我們想把工作項目依照時間排序,因為創造工作項目時,時間是隨機產生,所以畫面工作項目順序是隨機排列的,利用 js 的原生語法 sort 來進行時間排序。sort 內可以傳入一個函式做為規則,我們傳入一個 function 作為比較的規則,將傳入的兩個值進行比較,將不必要的分號取代成空值後,時間會變成四位數的字串來做比較。

todo 的畫面樣式如下,因為篇幅原因,先不針對不同工作項目給予不同樣式,如果同學想嘗試,做法與前面相同,只要對每一筆工作多設定一個值來記錄工作類別,針對不同的工作類別,給予不同的樣式即可。如果在製作上有問題,可以參考文章中附上老闆的成品,了解詳細的做法。

// HTML
#app
  .phone
    .calender
      .head ...
      .body ...
      .todos
        .item(v-for="(todo,id) in current_items", :key="todo")
          .time {{todo.time}}
          .title {{todo.title}}
// CSS
.head,.body
  border-bottom: solid 1px rgba(black,0.1)
  background-color: #f7f7f7

.body
  padding-bottom: 10px
  ...

.todos
  .item
    padding: 3px 10px
    display: flex
    height: 40px
    border-bottom: solid 1px rgba(0,0,0,0.1)

    .time, .title
      padding: 4px 10px

    .time
      width: 55px
      border-right: solid 2px
      border-color: #3ca9f2
// javascript
...
computed: {
  current_day(){
    return this.days[this.selected];
  },
  current_items(){
    var day=this.current_day;
    if(!day)
      return null;
    else
      return day.events
	        .sort((a,b)=>(parseInt(a.time.replace(":",""))-parseInt(b.time.replace(":","")) ));
  }
}
iOS風格動態月曆與待辦事項網頁:步驟五,待辦事項與項目的排序
iOS風格動態月曆與待辦事項網頁:步驟五,待辦事項與項目的排序

6. todo 新增與移除工作

除了顯示隨機產生的工作項目外,我們希望也能在每天的工作項目中產生新工作,或是刪除不需要的工作。

新增資料的部分,先在畫面產出兩個 input 區塊,來輸入工作名及時間,並新增一個 button 按鈕,來做最後的新增按鈕。在成品中,老闆還有放入工作類型的選擇,大家也可以挑戰看看。

新增資料需要注意的部分是,由於新增的資料是傳物件參考進去,所以要新增新的資料時,已經加進去的也會被移動,所以我們將物件字串化之後,需要再把它轉回物件,確保傳入的資料是一組全新的。

刪除資料部分,我們只要在 .item 中新增一個 div,當點擊它時,會從目前的 now_events,使用 splice 刪除被點選的該筆資料。

// HTML
#app
  ...
  .phone
    .calender
      ul.todos
        .item(v-for="(todo,id) in now_events", :key="todo")
          .time {{todo.time}}
          .title {{todo.title}}
          .close_btn(@click="now_events.splice(id,1)") x // 刪除工作項目按鈕
  .form
    input(name="title" v-model="newtodo.title" placeholder="標題")
    input(name="time"  v-model="newtodo.time" placeholder="時間")
    button(type="submit" @click="add_item") +
// CSS

.form
  box-sizing: border-box
  padding: 10px
  position: absolute
  bottom: 0px
  width: 100%
  left: 0
  +flex_center
  flex-direction: row
  input,select
    box-sizing: border-box
    margin-right: 10px
    padding: 5px 10px
    border-radius: 2px
    min-width: 150px
    height: 30px
    color: white
    background-color: transparent
    border: none
    border: solid 1px white
  input[name='title']
    width: 300px
// javascript
...
data: {
  ...
  newtodo: {
    title: '',
    time: ''
  }
},
methods: {
  ...
  add_item(){
    this.days[this.selected].events.push(JSON.parse(JSON.stringify(this.newtodo)));
  }
},
computed: {
  now_events(){
    var day = this.days[this.selected_day];
    if (day)
      return day.events;
    else
      return [];
    console.log();
  },
  current_day)_{
    ...
  }
}
iOS風格動態月曆與待辦事項網頁:步驟六,待辦事項的新增與移除
iOS風格動態月曆與待辦事項網頁:步驟六,待辦事項的新增與移除

7. 新增刪除工作加上動態

最後一步,要在執行新增或刪除時能加上動態,會使用到 vue 的 transition-group,使用方法如下:

ul.todos 改寫成 transition-group.todos,屬性中 tag=”ul” ,會將這層 DOM 換成 ul 使用。name 為要賦予的動態名稱,mode 為動畫進出方式,這邊使用的是舊的先離開,新的再進來。使用上需要注意,因為 transition-group 跑動態是群組進來群組出去,所以每個子元件都要獨一無二 key,需要在子層加上 key,讓 vue 辨識出每個 DOM 都是獨特的,避免產生奇怪的動畫。

接著來解說 name 裡面填寫的動態要怎麼使用,只是寫上 name 並不會有動態,我們要依照 vue 的規格,在 sass 中加上動畫的名稱,分別會使用到:

  • fade-enter-active, fade-leave-active:準備要進入的時候,準備要離開的時候
  • fade-enter, fade-leave-to:進來之前和離開之後的狀態
// HTML
...
transition-group.todos(tag="ul", name="fade", mode="out-in")
  .item(v-for="(todo,id) in now_events", :key="todo")
    .time {{todo.time}}
    .title {{todo.title}}
    .close_btn(@click="now_events.splice(id,1)") x
// CSS
.fade-enter-active, .fade-leave-active
  transition: 0.5s
.fade-enter, .fade-leave-to
  opacity: 0
iOS風格動態月曆與待辦事項網頁完成圖
iOS風格動態月曆與待辦事項網頁完成圖

老闆來結語

讓我們快速回顧一下製作動態日曆的流程:

  1. 行事曆的靜態資料切版、使用 vue 將星期名稱綁定到畫面中的方式
  2. 選定特定日期後,用 vue 動態改變屬性
  3. 農曆日期換算與偏移、天數的偏移:達成該份資料的第一天不是初一,也不是當月1號就從周日開始排序
  4. 使用 Math.random() 隨機產出每天的工作項目
  5. 待辦事項切版與項目依照時間排序
  6. 新增待辦事項與移除待辦事項
  7. 使用 vue – transition-group 新增與移除動作的動態

萬事起頭難,一個作品不可能一步到位,將最終目標拆分成不同階段任務,從一開始的雛型慢慢開發出每個區塊,最後組裝在一起,也可以加上個人的創意去實現其他功能,讓作品更豐富。

礙於直播時間,老闆沒有將所有功能都實現,但製作方式與前面提到的內容雷同,大家可以挑戰看看,例如工作項目多一個類別屬性,在不同類別時顯示不同的樣式,也可以發想其他功能沒提到的功能,例如月份時間用真實的時間去換算、切換年月份功能等…。

再附上這次範例的成品,讓大家在開發時參考。

如果你喜歡老闆的教學,歡迎加入老闆開的課程中一起學習,順便支持一下老闆,課程會帶你看看不一樣的作品,並引導大家一步步完成作品,透過每次的賞析、實作到修正作品,讓寫 code 不再是這麼困難的一件事情,將這個過程想像成,拿一隻比較難的畫筆在進行創作,如果有機會使用它,便能夠在網頁上做出與眾不同的創作。

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

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

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

墨雨設計banner

這篇文章 Vue.js入門:製作iOS風格的動態月曆與待辦清單網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
Vue.js入門:完成懷舊的井字圈叉遊戲動態網頁 https://creativecoding.in/2021/08/10/vue-js-%e6%87%b7%e8%88%8a%e7%9a%84%e4%ba%95%e5%ad%97%e5%9c%88%e5%8f%89%e9%81%8a%e6%88%b2%e5%8b%95%e6%85%8b%e7%b6%b2%e9%a0%81/ Tue, 10 Aug 2021 03:33:00 +0000 https://creativecoding.in/?p=1354 小時候都玩過圈圈叉叉的遊戲,隨手畫個井字就可以與玩伴鬥智,這次我們要利用Pug、Sass及Vue.js製作出這款動態小遊戲網頁啦!

這篇文章 Vue.js入門:完成懷舊的井字圈叉遊戲動態網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
想要探索 Vue 前端框架嗎?本次的圈圈叉叉範例可以帶你一同進入 Vue 世界中,認識前端框架的強大與方便之處。圈圈叉叉是我們每個人小時候都有玩過的遊戲,規則簡單易上手,在本次範例中會實作出以下功能:

  1. 透過 Vue 產生九宮格的框框,當玩家點擊時會顯示出圈圈叉叉
  2. 遊戲過程中系統會不斷比對資料,找尋是否有贏家產生,當有贏家產生時會顯示出哪一方勝利
  3. 可將遊戲畫面清除,重新開始進行一局新的遊戲
  4. 顯示當下該輪到哪一位玩家進行出手

在開始製作之前你該知道的 Js 操作與 Css 屬性

Js操作:

  • filter:篩選出符合贏家條件的資料
  • map:處理九宮格資料時的陣列的轉換
  • reduce:可將數字相加後,用於贏家判斷

Css排列版面的利器flex

  • display: flex:預設會將子層水平排列不斷行
  • flex-wrap: nowrap | wrap | wrap-reverse 預設為 nowrap,超過寬度的子元素是否換行
  • flex-direction: row | row-reverse | column | column-reverse 主軸線的元素排列方向,預設為橫向順排 row
  • justify-content: 元素在主軸上排列的方式
  • align-items: 元素在副軸上排列的方式

事前準備

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

1. 用CSS建立外框線以及圈圈圖示

首先建立名稱為 size 的 mixin,可以讓後續在使用上比較方便,像是繪製框線。

//HTML
.block
//CSS
@mixin size($w, $h:$w)
  width: $w
  height: $h

.block
  +size(150px)
  border: solid 1px

接著要來建立框線以及繪製圈圈的圖示,在繪製圈圈圖示上,會使用擬元素 beforeafter,這裡可以嘗試在 content 中加上符號來顯示看看效果。不過如果沒有要顯示字元的話,像是本次範例所要的是畫幾何圖形,一樣需要加上 content,雙引號中間空白無文字,不可直接省略。

//HTML
.block
.block.circle
//CSS
@mixin size($w, $h:$w)
  width: $w
  height: $h

.block
  +size(150px)
  border: solid 1px
  &.circle
    &:after, &:before
      content: ""
      display: block

不管是圈圈與叉叉,都是由 :after:before 這兩個偽元素左組成,在圈圈上,是兩個圓形疊在一起所產生視覺效果,透過將中間的圓圈所設定的顏與背景顏色相同,這樣看上去可產生甜甜圈形狀也就是圓形的效果。

這邊要注意的是,由於在圖層的排列順序上,:after 是在 :before 之上,所以比較大的圓是要寫在 :before 上,而中間的小圓則是寫在 :after 上。

在圓形的置中設定上,先將 :after:before 設定成絕對定位,而由於絕對定位的位置規則是會透過尋找上層非 static 的區塊,當作定位的起始點。為了使 .circle 當成定位起的點,所以在母元素 .circle 加上相對定位 position: relative。接著以左上角為參考點,向右以及向下各移動 50% ,再向上以及向左移動本身一半的長度 (transform: translate(-50%, -50%)),這樣一來就完成置中了。

//CSS
$color_blue: #46f
$color_red: #f35
$color_bg: #222

@mixin size($w, $h:$w)
  width: $w
  height: $h

html, body
  background-color: $color_bg
  margin: 0
  +size(100%)

.block
  +size(150px)
  border: solid 1px

  &.circle
    position: relative
    &:after, &:before
      content: ""
      display: block
      border-radius: 50%
      position: absolute
      left: 50%
      top: 50%
      transform: translate(-50%, -50%)
    &:before
      +size(90%)
      background-color: $color_red
    &:after
      +size(60%)
      background-color: $color_bg
井字圈叉遊戲:步驟一,建立外框線及圈圈
井字圈叉遊戲:步驟一,建立外框線及圈圈

2. 繼續使用CSS完成叉叉圖示

完成圈圈後,接著來做叉叉。

為了測試方便,所以就先以滑鼠移入區域,也就是 :hover 的方式來寫叉叉的效果。叉叉是由兩條長方形所組成的,所以將 size 的比例改成長方形,也將圓角設定變為 0px。

將長方形個別旋轉正負 45 度,形成交錯的叉叉,這邊可以發現,除了旋轉 (rotate) 之外,也保留了在圓形中所設定的移動 (translate)。原因在於,transform 會複寫掉前面的屬性,為了達到與圈圈一樣的效果,所以也必須指定移動的效果。

此時滑鼠移入有叉叉的效果了,但是切換時很生硬,要達成柔順切換的效果,只要在 &:after, &:before 上加上transition: 0.5s 就可以了。

//CSS
&:after, &:before 
  content: ""
  display: block
  position: absolute
  left: 50%
  top: 50%
  transform: translate(-50%, -50%)
  transition: 0.5s   // 圈叉變化時間 0.5秒
&:hover  // 與 .circle 同一層級
  &:after, &:before
    +size(90%, 15%)
    background-color: $color_blue
    border-radius: 0px
  &:before
    transform: translate(-50%, -50%) rotate(45deg)
  &:after
    transform: translate(-50%, -50%) rotate(-45deg)
井字圈叉遊戲:步驟二,滑鼠hover移上會轉成叉叉
井字圈叉遊戲:步驟二,滑鼠hover移上會轉成叉叉

3. 調整圈叉的共用CSS屬性

在叉叉測試測試完畢後,由於剛剛 hover 只是用來測試,使用 hover 模擬沒問題後,就將其改為 .cross ,並在 html 新增 .cross 的元素。

//HTML
.block
.block.circle
.block.cross
//CSS
.block
  +size(150px)
  border: solid 1px
  position: relative
  &:after, &:before //change the after and before position
    content: ""
    display: block
    position: absolute
    left: 50%
    top: 50%
    transform: translate(-50%, -50%)
    transition: 0.5s
  &.circle
    &:after, &:before
      border-radius: 50%
    &:before
      +size(90%)
      background-color: $color_red
    &:after
      +size(60%)
      background-color: $color_bg
  &.cross 
    &:after, &:before
      +size(90%, 15%)
      background-color: $color_blue
      border-radius: 0px
    &:before
      transform: translate(-50%, -50%) rotate(45deg)
    &:after
      transform: translate(-50%, -50%) rotate(-45deg)
井字圈叉遊戲:步驟三,調整圈叉的共用CSS屬性
井字圈叉遊戲:步驟三,調整圈叉的共用CSS屬性

4. 進入 Vue.js 世界,綁定建立的樣式

在正式進入 Vue.js 的世界前,我們先模擬使用 jQuery的情況 ,在Console中嘗試新增以及拿掉元素上的 class。

//Console
// 抓取 .block.circle,有 circle 這個 class 就將其拿掉
$(".block.circle").toggleClass("circle")
// 抓取 .block,無 circle 這個 class 就加上去
$(".block").toggleClass("circle")

假如使用 jQuery 來寫的話,首先會先建立一個長度為 9 的陣列 (var blocks = [1,0,-1] ),裡面放置的數字分別代表每一個格的狀態,比如說 1 代表圈圈、-1 代表叉叉、0 代表空,有了這些一串數字後,再根據數字去新增與刪減 Class。

這樣可以達到效果沒錯,但是如果可以當資料更新的時候,外觀也就自動更新,不需要自己再去新增與刪除 Class 的話,會是更好的方法,而我們接下來要使用的 Vue 便可以完成這部分。

在使用 Vue 的起手式上,首先需要一個溝通橋梁,類似搬運工的角色,名為 vm,接著要定義作用的範圍,設定為 el: "#app",而在 HTML 最外面一層上加上 #app。這樣就連接完畢,接下來在 Vue 中所做的資料更新都會做用到整個 HTML 上。

接下來要做的功能是,根據在 Js 中的資料,來判定在 HTML 上的元素要不要加上相對應的 Class。先處理 Vue 的部分,由於所設定的是資料相關,所以要擺放在 data{} 之中,裡面新增一個物件, key 為 blocks,value 為 { type: -1 }

接著來到 HTML,在切換 Class 上,需要 v-bind 這個標籤,它是用來綁定與參數相關的,像是Class、href 就會使用它。後面雙引號部分則是來決定要加上的 Class 以及可否能加的條件 ("{ Class : 成立條件 }"),這裡的 Class 不僅可設定一個,也可以是多個,只需要以逗號相隔開來即可。所以下方 HTML 中所代表的是,如果 type 是 1 的話,那就加上圈圈,而如果 type 是 -1 的話,那就加上叉叉。大家可以利用Console嘗試更新 blocks: { type: -1 } 中 type 數字(輸入 vm.blocks.type=1或0或-1),來觀察外觀上的變化。

//JS
var vm = new Vue({
  el: "#app",
  data: {
    blocks: { type: -1 },
  }
});
//HTML
#app
  .block(v-bind:class="{ circle: blocks.type == 1, cross: blocks.type == -1 }")
  //刪除先前的.block.circle及.block.cross

5. 用 Vue.js 的 Array 產生九個框框

在前一個步驟,完成了最左上角的單一方塊在外觀與資料的連結,但是圈圈叉叉是九宮格,所以老闆使用 Array.from 來產生九個 { type: 1 } 並存放到 Blocks 中,讓 Blocks 是長度為九的陣列。

接著我們來到了 HTML,回想起當需要存取陣列的每一個元素時,就使用迴圈,而迴圈中所設置的變數可依據需求自行定義,而在 Vue 中也是相同的概念,透過 v-for 來存取陣列中的每一個元素,使用方式為 v-for="自訂義名稱 in 陣列" ,由於 blocks 長度是九,所以 Vue 就會在畫面上產生九個.block元素,在這裡老闆使用了 block 來當作識別的變數,所以在判斷 Class 上名稱就是使用 block.type == 1,來判斷每一個 Block 所設置的 type 是不是等於 1。

由於現在設定上都是圈圈 (type: 1 ),老闆後來改以隨機變數的方式產生 -1、0、1 ( type: 1 - parseInt(Math.random() * 3) ),這樣一來畫面上不同方格就會各自呈現不同的形狀。

//JS
var vm = new Vue({ 
  el: "#app",
  data: {
    blocks: Array.from({ length: 9 }, function () {
      return {
        type: 1 
      }
    })
  }
});
//HTML
#app
  .block(v-for="block in blocks",
         v-bind:class="{circle: block.type == 1, cross:block.type == -1}")

v-for與v-bind的差別:v-for用來產生陣列裡的多個複製物,並存取或綁定陣列中的各個元素,v-bind:屬性可將符合條件的單個元素綁定屬性

井字圈叉遊戲:步驟五,用 Vue.js 的 Array 產生九個框框
井字圈叉遊戲:步驟五,用 Vue.js 的 Array 產生九個框框

6. 調整HTML及CSS排列成為九宮格

在步驟五,透過 v-for 自動產生九個方塊了,在方塊中也隨機擺放著不同的符號。不過在圈圈叉叉的設置上是水平三格與垂直三格所組成的九宮格,因此要在這組 .block上再新增一個母元素取名為 .block_area,用來規範整個九宮格的大小與位置。我們首先在每個框框中加入編號:

//HTML
#app
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}")
      .small_number {{ bid+1 }}

在 css 的設定上,由於裡面的框框預設上的寬高都是 150px,所以在外框的寬度上就乘上 3 倍。在排列方式上使用 flex,在預設上是當母元素也就是 .block_area 加上 flex 後,下層的子元素 .block 就會依序由左向右排列且不換行。為了達成換行的效果,會需要加上 flex-wrap: wrap ,這樣當寬度超過時,子元素就會自己自動往下排列了。

//CSS
.block_area
  +size(150px*3)
  display: flex
  flex-wrap: wrap
井字圈叉遊戲:步驟六,調整HTML及CSS排列成為九宮格
井字圈叉遊戲:步驟六,調整HTML及CSS排列成為九宮格

結果此時會發現還是不太對,確實有往下排列了沒錯,但是卻以兩格兩格的方式排列。原因在於,在預設上每一個元素的寬度 = Set width + border + padding,以現在的框框來說就會是 150px +1px*2 = 152px,所以當排列到第三個框框時就會超出範圍,自然地就會向下排來排列。為了要讓指定的寬度 150px 涵蓋 border 的寬度,那就要再加上 box-sizing: border-box 就可以囉。

//CSS
.block
  +size(150px)
  border: solid 1px rgba(white, 0.2)
  position: relative
  box-sizing: border-box
井字圈叉遊戲:步驟六ㄓ之之ㄧ之一之一,調整HTML及CSS排列成為九宮格

7. Vue.js中設定重新開始

要如何設定所有物件重置呢?回想一下在第五步驟中,提到框框中的圖形是根據 type 的數字來做變化的,所以要將畫面清空,僅需要將數值統一設定為零即可。然而將清空畫面屬於動作,是需要視情況不斷執行的,所以要將重置的設定寫在 methods 中。

設定好後,會發現畫面上的方格都消失了,原因在於一開始 Vue 的元件建立後,並不會自動執行寫在 methods 中的函式,為了讓元件建立後就初始化將每個方格方格設定為空,必須在 mounted() 也就是 Vue 初始化剛完成時,執行 restart() 函式。

//JS
var vm = new Vue({
  el: "#app",
  data: {
    blocks: [],
  },
  mounted() {
    this.restart()
  },
  methods: {
    restart() {
      this.blocks = Array.from({ length: 9 }, function () {
        return {
          type: 0
        }
      })
    }
  }
});

8. 點擊後下棋

設定好九宮格初始化的格式後,就要來使用滑鼠點擊。前面有提到,在 Vue.js 中與樣式有關的會使用 v-bind,而點擊是動作,這與事件有關則是使用 v-on。所以要達成當點擊框框內後會呈現圈圈的語法如下 v-on:click="block.type=1",表示當偵測到點擊時,就把 type 設定為 1 ,也就是圈圈的形式。此時會發現不管點擊畫面上哪一位置都會變成圈圈。

//HTML
#app
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
           v-on:click="block.type=1")
    .small_number {{ bid+1 }}

確定點擊效果沒問題後,接著來製作輪流下棋。輪流就是將 type 一開始設定為 1,再來就是 -1,並接續輪流交替,由於數值的切換是動作,所以透過將 v-on:click 後面包成一個動作 player_go(block),讓設定的細節到 Vue 中函式做處理。每當有方塊偵測到點擊時,就會傳送當下被點擊方塊設定 type,並且會交替的更換 turn 的數值,讓 turn 在 1 與 -1 兩者之間交替。可以注意到,在 data 中新增了資料 turn: 1,代表遊戲開始是由圈圈這方開始,若遊戲要從叉叉開始,那只要改為 turn: -1 就可以囉。

//HTML
#app
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
           v-on:click="player_go(block)")
      .small_number {{ bid+1 }}
//JS
var vm = new Vue({
  el: "#app",
  data: {
    blocks: [],
    turn: 1
  },
  mounted() {...
  },
  methods: {
    restart() {...
    },
    player_go(block) {
      block.type = this.turn
      this.turn = -this.turn
    }
  }
});

9. 顯示下棋者

在與九宮格同一的層級之下,新增一個 .block.small 的元素,另外會在綁定 vue 中的資料 turn,這樣一來加上相對應的 Class。另外,由於這只是作為提示框而非遊戲操作部分,所以將格子縮小會比較好看一些。

//HTML
#app
  .block_area
    ...
  .block.small(v-bind:class="{circle: turn == 1, cross: turn  == -1}")
//CSS
.block
  +size(150px)
  border: solid 1px rgba(white, 0.2)
  position: relative
  box-sizing: border-box
  &.small //縮小格子
    +size(60px)
井字圈叉遊戲:步驟九,顯示下棋者
井字圈叉遊戲:步驟九,顯示下棋者

10. 理出邏輯,再用Vue.js判斷贏家

本小節是此範例中最核心也是最有挑戰的的地方:要如何判斷有贏家產生,而贏家又是哪一方呢?以下先用圖示講解觀念,再著手進入程式。

圈圈叉叉井字遊戲判斷贏家

在畫面上,每一格都有相對應的數值,圈圈是 1,叉叉是 -1,而都沒有劃記則是 0,那可以得知只要有某一條線的所有數值相加起來為 3 或是 -3 ,這就代表有連成一線。接著要考慮可連成一線的方式有哪些情況,下面以格子的數字做為表示,一共有八種情況,分別是 123 / 456 / 789 / 147 / 258 / 369 / 159 / 357,每當有玩家點選方格時,便會依序檢查上述八種情況的任一是否有連線成功。

接著在進入撰寫連線的程式前,先來認識在 Vue 中新的小夥伴 – computed,它會自動監看在 Vue 中的數值是否有更動了。當有內部有數值更新時,所設定的屬性也會隨之更新。下面以 user() 作為範例,當數值 turn 改變時, user() 所計算過後回傳的值也會改變,而這個值可放在 HTML 中來使用。

//JS
var vm = new Vue({
  ...
  methods: {
    restart(){...
    },
    player_go(block) {...
    },
    computed:{
      user(){
        return this.turn == 1? "O'turn" : "X'turn"
      }
    }
  }
})
//HTML
#app
  h1 {{ user }}
  ...
井字圈叉遊戲:步驟十,顯示出輪到誰下棋
井字圈叉遊戲:步驟十,顯示出輪到誰下棋

接下來就要開來寫連線判定的部分囉,先將前面所提到的連線情況存成一組字串,接著透過字串分割的方式形成陣列,並回傳數值顯示在畫面上:

//JS
computed:{
  pattern_data(){
    var verify_list = "123,456,789,147,258,369,159,357"
    var result = verify_list.split(",")
    return result
  }
}
//HTML
#app
  h1 {{ pattern_data }}
  ...
井字圈叉遊戲:步驟十,寫贏家判斷列
井字圈叉遊戲:步驟十,寫贏家判斷列

為了可以顯示出每一個格子的序號與相對狀態,所以在原本顯示狀態的 type 之上加上屬性 id,由於程式是由 0 開始,格子的序號是由 1 開始,所以 id = i +1。

//JS
restart(){
  this.blocks = Array.from({ length: 9 }, function (d,i) {
    return {
      id: i+1, // 新增序號 
      type: 0
    }
  })
},

這樣子九宮格所呈現的資料會是如下,總共九個物件,每一個物件都會有相對應 id 以及 type

[ { "id": 1, "type": 0 }, { "id": 2, "type": 0 }, { "id": 3, "type": 0 }, { "id": 4, "type": 0 }, { "id": 5, "type": 0 }, { "id": 6, "type": 0 }, { "id": 7, "type": 0 }, { "id": 8, "type": 0 }, { "id": 9, "type": 0 } ]

回到 pattern_data(),依據剛剛所列出的驗證組合,取出相對應的物件,比如說:

[ { "id": 1, "type": 0 }, { "id": 2, "type": 0 }, { "id": 3, "type": 0 }]

在陣列的轉換上使用 map,以 vtext 來代表每一項驗證的組合。由於是要將長度九的陣列依照條件轉換成各個長度為三的陣列,透過使用 filter 來進行來過濾,過濾的條件則是察看序號是否相同,這裡使用到 indexOf,若有相同數值會回傳 1,否則則會回傳 -1 。

取出陣列組合後,可以點擊看看九宮格,確認 type 的是數值是會有變化的,而這個數值就是用來相加並進行判斷的,再次使用 map 來取出陣列中 type 數值的部分,並且透過 reduce 來計算三個數值相加總合。

//JS
var verify_list = "123,456,789,147,258,369,159,357"
var result = verify_list.split(",")
  .map((vtext)=>{
    var add = this.blocks
      .filter((d, i) => vtext.indexOf(i + 1) != -1)
      .map((d) => d.type)
      .reduce((a, b) => (a + b), 0);
    return add;
  })
return result

下方圖片標示出在做完每一項操作後,所產生出的結果。

井字圈叉遊戲:步驟十,.filter() 過濾出驗證的陣列
井字圈叉遊戲:步驟十,.filter() 過濾出驗證的陣列
井字圈叉遊戲:步驟十,.map() 數值 tpye
井字圈叉遊戲:步驟十,.map() 數值 tpye
井字圈叉遊戲:步驟十,.reduce() 相加數值
井字圈叉遊戲:步驟十,.reduce() 相加數值

目前有個每一個驗證規則的數值加總,但是只需要回傳贏家的判斷即可,也就是 3 (三個圈成一線)與 -3 (三個叉成一線),所以使用到 .filter() ,判斷條件為只要絕對值等於 3 就代表有贏家產生了。除了知道有贏家外,也希望可以知道是哪一條線成立的,所以在回傳值的上面也加上了 rule: vtext

//JS
var verify_list = "123,456,789,147,258,369,159,357"
var result = verify_list.split(",")
  .map((vtext)=>{
    var add = this.blocks
      .filter((d, i) => vtext.indexOf(i + 1) != -1)
      .map((d) => d.type)
      .reduce((a, b) => (a + b), 0);
    return { rule: vtext, value: add }
  })
result = result.filter((obj) => Math.abs(obj.value) == 3)
return result
井字圈叉遊戲:步驟十,贏家條件成立時,顯示驗證結果

11. 在畫面上顯示贏家

有了贏家之後,需要顯示在畫面中,我們在 computed:{} 新增另一個 win_text() 的函式。在預設上 winner = -1,代表沒有任何贏家產生,而當 this.pattern_data.length > 0,則表示有贏家產生數值時,將 winner 的值替換,再依據是正 3 還是負 3 顯示贏家。

//HTML
#app
  h1 {{ win_text }}  // win text
  .block_area
  .block(v-for="(block,bid) in blocks",
         v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
         v-on:click="player_go(block)")
    .small_number {{ bid+1 }}
  .block.small(v-bind:class="{circle: turn == 1, cross: turn == -1}")
//JS
win_text() {
  var winner = -1
  if (this.pattern_data.length > 0) {
    winner = this.pattern_data[0].value
  }

  if (winner == 3) {
    return 'O wins'
  } else if (winner == -3) {
    return 'X wins'
  }
  return (this.turn == 1 ? 'X' : 'O') + "' turn"
}

12. Vue.js中加上「防止每格內的重複點擊」的機制

在贏家產生後,照理來說遊戲就該停止了,但是現在遊戲還是可以不斷進行,原因在於還沒有防止重複點選機制。解決方式很簡單,只需要在點選前加上判斷格子是否為空的 block.type == 0 就可以了。

//JS
player_go(block) {
  if (block.type == 0) {
    block.type = this.turn;
    this.turn = -this.turn;
  } else {
    alert("Not allow")
  }
}

13. 增加按鈕重新開始遊戲

在遊戲結束後,需要清空畫面才能將再次進行遊戲,所以在最下方加上重新開始遊戲的按鈕。前面有提到 Click 動作點擊要搭配的前綴字為 v-on:click ,這可以縮寫成 @click,而清空的功能在步驟七的地方已經寫好了,所以這裡只需要呼叫就可以囉。

//HTML
#app
  h1 {{ win_text }}
  .block_area
    .block(v-for="(block,bid) in blocks",
           v-bind:class="{circle: block.type == 1, cross:block.type == -1}"
           v-on:click="player_go(block)") 
      .small_number {{ bid+1 }}
  .block.small(v-bind:class="{circle: turn == 1, cross: turn  == -1}")
   h2(@click="restart") Restart   // Restart 重新遊戲

14. 最後來CSS裡調整畫面

目前元素都靠向左側,我們希望調整到整個畫面的中心位置對齊,會比較好看。除了 htmlbody 要將子元素#app 排列置中, #app 裡的所有元素也必須排列整齊垂直置中,所以在元素設定上也一同加上了 #app

在排列上使用 flex,前面有提到,flex 預設上是橫列的由左向右排列,但是我們要的是由上而下的垂直排列,所以方向設定上改成 flex-direction: column,在主軸 justify-content 以及 交叉軸 align-items 都設定為置中 center。

//CSS
html, body, #app
  background-color: $color_bg
  margin: 0
  color: white
  +size(100%)
  display: flex
  justify-content: center
  align-items: center
  flex-direction: column
//HTML
h2 Player  // 在顯示輪到誰的圖示前面加上文字 Player
.block.small(v-bind:class="{circle: turn == 1, cross: turn  == -1}")
最後調整CSS,完成遊戲

結語

我們總結一下這次的圈圈叉叉遊戲的範例,製作過程可以分為五個部分:

  1. 以CSS擬元素(:before, :after)的方式繪製圈圈叉叉。
  2. 透過 Vue 的 Array 產生九個框框,並且透過綁定資料,讓畫面可以根據資料顯示對應的樣式。
  3. Restart – 重新遊戲,可在遊戲一開始或是遊戲中點擊 Restart 後清空畫面。
  4. 利用 v-on:click 選寫下棋功能。
  5. computed:{}計算出贏家成立的條件以及在畫面上顯示贏家。

這就是我們用 CSS及Vue.js 寫出來的圈圈叉叉遊戲啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。

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

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

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

墨雨設計banner

這篇文章 Vue.js入門:完成懷舊的井字圈叉遊戲動態網頁 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
利用Pug(HTML)、Sass(CSS)及Vue.js製作動態時間軸年表(直播筆記) https://creativecoding.in/2021/08/04/%e5%88%a9%e7%94%a8pug-sass-vue-js%e8%a3%bd%e4%bd%9c%e5%8b%95%e6%85%8b%e6%99%82%e9%96%93%e8%bb%b8%e5%b9%b4%e8%a1%a8/ Wed, 04 Aug 2021 03:36:00 +0000 https://creativecoding.in/?p=1320 不論個人或是品牌,都需要讓網站造訪者快速暸解你的歷史發展,時間軸年表是一個常見且好用的表示方法。利用Pug(HTML)、Sass(CSS)及Vue.js製作出一個屬於你的動態時間軸年表吧!

這篇文章 利用Pug(HTML)、Sass(CSS)及Vue.js製作動態時間軸年表(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>

凡走過必留下痕跡,時間軸年表常應用於紀錄故事和里程碑上,更是構築形象的第一步。不論是用於個人或是品牌,都能幫助網站造訪者能快速暸解關於你的歷史發展,一個常見且好用的表示方法。

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

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

在開始製作年表之前我們需要準備以下步驟:

  1. 資料──也就是呈現在年表上的內容,舉凡文案、年份、圖片等資料,一般是儲存在資料庫內
  2. 版面配置──物件位置關係規劃、色票和尺寸library建置
  3. 插入資料──讀取資料的trigger和loading動畫

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

《動態時間軸年表》開發環境設置
《動態時間軸年表》開發環境設置

資料準備

首先來準備資料,通常年表會包含這些資料,標題、內容、年份、日期,資料儲存的形式會是每一筆都有自己的年月份,再依照日期順序做排列。命名一個陣列為logs在裡面輸入yearcontent等內容,在content內使用另外一個陣列再做資料渲染彈性會比較大,也可以加入不同的tag區分同年份中的不同事件。

資料的樣式可參考以下範例:

//JS
var logs = [
  {
  year: 2014,
  content: [
    {
      tag: "開始接網頁專案",
    },
    {
      tag: "開始接網頁專案2"
    }
  ]
}]

在console裡面輸入logs,如果出現陣列代表資料準備完成。

《動態時間軸年表》陣列資料準備完成
《動態時間軸年表》陣列資料準備完成

接著我們把資料用ul列表的方式印出來,在JavaScript定義一個新的Vue並設定它的作用範圍、套用的資料為logs。在HTML的ul列表內,把logs印成一筆一筆的li,這時會看到我們原先設定的四筆資料,並且分為year、content等細部內容。

//JS
var vm = new Vue({
  el: "#app",
  data: {
  logs: logs
  }
});
//HTML
#app
  ul
    li(v-for="l in logs")
      h3.year {{l.year}}
      ul.content
        li(v-for="c in l.content") {{c.tag}}
《動態時間軸年表》資料顯示
《動態時間軸年表》資料顯示

由於某些網頁的HTML標籤會帶有原生的CSS,比如剛剛使用的ul自帶圓圈和margin的樣式,我們在codepen引入reset CSS做樣式重置。

延伸閱讀:[CSS] 跨瀏覽器的樣式重置 reset.css & normalize.css

版面配置

樣式重置完後我們就可以來做設計發想啦,時間軸年表的排列比較複雜,可以參考下面的簡易草稿來構思物件之間的位置關係。我們用 dialog_wrapperdialog 分別對應下圖的年份與事件,在HTML包覆相對應的資料。

《動態時間軸年表》設計發想
《動態時間軸年表》設計發想
//HTML
#app
  ul
    li.dialog_wrapper(v-for="l in logs")
      .dialog
        h3.year {{l.year}}
        ul.content
          li(v-for="c in l.content") {{c.tag}}

接下來建置色票和常用尺寸的library,本次使用的色票如下,常用到的寬高也建置 mixin 模組方便快速取用:

//CSS
$color_light_blue: #D4EBE8
$color_dark_blue: #4FBDBC
$color_white: white
$color_yellow: #F4DF38
$color_orange: #F4A373

@mixin size($w, $h: $w) //如果寬高數值一樣,取寬
  width: $w
  height: $h

這邊介紹大家一個好用的語法縮寫網站──Sass cheatsheet,熟練這些語法的話就可以增進切版的速度唷!

在版面設計的部分我們給背景壓上一層淡藍色,然後撐開寬高到100%。接著處理dialog的部分,可以切分為以下幾個元件:

//CSS
body
  background-color: $color_light_blue
  +size(100%)

.dialog
  // dialog本體的樣式設定
  background-color: #fff
  padding: 15px 20px
  cursor: pointer
  
  border-radius: 5px
  box-shadow: 15px 15px $color_dark_blue
  width: 250px
  position: relative
  transition: 0.5s // 漸變動畫較柔和
  
  // 裝飾性小方塊設定
  &:before
    content: ""
    display: block
    +size(20px)
    border-radius: 3px
    position: absolute
    right: -10px
    background-color: $color_white
    transform: rotate(45deg)
  //滑鼠移上去時,方塊往左上方、陰影往右下方移動
  &:hover
    transform: translate(-10px, -10px)
    box-shadow: 20px 20px $color_dark_blue
    
  
  // 標題文字設定
  .year
    font-size: 36px
    font-weight: 700
    margin-bottom: 10px
    letter-spacing: 2px

如果對於偽元素的運用不是那麽地熟悉,可以參考這篇文章──CSS 偽元素 ( before 與 after )

接著長出 timeline 讓他在畫面中上下左右置中。根據我們上方的草稿, dialog 的位置其實是由 dialog_wrapper 的相對關係所決定的,所以給予一些高度後在 dialog_wrapper 上增加 position: relative ,在 dialog 上改為 position: absolute

//HTML
#app
  ul.timeline //加個.timeline
    li.dialog_wrapper(v-for="l in logs")
      .dialog
        h3.year {{l.year}}
        ul.content
          li(v-for="c in l.content") {{c.tag}}
//CSS
#app
  display: flex
  align-items: center
  justify-content: center

.timeline
  height: 100vh
  width: 6px
  background-color: rgba($color_white, 0.4)
  padding-top: 50px

.dialog_wrapper
  height: 160px
  position: relative

.dialog
  ...
  position: absolute
《動態時間軸年表》製作時間軸以及事件外框樣式
《動態時間軸年表》製作時間軸以及事件外框樣式

那要如何讓 dialog 左右交錯排列呢?我們可以在 dialog_wrapper 裡面把它分為偶數和單數,用語法 :nth-child 選擇第 2n2n+1 個,可以暫時設定不同的顏色有助於判別。接著調整 dialogtimeline 的距離,記得 left 的值會優先於 right ,所以在偶數排設定 left: initial ,再透過偽元素 &:before 調整偏右對話框的小尾巴。

//CSS
.dialog_wrapper
  ...
    &:nth-child(2n+1)
    background-color: blue
    .dialog
      left: 40px
      &:before
        left: -10px
  &:nth-child(2n)
    background-color: red
    .dialog
      right: 40px
      left: initial

.dialog
  ...
  right: 0
《動態時間軸年表》左右交錯排列
《動態時間軸年表》左右交錯排列

接著利用 dialog_wrapper 的偽元素做出時間軸上的圓圈點,我們的時間軸年表樣式大致上完成囉。

//CSS
.dialog_wrapper
  height: 160px
  position: relative
  &:before
    content: ""
    display: block
    +size(20px)
    border: solid 5px white
    border-radius: 50%
    left: 50%
    transform: translateX(-40%)
    left: 0
    top: 0px
  ...

插入資料

新增用來插入資料的 button ,修改初始時 logs 為空值,定義他的 methodsinitial 時動態等於一開始所定義的 logs

//HTML
#app
  button.initial(@click="initial") 插入資料
  ul.timeline
  ...
//CSS
button.initial
  position: fixed
  right: 50px
  bottom: 50px
  background-color: $color_dark_blue
  color: white
  border: none
  border-radius: 5px
  padding: 5px 10px
  font-size: 16px
  cursor: pointer
//JS
//將JavaScript兩段程式碼濃縮成為一個
var vm = new Vue({
  el: "#app",
  data: {
    logs: []
  },
  methods: {
    initial(){
      this.logs=[];
      this.logs=[
        {
          year: 2014,
          content: [
            {tag: "開始接網頁專案",
            }
          ]
        },
        {
          year: 2015,
          content: [
            {tag: "成立墨雨設計工作室",
            }
          ]
        },
        {
          year: 2016,
          content: [
            {tag: "開設動態互動網頁程式入門",
            }
          ]
        },
        {
          year: 2017,
          content: [
            {tag: "開設動態互動網頁特效入門",
            }
          ]
        }
      ]
    }
  }
  
});
《動態時間軸年表》完成視覺設計
《動態時間軸年表》完成視覺設計

以目前及時出現的效果來說其實有些粗糙,所以我們來加上一些loading時的動畫提升質感吧!這次使用Vue官方的效果Transition Group,使用方法為在HTML套用官方已寫好CSS效果的class就好囉,記得也要把語法在CSS複製貼上唷!這樣點「插入資料」的按鈕,就可以看到進場的動畫了。當然也可以搭配設計的CSS互動動畫,這部分就留給大家發揮空間、腦力激盪一下~

//HTML
#app
  button.initial(@click="initial") 插入資料
  transition-group.timeline(tag="ul",name="fade")
    li.dialog_wrapper(v-for="l in logs", :key="1")
      .dialog
        h3.year {{l.year}}
        ul.content
          li(v-for="c in l.content") {{c.tag}}
//CSS
...
.fade-enter-active, .fade-leave-active
  transition: .5s
  transform: translateY(0px)
  
.fade-enter, .fade-leave-to
  opacity: 0
  transform: translateY(50px) rotate(10deg)
《動態時間軸年表》製作動態
《動態時間軸年表》製作動態

至於一個一個排序進入的動畫,我們同時抓出物品跟現在是第幾個的id,用 transition-delay 加上秒數,動態指定動畫時間delay多久。

//HTML
li.dialog_wrapper(v-for="(l,id) in logs", :key="l", :style="{'transition-delay':id/2+'s'}")

最後撒上如巧克力米般的 deco_bar 妝點整個畫面,然後再加上下雨般的動畫,我們就大功告成啦。

//HTML
#app
  button.initial(@click="initial") 插入資料
  transition-group.timeline(tag="ul",name="fade")
    li.dialog_wrapper(v-for="(l,id) in logs", :key="l", :style="{'transition-delay':id/2+'s'}")
      .dialog
        h3.year {{l.year}}
          .decor_bar
        ul.content
          li(v-for="c in l.content") {{c.tag}}
//CSS
@keyframes rain_in
    0%
      transform: translateY(-50px)
      opacity: 0
    100%
      transform: translateY(0px)
      opacity: 1  
  
.decor_bar
    &,&:before,&:after
      content: ""
      +size(8px,30px)
      background-color: $color_yellow
      border-radius: 5px
      position: absolute
      top: -35px
      left: 30px
      animation: rain_in 0.5s 0.5s both
      
    &:before
      background-color: $color_orange
      top: -30px
      left: -20px
      animation-duration: 1s
      animation-delay: 0.5s
        
    &:after
      background-color: $color_white
      top: -60px
      left: 20px
      animation-duration: 2s
      animation-delay: 0.5s

成品請參考這邊 👉🏻 https://codepen.io/frank890417/pen/rwrZwe?editors=0010

以上就是這次可愛的時間軸年表教學,相較於其他的直播內容,這次講解到CSS相關的切版觀念,讓我們再一次複習運用到哪些重點概念。

觀念大補帖

  1. 物件相對、絕對位置關係──層層相疊的物件如何使用position來呼應位置
  2. Animation的運用──如何使用Vue transition group與撰寫CSS的keyframes
  3. CSS選取器──運用:nth-child等語法選取肚子裡的子層

只要熟悉這些概念,相信成為切版高手的路就不遠啦!那麼,我們下次見啦👋👋👋

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

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

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

墨雨設計banner

這篇文章 利用Pug(HTML)、Sass(CSS)及Vue.js製作動態時間軸年表(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
來用可怕的三角函數做網頁吧! – Part 1 衛星繞月球(直播筆記) https://creativecoding.in/2021/05/10/%e4%be%86%e7%94%a8%e5%8f%af%e6%80%95%e7%9a%84%e4%b8%89%e8%a7%92%e5%87%bd%e6%95%b8%e5%81%9a%e7%b6%b2%e9%a0%81%e5%90%a7-part1-%e8%a1%9b%e6%98%9f%e7%b9%9e%e6%9c%88%e7%90%83/ Mon, 10 May 2021 01:30:00 +0000 https://creativecoding.in/?p=536 在這篇文章中,會先詳細介紹三角函數的計算並轉換為程式語言,再以三角函數的概念,使用 html (pug), css (sass) 與 js ,從零建構出衛星環繞月球的動態網頁效果。 本文翻自[週四寫程…

這篇文章 來用可怕的三角函數做網頁吧! – Part 1 衛星繞月球(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
https://imgur.com/iqS8cqe.gif

在這篇文章中,會先詳細介紹三角函數的計算並轉換為程式語言,再以三角函數的概念,使用 html (pug), css (sass) 與 js ,從零建構出衛星環繞月球的動態網頁效果。

本文翻自[週四寫程式系列] 來用可怕的三角函數做網頁吧! – Part 1,若是對文章內容有疑問,都可以觀看影片詳細內容。

前言

這次要跟大家聊聊三角函數,一聽到「三角函數」,肯定會勾起學生時期回憶,『天啊!老師在說什麼???』,我們這次準備了衛星繞月球的案例,將三角函數結合 CSS,實現衛星在圓周上的位置。也讓大家思考,如何在遊戲中使用三角函數?三角函數還能做出什麼變化?

在開始之前,先和大家分享之前經手的兩個案例 Dyverse Studio 與複雜生活節。在 Dyverse Studio (影片 4:29) 專案中,可以看到主頁裡,背景是由很多條線連結起來的,這個效果不可能手寫網頁結構去達成,直接使用圖片旋轉也欠缺真實感。要達成這些計算藝術的效果,三角函數、座標系、canvas 三者是不可或缺的工具。那這些即時繪圖有什麼好處呢?當使用者跟滑鼠互動或是調整一些參數時,網站能夠即時變化去調整顯示出來的效果,跟一般動畫寫死位移或是旋轉相比,多了更多的互動性。附錄中的 codepen 範例網址中,包括會動的海浪波型、 CD 旋轉軌跡等效果,都是利用三角函數製作出來的效果。由於 Dyverse Studio 主頁已改版,這邊提供影片中提到的畫面連結

Dyverse Studio主頁動畫

從遊戲開始出發

開始製作衛星繞地球前,我們要先了解三角函數。在網頁中要搖操控遊戲中的小精靈移動,就會使用 CSS 的 left, top 屬性,要往右走 left 增加,要往上走 top 減少,以此類推,隨著時間增加,移動的位置也跟著改變,就能營造出小精靈在移動的畫面。

這時問題來了,若是今天我們要讓小精靈斜上(右上45°)移動,該怎麼做呢?相信大家一定非常聰明可以想到,只要在同一時間往上和往右移動相同距離就可以了。

問題又來了,如果要改成往右上30°移動呢?右邊的距離跟上面的距離比例分別為1和√3(學生時期數學課的景象慢慢出現在腦海裡…),先不要恐慌這些數字怎麼來的,只要知道要往右上角30°的目標物移動,x 座標每次都加上√3單位,y 每次都往上加上1單位。

若是想要操作物體移動速度的快慢,只要調整每次加上單位的量(10 * 1 和 10 * √3),就能決定綠色球跑得快或慢。但是這樣的換算方式並不夠理想,每次我們要調整速度,就得再去算固定時間往上或往右移動的量。

版本1:知道 x 及 y 的變量,求得角色的下一個位置。

適用於目標位置不改變。

我們可以發現,在30°橫向距離與直向距離的比值不變,都是 1/√3 ( Δy / Δx, Δ, delta 變動值 ),在比值不變的狀態下,我們只要知道 x 或 y 的其中一邊,就能知道另一者。

x+=Δx
y+=Δy

版本2:知道 x 或 y 其中一者,求得角色的下一個位置。

適用於目標位置不改變。

既然我們知道 x 和 y 成比例關係,那我們就能將 y 每次增加的變量換算成Δx * 比例,反之我們也能將 x 每次增加的變量換算成 Δy * 比例。

x+=Δx
y+=(Δx*比例)

以上兩種版本都是在目標物在特定角度下,我們能夠計算出本體抵達目標物的移動方式,但是如果有更多角度的需求時,只有三種角度的比例肯定就不敷使用了。

版本3:知道斜邊 r 之後,r 乘上對應比例,就可以找到 x, y

這時我們就要感謝數學家們的努力,發明了三角函數,只要我們知道本體和目標物的距離 r 與角度 θ,就能利用 sinθ 或 cosθ 知道對應邊的長度了。

那麼 sinθ 的值怎麼來的呢?假設今天有一個座標軸,上面有一個半徑為1單位的圓,我們將不同角度的三角形放進去,讓所有三角形的 r 都剛好等於半徑1單位,就可以求出 sinθ ( r / y ),例如45°的時候,sin45°=1/√2。

有了這些,我們就可以拿本體到目標位置的距離乘上比例( sinθ ) ,來求出 y。同樣地要求出 x,我們就可以改使用 cosθ。但是這個比例的值到底是什麼呢?讓我們來看看第四個版本

版本4:知道 θ ,就能使用 r 結合三角函數作為比例

比例會根據θ而改變

// r = 5
x+=(5*比例)
y+=(5*比例)

// sinθ = y/r
// cosθ = x/r

x += r * cosθ
y += r * sinθ

衛星繞月球

有了這些三角函數的基本概念後,我們來嘗試做衛星繞月球的畫面,但為了強迫自己使用三角函數,我們要限制自己不能使用 css rotate 的屬性,只能利用 top, left (與畫面上方與左方的距離)結合 transform 來達成。

🔔關於 rotate : css 中有個屬性 transform,裡面有許多種值可以選擇,例如: translate, rotate, skew,其中的 rotate 則是以該物件中心為旋轉基準,根據使用者填入的值做旋轉。了解更多

這邊要注意的是,一般來說,旋轉的角度為逆時針,右邊水平線為 0°,順時針旋轉增加角度(圖左),而網頁座標中旋轉的角度與大家的想像不一樣,這邊在衛星旋轉的部分會透過操作詳細敘述。右圖中可以看到,只要知道綠色衛星與月球中心的距離 r 與 θ,就能利用三角函數換算出 y 與 x 座標。

我們這次用 codepen 來製作這個作品,環境調整為 pug 與 sass,會使用到 jq 來快速選擇兩顆衛星。這邊提供大家一個選顏色的工具 colorhunt ,沒有配色靈感時,能夠直接使用別人推薦的色碼。

場景 html 結構

在畫面中分別有星球背後的光暈(.space)、月亮、月亮上的四個坑洞、兩顆衛星,因此我們整個 html 結構可以寫成下面這樣

.space
.moon
  .hole
  .hole
  .hole
  .hole
.yellow
.blue

Sass 重複使用 – @mixin

首先我們先賦予場景基本的屬性,* 的存在是為了讓我們了解每個元件的外框,在完成作品後可以刪除這個 class 內容。

$color_space: ##2c3d4f

*
  border: solid 1px

html, body
  width: 100%
  height: 100%
  padding: 0
  margin: 0
  background-color: $color_space

// 背景光暈
.space
  width: 700px
  height: 700px
  border-radius: 50%
  background-color: lighten($color_space, 10)
  filter: blur(50px)
  position: absolute
  left: 50%
  top: 50%
  // 偏移處理
  transform: translate(-50%, -50%)

.moon
  background-color: #fff
  width: 200px
  height: 200px
  border-radius: 50%

我們可以發現畫面裡的月球、衛星、坑洞,有太多重複需要使用到圓的東西,我們來試試看怎麼將這些屬性整理在一起,讓這些屬性可以不用一直重複撰寫。

我們會發現 .moon 的寬高與圓角是構成圓形的三個屬性,要如何做才能重複使用這些 css 呢? sass 內有個工具叫做 mixin,可以傳入變數進去,在編譯階段就能產出我們需要的 css 內容,這種方式讓我們能減少撰寫重複的程式碼。宣告與使用的方法如下:

@mixin size($w, $h)
  width: $w
  height: $h

@mixin circle($r)
  +size($r, $r)
  border-radius: 50%

.moon
  background-color: #fff
  +circle(50px)

如果想偷懶一下,讓 +size 內只需傳入一個 $r,可以將 @mixin size 中加入判斷式改寫成如下,在 mixin size 中我們可以看到,當有傳入 $h 時, height 就是使用傳入的第二個參數,若是沒有則直接使用 $w 作為 height;寫法2中則是當 $h 參數沒有傳入時,則預設 $h 為 $w

// 寫法1
@mixin size($w, $h:false)
  width: $w	
  @if ($h)
    height: $h
  @else
    height: $w
// 寫法2
@mixin size($w, $h:$w)
  width: $w	
  height: $h

@mixin circle($r)
  +size($r)
  border-radius: 50%

.moon
  background-color: #fff
  +circle(50px)

這時候我們又發現,專案中頻繁地使用到絕對定位並水平垂直置中,理所當然也可以把這些屬性整理成 mixin:

@mixin ab_center
  position: absolute
  left: 50%
  top: 50%
  transform: translate(-50%, -50%)

製作陰影

我們的月球、月球坑洞與衛星都會用到陰影,css 中的 box-shadow 陰影預設是往外長,我們這邊可以多下一個 inset 值,讓陰影變成內陰影,改成 -20px 便是將亮面往左上移動,知道這個方式之後,讓我們試著做做看衛星與月亮坑洞的陰影,可以試著調整陰影顏色或是偏移量。

.moon
  background-color: #fff
  +circle(400px)
  +ab_center
  box-shadow: -20px -20px darken(#fff, 10) inset

月亮不同坑洞

月亮上四個坑洞的 classname 都是 hole ,我們該如何去客製這四個一模一樣的坑洞,讓它們在基本的屬性上再增加不同的位置或是大小?這邊我們使用到 css 的類別選擇器 nth-child,不僅位置可以客製,每個坑洞的大小也可以透過這種方式去調整。

.hole
  &:nth-child(1)
    left: 120px
    top: 130px

不知道大家在分別寫四個坑洞的位置時,有沒有查覺到我們也可以用剛剛的 mixin 去寫呢?同學可以挑戰看看。

@mixin pos($left, $top)
  top: $top
  left: $left

衛星軌道

這一小節中我們要製作衛星軌道,對好位置後,也能夠確認衛星沒有偏離,這邊只要增加 html 結構,並為它們賦予 css 即可,這邊我們只示範 .trace1 的寫法,要特別注意的就是衛星旋轉的圓型軌跡直徑,就是這個軌道寬度和高度:

HTML

.trace1
.trace2

CSS

.trace1
  width: 500px
  height: 500px
  border-radius: 50%
  border: 1px dashed #fff
  +ab_center

旋轉的衛星

幫衛星賦予樣式,在這邊我們想讓衛星有內陰影,以及淺淺的外層黑色光暈。所以我們在 .red 和 .yellow 的 css 上,加上一些程式碼。這邊也可以看到,我們將常用的顏色做成變數,方便之後快速調整,box-shadow 前面的部分是內陰影,逗號後面的則是外層黑色光暈,大家在寫這一段也要記得加上 z-index

這邊提供大家 .red 的 sass 檔,黃色衛星的內容大家可以挑戰看看。

.red
  $color_red: #f24
  background-color: $color_red
  +circle(50px)
  +ab_center
  box-shadow: -10px -10px darken($color_red, 20) inset, 0px 0px 40px rgba(black, 0.3)
  z-index: 100

接著我們要著手撰寫程式碼,讓程式碼動態修改角度,使衛星們順利動起來。

結合前面所解說的方式,利用三角函數來定義旋轉的位置。angle 為旋轉角度,這邊先釐清前面提到的旋轉方向,一般來說水平線右邊為0°,角度增加的方向為逆時針(左圖);但在網頁中 x, y 的方向有所不同,y 向下才是正值,角度增加旋轉的方向為順時針(右圖)。

我們先做一些測試,確定網頁中旋轉的角度是不是如右圖所示。

下面這段程式碼,angle 是我們要觀察的角度變數,r 為紅色衛星的半徑,x 的部份我們有提到要使用三角函數的 cosθ , y 則是使用 sinθ,那為什麼 θ 的值會使用到這麼大串運算式呢?這邊我們先一一理解整段程式碼,javascript 內要使用到 cos 要使用 API – Math.cos(),javascript 內角度不是直接寫數字,要換算成Math.PI,一圈 360° 為 2PI,所以我們將角度除以 360 後乘上 2PI,就能換算成 js 內的角度。

而最後的 – 25 則是偏移量,因為我們在 mixin ab_center 內有寫到translate(-50%, -50%),這邊我們要將這個偏移量修正回來,才不會讓衛星旋轉偏移。

var angle = 0
var r = 250
var x = r * Math.cos((angle/360)*(Math.PI*2))-25
var y = r * Math.sin((angle/360)*(Math.PI*2))-25
$(".red").css("transform", "translate("+x+"px, "+y+"px)")

大家可以慢慢增加 angle 的量,就能發現紅色衛星隨著角度的增加,從右邊水平線順時針轉。

https://imgur.com/p649GNo.gif

接著我們要讓衛星順暢的旋轉,這邊使用到 setInterval。我們先將剛剛的程式碼包裝成 function update,每隔一段時間就增加 time 的量,並更新畫面就能讓角度增加,使用 setInterval 每 30 毫秒呼叫一次,一個順暢的動態就產出了。

若是覺得衛星轉太慢或太快想調整衛星速度,我們只要把 var angle = time 多乘上一個值即可,聰明的大家應該有發現,如果我們乘上的值是負值,就輕鬆的達成反方向旋轉的功能了。

var time = 0
function update(){
  var r = 250
  // var angle = time
  // var angle = time * 0.2
  var angle = time * -0.5
  var x = r * Math.cos((angle/360)*(Math.PI*2))-25
  var y = r * Math.sin((angle/360)*(Math.PI*2))-25
  $(".red").css("transform", "translate("+x+"px, "+y+"px)")
  time+=1
}

setInterval(update, 30)
https://imgur.com/TGkqKLj.gif

這時候問題來了,紅色衛星完成了,黃色衛星的程式碼也類似紅色程式碼,差別只在旋轉半徑、速度、偏移修正不同,那我們要怎麼將它統一呢?這邊我們使用陣列去管理這兩個衛星物件,在 update 內使用 forEach 來修改兩個衛星的位置就可以了。

var time = 0
var stars=[
  {
    el: ".red",
    r: 250,
    speed: -2,
    width: 50
  },
  {
    el: ".yellow",
    r: 340,
    speed: 1,
    width: 70
  }
]

function update(){
  stars.forEach(function(star){
    var r = star.r
    var angle = time * star.speed
    var x = r * Math.cos((angle/360)*(Math.PI*2))-star.width/2
    var y = r * Math.sin((angle/360)*(Math.PI*2))-star.width/2
    $(star.el).css("transform", "translate("+x+"px, "+y+"px)")
  })
  time+=1
}

setInterval(update, 30)
https://imgur.com/iqS8cqe.gif

結語

這邊讓我們再複習一次整個製作過程

  1. 開始製作前,我們先將基本的結構與樣式完成(陰影、坑洞等),過程中我們發現許多重複使用的樣式,例如絕對定位、圓形等,所以我們學到了第一招 sass 中的 mixin。
  2. 我們利用三角函數模擬衛星繞星球旋轉的定位,並確定衛星旋轉的方向後,讓紅色衛星順利轉起來。
  3. 最後我們將旋轉兩個衛星整理成物件,並改寫 update 函式,讓兩顆衛星能共用相同函式旋轉。

數學裡面有許多奇怪的東西,但也感謝數學家們的努力,讓我們可以應用在遊戲或動態特效等地方,即使理解這些數學理論有些頭疼,但是一切都是為了做遊戲和特效。在 part 2 裡我們會再帶大家使用三角函數來製作其他有趣的東西。

下一篇老闆要繼續使用三角函數打造一個科技感的時鐘,讓我們一鼓作氣學習下去吧!

工商時間:老闆在 Hahow 有一堂課程 – 動畫網頁特效入門,裡面有一些數學的內容,誘使大家跳坑,一起去學這些恐怖的東西,老闆會將課程講解得有趣點,讓大家在比較沒有壓力的狀況下學習這些數學。(笑

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 來用可怕的三角函數做網頁吧! – Part 1 衛星繞月球(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
來做SVG動畫讓蔥油餅翻滾吧! (下):讓場景中的元件動起來(直播筆記) https://creativecoding.in/2021/01/15/%e4%be%86%e5%81%9asvg%e5%8b%95%e7%95%ab%e8%ae%93%e8%94%a5%e6%b2%b9%e9%a4%85%e7%bf%bb%e6%bb%be%e5%90%a7-%ef%bc%8d%e4%b8%8b%e7%af%87%ef%bc%9a%e8%ae%93%e5%a0%b4%e6%99%af%e4%b8%ad%e7%9a%84%e5%85%83/ Fri, 15 Jan 2021 10:46:07 +0000 https://creativecoding.in/?p=469 本文翻自 [週四寫程式系列] 來做SVG動畫讓蔥油餅翻滾吧!,若是對文章內容有疑問,或是想要老闆手把手帶你飛,都可以觀看影片詳細內容。 在上一篇中,老闆帶大家從發想素材、將素材引入網頁,到製作場景進出…

這篇文章 來做SVG動畫讓蔥油餅翻滾吧! (下):讓場景中的元件動起來(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>

本文翻自 [週四寫程式系列] 來做SVG動畫讓蔥油餅翻滾吧!,若是對文章內容有疑問,或是想要老闆手把手帶你飛,都可以觀看影片詳細內容。

在上一篇中,老闆帶大家從發想素材、將素材引入網頁,到製作場景進出的動畫。接下來我們將要賦予場景中的每個元件生命。

目標

本篇文章中,老闆會帶大家完成蔥油餅小島中角色與物件的動畫,也會提示大家每個部份需要注意的地方,以下動畫的設定數值都都只是參考,開發時大家都能試試看不同的數值。

  • 實際應用﹔讓場景中的元件動起來

讓素材動起來吧

驚嘆號

首先我們從驚嘆號下手,老闆讓驚嘆號分布在場景的各處,用意是要提示使用者這邊有驚喜,引導使用者靠近這個區塊。為了增加提示效果,我們利用左右晃動的動畫效果,固定時間後會再撥放。

這邊使用了新的 css 屬性 animation ,需要搭配 @keyframes 使用,在 keyframes 用百分比去註記不同時間該顯示的樣式,透過 animation 定義動畫總時間,這也代表這個 keyframes 可以提供給不同元件使用,使用的元件賦予不同的時間等屬性,就是一個全新的樣貌。

要注意元件會以左上角為旋轉中心點,適時加上 transform-origin, transform-box 讓動畫更加合理。

[data-name="sign"]{
	// 引用動畫名稱 時間 延遲播放時間 次數
  animation: drag 2s infinite;
  transform-origin: center center;
  transform-box: fill-box;
}

@keyframes drag {
  0%{transform: rotate(0deg);}
  80%{transform: rotate(0deg);}
  85%{transform: rotate(10deg);}
  90%{transform: rotate(-6deg);}
  95%{transform: rotate(3deg);}
  100%{transform: rotate(0deg);}
}

路人 trigger_door_man

利用前面寫過的 keyframes drag 與 data-name 的概念,我們可以快速完成這個部分的動畫。當滑鼠進入感應區塊,路人能夠開始搖擺。

[data-name="trigger_door_man"]{
	&:hover{
	  animation: drag 2s infinite;
	  transform-origin: center bottom;
	  transform-box: fill-box;
	}
}
驚嘆號與路人

阿伯與跳起來的蔥油餅 trigger_cookie

要讓蔥油餅動起來之前,先從阿伯的手開始,結合蔥油餅動畫之後,讓蔥油餅宛如真的被阿伯翻了起來。撰寫這邊的動畫時,要注意調整 transform-origin ,決定動畫旋轉的參考點,阿伯的上半身也是用相同的方式處理,若是動畫沒調好,很有可能瞬間變成凶殺案,阿伯被腰斬的畫面。

透過 animation 內延遲時間設為負值,讓動畫預先開跑到其他時間點,也是讓動畫更加順暢的小訣竅。

[data-name="trigger_cookie"]{
	&:hover {
		[data-name="man_upper"]{
			animation: bake_cookies 2s infinite ;
			transform-origin: center bottom;
  		transform-box: fill-box;
		}
		[data-name="hand"]{
			// 動畫名稱 動畫時間 次數 延遲時間
			animation: drag 2s infinite -0.8s;
			transform-origin: left center;
  		transform-box: fill-box;
		}
	}
}
@keyframes bake_cookies {
	40%{
			transform: rotate(5deg);
	}
	50%{
			transform: rotate(-6deg);
	}
	70%{
			transform: rotate(-10deg);
	}
	90%{
			transform: rotate(-6deg);
	}
	100%{
			transform: rotate(5deg);
	}
}

接著我們要來處理跳起來的蔥油餅了,蔥油餅的思維比較不同,我們要讓蔥油餅分段顯示。 keyframe 的動畫內容改為使用透明度,讓透明度跟著時間改變,營造出分段顯示的效果,利用 animation-delay 來讓四個位置的蔥油餅,在不同的時間點顯示。這邊起始為置的蔥油餅會出現大部分的時間,所以要特別為它寫一個 keyframe。大家也可以試試看,調出順暢有趣的效果。

[data-name="trigger_cookie"] {
	&:hover {
		...
		[data-name="cookie1"] {
			animation: bake_cookies_locus 2s infinite -1s;
		}
		[data-name="cookie2"] {
			animation: bake_cookies_locus 2s infinite -0.8s;
		}
		[data-name="cookie3"] {
			animation: bake_cookies_locus 2s infinite -0.6s;
		}
		[data-name="cookie4"] {
			animation: bake_cookies_locus 2s infinite -0.4s;
		}
		[data-name="now_cookie"] {
			animation: now_cookie 2s infinite;
		}
	}
}
@keyframes bake_cookies {
	40% {
		transform: rotate(5deg);
	}
	50% {
		transform: rotate(-6deg);
	}
	70% {
		transform: rotate(-10deg);
	}
	90% {
		transform: rotate(-6deg);
	}
	100% {
		transform: rotate(5deg);
	}
}
@keyframes now_cookie {
	0% {
		opacity: 1;
	}
	49% {
		opacity: 1;
	}
	50% {
		opacity: 0;
	}
	90% {
		opacity: 0;
	}
	91% {
		opacity: 1;
	}
}

小孩與媽媽 trigger_cookie

不可能整個畫面都使用同樣的搖擺動態吧?所以我們為小孩換一個動態效果,讓他原地跳躍,要達成這個效果,可以改變 y 的值來達成,媽媽則是繼續搖擺效果。

[data-name="trigger_cookie"] {
	&:hover {
		[data-name="child"] {
			animation: jump 2s infinite -1s;
		}
		[data-name="mother"] {
			animation: drag 5s infinite;
			transform-origin: left bottom;
			transform-box: fill-box;
		}
	}
}
@keyframes jump {
	0%{
		transform: translateY(0px);
	}
	50%{
		transform: translateY(0px);
	}
	51%{
		transform: translateY(0px);
	}
	75%{
		transform: translateY(-10px);
	}
	100%{
		transform: translateY(0px);
	}
}
跳動的小孩與搖擺媽媽

結語

跟著操作之後,應該有 svg + css 製作動畫的概念,這時不妨動手試試看,讓其他小島的元件動起來。也可以畫些素材,加上動畫賦予他們生命力。動畫沒有什麼對或錯,可以按照自己喜好,去改變動畫效果或是調整時間,但要注意撰寫動畫時的連續繼承問題,像是阿伯的手(子層)會跟隨著身體(父層)擺動。下面老闆也提供三個議題,供大家在開發時參考。

引用 animate.css

每次都要重新寫 keyframe,若是寫 keyframe 寫累了或是遇到急案,為了加速開發,老闆這邊準備了一些補品,讓你可以快速套用現成的動畫效果。例如:想要實現當滑鼠進入感應區塊時,讓驚嘆號動畫改為閃爍動畫,在這邊我們結合 animate.css 製作閃爍動畫。引用 ainmate.css 後,當 data-name=”trigger ” hover 時,裡面 data-name=”sign”的動畫都改為 animation: flash 2s infinite。( flash 為 animate.css 的動畫名稱)

電腦與手機

由於手機等行動裝置不會有 hover 的行為,記得補上 focus,讓整個網頁能夠更完善。

了解你的使用者

把作品完成後,如果能夠知道自己這些得意之作是不是真的有被使用,可以做 GA 追蹤,老闆也因為 GA 的關係,發現其實使用者不太常使用換頁,導致這些動畫做了卻沒人看,參考數據後做了相對應的處理,增加作品露面的機會。

課程推薦

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

打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。


墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 來做SVG動畫讓蔥油餅翻滾吧! (下):讓場景中的元件動起來(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
讓我們來做個互動天氣地圖吧!(直播筆記) https://creativecoding.in/2020/03/28/%e8%ae%93%e6%88%91%e5%80%91%e4%be%86%e5%81%9a%e5%80%8b%e4%ba%92%e5%8b%95%e5%a4%a9%e6%b0%a3%e5%9c%b0%e5%9c%96%e5%90%a7%ef%bc%81%ef%bc%88%e7%9b%b4%e6%92%ad%e7%ad%86%e8%a8%98%ef%bc%89/ Fri, 27 Mar 2020 19:15:54 +0000 https://creativecoding.in/?p=294 這次的直播要做一個互動地圖,當滑鼠滑過的時候顯示當地天氣資訊! 這次的直播內容主要有幾個部分: 取得地圖的 svg 的檔案,並修改成我們可以用的檔案類型。 取得台灣的地圖資料,包含行政區域名稱與代號,…

這篇文章 讓我們來做個互動天氣地圖吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
這次的直播要做一個互動地圖,當滑鼠滑過的時候顯示當地天氣資訊!

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

  1. 取得地圖的 svg 的檔案,並修改成我們可以用的檔案類型。
  2. 取得台灣的地圖資料,包含行政區域名稱與代號,並把資料對應到地圖上。
  3. 做出互動的頁面,包含滑鼠滑過時的移動、變色,還有右側的資料顯示。

筆記會著重在 3. 程式實作的部分,並把相關的資料與圖片連同程式碼一起放在 github 上面,供大家參考💻。

要跟著影片一起做也沒問題:


程式實作

主要會用到的工具與知識:

  1. HTML, CSS 與 JavaScript 的基礎觀念
  2. svg 的基本操作
  3. Vue.js 框架的操作
  4. 使用 axios 串接中央氣象局 open data API

讀取地圖,並操作樣式

要在我們的頁面讀取 svg 有幾種方法,使用 <img> tag 讀取檔案,或是直接把 <svg> 包覆的內容直接貼在 html 裡面,但是如果直接讀取檔案的話就沒有辦法對 svg 進行操作,所以我們選擇後者。

svg 檔案的格式跟 html 很像,都是一層一層的往下排列,不過相對於 html 裡面的內容是 <html> 包覆所有物件,svg 則是在 <svg> 包覆繪圖軟體裡面使用的「群組」<g>、「路徑」<path> 、「線段」<line> 或是 <ploygon><circle> 圖形等元件。

讀進 svg 的 html 大概會長這樣:

<html>

<head>
...
</head>

<body>
    <svg data-name="圖層 1" xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 595.28 841.89">
        <defs />
        <title>
            image.svg
        </title>
        <path id="161d ... 09.27h0Z" />
        ...
        <path id="41139c2a ... 1.22-1.29Z" />
    </svg>
</body>

</html>

我們先在 CSS 加上一些樣式與高度的限制,才不會讓圖片太大。另外把 svg 裡面的 path 元件加上顏色跟滑鼠移過(hover)時的變化。

這邊要注意,用 css 操作 svg 顏色的方式跟一般 dom 元件的方式不太一樣,如果要改變 path 的填色,不能用 background,而是要用 fill,線條的話不是 border,而是 stroke 喔!想要知道更多 svg 的屬性與使用方式可以參考這個 CSS-TRICK 的整理:SVG Properties and CSS

:root {
    --color-gold: #B99362;
}

body {
    background-color: #222
}

svg {
    height: 100vh;
}


path {
    stroke: white;
    fill: transparent;
    transition: 0.5s;
    cursor: pointer;
}

path:hover {
    fill: var(--color-gold);
    transform: translate(-5px, -5px);
}

到這邊,我們就有一個 hover 時會變色與區域浮起來的地圖囉!

篩選滑鼠移動過地區的地理資料

緊接著我們需要把地圖畫面跟資料連在一起,當滑動過某個地區的時候,要先知道現在滑鼠在哪一個縣市,才有辦法在畫面上顯示相對應的地理資訊~

我們先觀察一下 svg 裡面各個縣市 <path> 的結構,以台北市為例:

<path id="1e48e0bb-8964-4121-b347-b900162cf771" data-name="taipei_city" class="96fdfe13-4732-40bb-9e9c-cdc6e310fcb9" d="M466.27,77.17,465.42,79l-.85.85-.24.49-.85,1v1.83l-1.22.73L462,85.47l.49,1.59,1.22.85,3.9.49,2.44,2.32,1,1.83.12,5.61-1.83,2.56-1.22,1.1-.61,1.34.37,3.54.73,1.46,1.46-.12,1.34-.73.85-1.22.85-.12,2,.85,1.1.85.49,1.34v1.83l.85,1.46,1.58,1.1,1.71.12.73,2.93,2.56,1.71,6.83.73,1.46-.61h2l.12-1.22-3.29-1.59-.24-1.71.12-1.46-.85-2.8v-1.58l.85-1.34,1.22-.73,3.41.24,1.1-.73,1.46-.37,4.63.24.37-1-1.1-.49-1.58.49L497.86,103l-3.29-2.44L494,99.25l.85-1.1-.24-1.34-1-1,.73-.85,1.59-3.29-.37-3.17L490.29,84l.37-1.59,1-1.1-.37-1.22-2-1.71-.73-1.1-.12-3.54-2-2V70l.49-1.34V66.81l-3.17.24-1.34-2.44-1-1.1-1.71,2.68-1.34.61-.61,1.34-2.56,1.46-.61,1.59-1.1.73-1.71.12-1.34.61-2.07,2.44-.61,1.59-1.59.48h0Z" />

發現其實可以直接在 <path> 裡面加上自己定義的 data-* attribute,如此一來,就可以用類似 jQuery 的 attr() 方法,甚至是透過 DOM element 的 data 屬性取得這個名字的內容。

舉例而言,我們可以在直接在瀏覽器加上某個縣市的 onmouseover 監聽器,並在滑鼠移動的時候印出地圖位置的 data-name 的值,就可以這樣做:

// 使用原生 JavaScript 的寫法
// 先抓取台北市的 path 物件
const el_taipei_city = document.getElementById('1e48e0bb-8964-4121-b347-b900162cf771')
// 加上監聽器,打印出我們要的 data-* attribute 內容
el_taipei_city.onmouseover = function({console.log(this.dataset.name)}
// 結果 -> taipei_city

有了地圖的資訊之後,我們只要拿著這個地點的名稱去比對現有的天氣資料,就可以把相對應的資料放到畫面上了。地理資訊的資料型態長這樣:

var place_data=[
  {
   tag: "taipei_city",
   place: "臺北市",
   low: 16,
   high: 24,
   weather: "Rainy"
  },
  {
   tag: "new_taipei_city",
   place: "新北市",
   low: 15,
   high: 22,
   weather: "Rainy"
  }
  ...
]

假設拿著 taipei_city 字串,想要從 place_data 取得整個台北市的物件要怎麼做呢? 我們可以用 for 迴圈,一個一個比對,但是其實 JavaScript 有提供我們更簡潔的寫法,就是陣列的 filter 方法,我們可以直接回傳陣列裡面中,判斷函式結果為 true 的物件。

像是這樣:Do re mi so ~

current_place_obj = place_data.filter((obj)=>obj.tag === 'taipei_city')[0]

因此,我們可以很快速的獲得 tag 是 taipei_city 的整個物件,要小心 filter 方法回傳的也是一個陣列,所以如果要取值的話,要取第0項喔。

將資料用 Vue 綁定,並渲染在畫面上

這邊比較需要注意的地方有兩個,第一個是需要在組件 mounted 的時候把所有的 <path> tag 加上滑鼠移動的監聽器,當滑鼠移動的時候,就更新當前選擇到的 data-name 屬性更新到組件的 filter 資料上。第二個是可以利用 Vue 的 computed 計算屬性即時根據 filter 的變動即時更新要顯示當前資料的物件 now_area

const app = new Vue({
    el: '#app',
    mounted() {
        paths = document.querySelectorAll('path');
        let _this = this // 把這個 vm 本身存在 _this,以供後續函式內部使用
        paths.forEach(e => {
            e.onmouseover = function () {
                _this.filter = this.dataset.name
            }
        })
    },
    data: () => {
        return {
            filter: '',
            place_data: null,
        }
    },
    computed: {
        now_area() {
            let result = place_data.filter((obj) => obj.tag === this.filter)
            if (result.length == 0) {
                return null
            } else {
                return result[0]
            }
        }
    },
})

最後快速的加上標題與內容的樣式,就完成這次的作品了!🎉🎉🎉

加碼小單元-串接中央氣象局的 API 顯示實際的溫度與氣象

我們選擇使用中央氣象局氣象資料開放平台提供的資料,先註冊帳號後到這個網址:https://opendata.cwb.gov.tw/user/authkey,點擊下圖中的取得授權碼之後右邊就會出現你的 API token,要好好保管他這份資料呦。之後只要使用這個 token 就可以通行無阻的拿到我們需要的資料了~

開發指南可以看到呼叫 API 的規範大致上是長這樣:

※ URL: https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/{dataid}?Authorization={apikey}&format={format}
                
{dataid} 為各資料集代碼 (參照:資料清單)  ex.F-A0012-001
                
{apikey} 為會員帳號對應之授權碼  ex.CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678
                
{format} 為資料格式,請參照各資料集頁面確認可下載之檔案格式  ex.XML、CAP、JSON、ZIP、KMZ、GRIB2
                
※ 範例:https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-A0012-001?Authorization=CWB-1234ABCD-78EF-GH90-12XY-IJKL12345678&format=XML
                
並請加入快取功能,如上述所示。

因為我們需要的是以JSON格式顯現的鄉鎮天氣預報-台灣未來1週天氣預報,因此呼叫的規格大概是這樣: let url = 'https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-D0047-091?Authorization=你的API_token&downloadType=WEB&format=JSON'

我們使用 axios 的 get 方法拿取資料看看 axios.get(url).then(data => {console.log(data)}),可以拿到這個樣子的資料,打開之後發現我們需要的就是在 dataset -> locations 裡面的 location 陣列資料。

再往下看可以看出他的結構,locationName 是縣市名稱,descriptionName 是這個數值的名稱,這才發現,原來氣象局的資料還會依照時段區分,我們尋求最簡單的作法,直接抓離現在最近的時段就好。

weatherElement 裡面有很多資料提供的數值,如果不清楚意思的話也可以查閱檔案的欄位說明表,雖然文件通常又臭又長,但是好好讀一下都能省下不少開發的時間。最複雜的解析部分已經完成,接下來只要把資料源跟篩選方式改成從api抓回來的資料就可以了!

接下來先把 call 到的資料存到 data 的 weather_data 中,另外把 filter 方法換成 find,因為我們只需要第一個符合條件的結果。把回傳的資料格式修整一下,符合原本的資料型態就可以直接呈現囉~

mounted() {
    axios.get(url).then(data => {
        console.log(data)
        this.weather_data = data.data.cwbopendata.dataset.locations.location
    })
...
now_area() {
    let data = {}
    let result = this.weather_data.find((obj) => {
        return obj.locationName === this.filter
    })
    
    if (result) {
        let high = result.weatherElement.find(el => el.elementName === 'MaxT').time[0].elementValue.value
        let low = result.weatherElement.find(el => el.elementName === 'MinT').time[0].elementValue.value
        let weather = result.weatherElement.find(el => el.elementName === 'Wx').time[0].elementValue[0].value
        data = {
            place: this.filter,
            low: low,
            high: high,
            weather: weather
        }
    }
    return data
}

這樣就大功告成了!專案的原始碼可以在這邊查看:https://github.com/Monoame-Design/bosscoding-examples

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 讓我們來做個互動天氣地圖吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
老闆的網頁實驗室#2-實作 Canvas 遮罩動畫 https://creativecoding.in/2020/03/06/%e8%80%81%e9%97%86%e7%9a%84%e7%b6%b2%e9%a0%81%e5%af%a6%e9%a9%97%e5%ae%a42%ef%bc%8d%e5%af%a6%e4%bd%9c-canvas-%e9%81%ae%e7%bd%a9%e5%8b%95%e7%95%ab/ Thu, 05 Mar 2020 17:47:27 +0000 https://creativecoding.monoame.com/?p=95 案例解析 Louis Ansa — Portfolio 這次要分析的是在法國的設計師 Louis Ansa 的作品集網頁,在 Louis Ansa 的網頁中開始的載入與關於頁面都有出現的遮罩效果,讓老…

這篇文章 老闆的網頁實驗室#2-實作 Canvas 遮罩動畫 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
案例解析

Louis Ansa — Portfolio

這次要分析的是在法國的設計師 Louis Ansa 的作品集網頁,在 Louis Ansa 的網頁中開始的載入與關於頁面都有出現的遮罩效果,讓老闆帶著大家來看看這是如何實現的吧!

分析思路

這一頁的動畫主要有三個部分組成:

  1. 球型遮罩:在圓形裡面的文字會呈現不同的背景色與內文顏色
  2. 移動的球體:兩個球體的圓心沿著畫面的中心做圓周運動
  3. 變動的球體形狀:球體的邊界呈現不規則波動

首先針對1.的部分,要改變特定區域內的顏色效果,依據需要達成的效果不同,我們可以直接選擇改變區域內的顏色內容;或是使用兩層圖片,再將區域內不需要的上層元素移除掉。

如果只需要處理顏色的變換,沒有非常複雜的動畫,可以使用 css 的 mix-blend-mode 屬性來實現,mix-blend-mode 提供了saturationhuedifference等條件直接處理顏色。但是考量到後續如果需要做出多個物件、比較複雜的變形以及效能問題,從 Canvas 下手就會更靈活。

實作

1. 創建兩層 canvas

首先先使用兩層的canvas,並做出完全相同的長寬、內文、行高與字體大小的圖片:


canvas.draw = function() {
  ctx = this.ctx;
  requestAnimationFrame(() => {
   this.draw(ctx);
  });
ctx.fillStyle = this.backgroundColor;
  ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
  ctx.beginPath();
  ctx.fillStyle = this.fillStyle;
  ctx.lineWidth = 5;
  ctx.font = "bold 100px Montserrat";
  // 這邊先給出一個 text 的變數是用來測量行高,以便換行書寫、定位圖形中心
  text = "Monoame";
  var textWidth = ctx.measureText(text).width;
  var textHeight = parseInt(ctx.font.match(/\d+/), 10);
  ctx.fillText("Monoame", this.cx - textWidth / 2, this.cy - textHeight / 2);
  ctx.fillText("Studio", this.cx - textWidth / 2, this.cy + textHeight / 2);
 };
底層(透過遮罩看到)的背景與文字顏色
上層的背景與文字顏色

2. 設定遮罩圖形與透視的效果

這個步驟是整個案例的核心,我們使用到 canvas 的 globalCompositeOperation屬性,globalCompositeOperation可以指定 canvas 針對當前繪製圖形與背景的交互效果。

舉例來說,預設的值是 source-over即是直接覆蓋過背景的圖層,畫上新的路徑;而我們使用到的是 destination-out,可以將新舊圖形重疊的區域設定為透明,只在沒有重疊的的部分畫出圖形。

source-over 預設值。將新圖形畫在舊圖形之上。
destination-out
只保留新、舊圖形非重疊的舊圖形區域,其餘皆變為透明。

左圖片中,藍色方形是背景的原始圖形,紅色圓形是新繪製的圖形,我們可以比較一下兩種形式對於背景圖形的影響。關於 globalCompositeOperation的更多選項與說明可以參考 MDN 的 Canvas 教學

實作中先在上層的 canvas 中繪製出作為遮罩的圓形,並在繪製圓形之前將 ctx.globalCompositeOperation設定為"destination-out",如此一來,在這個圓形的範圍內,原本被上層遮住的黑底紅字的底層就會顯示出來。最後也別忘了,在繪製完遮罩的部分之後將參數設定回原本的 "source-over",這樣第一個部分就大功告成了!

ctx.globalCompositeOperation = "destination-out";
ctx.arc(mousePos.x, mousePos.y, this.r, 0, Math.PI * 2, false);
ctx.fill();
ctx.globalCompositeOperation = "source-over";
只有在圓形區域內的上層會顯示為透明

3. 創建圍繞著圖片中心轉動的動態效果

有了圓形遮罩之後,我們要如何讓他繞著圖形的中心點做圓周運動呢?

這裡我們可以活用 canvas 的 translate 與 rotate 方法,在每一次渲染的時候,先使用rotate 將畫布旋轉 1 度,之後再使用translate移動到遮罩的圓心位置,這樣就可以創造出像是衛星環繞的圓周運動效果了。這裡有個小地方要注意,就是我們在移動圍繞的中心點跟旋轉畫布之前,要先用 ctx.save()將目前的畫布資訊存起來,等到繪製完成之後,再使用 ctx.restore() 回復到旋轉跟移動之前的位置,才不會影響到其他部分的圖形繪製喔。

在每一禎的圖片的繪製中,我們都會先旋轉畫布,再將座標移動到遮罩的圓心

ctx.clearRect(0, 0, canvas.width, canvas.height);
angle = (angle+1) % 360;

ctx.save();
ctx.translate(centerX, centerY);
ctx.rotate(2* Math.PI* angle/360);
ctx.beginPath();
ctx.arc(0, 0, dotR, 0, 2 * Math.PI, false);
ctx.fillStyle = "#000000";
ctx.closePath();
ctx.fill();

ctx.translate(100, 0);

ctx.fillStyle = "#FF0000";
ctx.beginPath();
ctx.arc(0, 0, circleR, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();

ctx.restore();

旋轉的部分可以參考這個範例:

See the Pen Canvas rotate around point by Ankycheng (@ankycheng) on CodePen.

登愣,老闆上菜啦!

結合以上的步驟,最後的成品就是這樣:

See the Pen canvas mask effect by Ankycheng (@ankycheng) on CodePen.

這個案例使用遮罩加上簡單的動態實現靈活變動的效果,關於遮罩的應用還有很多,像是這個案例就使用了一樣的 canvas 特性做出類似刮刮卡的效果:https://codepen.io/dudleystorey/pen/yJQxLX。而形狀的部分,除了使用單純的圓形,我們也可以模擬原版中抖動的邊框,或是不同形狀的靈活變化。

有什麼有趣的想法都歡迎在留言告訴老闆,或是你覺得這樣的特性還有哪些可以靈活運用的地方呢?如果這篇文章超過 15 個留言的話,老闆將會進一步解密如何做出原本中華麗的動態球體!老闆的網頁實驗室,我們下回見囉!

手刀訂閱老闆來點寇汀吧!讓老闆帶你拆解更多有趣的程式案例 👨‍🍳

參考資料:

CSS: mix-blend-mode
Canvas: globalCompositeOperation

課程推薦

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

打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。期待在課程裡見到你!

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 老闆的網頁實驗室#2-實作 Canvas 遮罩動畫 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
老闆的網頁實驗室 #1 — 實作CSS拆字動畫 https://creativecoding.in/2020/03/06/%e8%80%81%e9%97%86%e7%9a%84%e7%b6%b2%e9%a0%81%e5%af%a6%e9%a9%97%e5%ae%a4-1-%e5%af%a6%e4%bd%9ccss%e6%8b%86%e5%ad%97%e5%8b%95%e7%95%ab/ Thu, 05 Mar 2020 17:27:19 +0000 https://creativecoding.monoame.com/?p=79 案例解析 https://rogue.studio/ 這次的老闆網頁實驗室,要來分析一個國外的工作室網站,在這個網站中,有標題一個一個字跑入的效果,這樣的效果怎麼達到的呢?其實這樣的效果並不複雜,程式…

這篇文章 老闆的網頁實驗室 #1 — 實作CSS拆字動畫 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
案例解析

https://rogue.studio/

這次的老闆網頁實驗室,要來分析一個國外的工作室網站,在這個網站中,有標題一個一個字跑入的效果,這樣的效果怎麼達到的呢?其實這樣的效果並不複雜,程式上也不會特別難達到,老闆特別喜歡他遮罩切入的感覺,要怎麼樣重現這個標題動畫套到網頁上呢?

https://rogue.studio/
https://rogue.studio/

在分析這個案例的時候很有趣的點是,如果一個一個自己手動去指定字最後擺放的位置會很麻煩,所以我們應該盡可能的利用 html 本身layout的功能,讓每個字母產生在我們希望他正常排列在文字裡面的位置之後,再把他拆成不同的元素做動畫。


第一個步驟 — 將文字拆成兩層span

首先,我們第一個目標是將整段文字,拆成分別的元素,才能去控制動畫。

內外框的拆字
內外框的拆字

在看原本的網頁時,你會發現切進來的效果彷彿原本字母的框框位置並沒有移動,超出框框的部分被切掉,所以在這種情況下,應該要分成內外兩個框框,外部的框框負責才切多出來的部分,內部的利用tranform移動,在不影響排版的情況下移動到框框裡面來。

我們希望html中內容可以越簡單越好,不用去管動畫,只要加上一個class — slideLetterIn,動畫就會自動處理跟套上去。

<h1 class=”slideLetterIn”> MONOAME STUDIO </h1>

在抓到所有有slideLetterIn的class後,我們可以利用span標籤,與inline-block屬性,把原本在同一個元素裡面的字母拆成一個一個的span,再整包放會去,讓他們當下仍參與整個字母的排列,所以乍看之下會跟還沒有拆開之前相同,但實際上已經變成內外兩層的方塊文字了!

這邊用到的是js的陣列操作方法 -map轉換跟join結合,中間的過程使用split(“”)把字串拆成一個一個文字,放入雙層的span,join成新的整坨html之後放回原本的元素裡面去。

var titleEls = document.querySelectorAll(".slideLetterIn")
titleEls.forEach(el=>{
  el.innerHTML = el.innerText
    .split("")
    .map(l=> `<span class='outer'>
                <span class='inner'>${l}</span>
              </span>`)
    .join("")
})

第二個步驟 — 製作 CSS 動畫跟控制

接下來,我們要製作一個組keyframe動畫,讓他套在每個字母上,並讓他們之間有時間差。

我們在看動畫控制的時候,可以先找出重複動作的部分,每個字母切進來的轉的角度跟移動相同,但是一個一個字會循序漸進進來。對於每一個字母我們可以套用相同的動畫,透過display: inline-block 讓他參與排列在同一行,同時有block屬性做transform。

.slideLetterIn 
  .outer
    overflow: hidden
    display: inline-block
  .inner
    display: inline-block
    transform: translate(70%) rotate(30deg)
  &.active /* 當字母進來時加上這個class */
    inner
      transform: translateX(0px)

然後字母根據他們是在整段文字裡面的第幾個元素來指定delay的時間,用scss(或sass)程式化的方式產生 nth-child(i),依序指定他們的transition-delay即可。

.slideLetterIn 
  span
    //使用loop指定第1-100個元素每個都會慢0.05秒開始動畫
    @for $i from 1 through 100
      &:nth-child(#{$i}) .inner
        transition-delay: #{$i*0.05s}
進出的delay效果

速度控制曲線參考: https://easings.net/#easeInOutSine

靈活的應用速度曲線,能比用預設的緩進緩出(ease-in-out) 帶來更生動的感覺,這次使用的easeOutQuint,是用四次方倍的動畫速度播放,因此動作開始時會比較快,創造俐落但是有彈性的感受,使用上只要把cubic-bezier指定進來到css的transition屬性即可。

速度曲線

第三個步驟 — 製作景深hover

第三個步驟是最後的點綴,網頁上滑鼠滑到字母上的時候,會有種往後移動失焦的感覺,滑鼠離開後慢慢的回復,在原始的網站中,一個字母變糊後會影響到周圍的幾個字也跟著糊,這邊製作簡易版的滑鼠上去會讓字糊化,離開時會需要一段時間恢復狀態製造連續的感覺。

Hover時模糊

直接套用預設的動畫速度看起來會很死板,因此我這邊技巧上讓他hover前後的變換時間不同,一但滑鼠移動到上面開始動畫後,就改變成快速進入的速度曲線到達最糊的狀態,滑鼠移出時,則是套用原本的緩入曲線,讓他慢慢回覆到原始的狀態,quadIn緩出的動畫速度套上去,然後變回來時再使用緩進的動畫速度。

span
  &.outer
    transition: 1s cubic-bezier(0.55, 0.055, 0.675, 0.19) //進出有不同的速度曲線
    cursor: pointer
    &:hover
      filter: blur(5px)
      transition: 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) //進出有不同的速度曲線
      opacity: 0.9
      transform: scale(0.97)

測試控制的部分我,我們加上一個checkbox,讓他改變時會去toggle最外層的class,加上了active是文字進入,會把單獨文字的transform設為0,文字就會進來,移除active就會套用原本文字的旋轉跟位移,讓文字跑出框框。

<div class="control">
  <label>Toggle
    <input id="toggle" type="checkbox" onchange="toggle()"/>
  </label>
</div>
function toggle(){
  document.querySelector(".slideLetterIn").classList.toggle("active")
}
setTimeout(function(){ 
  toggle()
},1000)

登愣,老闆上菜啦!

最後做出來的效果是這樣:

See the Pen SliceInTexts by Majer @Monoame Design (@frank890417) on CodePen.

其實這個範例的效果也可以用其他方式達到,比如canvas或是WebGL,就能夠達成更酷的像是文字扭曲、粒子特效、色散的效果。

如果大家有興趣看更多類似的分享的話,留言回覆你想要看什麼網站的拆解,老闆會帶著雞尾酒跟檸檬來拆解各式各樣有趣的網站,這篇如果超過15個留言的話,就來教大家怎麼去做區塊的模糊效果,以及能用哪些其他的方式去玩速度曲線,變化應用在不同的使用情境!

參考資料:

課程推薦

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

打好基本的互動網頁基礎之後,可以進階動畫互動網頁特效入門(JS/CANVAS),紮實掌握JavaScript 程式與動畫基礎以及進階互動,整合應用掌控資料與顯示的Vue.js前端框架,完成具有設計感的互動網站。期待在課程裡見到你!

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 老闆的網頁實驗室 #1 — 實作CSS拆字動畫 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>