共计 7291 个字符,预计需要花费 19 分钟才能阅读完成。
一、故事的开始,我要一个球
指标:用 canvas 做一个模仿自由落体静止的小球,小球是有弹性的。
初中物理学过万有引力,还记得有 高度,
速度
,重力加速度
,低空抛下后,小球自由落体,如果小球有弹性,那么还会回弹,那么应用 canvas 模仿一个试验场景吧,
-
搭建 html 模版,初始化 canvas 画布、画笔
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style>
html,
body,
canvas {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
window.onload = () => {const canvas = document.getElementById('canvas');
// 世界有多大舞台就要有多大????
// ps:这里的宽高不是 css 款式层面的宽高,是像素点哦
canvas.width = window.document.body.clientWidth;
canvas.height = window.document.body.clientHeight;
const ctx = canvas.getContext('2d');
// next do some things
// ...
}
</script>
</body>
</html>
好了,干干净净的画布就进去啦,
-
开始画球啦,定义一个类吧 明天不是 car 类也不是 foo 类而是
Ball
类。
class Ball {
// 初始化的特色
constructor(options = {}) {
const {
x = 0, // x 坐标
y = 0, // y 坐标
ctx = null, // 神奇的画笔????️
radius = 0, // 球的半径
color = '#000' // 色彩
} = options
this.x = x;
this.y = y;
this.ctx = ctx;
this.radius = radius;
this.color = color
}
// 渲染
render() {this.ctx.beginPath();
this.ctx.fillStyle = this.color;
// 画圆
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI)
this.ctx.fill()}
}
-
用这个
Ball
类,生成一个球
window.onload = () => {
// ...
const ctx = canvas.getContext('2d');
const ball = new Ball({
ctx,
x: ctx.canvas.width * 0.5, // 在画布的核心地位
y: ctx.canvas.height * 0.5,
radius: 20,
color: '#66cccc'
})
ball.render();}
class Ball {// ...}
小球诞生啦
-
让他动起来,利用好速度和加速度,
那么就要用到 requestAnimationFrame
办法,让咱们能够在下一帧开始时调用指定函数,
requestAnimationFrame 详解。
window.onload = () => {
// ...
ball.render();
// 循环绘画
const loopDraw = () => {requestAnimationFrame(loopDraw);
ball.render();}
loopDraw(); // 滴滴启动动画}
class Ball {// ...}
额~~~~小球还没动,是滴!还须要一个办法在更新小球的地位。持续加工 Ball
,加一个updata
办法
window.onload = () => {
// ...
const loopDraw = () => {requestAnimationFrame(loopDraw);
// 革除画布,不然能够看见每一帧的静止轨迹,这一块有嚼头,还能够做更炫酷的货色。ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ball.render();
ball.updata(); // 更新地位}
loopDraw();}
class Ball {
// ...
this.radius = radius;
this.color = color
// 速度
this.vy = 0; // 刚开始是静止的
// 加速度
this.gvy = 1;
render() {// ...}
updata() {
this.y += this.vy; // 每帧按速度变动的 y
this.vy += this.gvy; // 每帧速度依照加速度递增
// 触底碰撞检测,不然球飞出屏幕啦。if (this.y >= this.ctx.canvas.height - this.radius) {
this.y = this.ctx.canvas.height - this.radius;
// 回弹就是调整静止方向,那么数值上 180 度大转弯
this.vy = -this.vy * 0.75;// 速度损耗,粗略模拟受地心引力影响,随便调整到本人喜爱的值,大略。}
}
}
小球它动了,它动了!
-
小结
1、动画须要用到
requestAnimationFrame
, 当然也能够用setTimeout
或者setInterval
, 来模仿 loop。2、绘制下一帧前要革除上一帧的画布,不然上一帧的成果还会保留在画布上。当然你能够保留它,如果需要的话。
3、静止的速度能够了解成,每一帧须要静止的动量,绘制每一帧都是有耗费工夫的,而且每一帧的工夫还不肯定是固定的。依据这个特点动画还能够优化的更晦涩。
二、持续玩球
-
让球乌七八糟的静止,如果在太空中,受重力影响忽略不计的话
有 y
轴方向的静止教训,退出 x
轴的静止,去掉加速度,因为咱们在太空啦,
退出全方位碰撞检测
window.onload = () => {// ...}
class Ball {
// ...
// 速度
this.vx = -2; // 这是新成员
this.vy = 2;
// 加速度
this.gvx = 0;
this.gvy = 0; // 这次我不须要你了
render() {// ...}
updata() {
this.x += this.vx;
this.y += this.vy;
this.vy += this.gvy;
this.vx += this.gvx;
// 触顶
if (this.y - this.radius <= 0) {
this.y = this.radius
this.vy = -this.vy * 0.99 // 随
}
// 触底
if (this.y >= this.ctx.canvas.height - this.radius) {if (this.vy <= this.gvy * 2 + this.vy * 0.8) this.vy = 0;
this.y = this.ctx.canvas.height - this.radius;
this.vy = -this.vy * 0.75; // 便
}
// 触右
if (this.x - this.radius <= 0) {
this.x = this.radius
this.vx = -this.vx * 0.5 // 设
}
// 触左
if (this.x + this.radius >= this.ctx.canvas.width) {
this.x = this.ctx.canvas.width - this.radius
this.vx = -this.vx * 0.5 // 置
}
}
}
look! 活蹦乱跳的小球,到处碰壁。
-
多球静止
Ball
是一个类,那么初始化的时候多 new 几次,而后给球的速度随机一点
window.onload = () => {
// ...
const num = 100;
let balls = []
// 多姿多彩
const colors = ['#66cccc', '#ccff66', '#ff99cc', '#ff9999', '#666699', '#ff0033', '#FFF2B0'];
// 我要 100 个
for (let i = 0; i < num; i++) {
balls.push(new Ball({
ctx,
// 随机呈现在画布中任何一处
x: Math.floor(Math.random() * ctx.canvas.width),
y: Math.floor(Math.random() * ctx.canvas.height),
radius: 10,
color: colors[Math.floor(Math.random() * 7)]
}))
}
// 循环绘画
const loopDraw = () => {requestAnimationFrame(loopDraw);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((ball, index) => {ball.render();
ball.updata();})
}
}
class Ball {constructor(options = {}) {
// ...
// 速度
this.vx = (Math.random() - 0.5) * 10;
this.vy = (Math.random() - 0.5) * 10;
// 加速度
this.gvx = (Math.random() - 0.5) * 0.01;
this.gvy = (Math.random() - 0.5) * 0.01
}
// ...
}
唔~
三、让邻里之间多点分割
邻里只能是在肯定范畴内的,太远了可不是哦,那么就须要晓得两球之间的间隔,计算两点之间的间隔,好相熟啊,一位不出名的热心童鞋霎时说出了初中(大略)学过的公式
-
连线口头:两球之间用线连起来
Ball
中新的成员退场renderLine
, 画点与点之间的连线;
// js 版本的计算两点间隔公式
function twoPointDistance(p1, p2) {let distance = Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2));
return distance;
}
window.onload = () => {
// ...
// 循环绘画
const loopDraw = () => {requestAnimationFrame(loopDraw);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
balls.forEach((ball, index) => {ball.render();
ball.updata();
balls.forEach(ball2 => {const distance = twoPointDistance(ball, ball2)
// 排除本人和 100 像素开外的
if (distance && distance < 100) {ball.renderLine(ball2)
}
})
})
}
}
class Ball {
// ...
render() {// ...}
updata() {// ...}
renderLine(target) {this.ctx.beginPath();
this.ctx.strokeStyle = "ddd";
this.ctx.moveTo(this.x, this.y);
this.ctx.lineTo(target.x, target.y);
this.ctx.stroke();}
}
-
加一个非凡的纽带
如果咱们的色彩各不相同,那由咱们独特绘制一条纽带吧
把球变小 数量变多
window.onload = () => {// ...}
class Ball {
// ...
render() {// ...}
updata() {// ...}
renderLine(target) {
// ...
// 渐变色,由我和 target 组成
var lingrad = this.ctx.createLinearGradient(this.x, this.y, target.x, target.y);
lingrad.addColorStop(0, this.color);
lingrad.addColorStop(1, target.color);
this.ctx.strokeStyle = lingrad;
// ...
}
}
-
加点拖影 多姿多彩的幻影
window.onload = () => {
// ...
// 循环绘画
const loopDraw = () => {
//...
// 替换 clearRect, 使上一次的成果透明度变成 0.3
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// ..
}
}
class Ball {// ...}
-
再来一个圈子,
Ball
中加了个renderCircle
// ...
class Ball {
// ...
renderCircle(target, radius) {this.ctx.beginPath();
this.ctx.strokeStyle = this.color;
this.ctx.arc((this.x + target.x) / 2, (this.y + target.y) / 2, radius, 0, 2 * Math.PI);
this.ctx.stroke();}
}
-
再来一个。。。算了,篇幅无限。
四、优化
-
优化到每一帧
每一帧的工夫都不一样,那么不论是 x 轴还是 y 轴上的速度,心愿在每一毫秒中是一样的,这样就要获取每一帧的耗费工夫,而后调整下 updata 的速度增量,这样能够说动画更加顺滑
let delayTime = 0;
// 上一帧的工夫
let lastTime = +new Date;
// 循环绘画
const loopDraw = () => {requestAnimationFrame(loopDraw);
// 以后工夫
const now = +new Date;
delayTime = now - lastTime;
lastTime = now;
if (delayTime > 50) delayTime = 50;
balls.forEach((ball, index) => {ball.render();
// 依据工夫在 updata 中调整增量
ball.updata(delayTime && delayTime);
// ...
})
}
// ...
updata(delayTime) {
// 每一帧的工夫都不一样,那么应用每一毫秒
this.x += this.vx / (delayTime || 1) * 3;
this.y += this.vy / (delayTime || 1) * 3;
// ...
}
-
顺带撸了个帧率监视器
动画是有帧率的,那么就要有一个伎俩检测它,看看动画是否晦涩。小于 30 帧 -> 红色 大于 30-> 绿色。
如果帧率过低 就能够思考优化
requestAnimationFrame
的中的回调函数,看看是否做了多余的事件。
当然还有很很多优化伎俩,动画这块我也不是很懂。就不班门弄斧了
;
小插件其中的次要绘制办法
// 绘制办法
const FPS = (fpsList) => {
ctx.font = '14px serif';
ctx.fillStyle = "#fff"
const len = fpsList.length;
ctx.fillText(`FPS: ${fpsList[len - 1]}`, 5, 14);
ctx.lineWidth = '2'
fpsList.forEach((time, index) => {if (time < 30) {ctx.strokeStyle = '#fd5454';} else {ctx.strokeStyle = '#6dc113';}
ctx.beginPath();
ctx.moveTo(ctx.canvas.width - ((len - index) * 2), ctx.canvas.height);
ctx.lineTo(ctx.canvas.width - ((len - index) * 2), (ctx.canvas.height - time * 0.5));
ctx.stroke();});
// 删掉多余的
if (len > 50) {fpsList.shift()
}
}
最初
基于这些还能够持续拓展,比方做光标在画布上挪动,鼠标左近的小球主动连线;还能够牵引它的静止;小球之间的互相碰撞成果;想法一个个的冒出来,基于一个简略的球类,萌发出各个想法,从画一个圆开始,到前面各种炫酷的成果,越尝试惊喜越多,这是一个乏味的标签。而且实现这些并没有用到很简单的 API,canvas 的 画线 moveTo lineTo
画圆arc
等常见的 API,加上一点数学或物理常识。正因为这些惊喜,让我在学习之路上不会干燥。
更新
-
鼠标的不会迷失在动画中,焦点就是我,2020-11-11 21:30
// 加载图片
const loadImage = (src) => new Promise(resolve => {const img = document.createElement('img');
img.src = src;
img.onload = () => {return resolve(img);
}
})
window.onload = async () => {
// ...
const bg = await loadImage('./media/bg.jpg');
let mouseBall;
// ...
// 循环绘画
const loopDraw = () => {
// ...
balls.forEach((ball, index) => {
// ...
if (mouseBall) {const lineMouse = twoPointDistance(ball, mouseBall);
if (lineMouse && lineMouse < 100) {ball.renderLine(mouseBall)
}
}
// ...
})
}
window.addEventListener('mousemove', (e) => {
mouseBall = new Ball({
ctx,
x: e.pageX,
y: e.pageY,
radius: 1,
color: "#fff"
})
})
}
转载请注明出处,原文来自我的掘金分享