來用可怕的三角函數做網頁吧! – Part 1 衛星繞月球(直播筆記)

https://imgur.com/iqS8cqe.gif

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

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

前言

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

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

Dyverse Studio主頁動畫

從遊戲開始出發

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

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

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

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

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

適用於目標位置不改變。

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

x+=Δx
y+=Δy

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

適用於目標位置不改變。

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

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

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

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

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

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

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

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

比例會根據θ而改變

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

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

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

衛星繞月球

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

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

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

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

場景 html 結構

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

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

Sass 重複使用 – @mixin

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

$color_space: ##2c3d4f

*
  border: solid 1px

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

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

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

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

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

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

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

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

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

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

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

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

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

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

製作陰影

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

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

月亮不同坑洞

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

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

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

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

衛星軌道

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

HTML

.trace1
.trace2

CSS

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

旋轉的衛星

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

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

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

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

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

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

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

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

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

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

https://imgur.com/p649GNo.gif

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

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

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

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

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

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

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

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

結語

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

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

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

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

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

PHP Code Snippets Powered By : XYZScripts.com