一、故事的开始,我要一个球

指标:用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"    })  })}
转载请注明出处, 原文来自我的掘金分享