前言
如果你曾經在課堂上背誦三角函數的公式,卻始終覺得它們只是一堆抽象的數字遊戲,那麼這篇文章就是寫給你的。對我來說,真正理解三角函數的那一刻,不是在紙上推導公式的時候,而是我第一次在螢幕上畫出一個繞著單位圓旋轉的點,看到它的 y 座標隨時間展開成一條優美的波浪——那一瞬間,sin 從一個函數名稱變成了一種「運動」。
這篇教學會帶你從單位圓出發,透過 p5.js 的動畫,建立對 sin、cos、tan 的視覺直覺,最後我們會用這些函數畫出令人驚嘆的 Lissajous 圖形。
單位圓:一切的起點
三角函數最自然的定義來自單位圓——一個半徑為 1、圓心在原點的圓。想像一個點從 (1, 0) 出發,沿著圓周逆時針移動,走過的角度就是 θ(theta)。這個點在任意時刻的座標就是:
x = cos(θ)
y = sin(θ)
就是這麼簡單。cos 是 x 座標,sin 是 y 座標。當這個點繞圓一圈時,θ 從 0 走到 2π,cos 和 sin 就各自經歷了一個完整的週期。
p5.js 單位圓動畫
讓我們用程式碼把這件事情視覺化:
let angle = 0;
let history = [];
function setup() {
createCanvas(800, 400);
}
function draw() {
background(30);
// 單位圓
let cx = 200, cy = 200, r = 120;
stroke(100);
noFill();
ellipse(cx, cy, r 2, r 2);
// 旋轉的點
let x = cx + r * cos(angle);
let y = cy - r * sin(angle); // 螢幕座標 y 軸反轉
// 畫半徑線
stroke(255);
line(cx, cy, x, y);
// 畫點
fill(255, 80, 80);
noStroke();
ellipse(x, y, 12, 12);
// 投影到右方:繪製 sin 波
let waveX = 350;
stroke(255, 80, 80);
line(x, y, waveX, y); // 水平連線
// 記錄歷史
history.unshift(y);
if (history.length > 400) history.pop();
// 畫波形
noFill();
stroke(255, 80, 80);
beginShape();
for (let i = 0; i < history.length; i++) {
vertex(waveX + i, history[i]);
}
endShape();
// cos 的投影(垂直線)
stroke(80, 180, 255);
line(x, y, x, cy);
// 標示文字
fill(255, 80, 80);
noStroke();
textSize(14);
text("sin(θ) = " + nf(sin(angle), 1, 2), 350, 30);
fill(80, 180, 255);
text("cos(θ) = " + nf(cos(angle), 1, 2), 350, 50);
fill(255);
text("θ = " + nf(degrees(angle) % 360, 1, 1) + "°", 350, 70);
angle += 0.02;
}
這段程式碼做了幾件事:左半邊畫一個單位圓,一個紅點沿著圓周轉動;右半邊將紅點的 y 座標(也就是 sin 值)隨時間展開,形成一條經典的 sin 波。藍色的線段則表示 cos(x 方向的投影)。
跑起來之後,你會看到一件很美的事情:圓周運動和波動是同一件事情的兩種觀看方式。
sin 與 cos 的性格
它們是孿生兄弟
sin 和 cos 的波形完全相同,只是時間差了 π/2(90 度)。數學上寫成:
cos(θ) = sin(θ + π/2)
你可以這樣理解:cos 就是「提前出發了 90 度的 sin」。在動畫中,當 sin 從零開始慢慢爬升時,cos 已經從最高點開始下降了。
頻率、振幅與相位
一般化的正弦函數長這樣:
y = A sin(ω t + φ)
- A(振幅):波的高度,決定上下擺動的幅度
- ω(角頻率):波動得多快,ω 越大震動越快
- φ(相位):波的起始位置偏移
讓我們寫一個互動式的範例,用滑桿控制這三個參數:
let sliderA, sliderW, sliderP;
function setup() {
createCanvas(800, 300);
sliderA = createSlider(10, 150, 80); // 振幅
sliderW = createSlider(1, 10, 3); // 頻率
sliderP = createSlider(0, 628, 0); // 相位 (x100)
sliderA.position(20, 310);
sliderW.position(20, 340);
sliderP.position(20, 370);
}
function draw() {
background(30);
let A = sliderA.value();
let w = sliderW.value();
let phi = sliderP.value() / 100.0;
// 座標軸
stroke(80);
line(0, height / 2, width, height / 2);
// 波形
noFill();
stroke(255, 80, 80);
strokeWeight(2);
beginShape();
for (let x = 0; x < width; x++) {
let t = map(x, 0, width, 0, TWO_PI * 2);
let y = height / 2 - A sin(w t + phi);
vertex(x, y);
}
endShape();
strokeWeight(1);
// 標示
fill(255);
noStroke();
textSize(13);
text("振幅 A = " + A, 200, 325);
text("頻率 ω = " + w, 200, 355);
text("相位 φ = " + nf(phi, 1, 2), 200, 385);
}
tan:被遺忘的第三者
tan(正切)常常被學生忽略,但它在圖形學中其實很重要。tan 的定義是:
tan(θ) = sin(θ) / cos(θ)
當 cos(θ) 接近 0(也就是 θ 接近 90° 或 270°)時,tan 會趨近無窮大,這就是 tan 函數圖形上那些垂直漸近線的來源。
在視覺上,tan 的幾何意義是:從單位圓上的點畫一條切線(tangent line),這條線與 x 軸交會點的距離就是 tan 值。這也是「tangent(切線)」這個名字的由來。
tan 在實務中的應用
在 3D 圖形中,透視投影的視野角度(Field of View)就用到了 tan:
// 透視投影中,投影面的半高
let halfHeight = near * tan(fov / 2);
在物理模擬中,斜面上物體的滑動條件也和 tan 有關:當摩擦角等於斜面角度時,tan(θ) = μ(摩擦係數)。
Lissajous 圖形:三角函數的華爾滋
Lissajous 圖形是我最喜歡的三角函數應用之一。原理很簡單:讓一個點的 x 和 y 座標分別由兩個不同頻率的 sin 函數控制:
x = A sin(a t + δ)
y = B sin(b t)
當頻率比 a:b 是簡單的整數比(如 1:2、2:3、3:4)時,軌跡會形成封閉的美麗曲線。
let t = 0;
let a = 3, b = 2; // 頻率比
let delta = PI / 4; // 相位差
let trail = [];
function setup() {
createCanvas(600, 600);
background(30);
}
function draw() {
background(30, 30, 30, 15); // 半透明背景產生拖尾效果
let cx = width / 2, cy = height / 2;
let A = 200, B = 200;
let x = cx + A sin(a t + delta);
let y = cy + B sin(b t);
trail.push({ x: x, y: y });
if (trail.length > 2000) trail.shift();
// 畫軌跡
noFill();
for (let i = 1; i < trail.length; i++) {
let alpha = map(i, 0, trail.length, 0, 255);
stroke(255, 80, 80, alpha);
line(trail[i - 1].x, trail[i - 1].y, trail[i].x, trail[i].y);
}
// 當前點
fill(255);
noStroke();
ellipse(x, y, 8, 8);
// 顯示資訊
fill(255);
textSize(14);
text("a:b = " + a + ":" + b, 20, 30);
text("δ = π/" + round(PI / delta), 20, 50);
t += 0.01;
}
function mousePressed() {
// 點擊切換不同頻率比
a = floor(random(1, 6));
b = floor(random(1, 6));
delta = random(0, PI);
trail = [];
t = 0;
}
試試看改變 a 和 b 的值!當 a=1, b=1 時你得到一個橢圓(或直線,取決於相位差);a=1, b=2 時出現一個 8 字形;a=3, b=2 時會看到更複雜的圖案。這些圖形在示波器上也能看到,用 X-Y 模式輸入兩個不同頻率的訊號就行了。
三角函數在動畫中的經典用法
在日常的動畫與互動設計中,sin 和 cos 無處不在。以下列舉幾個常見的模式:
懸浮效果(Hover/Float)
// 讓物件上下輕輕浮動
let yOffset = 20 sin(frameCount 0.03);
ellipse(width / 2, height / 2 + yOffset, 50, 50);
脈搏效果(Pulse)
// 讓物件的大小有脈搏般的跳動
let size = 50 + 10 sin(frameCount 0.05);
ellipse(width / 2, height / 2, size, size);
圓形排列
// 把 N 個物件均勻排列在圓周上
let N = 12;
for (let i = 0; i < N; i++) {
let angle = TWO_PI / N * i;
let x = centerX + radius * cos(angle);
let y = centerY + radius * sin(angle);
ellipse(x, y, 20, 20);
}
波浪動畫
// 一排點形成波浪
for (let i = 0; i < 40; i++) {
let x = i * 20;
let y = height / 2 + 50 sin(frameCount 0.05 + i * 0.3);
ellipse(x, y, 10, 10);
}
最後一個特別值得注意:i * 0.3 這個偏移量讓每個點的相位略有不同,形成「傳播中的波」的效果。這就是相位在動畫中的威力。
小結
三角函數不只是數學課本上的公式,它們是描述「週期性運動」的語言。從單位圓的旋轉,到 sin/cos 的波形展開,再到 Lissajous 圖形的交織之美,每一個視覺化都在告訴我們同一件事:圓周運動和波動是同一枚硬幣的兩面。
當你下次在寫動畫時,試著用 sin 和 cos 來控制位置、大小、顏色、透明度——你會發現,幾乎所有自然的、舒服的週期性變化,都可以用三角函數來表達。
延伸閱讀
- Daniel Shiffman 的《The Nature of Code》第三章(振盪)
- 3Blue1Brown 的「Essence of Trigonometry」影片
- Freya Holmer 在 GDC 的演講「Math for Game Programmers: Juicing with Math」
- 嘗試在 p5.js Web Editor 中修改本文的範例,加入更多互動控制