creativecoding , 作者 Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/author/creativecoding/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Wed, 04 May 2022 14:42:31 +0000 zh-TW hourly 1 https://wordpress.org/?v=6.2.2 https://creativecoding.in/wp-content/uploads/2022/03/cropped-cct-logo-icon-2-32x32.png creativecoding , 作者 Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/author/creativecoding/ 32 32 【p5.js 創作教學】 色散海葵(直播筆記) https://creativecoding.in/2021/01/25/p5-js-%e5%89%b5%e4%bd%9c%e6%95%99%e5%ad%b8-%e8%89%b2%e6%95%a3%e6%b5%b7%e8%91%b5/ Sun, 24 Jan 2021 17:08:55 +0000 https://creativecoding.in/?p=492 這次的直播內容主要有四個部分: 透過繪製扭動的線條來呈現海葵觸鬚的樣子 (步驟一~步驟四) 加上滑鼠的互動以及視覺上的裝飾,包含了顏色以及材質,還有在外層加框框,形成了類似畫框的效果 (步驟五~步驟七…

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

]]>

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

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

主要會用到的 API :

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

Part 1 海葵

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

單一條線

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

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

左右搖擺

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

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

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

固定底部

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

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

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

多個線條

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

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

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

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

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

Part 2 互動與裝飾

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

加上顏色

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

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

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

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

滑鼠控制

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

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

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

加上材質

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

let overAllTexture   //定義材質

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

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

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

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

		blendMode(BLEND)
		background(0)
		noFill()

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

加框

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

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

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

Part 3 細海葵

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

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

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

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

細線上加入球球

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

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

Part 4 一叢海葵

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

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

最後的微調

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

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

結語

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

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

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

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

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

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

]]>

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

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

目標

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

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

讓素材動起來吧

驚嘆號

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

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

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

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

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

路人 trigger_door_man

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

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

阿伯與跳起來的蔥油餅 trigger_cookie

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

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

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

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

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

小孩與媽媽 trigger_cookie

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

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

結語

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

引用 animate.css

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

電腦與手機

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

了解你的使用者

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

課程推薦

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

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


墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

]]>
來做SVG動畫讓蔥油餅翻滾吧! (上):發想素材、將素材引入網頁(直播筆記) https://creativecoding.in/2021/01/15/%e4%be%86%e5%81%9asvg%e5%8b%95%e7%95%ab%e8%ae%93%e8%94%a5%e6%b2%b9%e9%a4%85%e7%bf%bb%e6%bb%be%e5%90%a7-%ef%bc%8d%e4%b8%8a%e7%af%87%ef%bc%88%e7%9b%b4%e6%92%ad%e7%ad%86%e8%a8%98%ef%bc%89/ Fri, 15 Jan 2021 10:34:05 +0000 https://creativecoding.in/?p=461 本文翻自 [週四寫程式系列] 來做SVG動畫讓蔥油餅翻滾吧!,若是對文章內容有疑問,或是想要老闆手把手帶你飛,都可以觀看影片詳細內容。 今天老闆要以曾經接手的 台北聲音地景計畫 做為案例,跟大家分享從…

這篇文章 來做SVG動畫讓蔥油餅翻滾吧! (上):發想素材、將素材引入網頁(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>

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

今天老闆要以曾經接手的 台北聲音地景計畫 做為案例,跟大家分享從專案發想、素材準備到 svg 網頁動畫。當初這個專案是做為捷運站音樂的競賽網站,希望透過聲音做為媒介,連結城市的生活空間。客戶為這個專案準備許多環境音素材,提供比賽使用,但僅是把聲音素材提供給參賽者,讓老闆覺得非常的可惜,該如何將這些素材效益最大化,聲音又要如何跟場景的記憶關聯起來,把故事跟企畫更加完善。最後老闆決定結合網頁動畫與聲音,讓使用者觀看網頁時,透過與現場環境的圖象互動,觸發聲音的回饋,讓使用者將環境與聲音做連結,於是有了這樣的專案產生。

目標

本篇文章中,老闆會帶大家完成元件的創作,並在網頁中將元件導入,完成基本的動畫操作,作為下一篇文章的暖身。

  • isometric 創作場景
  • 使用 vue, ajax 將 svg 放進網頁
  • 小試身手﹔操作 svg 元件,賦予簡單的互動與動態

素材準備

有許多方式和工具能夠製作網頁動態畫面,舉凡 css、svg、canvas、Pixi.js 等,但不管哪一種方式製作動畫,場景和演員是不可或缺的,接下來讓老闆分享動畫要使用到的素材發想過程,想看老闆怎麼完成素材,可以跟著前面提供的影片連結,和老闆一起動手操作。

創作工具

Adobe Illustrator (可到官網上選擇免費版,若是常使用也可選擇付費版)

素材收集與發想

這個專案的困難點,是因為捷運站眾多,創作前,需要到各場域中收集足夠的場景資料,才有足夠的資料來作為創作的依據。

引用自電獺少女

在風格方面,老闆選擇以 isometric 作為創作風格,等距視角(isometric)風格也稱為等軸測圖。採用45度視角,沒有消失點,達到平面上模擬 3D 效果,讓人產生有深度的錯覺。最有名的代表遊戲就是紀念碑谷,若是想知道更多相關設計,也可以到 Pinterest 上查詢相關風格的作品。

創作 isometric 作品的時候,可以利用等距網格輔助創作。

等距網格

用 Illustrator 創作時,記得要幫元件群組與命名,讓開發時,能夠快速選取目標套上動畫。

起手式 – 環境與素材載入

環境

影片中老闆是使用 codepen 來做為開發環境,好處是可以直接引入需要的工具,但載入素材時會遇到跨域問題要解決。這邊提供大家在本機開發的方式。

  1. 將開發需要用到的圖片整理到專案資料夾中
  2. 開始撰寫 html ,載入 CDN,這邊會使用到的 CDN 有兩隻,目的分別如下
    • jQuery – 負責將素材導入
    • vue.js -資料綁定畫面

資源載入

完成 html 結構且載入 CDN 之後,我們要準備讓 svg 動起來,這邊也獻上素材,提供給沒有素材的開發者們,可以直接進入開發。

這邊先解說 script 的內容,我們使用 Vue 來做為資料與畫面綁定的套件。宣告 Vue 的方式如下, el 為要將資料綁定在哪個區塊的畫面中,這裡填寫 #app,也就是到時資料會被綁定在 id=”app” 的 div 中。data 的內容為需要綁定的資料。mounted 為 Lifecycle 中的其中一個鉤子,代表已經抓到渲染的對象,且資料已經成功綁定,這個階段就是載入外部素材的最佳時刻,確保我們的導入的資料能夠成功綁定在對應的位置。

在 Vue 中,從元件初始化到註銷的過程中有許多 hook (created, mounted, destroyed…),提供使用者在元件不同階段中,能夠執行不同工作。詳細內容可以參考 官方文件

使用 jQuery 抓資料的方式如以下程式碼,這邊要小心 this 的使用,如果在外面沒有先宣告 vobj 把 this 記下來,直接寫 this.svg = red 會抓不到外層 vue 的實體資料。

var vm = new Vue({
	// 綁定的範圍
	el: '#app',
	// 綁定的資料
  data: {
		scene_door: ''
	},
	// vue 生命函數,資料載入完成後就執行
	mounted() {
		var vobj = this
		$.get('./imgs/scene.svg', function (res) {
			vobj.scene_door= res
		}, 'text')
	}
})

接著要把素材導入畫面了,這邊我們先在 <body></body> 中加入以下程式碼,id=”app” 是給 vue 辨識綁定畫面的位置。這邊可以看到 v-html=”scene_door” ,v-html 是 vue 的模板與法,會將 data 內的 scene_door 用 html 的方式導入。

在實務中,要小心使用,確定是安全的資料才渲染出來,避免使用者塞入惡意程式碼,但因為此專案是使用我們自己製作的內容,不用擔心被惡意攻擊。

<body>
	<div id="app">
		<div class="scene" v-html="scene_door"></div>
	</div>
</body>

成功之後我們會看到, svg 已經備載入到網頁中了,也可以撰寫 css 將畫面置中。

圖片成功引入

先備知識

接下來,藉由以下兩個目標,讓大家熟悉這個專案會頻繁使用到的概念,1. css 選擇器 2. transition

灰色框與神秘的互動區塊

在畫面中可以看到許多灰色的外框,這是老闆設計要來做為使用者觸發動畫偵測範圍。但是總不能讓這些灰框直接在正式專案上顯示吧?這時就得感謝先前在製作素材時,貼心的我們了。若是在製作素材時,有勤勞的為圖層命名,匯出時會提供選項讓大家選擇,現在我們就能直接在寫 css 使用 data-name 的方式去控制對應元件了。

為什麼不使用 id 呢?因為 id 名稱一個網頁只能有一個,為了避免日後撞名字的問題,這邊選擇使用 data-name。

我們使用開發者工具檢查時,可以觀察到老闆提供的素材中,灰框的 data-name 屬性值為 hidden,我們可以在 scss 中加入一些語法將它變成透明的,但是只把它透明度變成0還不夠,還必須幫它加上背景顏色,並在 trigger 加上改變滑鼠游標的語法。讓使用者滑入互動區塊時,藉由滑鼠的改變,提示使用者此區塊是可以互動的。

// 擁有 data-name 屬性,且該屬性的值為 hidden
[data-name='hidden'] {
	opacity: 0;
	stroke: transparent;
  fill: transparent;
}
// 擁有 data-name 屬性,且該屬性開頭的值為 trigger
[data-name*='trigger'] {
	cursor: pointer;
}
觀察滑鼠進出感應區塊的狀態

做個開關模擬網頁剛進入的狀況

畫面的感應區塊完成後,我們接著來製作開關模擬使用者剛進入網頁的狀況。首先先在 data 中新增一個新的變數,並綁定到畫面中的 input 。大家如果想觀察 vue 資料的變化,也可以去下載套件 vue devtool 輔助開發。透過 input 做為開關之後,場景也必須要跟著互動,這邊我們先使用 v-model 綁定資料,並用 v-bind 來綁定場景 class,當監聽到 trigger 為 true 時,程式會動態的幫我們在 .scene 加上 .active。

var vm = new Vue({
	...
	data: {
		trigger: true,
		scene_door: ''
	},
	...
})
<div id="app">
	<!-- v-model 綁定資料 trigger-->
	<input id="trigger" v-model="trigger" type="checkbox">
	<label for="trigger">場景開啟</label>
	<!-- v-bind 動態改變 class -->
	<div class="scene" v-html="scene_door" v-bind:class="trigger? 'active': ''"></div>
</div>

接下來要讓開關與動畫結合,我們希望當打開開關時,三個小島會有時間差的進入場景,這裡我們又再次使用到前面的技巧,使用 data-name 將所有 island 開頭的內容透明度改成0,在外層吃到 active 後輪流進場。要注意的是,svg 不吃 top 屬性,所以這邊我們改使用 translateY 來讓小島有浮起來的效果。透過 transition-delay 設定不同秒數,達成輪流進場的效果。svg 支援的 css 屬性可參考這篇文章。使用到 transition, transition-delay 也可以到 w3c 中深入了解

.active {
    [data-name*="island"] {
        opacity: 1;
        transform: translateY(0px);
    }
}

[data-name*="island"] {
    opacity: 0;
		// 補間動畫屬性 時間
    transition: all .5s;
		// 初始位置往下 30px
    transform: translateY(30px);
}
[data-name*="island_1"] {
    transition-delay: .1s;
}
[data-name*="island_2"] {
    transition-delay: .3s;
}
[data-name*="island_3"] {
    transition-delay: .5s;
}
  • 做個開關模擬剛進入畫面的狀況
div {
	// 補間動畫 屬性 時間
	transition: all .2s;
	// 動畫,要與 @keyframe 搭配,動畫名稱 動畫時間 延遲時間 次數
	animation: animationName 2s -0.2s infinite;
	// 為了避免動畫走鐘,加上動畫時可以設定動畫的中心點,也可以寫成百分比。
	transform-orign: center center;
	
	// 滑鼠經過時觸發的內容
	&:hover { ... }
	// 行動裝置不會有 hover, 改使用為 focus
	&:focus { ... }
}

@keyframes animationName {
 // 在整段時間的 0% 要做什麼動畫
	0% { ... }
	50% { ... }
	100% { ... }
}

結語

藉由以上的操作,相信大家已經能夠利用 vue, svg 製作簡易的動畫了,下一篇老闆將帶大家逐一將場景中的動畫實現,也會與大家分享每個區塊動畫的製作思維。

課程推薦

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 來做SVG動畫讓蔥油餅翻滾吧! (上):發想素材、將素材引入網頁(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
資料視覺化的動態長條圖製作(下):Vue/ D3(直播筆記) https://creativecoding.in/2021/01/12/%e8%b3%87%e6%96%99%e8%a6%96%e8%a6%ba%e5%8c%96%e7%9a%84%e5%8b%95%e6%85%8b%e9%95%b7%e6%a2%9d%e5%9c%96%e8%a3%bd%e4%bd%9c%ef%bc%88%e4%b8%8b%ef%bc%89%ef%bc%9avue-d3%ef%bc%88%e7%9b%b4%e6%92%ad%e7%ad%86/ Tue, 12 Jan 2021 15:44:10 +0000 https://creativecoding.in/?p=405 在上篇我們講解了如何使用jQuery製作網頁上常用的動態長條圖,在本篇文章中我們將實際操作另外兩種框架:Vue/ D3的製作方法。還沒看上一篇的快快補唷👉 資料視覺化的動態長條圖製作(上):…

這篇文章 資料視覺化的動態長條圖製作(下):Vue/ D3(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
在上篇我們講解了如何使用jQuery製作網頁上常用的動態長條圖,在本篇文章中我們將實際操作另外兩種框架:Vue/ D3的製作方法。還沒看上一篇的快快補唷👉 資料視覺化的動態長條圖製作(上):jQuery(直播筆記)

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

主要會用到的知識:

  1. HTML (Pug), CSS (Sass)與JavaScript/ jQuery的基礎觀念
  2. Vue框架概念
  3. D3框架概念

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


框架二:Vue

Vue就像你雇用了一位負責代換抄寫的小幫手,你所需要做的就是提供資料的類型和處理方式,小幫手就會自動化地填入。

首先將剛剛jQuery完成的圖表直接fork,fork是幫你複製一個新的pen,修改時不會動到原本的東西。在JS的設定裡面代入Vue。

https://i.imgur.com/uadgAsW.png

由於Vue的特性是在HTML寫模板,然後利用JS帶入資料,所以在HTML裡面新增.bar,JS的部分先把height和background-color的設定複製起來,整個forEach的部分拿掉,//更新的地方暫時註解掉。

https://i.imgur.com/OWM6T9l.png

接著把Vue這個小幫手請出來幫我們抄寫跟產生DOM物件,告訴他工作的範圍後,也要指定原本的資料datas,在HTML告訴他希望他幫我們重複打.bar這個木樁10遍,同時把木樁的數值也打上去,用toFixed(1)取到小數點第1位。

HTML

#app.graph
  .bar(v-for="data in datas") {{data.toFixed(1)}}

JavaScript

var datas = [];
var elements = [];
function generateData(){
  var temp = [];
  for( var i=0; i<10; i++){
    temp.push(Math.random()*10+5);
  }
  return temp;
}

for( var i=0; i<10; i++){
  datas.push(Math.random()*10+5);
}

var vm = new Vue({
  el: "#app",
  data:{
    datas: datas
  }
})

接著來設定長條方塊的樣式,透過Vue可以動態地去修改CSS屬性。在HTML新增:style="gen_style(data)",gen_style()像是一個方法,指導我們的小幫手說style要用data這個資料去算一個屬性(顏色、高度)做呈現。那小幫手要如何知道gen_style這個方法的實際內容呢?我們需在JS新增一個Method:{}來告訴他。Method就像一本作業指導書,在裡面我們需要回傳一個JSON物件,用來說明CSS屬性的設定。因為rgb數值設定的關係,記得data需要先用語法parseInt()處理成整數唷。

HTML

#app.graph
  .bar(v-for="data in datas",
       :style="gen_style(data)") {{data.toFixed(1)}}

JS

var vm = new Vue({
  el: "#app",
  data:{
    datas: datas
  },
  methods:{
    gen_style(data){
      var d = parseInt(data);
      return {
        "height": data*20+"px",
        "background-color":"rgb("+data*10+","+data*10+","+data*10+")"
      };
    }
  }
})

登愣,神奇的事情發生了!我們明明沒有實際地對DOM物件做屬性指定的動作,他卻乖乖地自動產生對應資料的長條圖,這就是Vue好用的地方啦,就像你買了一台3D印表機,只要告訴他規則、資料,他就會自動幫你蓋好房子了。

https://i.imgur.com/swO9EQf.png

剛剛我們用jQuery寫了一拖拉庫來達到每500毫秒自動更新的效果,在Vue只需要告訴他vm.datas = datas;就可以完成囉。

//更新
setInterval(()=>{
  datas = generateData();
  vm.datas = datas;
},500);

總地來說Vue的自動化很高、很方便,但相對他的產出就必須follow你訂下的規則,沒辦法一個一個做特定調整。


框架三:D3

D3有點像是jQuery的強化版,只是這次你拿的不是一支小破錘子,而是課金後的超炫砲無敵錘子,除了可以根據規則做重複的事情以外,還可以針對個體做客製化調整。

我們再fork一次剛剛jQuery完成的圖表。在JS的設定裡面代入D3。

https://i.imgur.com/YHScuKG.png

剛剛在jQuery我們透過forEach把物件一個一個抓出來改,現在我們不需要單獨抓出來了所以先註解掉,取而代之的是批次抓取做修改,setInterval()的地方也暫時註解掉。

  1. select()選擇graph裡面全部的bar
  2. 資料來源是datas請來這邊找。
  3. 因為現在bar還不存在而datas有10筆,透過enter()來補齊物件跟資料差距的量。當DOM數量少於data的數量,或者壓根一個都沒有的時候,我們一般會使用enter()幫忙建立。
  4. append顧名思義就是添加的意思,透過append()新增div。
  5. attr()是負責命名這個div的class為bar。
d3.select(".graph").selectAll(".bar")
  .data(datas)
  .enter()
  .append("div")
  .attr("class", "bar")
https://i.imgur.com/VSS87CD.png

有了十個bar之後接下來就是我們的重頭戲──調整高度和顏色。高度根據資料datas(d)中的第幾筆資料(i)去做調整,乘上20讓他差距更大、可視性更好,記得加上px因為他是CSS的屬性。

.style("height",(d,i)=>(d*20+"px"))

這邊要介紹D3中的一個概念:定義比例scale.linear()。我們可以把它理解成地圖上常見的比例尺,例如台灣面積大約3萬多平方公里,要塞進一張 A4 大小的地圖根本是不可能的任務,這時候就必須要用到比例了。首先,命名一個函數為yscale做線性轉換,domain()代表「原始的資料範圍」,range()則代表「轉換後的資料範圍」。先前我們在CSS把grpah的height設定為400px,所以設定轉換後range為0-400。

var yscale= d3.scaleLinear().domain([5,15]).range([0,400])

由console的結果可見yscale現在是一個可做等比例放大的函數,就像多拉O夢的放大燈ㄧ樣,藉由yscale我們也不需要再自己手動幫d乘上20增加他的級距了。

https://i.imgur.com/QP45dn7.png

顏色也是用相同的概念處理,除了數值的放大轉換外,D3也可以幫你把數值轉成顏色。命名一個函數為color,設定轉換成白色到紅色的區間。由console的結果可見color會自動幫我們把5-15的數值轉成rgb的色碼。

var color = d3.scaleLinear().domain([5,15]).range(['white','red'])

https://i.imgur.com/VKSRSvz.png

接著就來完成我們的圖表啦,使用語法style()做高度和顏色的設定,美美的圖表就大功告成了。

d3.select(".graph").selectAll(".bar")
  .data(datas)
  .enter()
  .append("div")
  .attr("class", "bar")
  .style("height",(d,i)=>(yscale(d)+"px"))
  .style("background-color", (d)=>color(d))  

https://i.imgur.com/RfLadnF.png

體會到D3的強大了嗎?雖然Vue的效能最好,但D3的優點在於他可以批次地處理一些華麗的動畫效果,例如上面的圖表還可以添加以下幾種語法,至於圖表會如何地變動就讓大家體驗看看囉!

  .style("margin-bottom", (d)=>(200 - yscale(d)/2 + "px"))
  .transition().duration(500)
  .style("margin-bottom","0px")

最後再來個結尾小bonus,或許你也會有一樣的疑問。

讀者:「如果我想要有Vue的效率配上D3的華麗,到底該用哪一個呢?」

老闆:『爭什麼,摻在一起做成撒尿牛丸啊!』

Vue跟D3該如何結合呢?把剛剛用Vue完成的圖表叫回來,可以看到我們在height和background-color那做了一拖拉庫的處理,現在這些都可以交給D3來代勞。把剛剛在D3圖表用scaleLinear處理的程式碼貼過來,data的部分用yscale和color這兩個函數做計算,這樣就大功告成囉。

return {
        "height": yscale(data)+"px",
        "background-color": color(data)
      };

https://i.imgur.com/1j70XT6.png

想看更複雜的結合使用,請往這邊走👉 codepen實作範例:長條圖_Vue&D3

框架比較

以上就是這次如何用三種框架實作長條圖的講解,希望這個介紹會讓大家對於各框架的優劣勢更加了解,在未來的專案中可以無痛使用,讓長條圖替你的網頁畫龍點睛吧!我們下次再見囉~

課程推薦

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 資料視覺化的動態長條圖製作(下):Vue/ D3(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
資料視覺化的動態長條圖製作(上):jQuery(直播筆記) https://creativecoding.in/2021/01/06/%e8%b3%87%e6%96%99%e8%a6%96%e8%a6%ba%e5%8c%96%e7%9a%84%e5%8b%95%e6%85%8b%e9%95%b7%e6%a2%9d%e5%9c%96%e8%a3%bd%e4%bd%9c%ef%bc%88%e4%b8%8a%ef%bc%89%ef%bc%9ajquery%ef%bc%88%e7%9b%b4%e6%92%ad%e7%ad%86/ Wed, 06 Jan 2021 11:04:55 +0000 https://creativecoding.in/?p=369 資料視覺化是網頁上常用的媒材,在本篇文章中我們將實際操作在jQuery/ Vue/ D3這三種框架下如何繪製出好用的動態長條圖,同時分析各個框架的優勢與劣勢,讓我們繼續往下看吧! 首先讓我們對長條圖有…

這篇文章 資料視覺化的動態長條圖製作(上):jQuery(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
資料視覺化是網頁上常用的媒材,在本篇文章中我們將實際操作在jQuery/ Vue/ D3這三種框架下如何繪製出好用的動態長條圖,同時分析各個框架的優勢與劣勢,讓我們繼續往下看吧!

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

首先讓我們對長條圖有個初步了解。長條圖多用於呈現相同性質但不同量級的資料,例如比較美國、台灣、日本這三個國家的人口數。用程式碼撰寫一個長條圖主要分為三個步驟:

  1. 資料整理,通常會用陣列表現相同性質且不斷重複的資料。
  2. 在網頁上產生相對應的物件(長條方塊)。
  3. 針對每個長條方塊設定他的屬性,例如:改變物件的高度、弧度、間距……等屬性。

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

主要會用到的知識:

  1. HTML (Pug), CSS (Sass)與JavaScript/ jQuery的基礎觀念
  2. Vue框架概念
  3. D3框架概念

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


框架一:jQuery

codepen實作範例:長條圖_jQuery

相信大家對jQuery都滿熟悉的,它的特性就是抓一個特定條件的東西,去修改他特定條件的屬性。

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

https://i.imgur.com/4krJiqp.png

接著來準備一組做為圖表資料的陣列。命名一個陣列為datas,運用JS的for迴圈,設定一個從1-10重複10遍的變數i,同時運用函數Math.random()每次推入一個隨機的變數,用console.log印出datas陣列內的資料。

https://i.imgur.com/hPNse2U.png

要產生圖表,需要製作一個放置長條圖、資料的容器,在HTML新增一個class為graph的div,在CSS針對graph的width, height, border進行屬性的設定增加可視性。

接著做進一步的資料處理,運用JS的forEach將陣列datas內的每一筆資料單獨取出(ES6簡化function為箭頭函式)給他一個參數index,console.log印出index, obj,即可看到陣列內單獨的資料和標號用的index。

https://i.imgur.com/aXgrclM.png

溫馨提醒:ES6中簡化function為箭頭函式()=>{}的寫法不是每個瀏覽器都吃,記得搭配webpack做轉換唷。

為了產生對應資料的長條圖,在forEach的迴圈內我們命名一個變數bar,bar是'<div class="bar"></div>'的HTML(注意單引號與雙引號的使用),用jQuery選取graph,利用語法append()在被選取的元素结尾插入bar的長條圖。

在CSS針對bar做width, border的屬性設定,在graph下屬性display: flex讓每個長條可以排排站。

https://i.imgur.com/gSX3fiW.png

接下來要替他們長不同的身高、上不同的顏色。首先在graph利用CSS flex的屬性align-items: flex-end讓長條方塊置底,在bar給每個長條margin-right: 10px

那要如何指定高度和顏色呢?我們利用jQuery選擇器的特性$(bar)把它命名為變數element,接著修改他CSS的height和background-color。由於obj為1-10的亂數,在高度呈現上的級距不明顯,所以把obj*20讓他的高度凸顯出來。

而CSS的顏色屬性是由rgb數值在0-255之間決定,如果只在1-10之間浮動的話會黑壓壓的一片,所以我們命名一個變數為color_val,利用語法parseInt()將obj轉成整數後再上15,這樣深淺鮮明、高度差異的長條圖就出現啦。

https://i.imgur.com/9KG7iRr.png

完成了靜態的長條圖,離動態更新長條圖就不遠了。由於接下來會比較複雜所以貼出JS的完整code給大家參考,步驟分為以下:

  1. 命名一個新的陣列為elements,用來儲存先前產生出的實體物件(變數element),利用elements.push(element)直接推進去。
  2. 利用語法setInterval()規定每500豪秒更新長條圖一次。
  3. 更新的動作利用函式generateData()來進行,把它想成一組亂數產生器,所以一樣命名一個陣列temp,把亂數塞進去。
  4. 接著讓datas = generateData();亂數更新一坨拉庫資料,有資料後就可以按照資料更新物件(長條方塊),因為剛剛把亂數產生包在陣列裡面了,所以記得先做初始化。運用forEach把每個物件抓出來玩一遍,根據對應的資料更新他的高度和背景顏色所以使用datas[index],第一筆資料對應第一個物件、第二筆資料對應第二個物件。

這樣我們就有一個會變化的長條圖了(撒花),但是否覺得變換瞬間有點卡卡的?只要在bar的CSS設定transition: 0.5s就可以解決囉!

var datas = [];
var elements = [];
function generateData(){
  var temp = [];
  for( var i=0; i<10; i++){
    temp.push(Math.random()*10+5);
  }
  return temp;
}

for( var i=0; i<10; i++){
  datas.push(Math.random()*10+5);
}
datas = generateData(); //function初始化
console.log(datas);

datas.forEach((obj, index)=>{
  console.log(index, obj);
  var bar = '<div class="bar"></div>';
  var color_val = parseInt(obj)*15;
  var element = $(bar);
  element.css("height", obj*20+"px")
         .css("background-color", "rgb("+color_val+","+color_val+","+color_val+")")
  $(".graph").append(element);
  elements.push(element);
});

//更新
setInterval(()=>{
  datas = generateData();
  elements.forEach((element,index)=>{
    var color_val = parseInt(datas[index])*15;
    element.css("height", datas[index]*20+"px")
         .css("background-color", "rgb("+color_val+","+color_val+","+color_val+")")
  });
},500);

最後一步是新增每個長條方塊所對應的數值,最快速的方法就是直接把數值包在長條bar裡面,然後使用CSS position的定位方法將數值定在長條方塊的下方。

把文字包在長條內var bar = '<div class="bar"><div class="text"></div></div>';

利用jQuery children選取element的小孩".text",用語法toFixed(1)取到小數點第1位。在下方setInterval的更新處重複ㄧ樣的步驟,記得把變數名稱obj修改成datas[index]

element.children(".text").text(obj.toFixed(1))
https://i.imgur.com/kE68ek9.png

接著在CSS針對bar和text的定位做設定,就可以看到長條圖表下方出現數值了。

.bar
  position: relative
  
.text
  position: absolute
  bottom: -30px

做到這裡你有沒有發現我們一直在做一件重複的事情?沒錯,就是針對實體物件的數值做修改。用jQuery寫圖表就有點像用一個簡單的錘子蓋房子,從建造到調整都需要針對每個DOM物件去做手動修改,土炮且自立自強。

下一篇文章中我們會講到另外兩個框架:Vue和D3,相較於如此「人工」的jQuery,這兩個框架比較偏自動化,就讓我們下次見囉~

課程推薦

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 資料視覺化的動態長條圖製作(上):jQuery(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
p5.js 快速上手:互動網頁教學 https://creativecoding.in/2020/04/24/p5-js-%e5%bf%ab%e9%80%9f%e4%b8%8a%e6%89%8b/ Fri, 24 Apr 2020 10:47:22 +0000 https://creativecoding.in/?p=325 什麼是 p5.js? 當我們想要在網頁上繪圖,最常用的除了 CSS 之外,就是 HTML5 提供的 Canvas 了,Canvas 的 API 接口讓我們可以實現有效率、高靈活性在網頁上繪圖的夢想,C…

這篇文章 p5.js 快速上手:互動網頁教學 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
什麼是 p5.js?

當我們想要在網頁上繪圖,最常用的除了 CSS 之外,就是 HTML5 提供的 Canvas 了,Canvas 的 API 接口讓我們可以實現有效率、高靈活性在網頁上繪圖的夢想,Canvas 的許多基礎都在 動畫互動網頁特效入門(JS/CANVAS)中提到,讓你可以從頭開始,完成豐富的網頁互動創作。

但是對於想要快速開始互動網站、藝術創作的人來說,Canvas 上像是繪圖、形狀、物理模擬或是常用的數學公式等功能都需要自己從頭處理。有沒有更快的方式呢?

答案是有的,就是今天要介紹的 p5.js,p5 是由 Processing 延伸而成的 JavaScript 函式庫,Processing 是設計給沒有程式基礎的人快速進行創作的平台,而 p5.js 可以理解為 Processing 的 JavaScript 版本。它將許多繪圖、數學、物理模擬等函式封裝好讓我們可以直接使用,如此一來我們可以在享受 Canvas 繪圖的同時不用費心思處理瑣碎的工具!

範例分享

在本篇文章中我們將會實作一個簡單的畫布,把滑鼠當作筆刷,滑鼠經過的路徑會留下彩色的軌跡,點擊的狀況下軌跡則變成方形的圖案,增加繪製的多樣性。

想要跟著影片一起看也沒問題,請從這邊走 👉

我們將以 OpenProcessing 作為本次範例的平台,這是一個像是 Code Pen 的線上程式碼編輯器,可以在創作的同時即時看到程式碼運做的狀況,十分方便!

p5.js 創作環境

如果我們在 OpenProcessing 創立一個新的專案,會看到一個預設的專案長這樣子:

介面非常簡單,左邊是程式碼的區塊,右邊是目前程式繪圖的結果。我們在左邊輸入的程式碼在按下上方的執行或是(cmd + Enter)之後就會即時更新在畫面上。

畫布設定 setup() 與 draw()

p5.js 的專案由兩個主要的函式構成,setup() 與 draw()。setup() 負責程式的初始化,只會執行一次;而 draw() 則會持續更新,我們也可以透過 print(frameCount) 把禎數這個內建的變數打印出來,在 console 中看到當下畫面更新了多少次。

我們通常會在 setup() 中設定一些通用的初始值,例如畫布的大小、背景、繪製型態(2D 或是 3D)之類,在 draw() 中撰寫會時時更新的程式碼,像是筆刷的更新、物件的移動、滑鼠位置、感測器的資料更新等。就算我們不懂 p5 的函式,也可以從 OpenProcessing 的範例中猜出 createCanvas(windowWidth, windowHeight); 是創建畫布,background() 是設定顏色;ellipse(mouseX, mouseY, 20, 20) 是用滑鼠的位置繪製一個寬高各 20 的圓形。

圖形繪製

如同一開始所說,在 p5 要繪製圖形非常容易,以方形來說,我們只要使用 rect() 函數,輸入要繪製的 x, y 座標以及長寬,就可以變出一個方形,圓形也是同理,使用 ellipse() 函數就可以變出來。如果想要知道更多內建的圖形函式,也可以參考文件中的 2D Primitives 部分。

這邊要注意 rectMode() 的設定,如果將他設定為 rectMode(CENTER) 的話繪製方形就會把左標訂定在方形的中間,否則就是方形的左上角喔!

動態改變顏色與線條

能夠繪製形狀之後我們可以試著給圖形一些顏色,在 p5 裡面,設定的函式是 fill() ,裡面就像是 background() 一樣,可以填入 HEX、RGB 等色碼、顏色的英文等參數。要設定顏色也很簡單,在繪製圖形之前加上 fill(‘你要的顏色或是色號’) 就行了。

此外我們也能用 stroke() 與 strokeWeight() 設定圖形邊線粗細或是顏色 noStroke() 則是不加任何的邊框。合併在一起看看吧:

加上判斷式

p5 當中提供了不少可以直接運用的狀態,像是 mouseIsPressed 就是當下滑鼠是否有點擊事件,或是 keyIsPressed 判斷使否有點擊鍵盤事件,甚至是行動裝置的感測器也有 accelerationX, Y, Z 等事件。讓偵測事件變得十分方便。

我們希望在點擊的當下,繪製出不同的形狀,只要在隨時更新的 draw() 當中加上判斷式 if(mouseIsPressed){繪製我要的圖形} 就可以了。

將上述的功能組合在一起

做點有趣的組合,讓作品隨著 frameCount 改變筆刷顏色、增加滑鼠點擊出現不同繪圖的互動,最後再使用隨機改變圖形大小,就完成一個有趣的互動作品了!是不是比自己用 Canvas 土砲慢慢做來的方便多了?來看看我們的成品吧:

以上就是這次的 p5.js 快速上手,p5 的文件寫得很完整,如果有遇到不懂的問題查閱一下,說不定就可以馬上解決,文件中更有互動的範例,簡直是無痛上手程式創作的最佳利器!

希望這個介紹對大家有幫助,如果想要學習更多程式創作的手法與內容,歡迎來看看墨雨設計最新開設的「互動藝術程式創作入門 Creative Coding」 課程,會從頭介紹程式創作的概念與手法,讓你快速做出生動豐富的互動作品喔!

等不及想用p5.js創作嗎?
【p5.js創作教學】電波電路Wave Circuit – 來做個逼哩逼哩送訊號的電路吧!(直播筆記)
【p5.js 創作教學】 色散海葵(直播筆記)

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 p5.js 快速上手:互動網頁教學 最早出現於 Creative Coding TW - 互動程式創作台灣站

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

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

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

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

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

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

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


程式實作

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

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

讀取地圖,並操作樣式

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

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

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

<html>

<head>
...
</head>

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

</html>

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

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

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

body {
    background-color: #222
}

svg {
    height: 100vh;
}


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

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

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

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

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

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

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

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

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

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

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

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

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

像是這樣:Do re mi so ~

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

]]>
用 Socket.io 做一個即時聊天室吧!(直播筆記) https://creativecoding.in/2020/03/25/%e7%94%a8-socket-io-%e5%81%9a%e4%b8%80%e5%80%8b%e5%8d%b3%e6%99%82%e8%81%8a%e5%a4%a9%e5%ae%a4%e5%90%a7%ef%bc%81%ef%bc%88%e7%9b%b4%e6%92%ad%e7%ad%86%e8%a8%98%ef%bc%89/ Tue, 24 Mar 2020 16:17:28 +0000 https://creativecoding.in/?p=277 這次的筆記是跟著這部影片來完成的,想要一邊看到會動的老闆請走這邊: 什麼是 Socket.io? socket.io 是一個可以讓應用程式建立即時通訊的 JavaScript 函式庫,透過在 Serv…

這篇文章 用 Socket.io 做一個即時聊天室吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
這次的筆記是跟著這部影片來完成的,想要一邊看到會動的老闆請走這邊:

什麼是 Socket.io

socket.io 是一個可以讓應用程式建立即時通訊的 JavaScript 函式庫,透過在 Server(伺服器)與Client(裝置)之間建立持續的連線,可以即時的傳送資料給對方。想要瞭解更多的話可以參考 socket.io 的通訊協定基礎 WebSocket

可以把由 socket 所建立的應用想像成是一個學校的廣播系統,學務處(Server)可以向每個班級(Client)統一廣播上課的鐘聲,而每個班級在點名完之後,也可以透過班長把點名的結果回報給學務處。要注意的是一般來說學務處可以單獨發送訊息給班級,但是班級之間沒有正規的溝通管道。

socket.io 的這幾個優點造成了他在即時通訊、遊戲上的活用:

  1. 簡化溝通:透過on方法,像 JavaScript 一樣接收事件並觸發 callback function。
  2. 即時:可以非常即時的同步資料。
  3. 資料同步:可以在執行的階段在瀏覽器同步保存資料。

應用範例 2018 印象清華 – 物聯網科技藝術節 作品

光譜原色:透過建立 WebSocket 連線即時變換湖面上的燈光。影片

英文8-2:將作答題目的結果與得分即時顯示在頁面上。


程式實作 – 即時聊天室

在這個範例中我們將會建立一個即時聊天室,透過 socket.io 來實現 Server 與多個 Client 之間的溝通,並在用戶登入的時候讀取所有對話記錄、送出訊息的時候發送到所有的用戶介面。

初始化 server 端專案

  1. 在 terminal 先新建並進入資料夾 mkdir socket-server && cd socket-server
  2. 接著安裝 socket.io 與 express(網頁伺服器框架) npm i socket.io express -s

設定 socket 與 http(s) 連線

建立 index.js ,index.js 是後端的主程式,負責處理用戶端傳來的事件並將結果廣播給所有用戶。我們首先設定 socket 與 api 的監聽端口。

*因為此範例都在本機電腦開發,並且直接連線到 localhost,故沒有設定 ssl 加密與 https。

var fs = require('fs')
// var https = require('https')
// 如果不用 https 的話,要改成引用 http 函式庫
var http = require('http')
var socketio = require('socket.io')

//https 的一些設定,如果不需要使用 ssl 加密連線的話,把內容註解掉就好
var options = {
    // key: fs.readFileSync('這個網域的 ssl key 位置'),
    // cert: fs.readFileSync('這個網域的 ssl fullchain 位置')
}

//http & socket port
var server = http.createServer(options);
server.listen(4040)
var io = socketio(server);
console.log("Server socket 4040 , api 4000")

//api port
var app = require('express')();
var port = 4000;
app.listen(port, function () {
    console.log('API listening on *:' + port);
});

//用 api 方式建立連線
app.get('/api/messages', function (req, res) {
    let messages = 'hellow world'
    res.send(messages);
})

//用 socket 方式建立連線
io.on('connection', function (socket) {
    console.log('user connected')
})

執行 npm index.js ,如果出現以下訊息的話,代表我們的 http server 初步建立完成囉~

Server socket 4040 , api 4000
API listening on *:4000

為了測試 api 連線是不是也是正常,我們直接使用瀏覽器連線到 server 監聽的 api 網址,成功看到透過 api GET 取的的回覆顯示在螢幕上,再打開 devtools 的 Network 也確認無誤,接下來可以進入 socket 的部分了!

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3575cada-13cd-4bbb-bf2b-91bd53a9963a/Screen_Shot_2020-03-24_at_00.15.34.png
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6b2fa09-b128-490e-9856-d6aa6560a30e/Screen_Shot_2020-03-24_at_00.13.05.png

我們一樣先測試 socket 的連線能不能成功,但是要怎麼讓瀏覽器端可以連線到 socket 呢?

只要在 html 的 head 引用 socket.io 的裝置端套件就可以了 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>。我們直接在 <script> 裡面建立與伺服器的連線:var socket = io("<http://localhost:4040>"),將瀏覽器打開之後如果看到 server 有打印 user connected 的話就是連線成功囉。

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e27f893c-ea9b-4e1b-b717-944801b77e07/Screen_Shot_2020-03-24_at_14.50.50.png
到 network 儀表版也可以看到我們的請求成功

基本的訊息傳送

不論在 server 或是 client,socket 都是透過 on 來監聽事件、用 emit 來發送事件,大致的關係會是這樣:

簡易 socket.io 通訊關係

Server 端建立連線/事件傳送方向Client 端
io.on(‘connection’, function (socket) {…})建立連線socket = io(“socket ip:port”)
io.emit(“要對所有 Client 廣播的事件名稱”, data)

socket.emit(“要對當前連線的 Client 發送的事件名稱”, data)
———>socket.on(“來自client 的事件名稱”, callback)
socket.on(“來自client 的事件名稱”, callback)<———socket.emit(“要對 server 發送的事件名稱”,data)

先由 client emit 一個最簡單的訊息看看,送出一個包含 name 與 message 的物件:

// index.html
// 建立與 server 的連線
var socket = io("<http://localhost:4040>")

// 發送一個 "sendMessage" 事件
socket.emit("sendMessage", {
            name: "majer",
            message: "hello everyone"
        })
// 監聽來自 server 的 "allMessage" 事件
socket.on("allMessage", function(message){
    console.log(message)
})

當然也別忘了在 server 加上”sendMessage” 事件的監聽器:

// index.js
io.on('connection', function (socket) {
    console.log('user connected')
    // 建立一個 "sendMessage" 的監聽
    socket.on("sendMessage", function (message) {
        console.log(message)
	// 當收到事件的時候,也發送一個 "allMessage" 事件給所有的連線用戶
	io.emit("allMessage", message)
    })
})

看一下 server 顯示的結果,果然把我們剛剛 emit 的資料印出來了

Server socket 4040 , api 4000
API listening on *:4000
user connected
{ name: 'majer', message: 'hello everyone' }

而瀏覽器也可以看到從 server 發送過來的 “allMessage” 事件:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/83cc5d8d-2f09-41b2-a22f-6155cfedb177/Screen_Shot_2020-03-24_at_15.42.19.png

萬丈高樓平地起,我們緊接著就可以在這個基礎之上建立聊天室的介面囉

聊天室的功能

我們列出聊天室會有的基本功能:

  1. 進入聊天室時印出聊天室目前的對話記錄
  2. 可以輸入用戶名稱與訊息,並且點擊後發送
  3. 可以接收別人發送的新訊息

Server 端 🖥

我們先在 server 端把所有的對話內容與用戶儲存在 messages 陣列內,每當有新的用戶建立連線,就把之前的對話透過 allMessage 傳送給用戶。除此之外,我們也監聽用戶發送的 "sendMessage" 事件,除了發送新訊息給所有用戶之外,也把新收到的訊息塞到 messages 裡面,讓新用戶進來的時候可以看到。

// index.js
var messages = [
    { name: "Majer", message: "Welcome!"  }
]

io.on('connection', function (socket) {
    console.log('user connected')
    // 發送之前的全部訊息
    io.emit("allMessage", messages)
    // 當此用戶發送訊息的時候,先把新訊息放到 messages 陣列裡面
    // 再 emit 給所有用戶
    socket.on("sendMessage", function (message) {
        console.log(message)
        messages.push(message)
        io.emit("newMessage", message)
    })
})

User 端 👨🏼‍💻

在進入頁面的時候,我們使用 on("allMessage") 把之前的對話記錄都儲存到 messages 裡面,再透過 v-for 處理 messages 陣列,把對話的內容與用戶名稱印在畫面上。此外,加上新訊息的監聽 on("newMessage"),如果有新的訊息,也更新到 messages 的最後面。

發送訊息的部分,我們使用 Vue 把用戶的名稱與訊息綁定在 temp 上,每次發送的時候就直接送出 temp 物件,再把 temp.message 設定成空字串 '' 清空。

<body>
    <div id="app">
        <ul>
            <li v-for="m in messages">
                <h4>{{m.message}}<span>-- {{m.name}}</span></h4>
            </li>
        </ul>
        <!-- 將 name 與 message 綁定到 data 的 temp 物件內 -->
        <input v-model="temp.message" placeholder="訊息" @keydown.enter="sendMessage" />
        <input v-model="temp.name" placeholder="你是誰?" />
        <button @click="sendMessage">送出</button>
    </div>
</body>

<script>
    var vm = new Vue({
        el: "#app",
        data: {
            messages: [],
            temp: {},
            socket: null,
        },
        mounted() {
            this.socket = socket = io("<http://localhost:4040>")

            // 進入聊天室時,會收到之前的全部訊息,並更新到 messages
            this.socket.on("allMessage", obj => {
                console.log('received all messages')
                this.messages = obj
            })

            // 設定接收到新訊息的監聽器
            this.socket.on("newMessage", obj => {
                console.log('received new message')
                this.messages.push(obj)
            })
        },
        methods: {
            sendMessage() {
                console.log('sending new message')
                this.socket.emit("sendMessage", this.temp)
                this.temp.message = ""
            }
        }
    })
</script>

如此一來,我們就完成了最基礎的聊天室介面與功能了!

快來看看實際運行起來的狀況吧:

其他延伸

我們也可以加入其他的功能,像是:

  • 顯示其他人在輸入中
  • 顯示用戶上線/下線
  • 設定用戶不重複的名字
  • 寄送私人訊息
  • 傳送圖片、gif

完成品

最後附上有加上輸入中版本的完整程式碼,或是也可以到專案的 github 看到這個範例呦:https://github.com/Monoame-Design/bosscoding-examples

server

var fs = require('fs')
// var https = require('https')
// 如果不需要用 https 的話,要改成引用 http 喔
var http = require('http')
var socketio = require('socket.io')

//https 的一些設定,如果不需要使用 ssl 加密連線的話,把內容註解掉就好
var options = {
    // key: fs.readFileSync('這個網域的 ssl key 位置'),
    // cert: fs.readFileSync('這個網域的 ssl fullchain 位置')
}

//http & socket port
var server = http.createServer(options);
server.listen(4040)
var io = socketio(server);
console.log("Server socket 4040 , api 4000")

//api port
var app = require('express')();
var port = 4000;
app.listen(port, function () {
    console.log('API listening on *:' + port);
});

//用 api 方式取得
app.get('/api/messages', function (req, res) {
    let messages = 'hellow world'
    res.send(messages);
})

var messages = [
    { name: "Majer", message: "Welcome!" }
]

var typing = false
var timer = null
//用 socket 方式取得
io.on('connection', function (socket) {
    console.log('user connected')
    socket.emit("allMessage", messages)

    socket.on("sendMessage", function (message) {
        console.log(message)
        messages.push(message)
        io.emit("newMessage", message)
    })

    socket.on('sendTyping', function () {
        console.log('typing')
        typing = true
        io.emit("someoneIsTyping", typing)
        clearTimeout(timer)
        timer = setTimeout(() => {
            typing = false
            io.emit("someoneIsTyping", typing)
        }, 3000)
    })
})

client

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
</head>

<body>
    <div id="app">
        <ul>
            <li v-for="m in messages">
                <h4>{{m.message}}<span>-- {{m.name}}</span></h4>
            </li>
        </ul>

        <div>{{ typing?'輸入中...':'' }}</div>
        <br>
        <!-- 將 name 與 message 綁定到 data 的 temp 物件內 -->
        <input v-model="temp.message" placeholder="訊息" @keydown.enter="sendMessage" @keypress="sendTyping" />
        <input v-model="temp.name" placeholder="你是誰?" />
        <button @click="sendMessage">送出</button>
    </div>
</body>

<script>
    var vm = new Vue({
        el: "#app",
        data: {
            messages: [],
            temp: {},
            socket: null,
            typing: false
        },
        mounted() {
            this.socket = socket = io("http://localhost:4040")

            // 進入聊天室時,會收到之前的全部訊息,並更新到 messages
            this.socket.on("allMessage", obj => {
                console.log('received all messages')
                console.log(obj)
                this.messages = obj
            })

            // 設定接收到新訊息的監聽器
            this.socket.on("newMessage", obj => {
                console.log('received new message')
                this.messages.push(obj)
            })

            this.socket.on("someoneIsTyping", value => {
                this.typing = value
            })
        },
        methods: {
            sendMessage() {
                console.log('sending new message')
                this.socket.emit("sendMessage", this.temp)
                this.temp.message = ""
            },
            sendTyping() {
                this.socket.emit("sendTyping")
            }
        }
    })
</script>

</html>

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 用 Socket.io 做一個即時聊天室吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

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

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

]]>
案例解析

Louis Ansa — Portfolio

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

分析思路

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

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

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

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

實作

1. 創建兩層 canvas

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

ctx.translate(100, 0);

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

ctx.restore();

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

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

登愣,老闆上菜啦!

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

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

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

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

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

參考資料:

CSS: mix-blend-mode
Canvas: globalCompositeOperation

課程推薦

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

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

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

]]>
案例解析

https://rogue.studio/

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

速度曲線

第三個步驟 — 製作景深hover

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

Hover時模糊

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

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

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

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

登愣,老闆上菜啦!

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

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

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

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

參考資料:

課程推薦

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

]]>