Canvas实现球体碰撞交互效果
一、Canvas简介
提到Canvas相信做前端开发的同学都不陌生,它是一个用于绘制图形的容器,我们会在一些特殊场景时需要用到Canvas,比如我们要在页面上显示一个流程图,这个流程图需要根据后端返回的数据动态显示时,就可以使用Canvas进行绘制,它可以实时的根据数据进行计算,再比如,我们要做一个动画效果,特别是需要有交互的动画效果时,我们就可以使用Canvas去实现。
二、文章简介
相信大部分同学使用Canvas也只是绘制静态界面,比如上面提到的流程图,如果要让你用Canvas做一个动画,或者是接下来我们将要实现的球体碰撞效果,你是否就会有点无从下手的感觉,这篇文章我将带你从零到一手撸一个球体碰撞的交互效果,不使用任何第三方库,详细为你讲解每一步的实现逻辑,真正做到保姆级教学,这样才能保证你能从中学到原理上的东西,以后做Canvas动画时就可以做到信手拈来。在文章的最后我也会将完整的源代码贴出来供大家参考。
下面是最终实现的效果
三、搭建开发界面
实现Canvas动画的核心原理就是使用window. requestAnimationFrame,一帧一帧不停地进行整个页面的绘制,就像是播放幻灯片一样,当播放的帧率达到一定时,那么人的肉眼就看不出中间的切换卡顿,而看到的是流畅的动画效果。后面我们写的逻辑代码都是在loop这个函数中循环执行的。
index.html内容
Canvas实现球体碰撞交互效果
index.js内容
const canvasWidth = window.innerWidth; // 画布宽度
const canvasHeight = window.innerHeight; // 画布高度
const canvasBgColor = "#222222"; // 画布背景颜色
const canvas = document.getElementById("myCanvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext("2d");
function loop() {
// 绘制整个画布的背景色
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = canvasBgColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
requestAnimationFrame(loop);
}
// 循环绘制(loop函数中继续调用requestAnimationFrame函数)
requestAnimationFrame(loop);
四、绘制球体
实现Canvas动画,我们要学会使用面向对象的编程思想,这里要写一个球体的类,每个球都是new出来的一个个实例对象,这些实例对象上面有很多的属性和方法,这样在操作每个球时只需要调用它们各自的方法就可以了,所以下面我们写一个球体的类出来,通过new实例化几个小球,并将它们绘制在画布上。
-
在开发时我们将各种属性值或配置项都写成变量,这样如果我们以后想要调整或修改值就非常方便,不用去修改逻辑代码。
-
这里为了让小球看起来更立体,咱们给小球添加径向渐变的背景颜色
-
我们习惯性是将垂直向下作为y轴的正方向,所以在开发时一定要注意y值向下为正值。
-
在绘制小球上的文字时由于使用的Georgia字体,计算出的文字与小球的中心点有偏差,所以我们将它的位置修正一下即可。
const canvasWidth = window.innerWidth; // 画布宽度
const canvasHeight = window.innerHeight; // 画布高度
const canvasBgColor = "#222222"; // 画布背景颜色
+ const globuleNum = 8; // 球的总数量
+ const oneRowGlobuleNum = 4; // 每行显示球的数量
+ const globuleHorizontalMargin = 34; // 两球之间水平方向的距离
+ const globuleVerticalMargin = 50; // 两球之间垂直方向的距离
+ const globuleRadius = 80; // 球半径
+ const globuleColor = '#005CC9'; // 球的颜色
+ const fontSize = 40; // 文字大小
+ const textColor = "#E76F5A"; // 文字颜色
const canvas = document.getElementById("myCanvas");
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext("2d");
+ const globuleList = []; // 保存所有小球实例
+ class Globule {
+ constructor(x, y, radius, color = "blue", text = "", fontSize = 30, textColor = "red") {
+ this.initX = x; // 保存小球的初始x位置
+ this.initY = y; // 保存小球的初始y位置
+ this.x = x; // x位置
+ this.y = y; // y位置
+ this.radius = radius; // 小球半径
+ this.color = color; // 小球颜色
+ this.vx = 0; // 小球在水平方向的速度
+ this.vy = 0; // 小球在垂直方向的速度
+ this.text = text; // 小球上显示的文字
+ this.fontSize = fontSize; // 文字大小
+ this.textColor = textColor; // 文字颜色
+ }
+
+ // 绘制
+ draw() {
+ ctx.beginPath();
+ const grd = ctx.createRadialGradient(this.x, this.y, 1, this.x, this.y, this.radius - 2);
+ grd.addColorStop(0, "#ffffff");
+ grd.addColorStop(1, this.color);
+ ctx.fillStyle = grd;
+ ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
+ ctx.fill();
+ if(this.text) {
+ ctx.font = `bold ${fontSize}px Georgia`;
+ ctx.fillStyle = this.textColor;
+ ctx.fillText(this.text, this.x - this.fontSize / 2 + 8, this.y + this.fontSize / 2 - 10);
+ }
+ }
+ // 移动
+ move() {
+ this.draw();
+ }
+ }
function loop() {
// 绘制整个画布的背景色
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = canvasBgColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+ globuleList.forEach(function(globule) {
+ globule.move();
+ });
requestAnimationFrame(loop);
}
+ // 需要显示小球的行数
+ const rowNum = Math.ceil(globuleNum / oneRowGlobuleNum);
+ // 计算出第一个小球所在的x位置
+ const firstGlobuleX = (canvasWidth - oneRowGlobuleNum * 2 * globuleRadius - (oneRowGlobuleNum - 1) * globuleHorizontalMargin) / 2 + globuleRadius;
+ // 计算出第一个小球所在的y位置
+ const firstGlobuleY = (canvasHeight - rowNum * 2 * globuleRadius - (rowNum - 1) * globuleVerticalMargin) / 2 + globuleRadius;
+ // 实例出所有小球,并保存到数组中
+ let xNow = firstGlobuleX;
+ let yNow = firstGlobuleY;
+ for(let i = 0; i < globuleNum; i++) {
+ const globule = new Globule(xNow, yNow, globuleRadius, globuleColor, (i + 1).toString(), fontSize);
+ globuleList.push(globule);
+ if((i + 1) % oneRowGlobuleNum === 0) {
+ xNow = firstGlobuleX;
+ yNow += 2 * globuleRadius + globuleVerticalMargin;
+ } else {
+ xNow += 2 * globuleRadius + globuleHorizontalMargin;
+ }
+ }
// 循环绘制(loop函数中继续调用requestAnimationFrame函数)
requestAnimationFrame(loop);
五、数学基础
由于后面的内容大量地使用到了数学里的三角函数、勾股定理和相似三角形的相关知识,所以这里专门用一个小节来简单地一起回顾一下我们上学时学的这些知识。因为两球之间连线的方向或小球受到的作用力不一定都是水平或者是垂直的,后面计算作用力或者小球速度时都要将其拆分成水平方向和垂直方向两个值,到时理解起来就不会很吃力。
勾股定理:直角三角形的两条直角边的平方和等于斜边的平方
(如下图所示,即a ² + b ² = c ²)
三角函数公式
相似三角形的判定定理之一:平行于三角形一边的直线和其他两边或两边的延长线相交,所构成的三角形与原三角形相似
相似三角的的性质定理之一:各个对应边的比例相等
如果DE // BC,则三角形ADE与三角形ABC相似,所以得出AD/AB= AE/AC = DE/BC。
共有 0 条评论