三角函數 彙整 | Creative Coding TW - 互動程式創作台灣站 https://creativecoding.in/tag/三角函數/ 蒐集互動設計案例、教學與業界資源,幫助你一起進入互動程式創作的產業 Tue, 20 Jul 2021 08:04:26 +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 【p5.js創作教學】Quantum Unstable 量子不穩定 – 發光糾纏的量子系統 https://creativecoding.in/2021/07/19/p5-js%e5%89%b5%e4%bd%9c%e6%95%99%e5%ad%b8-quantum-unstable-%e9%87%8f%e5%ad%90%e4%b8%8d%e7%a9%a9%e5%ae%9a/ Mon, 19 Jul 2021 03:37:00 +0000 https://creativecoding.in/?p=1278 利用p5.js製作出變形的同心圓動態,沒有特別的創作點子時就隨意調整、利用不同的變數增加變化,終究會等到那個靈光一閃,增加許多畫龍點睛的細節,打造出科技HUD風格的量子系統作品《量子不穩定Quantum Unstable》

這篇文章 【p5.js創作教學】Quantum Unstable 量子不穩定 – 發光糾纏的量子系統 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
當初老闆一開始在做這個作品時,是沒有甚麼特定想法的,過程中其實也是蠻迂迴的。

一開始先以極座標的方式去定義點的位子,並用 curveVertex() 將點連成線。過程中不斷去調整 curveVertex() 中 X與Y的位置,到了步驟五,呈現出一顆中心原子核的時候才比較確定這次的主題,接下來就決定朝向物理以及科幻的風格去做,所以後面就加上了在四周圍繞的游離粒子、標示刻度的數字以及線條來包圍中間的量子完成作品。

《量子不穩定》作品完成圖
《量子不穩定》作品完成圖

透過此次互動藝術創作教學,你會學到

  • 在製作 HUD 這類科幻類型主題時,可以使用哪些裝飾性的技巧
  • 如何應用極座標的概念,來繪製線條,並透過旋轉角度速度、內外層圓圈、圓周上位子等不同的參數來調整出所希望的效果。
  • 利用原有的素材 ( X與Y的位置),來產生出不一樣的物件效果 (游離粒子)

在開始製作之前你該知道的p5.js小技巧

  • 極座標 來繪製粒子位置,並將點的位子連接起來形成曲線
  • vertex() 有極座標位置後,以 vertex() 將點連成線,需要在畫線的前後加上 beginShape()endShape()
  • curveVertex()vertex() 相同將點連成線,差異在於 vertex() 所連成的線為直線,而 curveVertex() 連成的為曲線
  • 透過 noise() 產生隨機的效果,比起一般常用的 random() 來說會更自然、和諧一點。老闆的範例中會時常使用 noise,所以建議在開始之前點進去先稍微理解 noise 的概念
  • drawingContext 繪製陰影
  • push() / pop() 保存與還原畫布的狀態(可以參考老闆互動藝術程式創作入門的課程筆記:章節 7 進階繪圖 – 畫布操作與編織複雜圖形 (pop/push 的圖解)
  • setAlpha() 可設定顏色的透明度

就讓老闆帶你們一步步完成吧!

《量子不穩定》全步驟圖
《量子不穩定》全步驟圖

1. 繪製圓形與波形

在最初的時候我們先由畫布中心向外畫出同心圓,來確認一下每一個圓大致上的位置。(程式碼中的1. 同心圓版本)

接下來就將原本繪製圓形的 ellipse 替換成以極座標 cos 與 sin 來作呈現,詳細的原理可參考 來用可怕的三角函數做網頁吧! – Part 1 。這樣我們就可以動態地指定半徑 – rr 的大小。(程式碼中的2. 改成 vertex,並以 cos 以及 sin 來指定點的位置)

另外,也可以更改 ang 的角度,展示出不同的波形,而途中會產生動態旋轉效果的原因是因為老闆在設定 ang 中,加入了系統變量(也就是計算程式啟動以來的幀數 frameCount)。(程式碼中的3. 以 frameCount 來產生動態的角度變化 ang,隨時間製造出不同的波型)

這裡提供一個畫圓的示範連結,可以嘗試改變 cos 與 sin 裡面的 i ,以及後面所相乘的 50 ,透過修改去理解各個參數所代表的是甚麼含意。

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

function draw() {
  background(230)
  translate(width/2,  height/2)

  // 1. 同心圓版本
  for(var i=0; i<width; i+=100){
    ellipse(0, 0, width-i);
  }

  // 2. 改成 vertex,並以 cos 以及 sin 來指定點的位置
  for(var i=0; i<width; i+=100){
    beginShape()
    let rr = width-i;
    // ellipse(0, 0, width-i);
    for(var o=0; o<360; o+=3){
      vertex(cos(o)*rr, sin(o)*rr)
    }
    endShape()
  }

  // 3. 以 frameCount 來產生動態的角度變化 ang,隨時間製造出不同的波型
  for(var i=0; i<width; i+=100){
    beginShape()
    let rr = width-i;
    // ellipse(0, 0, width-i);
    for(var o=0; o<360; o+=3){
      let ang = o/1.2+frameCount;
      vertex(cos(ang)*rr, sin(ang)*rr)
    }
    endShape()
  }
}

2. 加上顏色

在顏色的選擇上,老闆一樣使用配色網站 https://coolors.co/ 抓取隨機產生的配色,在選定好顏色後,可以直接複製上面的網址到程式中處理,變成可以使用的色票格式。

而在波形上老闆有做三個更動:

  • 減少連接點的數量,360 個 → 100個。
  • 在點與點的連接上,由 vertex 改為 curveVertex,差異在於 curveVertex 在連接點與點之間會帶有弧形,會比較有滑順感,vertex 則是直線連接。
  • 在 ang 角度的設定上,若是把係數拿掉,會長成 o / i + frameCount / o ,其中 i 所代表的是由內而外的每一圈,當它被放在分母的時候,這也代表當越外圈( i 越小),o / i也會越大,再參數相加後,ang 也跟著變大,這樣就達成了越外圈頻率越快,越是內圈頻率越慢的效果。
/*1. 設定顏色 */
let colors = "3d348b-7678ed-f7b801-f18701-f35b04".split("-").map(a=>"#"+a)

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

function draw() {
  background(0)
  translate(width/2,  height/2)
  strokeWeight(2)
  noFill()
  for(var i=0; i<width; i+=100){
    beginShape()
    let rr = (width-i)*0.8;
    // ellipse(0, 0, width-i);
    /*1. 設定顏色 */
    stroke(colors[int(i/100)%5])
    /*2. 改變波型數量: 360 -> 100 ; 改變角度(ang)設定  ; Vertex -> curveVertex */
    for(var o=0; o<100; o+=3){
      let ang = o/(1.2+i/2000)+frameCount/(40+o);
      curveVertex(cos(ang)*rr, sin(ang)*rr)
    }
    endShape()
  }
}
《量子不穩定》步驟二階段性成果
《量子不穩定》步驟二階段性成果

3. 加上材質 調整波型大小

接著為了讓上面的波型有點分段,所以加上開始的角度 stAng ,在 stAng 中,老闆根據上該點是第幾圈 i,以及是在圓周上哪個位置上 o,來決定是哪個角度開始。

而在每一個點的位置上,老闆在 x 與 y 的座標上都加上 i 值,因為所有的點都同時加上一樣的正值,所以原本可以完整圓的畫面,現在看去只能顯示原本圖形的左上的部分,其餘的點並非消失,而是該點的位置已經超出畫面的範圍了。

/* 2. 加上  start angle */
for(var o=0; o<100; o+=3){
  let stAng = frameCount/1000 + (o/2) + (i/2)
  let ang = stAng+o/(1.2+i/2000)+frameCount/(40+o);
  /* 3. 在 x 與 y 的位置上都加上 i */
  curveVertex(i+cos(ang)*rr, i+sin(ang)*rr)
}

下面是以只有一個圓的簡化版本,並且標示出位置點,如此一來比較容易思考點在位置上的變化。

《量子不穩定》步驟三:比較位置點
《量子不穩定》步驟三:比較位置點

除了調整波型外,老闆也在這裡加上材質,新增材質共有三大步驟,分別是命名材質、設定材質、疊加材質。

  • 命名材質
    在全域中命名材質變數 let overAllTexture
/* 1. 加上材質 */
let overAllTexture
  • 設定材質
    setup() 中設定材質的樣式
  overAllTexture=createGraphics(width,height)
  overAllTexture.loadPixels()
  // noStroke()
  for(var i=0;i<width+50;i++){
    for(var o=0;o<height+50;o++){
      overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,40,80])))
    }
  }
  overAllTexture.updatePixels()
  • 疊加材質
    draw() 中改變與色塊的混合模式
push()
  blendMode(MULTIPLY)
  image(overAllTexture,0,0)
pop()
《量子不穩定》步驟三階段性成果
《量子不穩定》步驟三階段性成果

4. 調整縱向比例製造中心壓縮

這裡老闆先將原本上個步驟在 x 與 y 位置加上 i 的效果先拿掉。改以增加 x 參數中半徑的大小,在 rr 後加上了 i/2 ,這會左右拉伸,製作出被上下壓縮的效果。而在 y 在位置上則是改加上 i/2,產生出由上而下的,俯瞰的感覺。為什麼這裡不是一樣加上 i 呢 ? 這是因為這會使圖形超出畫面的範圍。

  /*3. 線條變粗 */
  strokeWeight(10)

  /*1. 將 X 變小 */
  for(var o=0; o<100; o+=5){
    let stAng = frameCount/1000 + (o/2) + (i/10)
    let ang = stAng+o/(3+i/5000)+frameCount/(30+o);
    curveVertex(cos(ang)*(rr+i/2), i/2+sin(ang)*rr)
  }

接著老闆替線條加上陰影,在陰影的設定上,有四個基本的參數,分別是顏色 (shadowColor)、 X 的偏移量(shadowOffsetX)、 Y 的偏移量(shadowOffsetY)以及模糊的程度(shadowBlur)。加上陰影是老闆在很多的創作時候會使用的手法,當這個陰影就是根據線條本身改變的時候,就會有拼貼的立體感出來。

function setup() {
  /*設定材質 */

  /*2. 加上陰影 */
  drawingContext.shadowColor = color(0,100)
  drawingContext.shadowOffsetY = 3
  drawingContext.shadowOffsetX = 3
  drawingContext.shadowBlur = 20
}
《量子不穩定》步驟四階段性成果
《量子不穩定》步驟四階段性成果

5. 移動中心點繞中心旋轉

再次調整波型呈現方式 !

這裡回歸到原始純粹的模樣,將 stAng 設定為 0,並且將原本加在 y 位置上的i/2 拿掉,這樣就像是一個圍繞的中心在旋轉的粒子球。

/*1. 回歸到最於原始,並且變成是繞中心點旋轉 */
let rr = (width-i)*0.5
...
for(var o=0; o<100; o+=5){
  let stAng = 0
  let ang = stAng+o/(3+i/5000)+frameCount/(30+o);
  curveVertex(cos(ang)*(rr+i/2), sin(ang)*(rr-i/10))
}

接著就把作品固定到中央,並且讓它慢慢地旋轉,此時突然發現這好像磁力線以及極光的感覺,所以就決定朝物理學的方向做,便是外圍先作圓形,在外面做一圈圓形,將原本裡面的線條做限制,並且將裡面的圓縮小且帶旋轉。

function draw() {

  /*4. 加上背景 */
  fill(30,30,100,0.85)
  rect(0,0,width,height)

  push()
    /*1. 將球中心置於畫布中心 */
    translate(width/2,height/2)
    /*3. 縮小並旋轉 */
    scale(0.7)
    rotate(frameCount/500)
    stroke(255,150)

    /*2. 外圍加上一圈 */
    noFill()
    strokeWeight(10)
    ellipse(0,0,900)
  
    for(var i=0; i<width; i+=100){
      ...
    }

  pop()
}
《量子不穩定》步驟五階段性成果
《量子不穩定》步驟五階段性成果

6. 加上發光效果跟調整材質

再加上圓形後,就開始覺得這個東西應該是會發光的,所以加上 Screen mode,不過視覺上又會覺得顏色有點太多層,所以加上了 setAlpha ,以降低透明度的方式來去做調整,而且老闆還特別將線條與陰影分開做設定,可以看到陰影與線條的顏色是一樣的 (let cc = color(clr)) ,只是透明度不同,透過這樣將 setAlpha 降低,畫面上就不會過曝,顏色看上去也就會比較明顯一點。

調整完色彩後,覺得可以再多加上一些變化,所以就再加上了 cos(ang*8+o/100)*rr/200 以及 sin(ang*8+o/100)*rr/200 ,前面這串程式看上去很長,加上現在圖形還同時在旋轉,不太容易看出效果,所以這邊可以試著將程式簡化,用一開始所提供的同心圓範例去修改成簡單版本的(如下方示意圖)所呈現的效果是可以看到不只是完整的大波,上面另外也會有一些小的褶皺。

push()
  ...
  /*1. SCREEN mode */
  blendMode(SCREEN)

  for(var i=0; i<width; i+=100){
    beginShape()
    strokeWeight(8)
    let rr = (width-i)*0.5
	
    /*2. 降低透明度,本體與陰影分開 */
    let clr = color(colors[int(i/100)%5])
    clr.setAlpha(150)
    stroke(clr)
	
    let cc = color(clr)
    cc.setAlpha(100)
    drawingContext.shadowColor =cc
	
    /*3. 加上小的波形 */
    for(var o=0;o<150;o+=5){
      let stAng =0 
      let ang = stAng+o/(3+i/5000)+frameCount/(30+o)+mouseY/100
      curveVertex(
        cos(ang)*(rr+i/2)+ cos(ang*8+o/100)*rr/200 ,
        sin(ang)*(rr-i/10) + sin(ang*8+o/100)*rr/200
      )
    }
    endShape()
  }

調到這裡其實這次的主題就明確了,就是物理量子力學的主題。

《量子不穩定》步驟六階段性成果
《量子不穩定》步驟六階段性成果

7. 加上外部裝飾

接下來,在做這種科幻型的主題時,老闆的慣用手法就是加上一些弧形或者是刻度。這裡老闆決定在四周畫上線條,有點像是準心的感覺,並且加上有一個似乎意義不明的數字,這樣一來就產生一種「好似有複雜的機制在後面運作」的感覺,為了避免顯示的數字太長,所以在數字上使用 toFixed(2) 來限制小數點後可顯示的位數為兩位數。另外在此星球旋轉上也加上了 mouseX ,讓滑鼠在移動時,也會進而影響星球旋轉的速度。

function draw() {
  push()
    /*1. 旋轉上加上滑鼠互動 */
    translate(width/2,height/2)
    scale(0.73)
    let totalRotate = frameCount/500+mouseX/100
    rotate(totalRotate)

    /*2. 加上四象的線條,像是準心 */
    stroke(255)
    line(480,0,550,0)
    line(-480,0,-550,0)
    line(0,-480,0,-520)
    line(0,480,0,520)
    noStroke()
    fill(255)
    textSize(20)
    text(totalRotate.toFixed(2),500,30)
  pop()
}
《量子不穩定》步驟七階段性成果
《量子不穩定》步驟七階段性成果

8. 加上游離粒子

這裡老闆想要加上一些游離粒子,而加上游離粒子的方式非常的特別,因為曲線看上去是一條線,但是實際上都是一個點一個點所連接起來的,所以老闆就想說那不如把 X 與 Y 拿來畫。

在將 X 與 Y 拿來使用前,先將畫曲線的點抽出來命名變數 let xx = cos(ang)*(rr+i/2)+ cos(ang*8+o/100)*rr/200 以及 let yy = sin(ang)*(rr-i/10) + sin(ang*8+o/100)*rr/200 ,這樣在畫曲線 curveVertex(xx,yy)ellipse(xx,yy,3) 粒子都會比較方便,程式碼也不會顯得太長。透過直接使用畫曲線的點去畫粒子,可以讓視覺上粒子與線條具有一致性的效果。

這裡有一點要注意,由於在粒子有使用 scale(1.4) 放大,為了不影響到其他物理,所以要用 pushpop把它包起來。

for(var o=0;o<150;o+=5){
  let stAng =0 
  let ang = stAng+o/(3+i/5000)+frameCount/(30+o)+mouseY/100
  let xx = cos(ang)*(rr+i/2)+ cos(ang*8+o/100)*rr/200
  let yy = sin(ang)*(rr-i/10) + sin(ang*8+o/100)*rr/200
  curveVertex(
    xx ,yy
  )
  /*1. 加上游離粒子 */
  if (random()<0.1){
    push()
    scale(1.4)
    ellipse(xx,yy,3)
    pop()
  }
}
《量子不穩定》步驟八階段性成果
《量子不穩定》步驟八階段性成果

9. 加上裝飾

再來加一些一些裝飾性的物件,希望可以達到畫龍點睛的效果。

外圈還缺甚麼呢?一開始想要加像是光場或是力場的東西把它包住,但是又覺得似乎有些干擾,而且目前已經有一圈白線,所以改畫個在 HUD 裡面蠻常見的手法 – 虛線,畫弧形虛線的方式就是用小小的 arc 來畫,在畫面的下方。

到這裡差不多要做結尾了,這邊老闆在思考如何讓畫面變得更豐富,不是說硬將東西塞上去,而是能不能透過輔助的小物件,讓整件作品更有故事性。一開始老闆有想說加上像是折線圖或條圖,代表星球當下的狀態或速度,讓人可以知道這個星球當下的狀態。後來決定在整體的旋轉上面加上 noise,製造出就像是一個羅盤旋轉的感覺。

/* 1. rotate 加上 noise */
translate(width/2,height/2)
scale(0.73)
let totalRotate = frameCount/500+mouseX/100 + noise(frameCount/1000)*5
rotate(totalRotate)

stroke(255,150)
noFill()
strokeWeight(4)
ellipse(0,0,900)

stroke(255)
line(480,0,550,0)
line(-480,0,-550,0)
line(0,-480,0,-520)
line(0,480,0,520)
/* 2. 裝飾性橢圓 */
ellipse(0,550,50,20)
ellipse(0,-550,50,20)
ellipse(0,580,30,10)
ellipse(0,-580,30,10)
noStroke()
fill(255)
textSize(20)
text(totalRotate.toFixed(2),500,30)

noFill()
blendMode(SCREEN)

/* 3. 裝飾性線條 */
for(var i=0;i<200;i+=10){
  stroke(255,50)
  arc(0,0,1200,1200,PI/200*i,PI/200*i+0.035)
}
《量子不穩定》步驟九階段性成果
《量子不穩定》步驟九階段性成果

10. 加上原子核

既然作品現在看上去像是一個粒子,所以總該要有個原子核,而這個核心很不穩定,而周圍有個力場包圍住。為了製造出不穩定的感覺,所以在原子的位置以及大小,都加上了 noise 來產生出隨機的效果。

/* 1. 中間不穩定的圓心 */
fill(random()<0.3?colors[2]:255)
ellipse(0 + (noise(frameCount/5,10)-0.5)*50,
	0+ (noise(frameCount/3,1000)-0.5)*50,
	30+ noise(frameCount/10)*40)
noFill()
《量子不穩定》步驟十成果
《量子不穩定》步驟十成果

結語

我們總結一下這次的量子系統,製作過程可以分為三大部分:

  1. 中心帶有磁力線量子效果的圓心是此次作品的核心,老闆使用極座標的方式是先去定義每一個點的位置,再使用 curveVertex 以將點連接成一成線條。在極座標的表示上是 (cosθ * 半徑,sinθ * 半徑),在過程中不斷透過改變半徑比例的調整以及點的位置。 在這邊只要一個小小的變化,像是將半徑數值除以二,就會形成一個被壓縮的球體,建議大家可以一開始所提供畫圓的範例,多去嘗試改變其中不同的參數去觀察所對應會產生什麼效果,也因為是各個點都是相對獨立的,所以在位置的變化上可以有相對多的彈性,像是老闆在過程中就嘗試了加上 stAng 以及 ang 來試試看能不能有些有趣的變化,說不定你在嘗試的過程中就會發展出另一個和老闆完全不一樣的作品出來。
  2. 延續前面所說,線條實際上是由每一個點來定義,所以老闆就利用這樣的機會,透過定義好的點,加上一點變化形成四周的游離粒子;增加畫面的豐富度、疊加材質,做出與滑鼠X與Y位置的小互動。
  3. 想要製作 HUD 風格的作品可以加上單一顏色外框以及虛線線條,尤其可加上看似神秘的數值會更有效果。

這就是我們用p5.js寫出來的簡單的小作品啦!老闆的成品這邊去,也非常歡迎大家到社團裡跟我們分享你們完成的作品。

相同的繪製原理還能應用在甚麼作品上呢?若對p5.js寫成的互動藝術程式創作有興趣,歡迎加入老闆開的Hahow互動藝術程式創作入門課程,與另外將近兩千位同學一起創作吧!

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

墨雨設計banner

這篇文章 【p5.js創作教學】Quantum Unstable 量子不穩定 – 發光糾纏的量子系統 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
【p5.js創作教學】來畫一個瘋狂的龍捲風吧!(直播筆記) https://creativecoding.in/2021/06/04/p5-js%e5%89%b5%e4%bd%9c%e6%95%99%e5%ad%b8-%e4%be%86%e7%95%ab%e4%b8%80%e5%80%8b%e7%98%8b%e7%8b%82%e7%9a%84%e9%be%8d%e6%8d%b2%e9%a2%a8%e5%90%a7/ Fri, 04 Jun 2021 02:23:00 +0000 https://creativecoding.in/?p=897 身為台灣人可能沒親眼看過龍捲風,但也大概它知道長甚麼樣子,威力又有多強大。今天我們要利用p5.js來完成瘋狂的龍捲風捲起乳牛與房屋的一張動態圖片。

這篇文章 【p5.js創作教學】來畫一個瘋狂的龍捲風吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
身為台灣人可能沒親眼看過龍捲風,但從新聞或影片畫面裡大概它知道長甚麼樣子,威力又有多強大。

瘋狂的龍捲風-成品

今天我們要利用p5.js來完成瘋狂的龍捲風捲起乳牛與房屋的一張動態圖片,在最一開始老闆當然是先嘗試著刻劃龍捲風的外型,接著分階段完成被龍捲風捲起的物體:乳牛以及穀倉,最後進行美術上的修改,包含了背景的顏色、加上材質以及調整龍捲風的線條等,最後加上滑鼠的互動來增加整體作品的豐富度。

透過此次互動藝術創作教學,你會學到

  • 以幾何圖形繪製龍捲風,其中涵蓋了線條的調整,使用透明的填色讓龍捲風有雲霧感
  • 如何應用極座標的概念,使用三角函數的觀念來繪製不規則飛行的物體的運動軌跡
  • 繪製漸層背景的技巧,以及透過旋轉來增加背景的動態感
想要更了解三角函數,歡迎閱讀這兩篇複習:
來用可怕的三角函數做網頁吧! – Part 1 衛星繞月球(直播筆記)
來用可怕的三角函數做網頁吧! -Part 2科幻時鐘(直播筆記)

在開始製作龍捲風之前,你該知道的p5.js小技巧

就讓老闆帶你們一步步完成吧!

利用p5.js創作《瘋狂的龍捲風》步驟拆解
利用p5.js創作《瘋狂的龍捲風》步驟拆解

1. 畫龍捲風

讓我們一起先來思考龍捲風看上去的樣子,它其實就是一層層圓圈,由下面的一個小圓圈開始慢慢地往上,圓圈變得越來越大。一開始我們先嘗試由上而下劃出一排圈圈,暫時不考慮大小的變化排列。

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

function draw() {
  translate(width/2, 0)
    for(var i=0; i<height; i+=2){
      let nn = noise(i/10, frameCount/10)
      ellipse(0, i, nn*100)
    }
}
步驟一:畫龍捲風
步驟一:畫龍捲風

下一步要讓圈圈有大小區分的排列,我們必須由 “nn” 這個變數上來調整,在後面加上 “i” 這個變數,這樣一來,nn 的數字大小也會相對應地跟著從小到大。

function setup() {
  createCanvas(800, 800)
  background(100)
}

function draw() {
  translate(width/2, 0)
  for(var i=0; i<height; i+=2){
    let nn = noise(i/10, frameCount/10) + i/10
    ellipse(0, i, nn*10, nn*3)
  }
}
步驟一:畫龍捲風,慢慢修改形狀

然而這樣上面小、下面大的樣式並不是我們所要的,所以在 nn 變數中的 i 要改成 “height-i”,這樣才能讓數字由大到小排列,所畫出的樣子才會像是龍捲風的模樣。除了大小排列外,我們也順便將圖形刪除填色,留下僅有線條的樣子。
我們使用 nn 這個變數,可以發現它在一開始的時候有加上 noise 的效果,這是一個可以產生出隨機的變數,有了它,龍捲風看上去更增添動態感。

function setup() {
  createCanvas(800, 800)
  background(100)
}

function draw() {
  fill(0)
  rect(0,0,width,height)
  translate(width/2, 0)
  stroke(255)
  noFill()
  for(var i=0; i<height; i+=3){
    strokeWeight(random(2))
    let nn = noise(i/10, frameCount/100)*20+(height-i)/9
    ellipse(0, i-100, nn*8, nn*3)
  }
}
步驟一:畫龍捲風慢慢成形
步驟一:畫龍捲風慢慢成形

2. 加上被捲飛的物體

一開始我們必須思考物體被龍捲風捲起時的運動軌跡,先不用繪製乳牛、車子這些我們最後真正所要呈現的物體,先以簡單的方塊完成軌跡之後,最後再替換掉即可。

在物體位置的繪製上,我們要使用極座標的方式表示,極座標需給定半徑r,以及角度 θ,有了這兩個參數,那一個點在平面座標上就可以表示為 [rcosθ,rsinθ]。老闆使用 ang 表示角度, r 來表示半徑。設定好後,就使用 translate 來進行偏移。現在的原點位於整張圖中間最上方的位置,為了讓物體平均分散在整個龍捲風的周圍,所以在 y 座標中,會再加上 o*50,來讓物體是由上而下來進行分布的。

// 龍捲風
for(var i=0; i<height; i+=3){
    strokeWeight(random(2))
    let nn = noise(i/10, frameCount/100)*20+(height-i)/9
    ellipse(0, i-100, nn*8, nn*3)
}

// 飛行物體
for(var o=0;o<10;o++){
    let ang = o+frameCount/10
    let nn = noise(i/10,frameCount/100)*20+(height-i)/9
    let r = nn*10
    fill('blue')
    push()
        translate(cos(ang)*r,sin(ang)*r+o*50)
        rect(0,0,50,50)
    pop()
}
步驟二:加上被捲起的物件
步驟二:加上被捲起的物件

現在看上去,物體仍旋轉得很制式,這是因為每一個物體的角度是直接根據它是第幾個來去設定它的角度的。為了增加隨機性,我們增加一個 rot 變數,並且透過 noise 增加隨機性,設定好再使用rotate做旋轉。

// 飛行物體
for(var o=0;o<10;o++){
    let ang = o*5+frameCount/50
    let nn = noise(i/10,frameCount/100)*20+(height-i)/9
    let r = nn*(30+noise(o,frameCount/50)*5)
    let rot = noise(o,frameCount/100)*10
    fill('blue')
    push()
        translate(cos(ang)*r,sin(ang)*r+o*50)
        rotate(rot)
        fill('white')
        rect(0,0,50,50)
    pop()
}
步驟二:為被捲起的物件增加隨機性

3. 畫牛

設定好物體的旋轉方式後,我們來將方塊換成牛隻。在牛的繪製上可大致分成頭部、身體以及腳。為了使牛看起來精緻可愛,所以這裡大多都是由基本形狀如圓形、方形等等所構成,在這裡就是要考驗大家一筆一筆繪製、調整的耐心了。 這裡比較要注意的是,為了讓牛看起來比較生動些,我們將腳加上一些動畫,不管是前腳還是後腳,老闆都讓它左右邊的腳相差大約一個 PI 的角度左右,腳擺動起來才不會那麼地生硬。

另外,由於現在物體都是被龍捲風吹起來亂亂飛的,有點看不清楚到底有沒有把牛畫正,所以這裡會在 draw() 加入 drawCow(50, height-50),讓下方有一個固定的牛隻方便參考。

function drawCow(x,y){
  push()
    //身體
    translate(x,y)
    fill(255)
    noStroke()
    rect(0,0,50,25,5)
    fill(0)
    rect(5,0,12,15,5)
    rect(25,10,10,10,5)
    rect(45,5,5,10,5)

    // 後腳
    fill(255)
    push()
      translate(3,20)
      push()
        rotate(sin(frameCount/35))
        rect(0,0,5,20,5)
      pop()
      push()
        translate(5,0)
        rotate(sin(frameCount/20+PI))
        rect(0,0,5,20,5)
      pop()
    pop()

    // 前腳
    push()
      translate(40,20)
      push()
        rotate(sin(frameCount/35))
        rect(0,0,5,20,5)
      pop()
      push()
        translate(5,0)
        rotate(sin(frameCount/20+PI))
        rect(0,0,5,20,5)
      pop()
    pop()

    //頭部
    translate(-22,-5)
    rect(0,0,20,25,5)
    fill(255, 179, 160)
    rect(0,15,20,10,5)
    fill(0,60)

    ellipse(6,20,6)
    ellipse(14,20,6)
    fill(0)
    ellipse(5,8,3)
    ellipse(15,8,3)
    fill(247, 216, 150)
    rect(0,-8,3,10,10)
    rect(16,-8,3,10,10)

  pop()
}

// 飛行物體
for(var o=0;o<10;o++){
    let ang = o*5+frameCount/50
    let nn = noise(i/10,frameCount/100)*20+(height-i)/9
    let r = nn*(30+noise(o,frameCount/50)*5)
    let rot = noise(o,frameCount/100)*10
    fill('blue')
    push()
        translate(cos(ang)*r,sin(ang)*r+o*50)
        rotate(rot)
        drawCow(0,0)
        // fill('white')
        // rect(0,0,50,50)
    pop()
}
drawCow(50, height-50)
步驟三:畫牛
步驟三:畫牛

4. 調整龍捲風視覺 + 天空上色

第四部分一開始先將天空調整成偏向綠色外,其餘都是調整龍捲風的視覺,調整內容有以下幾點:

  • 加上陰影
    不管是龍捲風或是飛行的牛隻,都在繪製之前加上陰影,先是指定顏色,接著再去設定其偏移的量
drawingContext.shadowColor = color(0,30);
drawingContext.shadowOffsetY = 0
drawingContext.shadowOffsetX = 0
  • 以 arc 繪製龍捲風線條
    原本繪製的方式是使用 ellipse 形成一個完整的圓,我們改以 arc 角度來繪製,使用 sin 來設定龍捲風的起始位置後,再放入 arc 中,這樣子這樣子看起來更像是旋風。
  • 左右移動
    身為一個龍捲風,左右搖擺起來是一定要的,所以在 translate 的 x 位置再加上 cos(frameCount/10)*10,讓整個龍捲風搖擺起來。
  • 線條與填色
    將構成龍捲風線條註解起來,並且新增填色 fill,讓龍捲風以一個雲霧的感覺呈現

上述幾點調整的位置都是 draw() 中,修改後程式碼如下

function draw() {
  fill("#9BC1BC")
  noStroke()
  rect(0,0,width,height)
  translate(width/2 + cos(frameCount/10)*10,0)

  // stroke(255,100) // 拿掉 stroke
  noFill()

  // 龍捲風
  drawingContext.shadowColor = color(0,30);
  drawingContext.shadowOffsetY = 0
  drawingContext.shadowOffsetX = 0
  for(var i=0; i<height; i+=4){
    strokeWeight(random(1)+2)
    fill(255,30) // 加上填色
    let nn = noise(i/10, frameCount/100)*20+(height-i)/9
    let stAng = sin(i+frameCount/2)
    arc(0,i-100,nn*8,nn*3,stAng,stAng+PI*1.5)
  }


  // 飛行物體
  noStroke()
  drawingContext.shadowColor = color(0,30);
  drawingContext.shadowOffsetY = 2
  drawingContext.shadowOffsetX = 2
  for(var o=0;o<10;o++){
    let ang = o*5+frameCount/50
    let nn = noise(i/10,frameCount/100)*20+(height-i)/9
    let r = nn*(30+noise(o,frameCount/50)*5)
    let rot = noise(o,frameCount/100)*10
    fill('blue')
    push()
      translate(cos(ang)*r,sin(ang)*r+o*50)
      rotate(rot)
      drawCow(0,0)
    pop()
  }
  drawCow(50, height-50)
}
步驟四:調整龍捲風視覺以及將天空上色
步驟四:調整龍捲風視覺以及將天空上色

5. 漸層天空

我們要把原本單色系背景改為漸層天空,先將原本的背景註解起來。在繪製漸層背景上,首先先指定一個稍微偏綠色的藍,接著每隔一層線條就加上數值五的藍色,使其越來越偏向藍色,但是單純只有漸層似乎還是有點單調,畢竟此次的主題是龍捲風,所以老闆同時也加上不規則的旋轉,使整體更加有一種不受控制的感覺。

// 漸層背景
let cc = color("#6AD5CB")

push()
  rectMode(CENTER)
  for(var i=0;i<height;i+=100){
    push()
      cc.setBlue(cc._getBlue()+5)
      fill(cc)
      rotate(random(-1,1)/10)
      rect(width/2,i,width*1.2,100)
    pop()
  }
pop()


// 原本的背景
// fill("#9BC1BC")
// noStroke()
// rect(0,0,width,height)
步驟五:將天空改為漸層有生命力
步驟五:將天空改為漸層有生命力

6. 加上材質

為了增加一點繪畫感,我們可以為整個畫面新增材質疊加。共有三大步驟,分別是命名材質、設定材質、疊加材質。

  • 命名材質
    在全域的區域命名材質變數 let overAllTexture
  • 設定材質
    setup() 中設定材質的樣式
overAllTexture.loadPixels()
// noStroke()
for(var i=0;i<width+50;i++){
  for(var o=0;o<height+50;o++){
    overAllTexture.set(i,o,color(100,noise(i/3,o/3,i*o/50)*random([0,30,50])))
  }
}
overAllTexture.updatePixels()
  • 疊加材質

setup() 中改變與色塊的混合模式

push()
    blendMode(MULTIPLY)
    image(overAllTexture,0,0)
pop()
步驟六:為畫面增添繪畫感
步驟六:為畫面增添繪畫感

加上材質後,畫面上黑色一條一條的痕跡更明顯了,我們稍微將它修正成沒有線條,並且改變透明度。

push()
let cc = color("#6AD5CB")
cc.setAlpha(100) // 設定透明度
noStroke() // 設定沒有線條
  push()
    rectMode(CENTER)
    for(var i=0;i<height;i+=100){
      push()
        cc.setBlue(cc._getBlue()+5)
        fill(cc)
        rotate(random(-1,1)/10)
        rect(width/2,i,width*1.2,100)
      pop()
   }
  pop()

// fill("#9BC1BC")
// noStroke()
// rect(0,0,width,height)
步驟六:修正背景線條
步驟六:修正背景線條

7. 加入穀倉

在畫穀倉上也沒有太高深的技巧,與繪製牛一樣,像個手工業一樣慢慢一筆一畫描繪出來。不過有一個比較少看到的用法是 shearX ,它可以將物體沿著X軸歪斜指定角度,用來協助繪製梯形的屋頂。 另外老闆在生成這些穀倉時,並沒有再多寫一個迴圈,而是直接用餘數的方式去決定要生成牛還是穀倉。原先放在左下角當成繪製參考的穀倉也沒有拿掉,因為老闆後來覺得將單一穀倉就這樣固定在那裏也不錯。

function drawHouse(x,y){
  push()
    translate(x,y)
    // rect(0,0,100,50)
    fill(199, 77, 93)
    rect(20,0,80,40)
    fill(219, 77, 93)
    rect(0,0,80,40)
    fill(255,50)
    rect(0,0,80,6)

    stroke(255,100)
    strokeWeight(2)
    noFill()
    rect(40,10,20,10)
    rect(40,20,20,10)
    rect(40,10,10,20)
    noStroke()

    //屋頂
    shearX(PI/12)
    fill(68, 61, 63)
    rect(20,0,80,-20)
    shearX(-PI/6)
    fill(58, 51, 53)
    rect(0,0,80,-20)
  pop()
}
// drawCow(0,0)
if (o%6!=0){
  drawCow(0,0)
}else{
  drawHouse(0,0)
}
步驟七:繪製穀倉
步驟七:繪製穀倉

8. 修飾細節跟最後收尾

translate 加上與滑鼠 mouseX 做互動

// translate(width/2 + cos(frameCount/10)*10,0)
translate(width/2 + cos(frameCount/20 + mouseX/50)*50,0) // 加上 mouse

龍捲風的細線條隨機出現

for(var i=0; i<height; i+=4){
  strokeWeight(random(1)+2)
  fill(255,10) // 加上填色

  // 隨機出現線條
  if (random()<0.1){
    stroke(255)
  }else{
    noStroke()
  }

  let nn = noise(i/10, frameCount/100)*20+(height-i)/9
  let stAng = sin(i+frameCount/2)
  arc(0,i-100,nn*8,nn*3,stAng,stAng+PI*1.5)
}
步驟八:最後修飾及收尾即完成

結語

我們總結一下這次的龍捲風小品,製作過程可以分為三大部分:

  1. 龍捲風的繪製,思考龍捲風的模樣以及它具有什麼特性,我們可以用什麼圖形以及線條去模擬,從最一開始使用圓形來構成龍捲風的架構,慢慢地調整細節,讓它更加接近真實的模樣。
  2. 被捲起的物體 – 乳牛與穀倉兩者所使用技巧都是用基本的圖形建構而成。另外使用極座標表示被捲起的軌跡也是相當值得學習的技巧。
  3. 視覺上的調整,加入材質以及漸層背景等應用,以及滑鼠的簡單互動讓作品更加完整。

這就是我們用p5.js寫出來的簡單的小作品啦!相同的繪製原理還能應用在甚麼作品上呢?你們也可以參考老闆這件作品的OpenProcessing頁面,一起來切磋吧!

若對互動藝術程式創作有興趣,歡迎加入老闆開的Hahow課程互動藝術程式創作入門,與其他兩千位同學一起創作吧!

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 【p5.js創作教學】來畫一個瘋狂的龍捲風吧!(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
來用可怕的三角函數做網頁吧! -Part 2科幻時鐘(直播筆記) https://creativecoding.in/2021/05/14/%e4%be%86%e7%94%a8%e5%8f%af%e6%80%95%e7%9a%84%e4%b8%89%e8%a7%92%e5%87%bd%e6%95%b8%e5%81%9a%e7%b6%b2%e9%a0%81%e5%90%a7-part2-%e7%a7%91%e5%b9%bb%e6%99%82%e9%90%98/ Fri, 14 May 2021 02:16:00 +0000 https://creativecoding.in/?p=757 上一篇中,老闆利用三角函數幫 DOM 做 CSS 的絕對定位,這次我們雖然也是使用三角函數,但是改使用 canvas 在網頁上繪圖,製作科幻效果的時鐘。

這篇文章 來用可怕的三角函數做網頁吧! -Part 2科幻時鐘(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

]]>
利用三角函數與 canvas 製作科幻感時鐘
利用三角函數與 canvas 製作科幻感時鐘

本文翻自[週四寫程式系列] 來用可怕的三角函數做網頁吧! – Part2直播影片,對文章內容有疑問,或是想要老闆手把手帶你飛,都可以觀看影片詳細內容。

上次老闆帶大家利用三角函數製作衛星繞月球,還沒自己動手操作的同學,可以回上一篇挑戰,複習三角函數如何結合到網頁中。雖然數學看起來很可怕,但是它卻可以協助你製作有趣的效果,而數學中的表達式在各個地方都通用,不管是製作特效、資料分析,都可以讓你優雅地描述一件事的方式。

這次要製作的科幻效果,使用 FUI 風格,FUI 設計有許多不同的全名 (fake user interface, fictional user interface, fake user interface, 虛構使用者介面),這些 f 開頭的單字,代表還不存在的人機互動介面,常見於科幻電影場景中主角操作的機器介面,或是鋼鐵人 AI 介面…等,透過數據運算組成各種元件,讓畫面看起來科技感十足。

前情提要

開始製作前,老闆這邊提供這次範例的成果網址,讓大家在實作時可以參考。

上一篇中,老闆利用三角函數幫 DOM 做 CSS 的絕對定位,這次我們雖然也是使用三角函數,但是改使用 canvas 在網頁上繪圖。首先,讓我們快速回顧上一篇中提到的,應用三角函數幫 DOM 做絕對定位,讓遊戲元件在正確位置顯示,以砲台射擊遊戲為例,假設右上方有個目標物,我們要讓射出的砲彈的角度一直更新,才能能夠射到目標物,而每段時間砲彈要前進多少距離,則透過三角函數取得每次要調整的距離 Δy, Δx。

<canvas> 是一個 HTML 元素,我們可以利用程式在這個元素上繪圖(通常是用 JavaScript)- MDN

那麼又該如何知道 Δy, Δx 是多少呢?我們知道常見的三角形(30°, 45°, 60°)各邊的比例,但是當不是這些常見的角度時,就束手無策了。這時候我們就要感謝偉大數學家們的努力,只要知道 r 和 θ,就可以將 Δy 與 Δx 用三角函數去換算成 r * sinθ 與 r * cosθ。

數學家也從不同角度的三角形中發現,sinθ 與 cosθ 是一個規律的波浪圖,這代表著 r, Δy 與 Δx 值是成比例的關係。有了這些知識,就能開始著手製作 FUI 風格的時鐘了。以上我們帶大家快速回顧,要看更詳細的三角函數解說,可以參考 Part 1 影片和文章

畫個圓形吧

畫圖前,老闆先帶大家理解畫圓的觀念如下:從三角形、四角形,慢慢到多邊形,當與中心點距離一樣的點數亮夠多,視覺上就會像是畫出一個圓。首先,我們將中心點作為基準,每一次畫出的點與 r 的距離都相同,以 x 軸上的點做為起始點,將一圈 360° 均分為三點時,可以畫出三角形(左圖),四點的時候是四角形(右圖),以此類推,當點的數量夠多的時候就趨近圓形。

該如何將前面的這段敘述用程式寫出來呢?我們可以理解成將 360° 分成特定的份數,在座標軸上右邊 0° 是起始點,逆時針旋轉增加角度,假想有一個固定單位半徑 r 的圓在這個座標軸上,我們要畫出三角形,首先先將 360° 分成三等份,從 0° 的位置先點上一點,逆時針轉 120° 畫一點,最後再逆時針旋轉 120° 畫一點,三個點互相連接後,就成為了三角形,其他多邊形都能用這種方式去畫。聰明的大家這時候就可以發現,如果這個圓上有無限多個點,連接起來之後就趨近圓形。

那麼又該如何算出下圖中點 2 的座標呢?假設今天我們要畫一個 n 邊形,先將圓平分成 n 個點後,每次增加的角度是 360°/n,第 i 個點的角度就是 i *(360° / n)。用前面提到的三角形來做檢驗,第一個點就是 1 * (360° / 3) = 120°,第二個點就是 2 * (360° / 3) = 240°,第三個點就是 3 * (360° / 3) = 360°,也就回到了原點。第二點的座標算法為:x 座標 r * cosθ,y 則是 r * sinθ,其他點的座標算法一樣。接下來,就讓大家跟著老闆開始畫圖吧。

動手做

在我們要開始動手做之前,有幾個重點:

  1. 不能使用 canvas 中畫圓的函數。
  2. 因為電腦無法解讀 ” 從中心畫線到 30° 的位置 ” 這種口語的陳述,所以我們要去換算每個點的 x 與 y 的座標,並將所需要的點連線,才能畫出我們要的圖。

畫圖

理解畫圓的思維邏輯後,先跟大家介紹網頁畫圖的技術,在網頁畫圖我們會使用 canvas 來畫圖,畫圖的方式跟網頁定義 DOM 位置的不同,需要定義每個點的位置,點相連之後畫出我們需要的圖。

開始畫圖前,大家可以先將 codepen 的開發環境以及程式碼設置成跟老闆一樣,避免操作上的出入:

  • html 使用 pug
  • css 使用 sass
  • js 區塊將 jQuery 引用進來

HTML

canvas#myCanvas

CSS(Sass)

html, body
  width: 100vw
  height: 100vh
  margin: 0
  padding: 0
  overflow: hidden
  background-color: #000
canvas
  background-color: #fff

當我們把上面程式碼輸入後,會發現 canvas 的區塊沒有撐滿整個螢幕(下圖白色部分,這邊設定成白色背景是方便大家了解 canvas 的變化,之後在第5步驟開始畫方塊時,會將 canvas 的背景色拿掉)。

該怎麼調整 canvas 畫布跟螢幕一樣呢?我們在 js 區塊中撰寫以下程式碼,步驟如下:

  1. 利用 id 取得畫布的 DOM
  2. 透過取得的 canvas 來取得繪圖元件
  3. 設定畫布的寬高
    我們希望 canvas 可以撐開整個畫面,透過 outerWidth() 以及 outerHeight() 取得視窗的寬高,並將 canvas 畫布的寬高設定成這些值
    JavaScript
// 1. 取得 canvas 畫布
var c = document.getElementById('myCanvas');
// 2. 畫布繪圖,後面開始繪圖才會使用到這個變數
var ctx = c.getContext("2d");

// 3. canvas 畫布設定
var ww = $(window).outerWidth();
var hh = $(window).outerHeight();
// 3. 將螢幕寬高指定給畫布
c.width = ww;
c.height = hh;

4. 更新畫布大小

這時候若是調整螢幕大小,也就是網頁的寬高改變,會發現畫布沒有跟著撐滿版,所以我們必須監聽畫面的 resize,當畫面縮放的時候,動態地抓取當下畫面的寬高並重新設定 canvas 畫布。所以我們將程式碼調整成:

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

var ww = $(window).outerWidth();
var wh = $(window).outerHeight();

// 將重複使用的程式碼整理成 function
function getWindowSize() {
  ww = $(window).outerWidth();
  wh = $(window).outerHeight();
  c.width = ww;
  c.height = wh;
}

// 進入網頁後先做一次畫布大小調整
getWindowSize();
// 監聽畫面 resize
$(window).resize(getWindowSize);

5. 開始畫圖 – 方塊

要注意在canvas 中畫圖比較特別的是,我們要定義兩點 a 和 b 才能連成線;若是要畫出一個方塊,可以直接透過 canvas 內的 api 拉出方塊後填色,讓我們利用以下幾個語法,畫出位於 (0, 0) 寬度 50px 的正方形:

  • ctx.fillStyle = “white” 填色顏色為白色
  • ctx.beginPath() 開始繪圖
  • ctx.rect(x, y, width, height) 在座標 (x, y) 上畫一個寬 width 高 height 的矩形
  • ctx.fill() 填色
function draw(){
  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.rect(0, 0, 50, 50);
  ctx.fill();
}

draw();

6. 隨時更新畫布

我們想讓正方形隨著時間往右移動,所以多加了一個時間變數 time,讓 draw function 被呼叫的時候增加 time 的值,並往右移動,所以我們將原本座標 x 跟著時間改變,就產生出一個白色方塊一直向右移動。

var time = 0;
function draw(){
  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.rect(time, 0, 50, 50);
  ctx.fill();
  time+=1;
}

setInterval(draw, 10);

7. 清空畫布再更新

這時候我們可以發現,方塊是往右了,但是卻變成一條白色的線,那是因為每次畫上新的方塊時,畫布沒有清空。所以我們在每次要畫方塊前,都先畫一個覆蓋整個畫面的長方形,將原本的畫面蓋掉後,再更新正方形。 若是覆蓋的長方形是半透明的會發生什麼事?沒錯!正方形移動時就會產生殘影,大家可以嘗試不同數值試試看。

...
var time = 0;
function draw(){
	ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
  ctx.beginPath();
  ctx.rect(0, 0, ww , wh);
  ctx.fill();

  ctx.fillStyle = "white";
  ctx.beginPath();
  ctx.rect(time, 0, 50, 50);
  ctx.fill();
	time+=1;
}

setInterval(draw, 10);

座標軸基礎設置

透過以上操作,讓大家了解繪圖的基本操作並讓方塊順利移動後,接著準備進入正題。首先我們要先繪製座標軸,但以網頁來說,左上角為坐標(0,0),而這次專案中我們希望座標軸的中心點(0, 0) 改在正中央,所以我們需要將中心點開始移動。

  1. 使用中心點作為初始值

要達成這個目標,我們直覺地想到把方塊移到中心,所以我們多了一組變數來記錄整個螢幕的中心點,並將這個中心點加到白色方塊的初始位置上。這邊還要注意的是,當螢幕 resize 時,center 值也必須更換,總共有以下三個部分的程式碼要調整。

// 多一組變數記錄中心點
var center = {
  x: 0,
  y: 0
};

function getWindowSize() {
  ww = $(window).outerWidth();
  wh = $(window).outerHeight();
  c.width = ww;
  c.height = wh;
  // 重新計算中心點
  center.x = ww/2;
  center.y = wh/2;
}

function draw(){
  ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
  ctx.beginPath();
  ctx.rect(0, 0, ww, wh);
  ctx.fill();
  
  ctx.fillStyle = "white";
  ctx.beginPath();
  // 每次繪製方形的座標時要加上 center 值
  ctx.rect(time + center.x, 0 + center.y, 50, 50);
  ctx.fill();
  time+=1;
}

2. 更改畫布中心

如果我們每次要繪製任何東西時,都要手動加上這個 center 的值,非常地不方便,甚至還有可能遺漏它。所以這時候我們可以使用 canvas 內建的功能,直接改變畫布位置,就不用每次繪製時都要在該元件多加上偏移的座標。這邊我們將 getWindowSize 調整成:

function getWindowSize() {
  ww = $(window).outerWidth();
  wh = $(window).outerHeight();
  c.width = ww;
  c.height = wh;
  
  center.x = ww/2;
  center.y = wh/2;
  
  ctx.restore();
  // 移動畫布座標
  ctx.translate(center.x, center.y);
}

3. 調整 draw()

完成更改畫布中心之後會發現,方塊沒出現在畫面裡,因為我們除了調整中心的畫布之外,也要將方塊的初始值調整回來。大家可以試著調整 draw 中清畫布的顏色,會發現這塊清除的色塊並沒有覆蓋整個版面,也是從中心點往右下長一個方塊,所以這塊清除的畫布的位置也需要調整。

function draw(){
  // 清畫布方塊顏色
  ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
  ctx.beginPath();
  // 調整清除畫布的初始位置
  ctx.rect(-center.x, -center.y, ww , wh);
  ctx.fill();
  
  ctx.fillStyle = "white";
  ctx.beginPath();
  // 白色方塊初始值
  ctx.rect(time, 0, 50, 50);
  ctx.fill();
  time+=1;
}

座標軸

透過白色方塊了解 canvas 繪圖和畫布座標移動後,總算要進入正題。

我們首先先繪製靜態的畫面,第一個就是我們的座標軸,比較不一樣的是,剛剛都是直接畫一個形狀填色,這次要改成用點連出我們需要的線並填色。會用到的程式碼如下:

  • ctx.stokeStyle = ‘rgba(255, 255, 255, 0.05)’ 畫筆顏色
  • ctx.strokeWidth = 1 畫筆粗度
  • ctx.moveTo(x, y) 將畫筆移到 (x, y)
  • ctx.lineTo(x2, y2) 從上一個位置拉一條直線到 (x2, y2)
  • ctx.stroke() 畫圖
function draw(){
  ...
  
  // 座標軸
  ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.moveTo(-ww/2, 0);
  ctx.lineTo(ww/2, 0);
  ctx.moveTo(0, -wh/2);
  ctx.lineTo(0, wh/2);
  ctx.stroke();
  
  ...
}

從方形到圓形

有了座標軸後,我們要在上面畫一個圓形,這邊老闆一樣帶大家從四角形開始理解,了解四角形後,只要畫的點增加到一定數量,繪製出來的多邊形就會趨近於圓形。

我們嘗試畫個半徑為 200 的四邊形,首先第一個點為右邊,要注意的是,網頁的座標跟大家認知上的座標 y 軸方向是相反的,往下是正數,所以第二點是下面,第三個點是左邊,逆時針旋轉,以此類推。

另外要注意的是,網頁中畫圓是用弧度來計算,一圈 360° 等於 2PI,所以我們設一個變數來把角度轉換成弧度,若是沒有用弧度繪圖,就會出現右邊圖裡的窘境。這邊大家也可以試試看,當 n 增加到一定的量,畫出的圖就會趨近於圓形。

function draw(){
  ...
  // 每個點與中心的距離
  var r = 200;
  // 角度轉換成弧度
  var deg_to_pi = Math.PI * 2 / 360 ;
  // 畫 4 個點
  var n = 4;
  ctx.beginPath();
  for(var i = 0; i<=n; i++){
    var deg = i * (360 / n) * deg_to_pi;
    ctx.lineTo(
      r * Math.cos(deg),
      r * Math.sin(deg)
    );
  }
  ctx.stroke();
  ...
}

波浪線

那要怎麼畫旋轉的波浪圓形呢?我們可以觀察到,這個波浪圓形上的每一個點,只是在做半徑的調整,所以我們可以在 r 上面做些手腳,讓它成為一個動態的半徑,就能完成波浪的圓形,至於要如何調整才能讓這個點是在一定的範圍內變動,大家是不是想到 sin 和 cos 的圖了呢?沒錯,只要利用它們和 r 做組合,就可以調整出不一樣的波浪圖。老闆這邊提示大家:

  1. 因為 sin 和 cos 的最大到最小值分別是 1, -1,所以可以多乘上一個數,來增加波浪起伏
  2. 角度變化的越快就會讓波讓越密集,所以在代入 sin 的角度中,也多乘上一個數
  3. 波浪沒有完整的接合起來,是因為沒有把角度完整走完一個圓,一個完整的圓角度是 2PI,只要再多乘上 2PI 運算即可。

大家可以嘗試看看不同數值乘線的效果,那又該怎麼讓波浪圓形動起來呢?這邊我們要在 now_r 變數中的角度部份加入會一直變動的數值,才有辦法讓半徑多加上這個 1 單位到 -1 單位的動態半徑,我們可以觀察到, time 會隨著時間一值增加,只要活用 time 就能讓波浪圓形動起來。

function draw () {
  ...
  // 波浪圓形
  var deg_to_pi = Math.PI / 360 * 2;
  // 基礎半徑值 200
  var r = 200;
  // 200 個點的多邊形
  var n = 200;
  ctx.beginPath();
  for(var i = 0; i<=n; i++){
    // 2 * sin() 增加動態半徑的 range
    // 2 * Math.PI 確保畫出完整的圓
    var now_r = r + 2 * Math.sin(2 * Math.PI *  i / 10 + time / 20);
    var deg = i * (360 / n) * deg_to_pi;
    ctx.lineTo(
      now_r * Math.cos(deg),
      now_r * Math.sin(deg)
    );
  }
  ctx.stroke();
  ...
}

刻度

畫刻度的想法又跟畫圓形不太一樣,兩者都需要使用到角度來輔助畫圖,波浪是每畫一點就改變角度,而畫刻度不一樣的地方在於,是固定角度在 r 畫上一點後,改變 r 的長度後再畫一點連起來,完成一個角度後再到下一個角度用相同方式繪製線。

以下圖為例子,假設我們想畫一個角度 θ ,從半徑 3 連到半徑 5 的線,則先將畫筆移到(3cosθ, 3sinθ),再連線到 ((3+2)*cosθ, (3+2)*sinθ)。用基本的 r 加上某個數值,成為下一個要連線的點,這樣做的好處是,如果今天我們想變成比較長的刻度,只要調整加上的值就可以達成。

我們要畫出的圖需求如下:

  1. 畫一圈半徑為 220 共 240 個刻度所繞出的圓。
  2. 每隔10個刻度,就會有一條中間長度的刻度
  3. 上中下右(也就是每隔 240/4 = 60)有一條最長長度的刻度,

總共有三種樣式的刻度,透過三元運算子,可以先將其中一段拆解出來看,活用三元運算子就能用刻度來組出不同的長度,以下面這個三元運算子解讀的方式如下,當判斷式為 true 時,會執行前者 true 的內容,否則就執行後者 false。

判斷式 ? true : false

所以「當 i 除以 10 的餘數為 0 的時候則會使用 4,否則就使用 0」的三元運算子應該這樣寫:

i % 10 == 0 ? 4 : 0

同樣的,我們也可以將透明度結合三元運算子,在畫線前,賦予畫筆顏色不同透明度。接下來就可以設定兩個點(start_r 到 end_r)與角度,記得角度要乘上前面宣告的 deg_to_pi。

萬事俱備,就能開始繪圖了,一樣使用前面提到的 beginPath(), moveTo(), lineTo(), stroke() 來進行繪製。

var r = 220;
var count = 240;
for(var i=0; i<=count; i++){
  // len 為不同刻度有不同長度的增加量
  var len = 4 + (i % 10 == 0 ? 4 : 0) + (i % 60 == 0? 8 : 0);
  var opacity = (len > 4 ? 1 : 0.5);
  var start_r = r;
  var end_r = start_r + len;
  //最基本的角度分佈
  var deg = 360*(i/count)*deg_to_pi;
  ctx.beginPath();
  ctx.moveTo(
    start_r * Math.cos(deg),
    start_r * Math.sin(deg)
  );
  ctx.lineTo(
    end_r * Math.cos(deg),
    end_r * Math.sin(deg)
  );

  // 決定畫筆樣式
  ctx.lineWidth = 1;
  ctx.strokeStyle = "rgba(255, 255, 255, "+opacity+")";

  ctx.stroke();
}

外圈

外圈的作法很簡單,與剛剛畫內圈的方式雷同,調整 r 以及減少點(count)的數量就可以達成,大家可以按照喜好嘗試調整看看。

function draw () {
  ...
  var r = 400;
  // 共畫 60 個刻度
  var count = 60;
  for (var i = 0; i <= count; i++) {
    // 上中下右刻度要不一樣,等於是每十五個刻度就要不同長
    var len = 4 + (i % 15 == 0 ? 8 : 0);
    var opacity = len > 4 ? 1 : 0.5;
    var start_r = r;
    var end_r = start_r + len;
    var deg = 360 * (i / count) * deg_to_pi;
    ctx.beginPath();
    ctx.moveTo(start_r * Math.cos(deg), start_r * Math.sin(deg));
    ctx.lineTo(end_r * Math.cos(deg), end_r * Math.sin(deg));

    ctx.lineWidth = 1;
    ctx.strokeStyle = "rgba(255, 255, 255, " + opacity + ")";
    ctx.stroke();
  }
  ...
}

秒針、分針、時針

要畫秒針、分真或時針,我們需要知道目前時間,獲得目前時間的方式會用到以下程式,我們也利用 jQuery,將時間放在畫面上,方便我們在製作時能夠參照畫面是否和我們期望的相同。

HTML

canvas#myCanvas
.time +00:00:00

CSS(Sass)

canvas
  transform: scaleY(-1)

JavaScript

var nowTime = new Date();
var sec = nowTime.getSeconds();
var min = nowTime.getMinutes();
var hour = nowTime.getHours();
  
$('.time').text('+00:'+hour+':'+min+':'+sec);

先用固定的角度來測試畫出來的圖形是否跟真實的時鐘一樣,我們傳入 45 作為角度參數後發現一些問題,跟著下面的步驟去微調就能解決:

  1. 將角度多乘上畫圓的角度:畫出來的角度不是 45 度。只要跟前面一樣,將角度轉換成畫圓要用的角度即可。
  2. 從正上方旋轉 45 度:給 45 度為什麼是指向右下呢?我們期望會從正上方作為0度開始旋轉,所以 45 度應該要指著右上方,大家還記得在上一篇有提到,網頁中的座標系 y 軸向下才是正值(左圖),老闆這邊使用偷吃步的方式,只要修改畫布將整張畫布垂直翻轉,45度指的方向就對了。
  3. 讓秒針跟著時間旋轉:緊接著,將角度位置改使用 sec 變數讓秒針動起來。對於秒針而言一圈有 60 個刻度,我們先將 0 到 60 個刻度換算成 0 到 1,再換算 0 到 360,所以就等於 sec / 60 * 360,就是秒針每秒要改變的角度。
  4. 調整旋轉方向:我們可以看到現在以逆時針旋轉,所以我們多乘以一個負值,讓旋轉的方向變成順時針轉的方向。
  5. 補上初始值:最後我們察覺到正確的秒針位置總是少了90度,這就是因為我們現在的座標系初始值 0 度是在右邊,只要將初始值的0換到正上方,這邊我們在函式轉換的角度位置中加上初始值(90度)即可。
function drawPointer(r, deg, lineWidth) {
  // 決定畫筆
  ctx.lineWidth = lineWidth;
  ctx.strokeStyle = "rgba(255, 255, 255, .5)";
	
  // 調整角度初始值與轉化成圓的角度
  var now_deg = (deg + 90) * deg_to_pi;
    
  ctx.beginPath();
  ctx.moveTo(0,0);
  ctx.lineTo(r * Math.cos(now_deg), r * Math.sin(now_deg));

  ctx.stroke();
}

// 秒針分針時針畫法
drawPointer(400, -360 * (sec / 60), 1);
drawPointer(210, -360 * (min / 60), 1);
drawPointer(150, -360 * ((hour + min / 60) / 12 ), 4);

要注意的是,時針畫法比較不一樣,我們會希望時針不是單純指在對應的數字上,還要多加上過了幾分鐘的變化,所以先將過了幾分鐘除以 60,獲得目前經過一小時中的幾分鐘後,再加上小時除以12,因為一圈只有12個刻度,就能獲得目前的度數。

結語

老闆這邊附上這次範例的成果網址,讓大家在實作時可以參考。

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

  1. canvas 網頁繪圖:若是要讓畫布和螢幕同樣大小,要監聽 resize 並重新調整畫布大小。
  2. canvas 動態效果:相當於在畫布上一直重新繪圖,要記得清空畫布後再畫上新東西。
  3. 調整 canvas 座標:canvas 預設左上角為 (0, 0),可以利用 ctx.translate(x, y)調整畫布的位置。
  4. 繪製座標軸。
  5. 從四角形到圓形:利用三角函數來畫出不同形狀,當點的數量足夠時,連起來就會像是圓形,要注意要將角度換算成弧度。
  6. 會動的波浪圓形:在畫圓的時候,透過三角函數來製造波浪,並結合時間就能讓波浪圓動起來。
  7. 繪製刻度:這次範例中的內圈與外圈都是用這種方是去完成,在不同角度上點出兩個點連起來就變成刻度,結合三元運算子就能在不同角度時有不同的樣式。
  8. 繪製秒針、分針與時針:類似刻度的畫法,只是這次第一個點是圓心,再將時分秒換算成角度後畫出第二點並連線。

數學裡面有許多奇怪的東西,但也感謝數學家們的努力,讓我們可以應用在遊戲或動態特效等地方。在第一篇我們用三角函數來幫 DOM 做定位,這一篇則是在 canvas 上繪圖,並適時結合三角函數,產出許多有趣的效果,各位同學挑戰完兩篇三角函數教學之後,可以回頭思考看看兩者的差異,期待大家能利用三角函數做出更多有趣的效果。

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

這篇文章 來用可怕的三角函數做網頁吧! -Part 2科幻時鐘(直播筆記) 最早出現於 Creative Coding TW - 互動程式創作台灣站

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

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

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

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

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

前言

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

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

Dyverse Studio主頁動畫

從遊戲開始出發

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

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

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

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

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

適用於目標位置不改變。

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

x+=Δx
y+=Δy

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

適用於目標位置不改變。

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

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

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

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

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

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

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

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

比例會根據θ而改變

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

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

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

衛星繞月球

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

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

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

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

場景 html 結構

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

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

Sass 重複使用 – @mixin

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

$color_space: ##2c3d4f

*
  border: solid 1px

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

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

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

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

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

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

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

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

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

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

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

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

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

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

製作陰影

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

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

月亮不同坑洞

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

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

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

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

衛星軌道

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

HTML

.trace1
.trace2

CSS

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

旋轉的衛星

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

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

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

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

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

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

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

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

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

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

https://imgur.com/p649GNo.gif

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

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

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

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

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

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

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

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

結語

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

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

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

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

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

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

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

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

]]>