為用到了一點點物理知識, 我們可以稱這為極簡化的Javascript物理引擎
大叔慣例,先上效果
下落是個重力加速度的過程
本例通過canvas畫布來實現
雖然很簡單, 也是先把舞臺準備一下
1.準備個HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
</head>
<body></body>
</html>
2.加個基礎樣式
<style>
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
main {
width: 100vw;height: 100vh;
background: hsl(0deg, 0%, 10%);
}
</style>
樣式的作用是去掉所有元素的外邊距、內間距,并把 <main> 元素的寬高設置為與瀏覽器可視區域相同,背景色為深灰色。
hsl(hue, saturation, brightness) 為 css 顏色表示法之一,參數分別為色相,飽和度和亮度
3.添加 canvas 元素
<main>
<canvas id="gamecanvas"></canvas>
</main>
4.然后就可以用JS來畫圖了
const canvas = document.getElementById("gamecanvas"); //通過 canvas 的 id 獲取 canvas 元素對象。
const ctx = canvas.getContext("2d"); // getContext() 需要一個參數,用于表明是繪制 2d 圖像,還是使用 webgl 繪制 3d 圖象,這里選擇 2d
canvas.width = window.innerWidth; //寬高設置為瀏覽器可視區域的寬高
canvas.height = window.innerHeight;
let _width = canvas.width;
let _height = canvas.height;
ctx.fillStyle = "hsl(170, 100%, 50%)"; //給 context 設置顏色
ctx.beginPath(); //開始繪圖
ctx.arc(150, 100, 50, 0, 2 * Math.PI); //繪制圓形,它接收 5 個參數,前兩個為圓心的 x、y 坐標,第 3 個為半徑長度, 第 4 個和第 5 個分別是起始角度和結束角度
ctx.fill(); //給圓形填上顏色
試運行看看
在瀏覽器指定坐標畫圖成功
這個時候小球還是處于靜止狀態, 要讓它動起來, 就要通過程序修改它的圓心坐標
讓小球移動過程其實就是: 畫圓 > 擦除 > 在新坐標1上畫圓 > 擦除 > 在新坐標2上畫圓...
因為人眼的視覺停留效應, 只要這個過程足夠快, 那么在人眼看來這個球就是在做連續的運動而不會看到閃動.
需要達到多快呢?
畫圓 > 擦除 > 再畫圓 這么一個過程可以看作"一幀"
然后每秒超過24幀就可以, 幀數越高看上去運動就越平滑.
在 JavaScript 中,瀏覽器提供了 window.requestAnimationFrame() 方法,它接收一個回調函數作為參數,每一次執行回調函數就相當于 1 幀動畫,我們需要通過遞歸或循環連續調用它,瀏覽器會盡可能地在 1 秒內執行 60 次回調函數。那么利用它,我們就可以對 canvas 進行重繪,以實現小球的移動效果。
基礎代碼結構看上去的樣子:
function drawBall() {
window.requestAnimationFrame(drawBall);
}
window.requestAnimationFrame(drawBall);
這個drawBall()函數, 就是60次/秒的函數
把剛才的代碼重構一下
let x = 150; //坐標x
let y = 100; //坐標y
let r = 60; //半徑
function drawBall(now) {
ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
window.requestAnimationFrame(drawBall);
}
window.requestAnimationFrame(drawBall);
計算圓心坐標 x、y 的移動距離,我們需要速度和時間, 速度就是vy, 還需要有時間
window.requestAnimationFrame() 會把當前時間的毫秒數(即時間戳)傳遞給回調函數,我們可以把本次調用的時間戳保存起來,然后在下一次調用時計算出執行這 1 幀動畫消耗了多少秒,然后根據這個秒數和 x、y 軸方向上的速度去計算移動距離,分別加到 x 和 y 上,以獲得最新的位置。
改進代碼如下
let x = 100; //坐標
let y = 100;
let r = 60; //半徑
let vy = 25; //移動Y軸的速度
let startTime;
function drawBall(now) {
if (!startTime) {
startTime = now;
}
let seconds = (now - startTime) / 1000;
startTime = now;
y += vy * seconds; // 更新Y坐標
ctx.clearRect(0, 0, width, height); // 清除畫布
ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
window.requestAnimationFrame(drawBall); //每次執行完畢后繼續調用 window.requestAnimationFrame(process)進行下一次循環
}
window.requestAnimationFrame(drawBall);
現在會動了(請忽略一閃而過的鼠標...)
目前只是個勻速運動, 我們要為它加上重力效果
重力加速度的公式: v=gt2
加速度常量g是個恒定值9.8
//先在函數外添加一個常量
const gravity = 9.80;
...
//函數內部, 在計算y坐標前面加一行:
vy += gravity * (seconds^2); // 重力加速度 v=gt2
y += vy * seconds;
這里我們不希望球跑到屏幕外面去, 同時加個邊界判斷
//邊界檢查
let oy = y + r; //y+r
if(oy<height){
window.requestAnimationFrame(drawBall); //每次執行完畢后繼續調用 window.requestAnimationFrame(process)進行下一次循環
}
這樣就可以達成文章最開頭的運動效果了
很簡單, 同時其實也很有意思
比如加個半徑變化控制, 就會看到球越往下掉就越小/大
r = r - 0.8;
下面是全部代碼
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<style>
* {
box-sizing: border-box;
padding: 0;
margin: 0;
font-family: sans-serif;
}
main {
width: 100vw;
height: 100vh;
background: hsl(0deg, 0%, 10%);
}
</style>
</head>
<body>
<main>
<canvas id="gamecanvas"></canvas>
</main>
</body>
<script>
const canvas = document.getElementById("gamecanvas"); //通過 canvas 的 id 獲取 canvas 元素對象。
const ctx = canvas.getContext("2d"); // getContext() 需要一個參數,用于表明是繪制 2d 圖像,還是使用 webgl 繪制 3d 圖象,這里選擇 2d
canvas.width = window.innerWidth; //寬高設置為瀏覽器可視區域的寬高
canvas.height = window.innerHeight;
let width = canvas.width;
let height = canvas.height;
const gravity = 9.80;
let x = 100; //坐標
let y = 100;
let r = 60; //半徑
let vy = 25; //移動Y軸的速度
let startTime;
function drawBall(now) {
if (!startTime) {
startTime = now;
}
let seconds = (now - startTime) / 1000;
startTime = now;
vy += gravity * (seconds^2); // 重力加速度 v=gt2
y += vy * seconds;
r = r - 0.8;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = "hsl(170, 100%, 50%)";
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
//邊界檢查
let oy = y + r;
if(oy<height){
window.requestAnimationFrame(drawBall); //每次執行完畢后繼續調用 window.requestAnimationFrame(process)進行下一次循環
}
}
window.requestAnimationFrame(drawBall);
</script>
這期就醬,下期再見[酷拽]
復雜的問題簡單化
每次只關注一個知識點
對技術有興趣的小伙伴可以關注我, 我經常分享各種奇奇怪怪的技術知識
光線跟蹤算法里,有一個子問題:如何在一個半徑為1的單位球里面,產生一個均勻分布的隨機的點(相同的體積里有相同數量的點)。下面這篇文章里給出了5種可能的方法 (參考文獻[3])。當然,后面我們會看到,這5種方法并不都是對的。
球里面均勻分布的點
這個方法是這樣,首先我們在x,y,z分別在 [-1, 1] 里均勻采樣,這樣就能實現在一個立方體里均勻采樣。但這樣的點可能不在球里,我們把不在球里的點拒絕掉就行。
function getPoint() {
var d, x, y, z;
do {
x = Math.random() * 2.0 - 1.0;
y = Math.random() * 2.0 - 1.0;
z = Math.random() * 2.0 - 1.0;
d = x*x + y*y + z*z;
} while(d > 1.0);
return {x: x, y: y, z: z};
}
這里,Math.random() 會產生一個[0,1]之間的均勻分布的隨機數。
這個方法選擇一個隨機的向量,然后把它歸一化到一個隨機的半徑。
function getPoint() {
var x = Math.random() - 0.5;
var y = Math.random() - 0.5;
var z = Math.random() - 0.5;
var mag = Math.sqrt(x*x + y*y + z*z);
x /= mag; y /= mag; z /= mag; // 把一個隨機的點歸一化到單位球面上
var d = Math.random(); // 一個隨機的半徑
return {x: x*d, y: y*d, z: z*d};
}
這個方法會在同樣的半徑區間里產生同樣多的點,比如 半徑在 (0.1, 0.2)的區域內產生的點數和半徑在 (0.2, 0.3)的區域內的點數,期望是相同的。但是這兩個區間的體積卻不一樣。所以,這個方法并不能產生球內均勻分布的點。
球坐標下,一個點由 r (到原點的距離),theta(和z軸的夾角),phi(向量在x-y平面的投影和x軸的夾角)三個變量控制。這個坐標表示如果表示成笛卡爾坐標系,就是
x = r sin(phi) cos(theta)
y = r sin(phi) sin(theta)
z = r cos(phi)
function getPoint() {
var theta = Math.random() * 2.0*Math.PI;
var phi = Math.random() * Math.PI;
var r = Math.random();
var sinTheta = Math.sin(theta);
var cosTheta = Math.cos(theta);
var sinPhi = Math.sin(phi);
var cosPhi = Math.cos(phi);
var x = r * sinPhi * cosTheta;
var y = r * sinPhi * sinTheta;
var z = r * cosPhi;
return {x: x, y: y, z: z};
}
球體里的一小塊體積
根據上圖,球體里的一下快體積正比于 sin(phi)d(theta)d(phi) = d(theta) d(cos(phi))。因此,可以看到,如果phi是均勻分布,但是sin(phi)不是均勻分布。
另外,這個算法里,半徑r是均勻分布的,但是半徑為r的球和單位球的體積比例是r的三次方。也就是說,r越小,點會越密集。
因為這兩個原因,這個方法也不符合要求。
我們首先看代碼
function getPoint() {
var u = Math.random();
var x1 = randn(); // 0為均值,1為方差的高斯分布隨機數
var x2 = randn();
var x3 = randn();
var mag = Math.sqrt(x1*x1 + x2*x2 + x3*x3);
x1 /= mag; x2 /= mag; x3 /= mag;
var c = Math.cbrt(u); // 立方根
return {x: x1*c, y:x2*c, z:x3*c};
}
這個地方要對 u 取立方根,是因為半徑為u的球的體積是半徑為1的球的體積的 u 三次方倍。
這個地方比較奇怪的是,x,y,z為什么要是高斯分布(正態分布)。這個可以參考參考文獻[5][6]。
簡單解釋一下如下:
正態分布的形式是 f(x) = exp{-(x*x)/2} / sqrt(2PI)
因此,f(x, y, z) = exp{ - (x*x + y*y + z*z)/2 } / Const = exp{ - (r*r)/2 } / Const
因此這樣產生的點只和它的模長相關,和各種角度都無關。
在上面的球坐標法中,有2個問題,1. 角度phi均勻分布不代表cos值均勻分布 2. 球的體積和半徑的三次方成正比。因此,我們可以直接讓cos(phi)值均勻分布,而不是角度均勻分布,同時,對半徑開立方根,來保證體積的均勻分布。
function getPoint() {
var u = Math.random();
var v = Math.random();
var theta = u * 2.0 * Math.PI;
var phi = Math.acos(2.0 * v - 1.0);
var r = Math.cbrt(Math.random());
var sinTheta = Math.sin(theta);
var cosTheta = Math.cos(theta);
var sinPhi = Math.sin(phi);
var cosPhi = Math.cos(phi);
var x = r * sinPhi * cosTheta;
var y = r * sinPhi * sinTheta;
var z = r * cosPhi;
return {x: x, y: y, z: z};
}
ox-decoration-break property,padding-left property,padding-right property,padding-bottom property。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。