動態網頁教學 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/動態網頁教學/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Sun, 08 May 2022 09:56:14 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.2.2 https://creativecoding.in/wp-content/uploads/2022/03/cropped-cct-logo-icon-2-32x32.png 動態網頁教學 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/動態網頁教學/ 32 32 【Vue.js入門】一小時學會 Vue.component,完成動態飯店房間清單 https://creativecoding.in/2022/05/17/vue-js-hotel-room-list/ Tue, 17 May 2022 02:51:00 +0000 https://creativecoding.in/?p=2593 我們會從 Vue.js 基本概念開始講起,非常適合剛開始接觸 Vue 的朋友們,接著我們用 Vue component 實作一個動態的飯店編輯頁面,你會學到:Vue.js 基本概念、Vue.js 語法指令及深入操作 Vue.component。

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

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

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

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

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

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

Vue.js 基本概念

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

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

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

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

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

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

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

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

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

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

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

computed:前處理,先行運算

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

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

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

v-bind:屬性綁定

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

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

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

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

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

Vue 基本概念實作練習

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

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

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

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

Codepen裡的設定
Codepen裡的設定

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

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

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

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

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

<div id="app">

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

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

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

computed:前處理、先行運算

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

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

定義final_price算法
定義final_price算法

v-model:資料雙向綁定

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

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

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

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

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

v-for 迴圈

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Vue Component 實作練習

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

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

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

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

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

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

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

定義 Vue.component 並繼承資料

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

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


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

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

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

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

在 HTML 中呼叫繼承的資料

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

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

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

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

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

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

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

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

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

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

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

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

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

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

定義 computed 屬性做前運算

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

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

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

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

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

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


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

增添 CSS 美化版面

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

// CSS
$color_red: #DB4343 

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

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

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

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

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

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

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

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

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

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

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

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

增加飯店資訊編輯區塊

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

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

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

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

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

增加房間總折數編輯欄位

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

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

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

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

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

增加服務費編輯欄位

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

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

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

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

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

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

增加各個房間編輯區塊

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

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

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

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

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

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

.room_edit 
  margin-top:30px

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

增加刪除功能

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

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

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

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

// CSS
	.cursor_pointer 
	  cursor: pointer

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

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

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

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

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

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

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

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


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

收整房間編輯區塊

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

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

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

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

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

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

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

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

總結

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

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

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


老闆的工商時間

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

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

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

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

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

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

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

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

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

事前準備

開發環境

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

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

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

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

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

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

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

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

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

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

在Code Sandbox裡加入gsap套件

接下來會使用到的 API:

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

Vue

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

gsap

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

js – getUserMedia

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

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

跟著老闆開始動手做

操作一段與多段動畫

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

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

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

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

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

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

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

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

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

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

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

直播畫面與 live 標籤

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

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

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

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

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

表情符號功能

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

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

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

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

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

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

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

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

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

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

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

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

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

留言區塊

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

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

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

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

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

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

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

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

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

新增/刪除訊息

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

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

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

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

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

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

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

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

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

老闆來結語

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

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

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

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

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

墨雨設計banner

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

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

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

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

透過此次教學,你會學到

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

認識常用的 Canvas 屬性

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

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

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

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

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

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

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

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

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

圖1-2

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

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

圖1-3
圖1-3

前置作業

設定 Code Pen

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

引入雲端字形

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

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

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

一、基礎版面

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

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

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

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

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

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

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

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

canvas
  background-color: #333

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

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

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

二、基礎版面樣式設置

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

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

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

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

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

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

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

  c.width = ww
  c.height = wh

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

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

三、繪製一個矩形

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

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

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

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

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

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

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

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

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

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

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

四、畫垂直線

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

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

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

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

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

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

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

五、利用極座標畫線條

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

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

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

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

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

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

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

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

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

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

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

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

六、扇形掃描線

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

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

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

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

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

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

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

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

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

//JavaScript
function draw() {
  ...

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

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

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

七、敵人系統

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

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

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

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

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

function draw(){...}

7-1 隨機產生敵人

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

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

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

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

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

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

7-2 敵人在畫面上顯示

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

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

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

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

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

//JavaScript
function draw(){
  ...

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

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

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

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

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

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

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

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


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

7-4 修改敵人樣式

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

八、修改左下角文字

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

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

九、外圍刻度

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

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

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

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

function draw(){
  ...

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

十、畫龍點睛的線條

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

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

function draw(){
  ...

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

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

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

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

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

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

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

function draw(){
  ...

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

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

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

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

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

function draw(){
  ...

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

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

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

回顧

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

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

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

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

墨雨設計banner

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

]]>
Vue.js入門:製作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 - 互動程式創作台灣站

]]>
用Vue.js做快速換色與整理的便利貼牆吧!(下)(直播筆記) https://creativecoding.in/2021/07/09/%e7%94%a8vue-js%e5%81%9a%e5%bf%ab%e9%80%9f%e6%8f%9b%e8%89%b2%e8%88%87%e6%95%b4%e7%90%86%e7%9a%84%e4%be%bf%e5%88%a9%e8%b2%bc%e7%89%86%e5%90%a7-%e4%b8%8b/ Fri, 09 Jul 2021 02:31:00 +0000 https://creativecoding.in/?p=1178 需要發想靈感、紀錄個人代辦清單,或和他人討論嗎?製作一個能夠自由編輯、增刪、變色、拖曳編排的便利貼牆網頁,多個願望一次滿足。下集將便利貼牆美化、功能變得更完善,更連結firebase即時資料庫,再多張便利貼也不怕。

這篇文章 用Vue.js做快速換色與整理的便利貼牆吧!(下)(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
上一篇我們製作了可以新增、拖曳、換顏色的簡易版本便利貼,這次除了調整細節與動畫外,也會串接Firebase資料庫,讓便利貼牆可以多人同時編輯與更新。

用Vue.js做快速換色與整理的便利貼牆吧!(下)完成圖
用Vue.js做快速換色與整理的便利貼牆吧!(下)完成圖

這次的教學將有以下幾個重點,主要聚焦在既有功能的優化與新功能的添加:

  1. 點擊便利貼文字時因滑鼠與左上角距離設定的關係,會造成距離差而產生的跳動
  2. 加入刪除便利貼的功能
  3. 加入新增或刪除便利貼時,放大縮小的transition
  4. 修改顏色的控制列需和正在使用中的便利貼位置相對應(上次我們處理的方式是都先暫時放在畫面右手邊,這次把它修改得人性化一些)
  5. 編輯文字和多行文字時呈現的大小
  6. 串接Firebase保存資料,即時整理與更新

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

如果想搭配直播影片一起實作,請往這邊走

修正點擊便利貼文字造成的距離差

首先fork一份上一次的檔案,fork就像是複製,這樣接續修改也不會動到最初的原始檔唷。

接下來我們在CSS的.postit裡面加上.textpointer-events: none,讓他停止觸發任何的點選事件,在製作滿版圖面但不希望阻礙滑鼠事件時,可以使用這種處理方式(比如:市面網站常見透過hamburger選單收合的滿版menu)。

修正點擊便利貼文字造成的距離差
修正點擊便利貼文字造成的距離差

加入刪除便利貼功能

刪除的概念可以想成「從包著便利貼id的陣列中,運用語法 splice() 切掉該張便利貼的id」。在colorList中新增刪除按鈕如下:

button.btn(@click="postits.splice(pid,1)") 刪除

新增 / 刪除的transition

接著加入新增和刪除的動畫,我們使用vue的transition group處理。在使用上有以下幾點特性:

  1. 在HTML新增,transition-group(name="fade"),注意transition Group需要包成一個div使用,這邊用tag="ul"處理,而便利貼為li
  2. Group內的每個物件都需要名字,才知道控制動畫的元件範圍,給他:key="pid",每張便利貼都有個獨一無二的id

利用vue官方提供的效果fade稍微調整一下語法,加入scale讓便利貼有從小變大、長出來的效果。

//HTML
#app
  transition-group(name="fade", tag="ul")
    li.postit(v-for="(p,pid) in postits",
            //為每張便利貼加上一個id
            :key="pid",
            :style="postitCss(p)",
            @mousedown="selectId($event,pid)")
      .text {{p.text}}
//CSS
.fade-enter-active, .fade-leave-active 
  transition: .5s

.fade-enter, .fade-leave-to 
  opacity: 0
  transform: scale(0.1)
新增 / 刪除的transition
新增 / 刪除的transition

調整顏色控制列的位置

為了讓修改顏色的功能更人性化一些,我們把.colorList整包移到.postit裡面,用position: absolute定位在便利貼下方。這時你會發現熟悉的點擊距離差問題又回來了,但因爲修改顏色和刪除的機制也是透過滑鼠,所以無法使用之前的方法來解決。

先前拖曳功能的設定為——點擊便利貼時,將該張便利貼設定id為0,當滑鼠移動時同步更新設定。現在把控制列跟便利貼拆成不同部分,也就是說點擊控制列時不進行id的設定。

selectId的event中判斷source element是否含有blockbtn,如果沒有,則進行id的設定,如果有,則nowId=-1

//CSS
.colorList
  position: absolute
  bottom: -80px
  display: flex
  flex-direction: row
  .block
    margin-right: 10px
//JavaScript
selectId(evt,id){
  console.log(evt)
  let isBlock = evt.srcElement.classList.contains('block')
  let isBtn = evt.srcElement.classList.contains('btn')
  if (!isBlock && !isBtn ){
    this.nowId=id //滑鼠點下去
    this.startMousePos = {
      x: evt.offsetX,
      y: evt.offsetY
    }
  }else{
    this.nowId=-1
  }
}
調整顏色控制列的位置
調整顏色控制列的位置

文字編輯和多行文字時呈現的大小

如同調整顏色的控制列,我們希望在編輯文字部分可以有更好的使用者體驗,透過點擊該張便利的「編輯」按鈕即可修改。在Vue裡增加 setText 這個方法,利用語法 prompt() 跳出修改視窗,並在input欄位顯示原始的文字(透過抓取pid知道是哪張便利貼、上面有甚麼文字):

//HTML
//新增編輯按鈕
button.btn(@click="setText(pid)") 編輯
//JavaScript
methods:{
  ...
  setText(pid){
    //彈出視窗修改文字
    let text = prompt("請輸入新的文字", this.postits[pid].text)
    //送出之後再便利貼上更新文字
    if (text){
      this.postits[pid].text=text
    }
  }
}
文字編輯和多行文字時呈現的大小
文字編輯和多行文字時呈現的大小

連接Firebase資料庫

接下來進入今天的重頭戲——串接Firebase資料庫。Firebase是Google提供的雲端開發平台,協助 開發者在雲端快速建置後端服務,提供即時資料庫。這種noSQL(非關聯式)類型的資料後端平台可能是未來的趨勢,noSQL代表你不會用像select all member這種特殊的資料查詢語法,他就是一張樹狀圖,把所有東西塞進去,所以你可以看到便利貼在頁面上即時地移動與資料修改,在Firebase資料庫裡也可以看到頁面上的改動。

首先前往Firebase的控制台,新增一個for便利貼的專案。

接著選擇Realtime Database,在專案中新增child如下:

tips: 右邊的值必須要先輸入一個default數值,之後有資料存入時便會被取代掉了。

接著引入我們的codepen網頁,在codepen引進CDN,再初始化資料庫。

Step 1:進到Overview,點選「網頁」。

Step 1:進到Overview,點選「網頁」。
Step 1:進到Overview,點選「網頁」。

Step 2:將Firebase新增至codepen。CDN為第一個script內的src,初始化config為下方的firebaseConfig。

Step 2:將Firebase新增至codepen,初始化config為下方的firebaseConfig。

Step 3:引入CDN,第一個script內的src貼入codepen settings。

Step 3:引入CDN,第一個script內的src貼入codepen settings。
Step 3:引入CDN,第一個script內的src貼入codepen settings。

Step 4:初始化資料庫,將Step 2裡面第二個script中的程式碼複製貼在我們JS程式碼的最上方。

Step 4:初始化資料庫,將Step 2裡面第二個script中的程式碼複製貼在我們JS程式碼的最上方。
Step 4:初始化資料庫,將Step 2裡面第二個script中的程式碼複製貼在我們JS程式碼的最上方。

接著透過Firebase手動新增一張便利貼如下,注意資料的層級,尤其是代表便利貼位置的x, y是在pos下面。

透過Firebase手動新增一張便利貼
透過Firebase手動新增一張便利貼

再把codepen跟建立好的firebase資料庫串接,並監聽他的value做即時更新,可以看到我們剛剛手動在資料庫新增的便利貼。Firebase語法可參考官方文件

//JavaScript
var postitsRef = firebase.database().ref("postits2"); //建立連結
  postitsRef.on('value', (snapshot)=>{
   vm.postits = snapshot.val() //即時更新
  })

註:firebase串接語法已更新成firebase.database().ref(),直播內容的firebase.database.ref()為舊版。

把codepen跟建立好的firebase資料庫串接,並監聽他的value做即時更新
把codepen跟建立好的firebase資料庫串接,並監聽他的value做即時更新

遠端新增 / 刪除便利貼

可以透過資料庫新增並呈現在頁面上後,我們這邊試試透過codepen push便利貼進去資料庫,修改methods addPostits的地方,讓他不是新增在vm這邊而是postitsRef

//JavaScript
addPostits(){
  postitsRef.push(
    {
      text: "文字",
      color: "yellow",
      pos: {x: 200+Math.random()*100, y: 200+Math.random()*100 }
    }

我們希望能刪除特定便利貼的節點,也就是postitsRef下的子結點,記得也需修改HTML刪除按鈕的語法為@click="deletePostit(pid)",並在JS新增以下methods:

deletePostit(pid){
  postitsRef.child(pid).remove();
}

同步顏色 / 文字 / 拖移位置

除了更新頁面上便利貼的位置,也同步更新遠端資料庫的位置,所以在mousePos抓這張便利貼this.nowId,設定set更新遠端資料庫的值。

//JavaScript
postitsRef.child(this.nowId).set(this.postits[this.nowId])

同步文字的邏輯也是類似的,在setText做完本地更新後,也一起更改資料庫的文字。

//JavaScript
postitsRef.child(pid).set(this.postits[pid])

最後一個則是顏色,我們原先是讓便利貼的顏色等於顏色的名字p.color=color.name,現在為了更新遠端資料 ,我們把它包成一個function叫setColor。在methods定義setColor的作用,概念和setText類似。

//HTML
.colorList
  .block(v-for="color in colorList",
     :style="{backgroundColor: color.color}",
         //修改成成為setColor function
     @click="setColor(pid, color.name)")
//JavaScript
setColor(pid,colorname){
  this.postits[pid].color=colorname
  postitsRef.child(pid).set(this.postits[pid])
},

以上就是這次的直播內容,主要聚焦於功能的優化與資料庫的串接,後面firebase的部分對於初次接觸的人可能會需要一段時間的摸索,但只要成功串接起來、再多研究一下文件,就會比較好入手。

步驟總結

這次的直播內容比較複雜,所以在最後來個總重點整理一下。在上篇我們先建立便利貼的基礎,從樣式雛形到基本資料處理,可以分為以下幾個重點:

1. 便利貼樣式與資料處理 – 建立便利貼架構,色票、文字樣式和文字位置
2. 加上滑鼠互動事件 – 紀錄滑鼠移動的位置並儲存在evt,運用nowId判斷滑鼠在哪一張便利貼的範圍裡面
3. 新增便利貼與修改顏色 – 做一個button點擊觸發function addPostits,在addPostits推入新的陣列

下篇的部分我們著重在既有功能的微調、更精緻化,還有資料庫的串接:

1. 修正點擊便利貼文字造成的距離差 – pointer-events: none停止觸發任何的點選事件
2. 刪除便利貼功能 – 運用語法splice()切掉該張便利貼的id
3. 新增 / 刪除的transition – 使用vue的原生transition group處理,transition-group(name=”fade”)
4. 調整顏色控制列的位置 – 把colorList整包移postit裡面,用position: absolute定位在便利貼下方
5. 文字編輯和多行文字時呈現的大小 – 利用語法prompt()跳出修改視窗,優化文字編輯的使用者體驗
6. 連接Firebase資料庫 – 在Firebase建立專案與Realtime Database,與Codepen資料連接
7. 同步顏色 / 文字 / 拖移位置 – 在mousePos抓特定便利貼this.nowId,設定set更新遠端資料庫的值

課程推薦

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

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

那我們下次再見啦👋👋👋

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

墨雨設計banner

這篇文章 用Vue.js做快速換色與整理的便利貼牆吧!(下)(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
用Vue.js做快速換色與整理的便利貼牆吧!(上)(直播筆記) https://creativecoding.in/2021/07/05/%e7%94%a8vue-js%e5%81%9a%e5%bf%ab%e9%80%9f%e6%8f%9b%e8%89%b2%e8%88%87%e6%95%b4%e7%90%86%e7%9a%84%e4%be%bf%e5%88%a9%e8%b2%bc%e7%89%86%e5%90%a7-%e4%b8%8a/ Mon, 05 Jul 2021 01:31:00 +0000 https://creativecoding.in/?p=1154 需要發想靈感、紀錄個人代辦清單,或和他人討論嗎?製作一個能夠自由編輯、增刪、變色、拖曳編排的便利貼牆網頁,多個願望一次滿足。上集我們使用Pug、Sass及Vue.js刻出便利貼的基本功能。

這篇文章 用Vue.js做快速換色與整理的便利貼牆吧!(上)(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
在需要發想靈感的職業日常中,便利貼常常作為靈感紀錄與討論道具,有著不可取代的重要性,但困擾的是常常黏性一過,原先貼得整整齊齊的便利貼只要一有風吹草動,就如同十二月的雪一樣翩翩飛舞。如果可以把這些小傢伙電子化,豈不是美事一樁?今天我們就要用Vue.js來實作可以快速地新增、輸入內容、換色、刪除甚至拖曳編排的便利貼牆。這一次的案例因為比較複雜,所以切成兩篇來做,請上下兩篇搭配一起服用唷~

用Vue.js做換色與快速整理的便利貼牆吧!(上)成品圖
用Vue.js做換色與快速整理的便利貼牆吧!(上)成品圖

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

如果想搭配直播影片一起實作,請👉🏻往這邊走

了解架構

在開始之前我們先來概念性的發想,便利貼的結構設計可分為以下這些方向:

  1. 儲存的資料種類—文字(顏色名稱)和色票
  2. 文字呈現方式—文字在便利貼上應該滿版呈現,隨著文字多寡而動態調整大小
  3. 拖曳功能—滑鼠點擊在便利貼上時紀錄初始座標,到結束時動態計算中間的滑動呈現
  4. 小功能—切換文字、刪除等功能

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

準備好Code Pen中的環境
準備好Code Pen中的環境

便利貼樣式與資料處理

接著我們開始從第一張便利貼刻起,便利貼的結構簡單來說就是一個裝有文字的框框,文字資料我們先暫時寫死,稍後再用Vue.js動態更新。

在HTML給他一個容器名為postit裡面裝一些文字text,接著在CSS設定他的樣式如下,如次便能得到一個文字在中間的基礎白色便利貼:

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans&display=swap')
*, *:before, *:after
  border: solid 1px //之後再把邊線取消掉
  font-family: 'Noto Sans', sans-serif
@mixin size($w, $h:$w)
  width: $w
  height: $h

$colorBlack:#3E3A39

html, body
  // background-color: $colorBlack //背景色,設定樣式時先拿掉方便識別
  
.postit
  +size(240px)
  font-size: calc( 240px / 4 - 5px ) //便利貼寬÷字數-預留空隙
  display: flex
  justify-content: center
  align-items: center
第一章便利貼

接下來替便利貼上一些活潑的顏色,讓他變得好玩一些,同時調整字的顏色跟上一些陰影來增加設計的細緻度,我們的第一張便利貼就完成了。使用的色票和陰影處理如下,大家也可以自由選擇喜歡的顏色唷:

//修改字體顏色不要太死黑
color: #44403F

//便利貼顏色
$colorYellow: #FFEB67
$colorBlue: #A5D8D6
$colorRed: #EF898C
$colorGreen: #CBE196

.postit
	background-color: $colorYellow
  letter-spacing: 5px
  font-weight: 500
  box-shadow: 15px 10px 40px rgba(black,0.4)
調整便利貼的顏色、陰影
調整便利貼的顏色、陰影

接下來進行資料的處理,在JavaScript裡給予一組陣列名為postits,裡面裝便利貼會用到的參數textcolorposition。接著新增一個new Vue並指定作用範圍為#app,抓出物件便利貼跟他是第幾張(p,pid) in postits,將文字套進{{p.text}}。用左下角的Console檢查資料是否連接好,先輸入vm之後,再輸入vm.$data.postits[0].text="temp"資料,如果出現”temp”替換掉原先的文字「都市更新」就代表成功。

//Vue.js
var vm = new Vue({
  el: '#app',
  data: {
    postits: [
      {
        text: "都市更新",
        color: "yellow",
        pos: {x:20, y:0}
      }
    ]
  }
})
可替換內文的便利貼
可替換內文的便利貼

加上滑鼠互動事件

為了增加自由度,我們來替便利貼加上拖曳的功能。拖曳代表著我們必須在螢幕上做絕對定位,當在拖動物件時,左上角的距離不斷地重複更新,要做到這項事情,首先我們必須將資料與定位綁定起來。

在CSS的.postit裡加上position: absolute,有時在Vue我們會動態地加上style,但這容易造成HTML程式碼裡拖了一長串反而不好閱讀,所以像這種共用性高的style可以考慮直接在CSS做設定。

接著在JS設定style的[methods](<https://cythilya.github.io/2017/04/17/vue-methods-and-event-handling/>)如下,然後帶入HTML的postit後,可在Console透過剛剛上面的方法改變文字來測試字型大小的調整是否成功。

methods:{
  postitCss(p){
    return {
      left: p.pos.x+"px", //動態地更新便利貼的位置
      top: p.pos.y+"px",
      'font-size': ((240-30) / p.text.length) +'px' //根據文字長度動態設定大小
    }
  }
}
修改CSS與JS綁定文字資料及位置,以便加上拖曳功能
修改CSS與JS綁定文字資料及位置,以便加上拖曳功能

顏色的設定跟文字的方法差不多,在JS的data裡面新增一個名為colorList的陣列,裡面塞入我們剛剛的色票與相對應的名字:

colorList: [
  {
    name:"yellow",
    color: "#FFEB67"
  },{
    name:"blue",
    color: "#A5D8D6"
  },{
    name:"red",
    color: "#EF898C"
  },{
    name:"green",
    color: "#CBE196"
  },{
    name:"black",
    color: "#3E3A39"
  }
],

接著在下方的methods裡return他的值。在Vue裡面你要抓他的值可以直接用this指向,定義條件用find過濾符合的資料。

//JavaScript
'background-color': this.colorList.find(o=>o.name==p.color).color

這時我們可以製作一個control pannel來快速調整便利貼內的文字跟顏色,省去一直打開Console輸入指令測試的重複步驟。在HTML新增一個ul放入li和輸入欄位inputinput分別對應到p.text抓取輸入文字內容和p.color選擇便利貼顏色,再設定他的css。

//HTML
ul.datalist
  li(v-for="(p,pid) in postits")
    input(v-model="p.text")
    input(v-model="p.color")
//css
.datalist
  position: fixed
  right: 20px
  top: 20px
  width: 30%
設定便利貼顏色
設定便利貼顏色

前面做了資料和定位的綁定後滑鼠靜止的部分搞定,接下來要做滑鼠移動時的行為,這部分比較複雜可能需要多一點時間理解唷。我們在整個畫面上紀錄滑鼠移動的位置並儲存在evt內,右鍵檢查裡面有一個參數offset代表滑鼠距離左上角的相對位置。

小筆記:evt代表event,每次滑鼠移動時會觸發的一連串事件,包括滑鼠位置、點擊放開等,都會儲存在這裏面。

//JavaScript
window.onmousemove = (evt)=>{
  //滑鼠移動時,將最新位置記錄到vue中
  // console.log(evt)
  vm.postits[0].pos.x=evt.pageX //設定第一張便利貼的x距等於滑鼠在頁面上的x距
  vm.postits[0].pos.y=evt.pageY //設定第一張便利貼的y距等於滑鼠在頁面上的y距
}
滑鼠移動更新其在Vue.js內的位置

現在便利貼會跟著滑鼠移動,但我們希望移動是滑鼠點擊觸發之後才發生的。所以需要在data的地方多儲存一個nowId: -1nowId代表滑鼠點擊但還沒放開時,滑鼠在哪一張便利貼的範圍裡面。

我們在HTML裡加上@mousedown="selectId(pid)"並綁定點擊事件放在methods裡面,新增滑鼠移開的事件。為了方便識別可以把nowId印在畫面上。

//JavaScript
var vm = new Vue({
  ...	
  data: {
    ...
    nowId: -1
  },
  methods: {
    ...
    selectId(id){
      console.log(id)
      this.nowId=id //滑鼠點擊時選擇該張便利貼
    }
  }
})

//滑鼠移動時,將最新位置記錄到vue中
window.onmousemove = (evt)=>{
  // console.log(evt)
  if (vm.nowId!= -1){
    vm.postits[vm.nowId].pos.x=evt.pageX //抓取第nowId張便利貼的x距等於滑鼠在頁面上的x距
    vm.postits[vm.nowId].pos.y=evt.pageY //抓取第nowId張便利貼的y距等於滑鼠在頁面上的y距
  }
}

window.onmouseup = (evt)=>{
  vm.nowId = -1 //滑鼠未點擊時沒有選擇任何便利貼
}
//HTML
#app
  .postit(v-for="(p,pid) in postits",
          :style="postitCss(p)",
          @mousedown="selectId(pid)")
    .text {{p.text}}


ul.datalist
//把nowId印在畫面上
  li
    h1(style="color: white") {{nowId}}
  ...
設定滑鼠點擊與未點擊時的判別與動作
設定滑鼠點擊與未點擊時的判別與動作

單用window.onmousemove = (evt)監測時可能會有些狀況,我們希望當滑鼠有變動時,就在Vue裡偵測資料的變動並針對位置做更新,所以我們新增watch偵測是否有選擇便利貼,如果有就把這張便利貼抓出來。這張便利貼會等於所有便利貼的第nowId個,知道第幾張後就去設定他的位置。

var vm = new Vue({
  data: {
    ...
    mousePos: {
      x:0, y:0
    }
  },	
  watch: {
    mousePos(){
      if (this.nowId!= -1){
        let nowPostit = this.postits[this.nowId]
        nowPostit.pos.x = this.mousePos.x
        nowPostit.pos.y = this.mousePos.y
      }
      console.log(this.mousePos)
    },
  ...
})

window.onmousemove = (evt)=>{
  // console.log(evt)
  vm.mousePos = {x: evt.pageX, y: evt.pageY}
  //if (vm.nowId!= -1){
    //vm.postits[vm.nowId].pos.x=evt.pageX
    //vm.postits[vm.nowId].pos.y=evt.pageY
  //}
  
}

記得把作用範圍撐開跟window一樣大,不然會無法運作。

//css
html, body, #app
  background-color: $colorBlack
  padding: 0
  margin: 0
  overflow: hidden
  +size(100%)

拖曳的功能就完成了,但這時點擊便利貼的右下角時會有一些偏移、跳一下,我們可以記錄這個偏移量並加上點擊的位置,這樣一減一加之後我們就可以讓他乖乖待在位置上。在HTML的selectId新增$event,也記錄第一個點下去的位置startMousePos

@mousedown="selectId($event,pid)"
var vm =new Vue({
  ...
  data: {
    ...
    startMousePos: {
      x: 0,
      y: 0
    }
  }
  ...
  methods: {
    ...
    selectId(evt,id){
      console.log(id)
      this.nowId=id //滑鼠點下去
      this.startMousePos = {
        x: evt.offsetX,
        y: evt.offsetY
      }
    }
  }
})

watch減掉偏移量,有滑鼠移動而且我們判斷當下那張便利貼存在的時候。這樣拖曳功能就大功告成啦。

watch: {
  mousePos(){
    if (this.nowId!= -1){
      let nowPostit = this.postits[this.nowId]
      nowPostit.pos.x = this.mousePos.x-this.startMousePos.x
      nowPostit.pos.y = this.mousePos.y-this.startMousePos.y
    }
    console.log(this.mousePos)
  }

如果想要將滑鼠拖曳的效果變得更滑順,可以在CSS的.postit裡面加上cursor: pointer試試效果。

新增與修改顏色

接下來我們來做「新增」的功能,做一個button點擊觸發functionaddPostits。在addPostits推入新的陣列,位置給予亂數Math.random才不會覆蓋在原有的便利貼上。

新增便利貼功能
新增便利貼功能
//HTML
  ul.datalist
    ...
    button(@click="addPostits") +新增便利貼

最後做個改顏色的功能,透過點擊動態渲染的小方塊來改變便利貼的顏色,參考如下。

ul.datalist
  li
    ...
    .colorList
      .block(v-for="color in colorList",:style="{backgroundColor: color.color}", @click="p.color=color.name")
//css
.block
  +size(30px)
  background-color: #fff
  display: inline-block
//JavaScript
methods: {
  //直接從現有顏色清單裡選取一顏色
  getColor(name){
    return this.colorList.find( o=>o.name==name)  
  }
  ...
}
製作顏色小方框,方便直接更換便利貼顏色
製作顏色小方框,方便直接更換便利貼顏色

總結

以上就是前半段的便利貼教學,我們先建立便利貼的基礎,從樣式雛形到基本資料處理,可以分為以下幾個重點:

1. 便利貼樣式與資料處理 – 建立便利貼架構,色票、文字樣式和文字位置

2. 加上滑鼠互動事件 – 紀錄滑鼠移動的位置並儲存在evt,運用nowId判斷滑鼠在哪一張便利貼的範圍裡面

3. 新增便利貼與修改顏色 – 做一個button點擊觸發function addPostits,在addPostits推入新的陣列

下一次我們會接續進行刪除、背景調整、縮放動畫等的功能,我們下次見啦👋👋👋

老闆的互動網頁課程

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

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

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

墨雨設計banner

這篇文章 用Vue.js做快速換色與整理的便利貼牆吧!(上)(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>