一、 Canvas基本介绍
Canvas元素是在HTML5中新增的标签用于在网页实时生成图像,并且可以操作图像内容,基本上它是一个可以用JavaScript操作的位图(bitmap)【像素图】。Canvas 由一个可绘制区域HTML代码中的属性定义决定高度和宽度。JavaScript代码可以访问该区域,通过一套完整的绘图功能的API生成动态的图形。
为什么要去学习Canvas??? 2014年发布HTML5,至今10年已久,还有必要学习嘛???
1 动态图形和动画: <canvas> 允许使用 JavaScript 动态地绘制图形和创建动画。这对于网页上的交互性和吸引力非常有用,特别是在创建数据可视化和游戏方面。
实时更新: <canvas> 提供了一种在用户与网页交互时实时更新图形的方式。这对于需要实时反馈的应用程序和游戏来说是非常重要的。
2 图形处理: 学习 <canvas> 有助于理解图形处理的基本概念,例如像素操作、绘图算法等。这对于计算机图形学和图像处理的进一步学习是很有帮助的。
3 交互性: 通过 <canvas>,你可以实现用户与图形进行交互的功能,例如拖动、缩放等。这对于创建用户友好的界面和可视化效果至关重要。
4 跨平台兼容性: HTML5 标准已经得到广泛支持,并且 <canvas> 在现代浏览器中得到了很好的兼容。同时在wx小程序中也可以采用<canvas>进行绘图。
虽然 HTML5 已经发布了一段时间,但 <canvas> 仍然是创建富交互式网页和应用程序的有力工具。
二、canvas,svg区别与优势
1.绘图需求
Canvas是基于像素的绘图技术,能够以很高的速度绘制大量的元素。它提供了各种API,如绘图线条、路径、填充颜色等,可以用于绘制复杂的动态图形效果,如图表、游戏等。Canvas使用JavaScript编写代码,比较灵活,可以在绘图中使用复杂的算法和逻辑。
相比之下,SVG是基于矢量的绘图技术,它使用XML来描绘图形,可以在不失真的情况下进行无限的缩放。SVG也提供了各种API,如路径、文本、形状、渐变等,可以用于绘制静态图形和动态图形效果。与Canvas相比,SVG更适用于用于静态图像,如标志、图标、矢量图等。
2.浏览器兼容性
在浏览器兼容性方面,SVG相对来说具有更好的支持度。因为SVG是一种标准的XML文件格式,大多数现代浏览器都可以很好地支持。相比之下,Canvas只是一个HTML5的新特性,可能会受到一定程度的局限。
3.可维护性
SVG对于可维护性有一定的优势,因为它使用的是XML格式,易于处理和修改。另一方面,Canvas使用JavaScript进行开发,需要不同的开发人员具备较高的技能水平,因此可维护性相对较弱。
总结
综合上面的介绍,我们可以得出结论:
如果需要对复杂图形进行动态渲染,可以使用Canvas;
如果需要创建静态图像,在保留清晰度和可伸缩性方面,可以使用SVG。
三、实际使用
例子1 奔跑的小恐龙
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<canvas
id="canvas"
height="600"
width="700"></canvas>
</body>
</html>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 存储图片的是src:
const imgSrcs = [
'http://panpan.dapanna.cn//image-20221015115049427.png',
'http://panpan.dapanna.cn//image-20221015115033342.png',
'http://panpan.dapanna.cn//image-20221015115015133.png',
'http://panpan.dapanna.cn//image-20221015114950581.png',
'http://panpan.dapanna.cn//image-20221015114245445.png',
'http://panpan.dapanna.cn//image-20221015114437817.png',
'http://panpan.dapanna.cn//image-20221015114526684.png',
'http://panpan.dapanna.cn//image-20221015114610049.png',
'http://panpan.dapanna.cn//image-20221015114653366.png',
'http://panpan.dapanna.cn//image-20221015114722067.png',
'http://panpan.dapanna.cn//image-20221015114802665.png',
'http://panpan.dapanna.cn//image-20221015114927924.png'
];
const img = new Image();
var i = 0;
// 间隔70ms绘制一次图片,:
mySetInterVal(() => {
img.src = imgSrcs[i];
img.onload = () => {
ctx.drawImage(img, 60, 120); // 绘制图片
};
i++;
if (i === 12) {
i = 0;
}
}, 70);
function mySetInterVal(func, detay) {
let i = 0;
myReq = requestAnimationFrame(function fn() {
// 判断现在处于60帧的第几帧,如果是目标帧的话,调用func函数:
if (i % parseInt(60 / (1000 / detay)) == 0) {
func();
}
i++;
// 让i值每秒增加60,循环调用func函数:
requestAnimationFrame(fn);
});
}
</script>
写到这里,突然想到上在wx小程序首页采用lotttie原理也是采用动画效果,返回回去又去研究一遍发现底层实际也是canvas
const loadLotties = () => {
uni
.createSelectorQuery()
.in(vm)
.select('#top-banner-canvas')
.node((res: any) => {
const canvas = res.node;
const context = canvas.getContext('2d');
canvas.width = 750 * devicePixelRatio.value;
canvas.height = 536 * devicePixelRatio.value;
//主要加载方法
lottie.setup(canvas);
// 动画方法
defaultBanner.value = lottie.loadAnimation({
loop: false,
autoplay: false,
// path: basepicpath + '/v2/lottieJson/homeLottieBanner.json',
animationData: getApp().globalData?.defaultBanner,
rendererSettings: {
context
}
});
defaultBanner.value.play();
})
.exec();
};
在之前wx小程序开发lotties动画中,实际上它采用的是canvas画布实现原理。动画可以拆分成每一帧,当前帧(静态)图像的属性数据或者形态(形状)的变更,把这样很多帧连贯起来,就形成动画。
大概分析lotties动画形成原理:
interface LoadAnimationParameter {
renderer?: 'canvas';
loop?: boolean | number;
autoplay?: boolean;
name?: string;
rendererSettings?: CanvasRendererConfig;
animationData?: any;
path?: string;
}
// 加载动画采用方法
interface LoadAnimationReturnType {
play(): void;
stop(): void;
pause(): void;
setSpeed(speed: number): void;//设置播放速度
goToAndPlay(value: number, isFrame?: boolean): void;//跳转某个帧并播放
goToAndStop(value: number, isFrame?: boolean): void;//跳转某个帧并停止
setDirection(direction: AnimationDirection): void;
playSegments(segments: AnimationSegment | AnimationSegment[], forceFlag?: boolean): void;
setSubframe(useSubFrames: boolean): void;
destroy(): void;
getDuration(inFrames?: boolean): number;
//triggerEvent 方法:
//该方法接收两个参数:name(必需)表示事件名称,args(可选)表示事件参数。
//该方法返回 void 类型,表示没有返回值。
//实现原理:触发一个名为 name 的动画事件,并将 args 作为参数传递给事件处理函数。
//用途:在动画系统中,通常会通过这个方法来触发动画事件,从而控制动画的播放。
//注意事项:name 参数必须是 AnimationEventName 类型,args 参数必须是 T 类
//T 默认值为 any,表示可以传递任何类型的参数。
triggerEvent<T = any>(name: AnimationEventName, args: T): void;
// addEventListener 方法:
//该方法接收两个参数:name(必需)表示事件名称,callback(必需)表示事件处理函数。
//该方法返回 void 类型,表示没有返回值。
//实现原理:监听一个名为 name 的动画事件,当事件触发时,会调用 callback 函数。
//用途:在动画系统中,通常会通过这个方法来监听动画事件,以便在事件触发时执行相应的操作。
//注意事项:name 参数必须是 AnimationEventName 类型,callback 参数必须是 AnimationEventCallback<T> 类型,T 默认值为 any,表示可以监听任何类型的参数。
addEventListener<T = any>(name: AnimationEventName, callback: AnimationEventCallback<T>): void;
//removeEventListener 方法:
//该方法接收两个参数:name(必需)表示事件名称,callback(必需)表示事件处理函数。
//该方法返回 void 类型,表示没有返回值。
//实现原理:移除一个名为 name 的动画事件中的 callback 监听器。
//用途:在动画系统中,当不需要再监听某个事件时,可以通过这个方法来移除监听器,以避免不必要的内存占用。
//注意事项:name 参数必须是 AnimationEventName 类型,callback 参数必须//是 AnimationEventCallback<T> 类型,T 默认值为 any,表示可以移除任何类型的监听器。
removeEventListener<T = any>(name: AnimationEventName, callback: AnimationEventCallback<T>): void;
}
declare module lottie {
var loadAnimation: (options: LoadAnimationParameter) => LoadAnimationReturnType;
var setup: (node: any) => void;
}
接下来看看setup方法,这里这是截取部分代码 没有截取全部
export const setup = (canvas) => {
const {window, document} = g
g._requestAnimationFrame = window.requestAnimationFrame
g._cancelAnimationFrame = window.cancelAnimationFrame
// lottie 对象是单例,内部状态(_stopped)在多页面下会混乱,保持requestAnimationFrame 持续运行可规避
window.requestAnimationFrame = function requestAnimationFrame(cb) {
let called = false
setTimeout(() => {
if (called) return
called = true
typeof cb === 'function' && cb(Date.now())
}, 100)
canvas.requestAnimationFrame((timeStamp) => {
if (called) return
called = true
typeof cb === 'function' && cb(timeStamp)
})
}
window.cancelAnimationFrame = canvas.cancelAnimationFrame.bind(canvas)
g._body = document.body
g._createElement = document.createElement
document.body = {}
document.createElement = createElement.bind(canvas)
const ctx = canvas.getContext('2d')
if (!ctx.canvas) {
ctx.canvas = canvas
}
这段代码的目的是在全局范围内设置一个特定画布(canvas)的动画引擎,以便在多页面环境下避免由于多个页面同时请求动画帧导致的不确定性。实现原理是通过修改 window 对象和 document 对象来实现。
用途:
导出一个名为 setup 的函数,该函数接受一个画布(canvas)作为参数。
从 window 对象中获取 requestAnimationFrame 和 cancelAnimationFrame,并将它们存储在 g 对象中。
使用 setTimeout 和画布的 requestAnimationFrame 方法来模拟 window 的 requestAnimationFrame 行为。
将 document 的 body 和 createElement 方法替换为空对象和自定义的 createElement 方法,以防止在其他页面中创建元素。
使用 wrapMethodFatory 函数来包装画布的 setLineDash 和 fill 方法,以便在调用这些方法时执行额外的操作。
自己想法: 在代码中使用了 setTimeout 来模拟 window 的 requestAnimationFrame 行为,这是一种不太稳定的解决方案,可能会在某些情况下导致问题。例如 定时器仍然有可能因为当前页面(或者操作系统/浏览器本身)被其他任务占用导致延时。 需要被强调是, 直到调用 setTimeout()的主线程执行完其他任务之后才会执行,可能会出现不精确
接下来 简单了解json文件格式 进一步了解实现动画方式
w 和 h: 宽 1125、高 807
v:版本号
fr:帧率 25fps
ip 和 op:开始帧 0、结束帧 100
assets:静态资源信息(如图片)
layers:图层信息(动画中的每一个图层以及动作信息)
ddd:是否为 3d
重要信息存储在layers层
这是一个图层的信息集 最重要的是在ks中
o:opacity 透明度 r:rotation 旋转 p position 位置 a: anchor 锚点 s:scale 缩放
渲染流程如下图
按照自己理解,其实也就是按照加载每一帧的显示不同的图层信息集该在那个位置
返回到canvas这里 以下是这里模仿写两个demo
贪吃蛇
<script>
draw();
function draw() {
const canvas = document.getElementById('cvs');
const ctx = canvas.getContext('2d');
// 全局 判断是否吃到食物
let isEatFood = false;
function Rect(x, y, w, h, color) {
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.color = color;
}
Rect.prototype.draw = function () {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.strokeRect(this.x, this.y, this.w, this.h);
};
function Snake(length = 0) {
this.length = length;
// 头
this.head = new Rect(canvas.width / 2, canvas.height / 2, 40, 40, 'red');
// 身体
this.body = [];
let x = this.head.x - 40;
let y = this.head.y;
for (let i = 0; i < this.length; i++) {
const rect = new Rect(x, y, 40, 40, 'yellow');
this.body.push(rect);
x -= 40;
}
}
Snake.prototype.drawSnake = function () {
if (isHit(this)) {
clearInterval(timer);
const con = confirm('游戏结束,重新开始?');
if (con) {
draw();
}
return;
}
this.head.draw();
// 绘制蛇身
for (let i = 0; i < this.body.length; i++) {
this.body[i].draw();
}
};
Snake.prototype.moveSnake = function () {
const rect = new Rect(this.head.x, this.head.y, this.head.w, this.head.h, 'yellow');
this.body.unshift(rect);
// 判断蛇头是否与食物重叠,重叠就是吃到了,没重叠就是没吃到
isEatFood = food && this.head.x === food.x && this.head.y === food.y;
if (!isEatFood) {
this.body.pop();
} else {
food = randomFood(this);
food.draw();
isEatFood = false;
}
switch (this.direction) {
case 0:
this.head.x -= this.head.w;
break;
case 1:
this.head.y -= this.head.h;
break;
case 2:
this.head.x += this.head.w;
break;
case 3:
this.head.y += this.head.h;
break;
}
};
document.onkeydown = function (e) {
// 键盘事件
e = e || window.event;
// 左37 上38 右39 下40
switch (e.keyCode) {
case 37:
// 三元表达式,防止右移动时按左,下面同理(贪吃蛇可不能直接掉头)
snake.direction = snake.direction === 2 ? 2 : 0;
snake.moveSnake();
break;
case 38:
snake.direction = snake.direction === 3 ? 3 : 1;
break;
case 39:
snake.direction = snake.direction === 0 ? 0 : 2;
break;
case 40:
snake.direction = snake.direction === 1 ? 1 : 3;
break;
}
};
function randomFood(snake) {
let isInSnake = true;
let rect;
while (isInSnake) {
const x = Math.floor(Math.random() * ((canvas.width - 40) / 40)) * 40;
const y = Math.floor(Math.random() * ((canvas.height - 40) / 40)) * 40;
console.log(x, y);
rect = new Rect(x, y, 40, 40, 'blue');
if ((snake.head.x === x && snake.head.y === y) || snake.body.find((item) => item.x === x && item.y === y)) {
isInSnake = true;
continue;
} else {
isInSnake = false;
}
}
return rect;
}
function isHit(snake) {
const head = snake.head;
// 是否碰到左右边界
const xLimit = head.x < 0 || head.x >= canvas.width;
// 是否碰到上下边界
const yLimit = head.y < 0 || head.y >= canvas.height;
// 是否撞到蛇身
const hitSelf = snake.body.find(({ x, y }) => head.x === x && head.y === y);
// 三者其中一个为true则游戏结束
return xLimit || yLimit || hitSelf;
}
const snake = new Snake(3);
// 默认direction为2,也就是右
snake.direction = 2;
snake.drawSnake();
// 创建随机食物实例
let food = randomFood(snake);
// 画出食物
food.draw();
function animate() {
// 先清空
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 移动
snake.moveSnake();
// 再画
snake.drawSnake();
food.draw();
}
let timer = setInterval(() => {
animate();
}, 500);
}
</script>
<style>
#cvs {
border: 1px solid;
}
</style>
五子棋
play();
function play() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 绘制棋盘
// 水平,总共15条线
for (let i = 0; i < 15; i++) {
ctx.beginPath();
ctx.moveTo(20, 20 + i * 40);
ctx.lineTo(580, 20 + i * 40);
ctx.stroke();
ctx.closePath();
}
// 垂直,总共15条线
for (let i = 0; i < 15; i++) {
ctx.beginPath();
ctx.moveTo(20 + i * 40, 20);
ctx.lineTo(20 + i * 40, 580);
ctx.stroke();
ctx.closePath();
}
let isBlack = true;
let cheeks = [];
for (let i = 0; i < 15; i++) {
cheeks[i] = new Array(15).fill(0);
}
canvas.onclick = function (e) {
const clientX = e.clientX;
const clientY = e.clientY;
// 对40进行取整,确保棋子落在交叉处
const x = Math.round((clientX - 20) / 40) * 40 + 20;
const y = Math.round((clientY - 20) / 40) * 40 + 20;
// cheeks二维数组的索引
// 这么写有点冗余,这么写你们好理解一点
const cheeksX = (x - 20) / 40;
const cheeksY = (y - 20) / 40;
// 对应元素不为0说明此地方已有棋,返回
if (cheeks[cheeksY][cheeksX]) return;
// 黑棋为1,白棋为2
cheeks[cheeksY][cheeksX] = isBlack ? 1 : 2;
ctx.beginPath();
// 画圆
ctx.arc(x, y, 20, 0, 2 * Math.PI);
// 判断走黑还是白
ctx.fillStyle = isBlack ? 'black' : 'white';
ctx.fill();
ctx.closePath();
// canvas画图是异步的,保证画出来再去检测输赢
setTimeout(() => {
//vue实现 监听帧
if (isWin(cheeksX, cheeksY)) {
const con = confirm(`${isBlack ? '黑棋' : '白棋'}赢了!是否重新开局?`);
// 重新开局
ctx.clearRect(0, 0, 600, 600);
con && play();
}
// 切换黑白
isBlack = !isBlack;
}, 0);
};
function isWin(x, y) {
const flag = isBlack ? 1 : 2;
// 上和下
if (up_down(x, y, flag)) {
return true;
}
// 左和右
if (left_right(x, y, flag)) {
return true;
}
// 左上和右下
if (lu_rd(x, y, flag)) {
return true;
}
// 右上和左下
if (ru_ld(x, y, flag)) {
return true;
}
return false;
}
function up_down(x, y, flag) {
let num = 1;
// 向上找
for (let i = 1; i < 5; i++) {
let tempY = y - i;
console.log(x, tempY);
if (tempY < 0 || cheeks[tempY][x] !== flag) break;
if (cheeks[tempY][x] === flag) num += 1;
}
// 向下找
for (let i = 1; i < 5; i++) {
let tempY = y + i;
console.log(x, tempY);
if (tempY > 14 || cheeks[tempY][x] !== flag) break;
if (cheeks[tempY][x] === flag) num += 1;
}
return num >= 5;
}
function left_right(x, y, flag) {
let num = 1;
// 向左找
for (let i = 1; i < 5; i++) {
let tempX = x - i;
if (tempX < 0 || cheeks[y][tempX] !== flag) break;
if (cheeks[y][tempX] === flag) num += 1;
}
// 向右找
for (let i = 1; i < 5; i++) {
let tempX = x + i;
if (tempX > 14 || cheeks[y][tempX] !== flag) break;
if (cheeks[y][tempX] === flag) num += 1;
}
return num >= 5;
}
function lu_rd(x, y, flag) {
let num = 1;
// 向左上找
for (let i = 1; i < 5; i++) {
let tempX = x - i;
let tempY = y - i;
if (tempX < 0 || tempY < 0 || cheeks[tempY][tempX] !== flag) break;
if (cheeks[tempY][tempX] === flag) num += 1;
}
// 向右下找
for (let i = 1; i < 5; i++) {
let tempX = x + i;
let tempY = y + i;
if (tempX > 14 || tempY > 14 || cheeks[tempY][tempX] !== flag) break;
if (cheeks[tempY][tempX] === flag) num += 1;
}
return num >= 5;
}
function ru_ld(x, y, flag) {
let num = 1;
// 向右上找
for (let i = 1; i < 5; i++) {
let tempX = x - i;
let tempY = y + i;
if (tempX < 0 || tempY > 14 || cheeks[tempY][tempX] !== flag) break;
if (cheeks[tempY][tempX] === flag) num += 1;
}
// 向左下找
for (let i = 1; i < 5; i++) {
let tempX = x + i;
let tempY = y - i;
if (tempX > 14 || tempY < 0 || cheeks[tempY][tempX] !== flag) break;
if (cheeks[tempY][tempX] === flag) num += 1;
}
return num >= 5;
}
}
</script>