为用到了一点点物理知识, 我们可以称这为极简化的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=gt²
加速度常量g是个恒定值9.8
//先在函数外添加一个常量
const gravity = 9.80;
...
//函数内部, 在计算y坐标前面加一行:
vy += gravity * (seconds^2); // 重力加速度 v=gt²
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=gt²
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小时内与您取得联系。