Day13 - 物理模拟篇 - 弹跳球世界IV - 成为Canvas Ninja ~ 理解2D渲染的精髓

终於来到弹跳球的最後一部分~ 这篇我们主要就是要讲解倾斜面存在的状况下,程序的撰写方法!

老实说我原本是打算在一篇文内把倾斜面的范例处理完毕的。
但是因为碰上一些工作上的问题,时间有点不太够用 :(

所以我还是把这部分拆成两篇文来写了(罚跪

也就是说~这一篇是前篇,我在这篇会先讲解这个案例的大概样貌,还有场景的架设要怎麽处理~

img

这边就先来看看我构建场景的源码~ 基本上因为这个案例偏复杂,所以我会使用webpack搭配ESModule来编写这个案例。
之後完整案例的源码我会贴在我自己的public repo上面,供各位看官查阅~

关於源码中倾斜面碰撞侦测的原理可以看之前的文

// Vector2D 就是我们前面建立的向量类,
// 我这边补了一个 Point2D类,用途是用来产生一个具有x和y property 的座标物件
import { Vector2D, Point2D } from './class';

const CANVAS = {
  width: 600,
  height: 600,
  background: 'gray'
}

const BALL = {
  radius: 5,
  color: '#333'
}

// 先用阵列描述六面墙壁的端点位置
const WALLS = [
  [new Point2D(50, 50), new Point2D(50, 550)],// 左边界
  [new Point2D(550, 50), new Point2D(550, 550)],//右边界
  [new Point2D(50, 550), new Point2D(550, 550)],//下边界
  [new Point2D(125, 150), new Point2D(475, 100)],// 第一斜坡
  [new Point2D(75, 250), new Point2D(425, 300)],// 第二斜坡
  [new Point2D(125, 450), new Point2D(550, 400)]// 第三斜坡
]

// 这个是球的类,球本身会具有随机性的初速,和向下的加速度,同时他会具备可以更新自身速度和位置的方法
class Ball {
  constructor(x, y, color = BALL.color, radius = BALL.radius, randomSpeed = true) {
    this.x = x;
    this.y = y;
    this.color = color;
    this.radius = radius;
    this.gravity = new Vector2D(0, 4);
    this.friction = 0.999;
    if (randomSpeed) {
      this.velocity = new Vector2D(
        (Math.random() * this.radius * 2 - radius) * 10,
        (Math.random() * this.radius * 2 - radius)
      )
    }
    else {
      this.velocity = new Vector2D(0, 0);
    }
  }
  draw(ctx) {
    ctx.save()
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();
    ctx.restore();
  }
  refreshLocation(dt) {
    this.x += this.velocity.x * dt;
    this.y += this.velocity.y * dt;
  }
  refreshSpeed(dt) {
    this.velocity.scaleBy(this.friction);
    this.velocity.incrementBy(this.gravity.multiply(dt));
  }
}

// 这个是用来统合所有墙壁的类
class Boundary {
  constructor() {
    this.walls = WALLS;
  }

  draw(ctx) {
    this.walls.forEach((o, i) => {
      ctx.beginPath();
      ctx.moveTo(o[0].x, o[0].y);
      ctx.lineTo(o[1].x, o[1].y);
      ctx.closePath();
      ctx.lineWidth = 5;
      ctx.lineJoin = 'round';
      ctx.strokeStyle = 'white';
      ctx.stroke();
    })
  }
}

// 这个是主要的入口
class InclinedWallsAndBouncingBallsAnimation {
  constructor(ctx) {
    this.ctx = ctx;
    this.cvs = ctx.canvas;
    // balls 是一个"球池"的概念,用来集中放置所有因爲滑鼠点击而产生的球
    this.balls = [];
    this.frameIsPaused = false;
    //入口方法
    this.init();
  }

  init() {
    this.time = 0;
    // 动态决定canvas大小
    this.setCanvasSize();
    // 在初始的时候先绘制一次墙壁
    this.initBoundary();
    // 绑定滑鼠点击事件和document的visibilityChange 事件
    this.initEvents();
    // 启动动画
    this.animate();
  }
  // 初始化墙壁
  initBoundary() {
    this.boundary = new Boundary();
    this.boundary.draw(this.ctx);
  }
  // 绑事件
  initEvents() {
    this.initVisibilityChangeEvent();
    this.initClickEvent();
  }
  //visibilityChange事件
  initVisibilityChangeEvent() {
    window.addEventListener('visibilitychange', () => {
      if (document.visibilityState !== "visible") {
        this.frameIsPaused = true;
      }
      else {
        this.frameIsPaused = false;
        this.time = performance.now();
      }
    });
  }
  // 每按一次滑鼠就会在按下去的座标生成一颗球
  initClickEvent() {
    this.cvs.addEventListener('click', (e) => {
      const rect = e.target.getBoundingClientRect();
      const mouseX = e.clientX - rect.left;
      const mouseY = e.clientY - rect.top;

      this.balls.push(new Ball(mouseX, mouseY))
    })
  }

  animate() {
    if (this.frameIsPaused) {
      this.animate();
    }
    const $this = this;
    const dt = (performance.now() - this.time) / 100;
    this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height);
    this.animateBalls(dt);
    this.boundary.draw(this.ctx);
    this.time = performance.now();
    requestAnimationFrame(this.animate.bind($this));
  }

  animateBalls(dt) {
    //这边就是去遍历过整个球池,把每一颗球都根据其位置/大小/颜色画出来,然後就跟之前的范例一样,接着更新位置和速度
    this.balls.forEach((o, i) => {
      o.draw(this.ctx);
      // 更新位置数据
      o.refreshLocation(dt);
      // 这段是我用来防止已经飞出画面外的球仍然停留在球池物件中,导致重复计算而效能爆炸,做一个简单的消除
      if (o.x > this.cvs.width || o.y > this.cvs.height || o.x < 0) {
        this.balls.splice(i, 1);
      }
      //更新速度数据
      o.refreshSpeed(dt);
      this.checkBoundary();
    })
  }
  //用来动态设定 canvas大小的方法
  setCanvasSize() {
    this.cvs.width = CANVAS.width;
    this.cvs.height = CANVAS.height;
    this.cvs.style.backgroundColor = CANVAS.background;
  }
  // 这部分就是侦测碰撞
  checkBoundary() {
    // 先遍历每一面墙壁
    this.boundary.walls.forEach((o, i) => {
      const vectorAB = new Vector2D(
        o[1].x - o[0].x,
        o[1].y - o[0].y
      )
      // 在遍历每一颗球
      this.balls.forEach((ball, index) => {
        // 这边其实就是我们前面有提到的倾斜面的碰撞侦测

        //墙壁端点A到球心的向量
        const vectorAToBall = new Vector2D(
          ball.x - o[0].x,
          ball.y - o[0].y
        );
        //墙壁端点B到球心的向量
        const vectorBToBall = new Vector2D(
          ball.x - o[1].x,
          ball.y - o[1].y
        );
        //墙壁端点A到球心的向量映射在墙壁的向量
        const vectorAToBallProj = vectorAToBall.project(vectorAB);
        //墙壁端点B到球心的向量映射在墙壁的向量
        const vectorBToBallProj = vectorBToBall.project(vectorAB);
        //向量互减
        const dist = vectorAToBall.substract(vectorAToBallProj).length();
        if (!dist) return;
        // 这个条件就是在讲球离墙壁的距离要低於半径,且球到墙壁两端点的向量映射在墙壁上的长度不能超过墙壁长度(意思就是指球是位於墙壁的两个端点中间)
        const collisionDetection =
          dist < ball.radius &&
          vectorAToBallProj.length() < vectorAB.length() &&
          vectorBToBallProj.length() < vectorAB.length();
        if (collisionDetection) {
          // console.log('boom!!!')
        }
      })

    })
  }
}


document.addEventListener('DOMContentLoaded', () => {
  let ctx = document.querySelector('canvas').getContext('2d');
  let instance = new InclinedWallsAndBouncingBallsAnimation(ctx);
})

测试了一下~ 确认有侦测到碰撞 ! (YA)
(但是因为还没有处理反弹机制,所以球直接穿墙飞出去了XD)

img

在明天的部分,我们会接着继续把这个案例的reposition反射 运算的部分做完! 敬请期待 :D ~


<<:  【Day13】Latch 的生成条件以及如何避免(下)

>>:  JS 13 - Getter & Setter

<Day7>以模拟帐户作示范 — 登入 Shioaji API

● 接下来几章都是先以模拟帐户作登入,尚未使用正式证券户帐户登入 如果尚未有永丰金证券帐户的朋友,但...

ASP.NET MVC 从入门到放弃(Day13) -C# HttpClient 泛型功能介绍

接着来讲讲泛型的部分.... 简单来说泛型就是传入值、传回值不固定的情况下这时候就可以使用泛型......

课堂笔记 - 物联网概论(2)

感知层 将具有感测与辨识能力的元件嵌入连结上真实的物体里面,进而能够对环境进行监控与感知。 分别有...

Day28:继续歪楼(全英文笔记 - II)

继续昨天的歪楼笔记,昨天只有写 webpack-dev-server, 今天来加上一些基本的插件还有...

Free Ringtone For Mobile Phones

In the beginning, the only klingeltöne available w...