一、简介
文本主要回顾 Canvas API 基础,同时借助 ES6 class 与 RxJS 能力快速的完成 Canvas 基础。为什么选择 RxJS 因为 RxJS? 原因考虑到可以在订阅时行可以轻松的绘制和清除操作,重复进行可递归的进行操作,其他的内容都封装在 class (面向对象)或者 函数内部。学习 Canvas 和 RxJS 可以双向提高编程技巧。
二、创建 canvas 元素
- 使用脚本创建 cavans 元素
ts
class MyCanvas {
constructor(width, height) {
this.canvas = document.createElement('canvas');
this.canvas.width = width;
this.canvas.height = height;
document.body.appendChild(this.canvas);
}
}
const canvasInstance = new MyCanvas(400, 200);
- 使用 html 创建 canvas 元素
html
<canvas id="myCanvas" width="400" height="200"></canvas>
三、获取 canvas 元素
ts
var canvas = document.getElementById('myCanvas');
var canvas = document.querySelector('canvas'); // 获取第一个匹配的Canvas元素
四、获取 canvas 元素中的上下文
ts
var canvas = document.getElementById('myCanvas');
// 获取2D绘图上下文
var context = canvas.getContext('2d');
五、绘制基本形状
fillRect(x, y, width, height)
:填充
矩形。strokeRect(x, y, width, height)
:绘制
矩形边框。clearRect(x, y, width, height)
:清除
矩形区域。
html
<!DOCTYPE html>
<html>
<head>
<title>Canvas 示例</title>
</head>
<body>
<canvas id="myCanvas" width="400" height="200"></canvas>
<script src="your_script.js"></script>
</body>
</html>
ts
import { fromEvent } from 'rxjs';
import { filter, take } from 'rxjs/operators';
class MyCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
// 绘制填充
drawFilledRectangle() {
this.context.fillStyle = 'blue';
this.context.fillRect(50, 50, 200, 100);
}
// 绘制描边(边框)
drawStrokedRectangle() {
this.context.strokeStyle = 'red';
this.context.lineWidth = 3;
this.context.strokeRect(50, 50, 200, 100);
}
clearRectangle() {
this.context.clearRect(100, 100, 100, 50);
}
draw() {
fromEvent(document, 'DOMContentLoaded')
.pipe(
take(1),
filter(() => this.canvas) // 在 this.canvas 只存在一次的时候触发
)
.subscribe(() => {
this.drawFilledRectangle(); // 绘制填充
this.drawStrokedRectangle(); // 绘制描边
this.clearRectangle();
});
}
}
const MyCanvas = new MyCanvas('myCanvas');
MyCanvas.draw(); // 实例化开始绘制
六、绘制路径
beginPath()
:开始路径。moveTo(x, y)
:将路径移动到指定点。lineTo(x, y)
:绘制直线。arc(x, y, radius, startAngle, endAngle, anticlockwise)
:绘制弧线。closePath()
:关闭路径。fill()
:填充路径区域。stroke()
:绘制路径边框。
ts
import { fromEvent } from 'rxjs';
import { filter, take } from 'rxjs/operators';
class MyCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
drawPath() {
this.context.beginPath();
this.context.moveTo(100, 100);
this.context.lineTo(200, 100);
this.context.arc(150, 100, 50, 0, Math.PI, false);
this.context.closePath();
}
fillPath() {
this.context.fillStyle = 'blue';
this.context.fill();
}
strokePath() {
this.context.strokeStyle = 'red';
this.context.stroke();
}
draw() {
fromEvent(document, 'DOMContentLoaded')
.pipe(
take(1),
filter(() => this.canvas)
)
.subscribe(() => {
this.drawPath();
this.fillPath();
this.strokePath();
});
}
}
const MyCanvas = new MyCanvas('myCanvas');
MyCanvas.draw();
七、文本绘制
fillText(text, x, y)
:填充文本。strokeText(text, x, y)
:绘制文本边框。font
:设置文本字体和大小。textAlign
:文本水平对齐方式。textBaseline
:文本垂直对齐方式。
ts
import { fromEvent } from 'rxjs';
import { filter, take } from 'rxjs/operators';
class MyCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
drawText() {
this.context.font = '24px Arial'; // 设置字体和大小
this.context.textAlign = 'center'; // 设置水平对齐方式
this.context.textBaseline = 'middle'; // 设置垂直对齐方式
this.context.fillText('Hello Canvas and RxJS', this.canvas.width / 2, this.canvas.height / 2);
this.context.strokeText('Hello Canvas and RxJS', this.canvas.width / 2, this.canvas.height / 2);
}
initializeCanvas() {
fromEvent(document, 'DOMContentLoaded')
.pipe(
take(1),
filter(() => this.canvas)
)
.subscribe(() => {
this.drawText();
});
}
}
const MyCanvas = new MyCanvas('myCanvas');
MyCanvas.initializeCanvas();
八、样式和颜色
fillStyle
:设置填充样式。strokeStyle
:设置边框样式。lineWidth
:设置线条宽度。globalAlpha
:设置全局透明度。
ts
import { fromEvent } from 'rxjs';
import { filter, take } from 'rxjs/operators';
class MyCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
drawText() {
this.context.font = '24px Arial'; // 设置字体和大小
this.context.textAlign = 'center'; // 设置水平对齐方式
this.context.textBaseline = 'middle'; // 设置垂直对齐方式
this.context.fillStyle = 'blue'; // 设置填充颜色
this.context.fillText('Hello, Canvas and RxJS!', this.canvas.width / 2, this.canvas.height / 2);
this.context.strokeStyle = 'red'; // 设置边框颜色
this.context.strokeText('Hello, Canvas and RxJS!', this.canvas.width / 2, this.canvas.height / 1.2);
}
draw() {
fromEvent(document, 'DOMContentLoaded')
.pipe(
take(1),
filter(() => this.canvas)
)
.subscribe(() => {
this.drawText();
});
}
}
const MyCanvas = new MyCanvas('myCanvas');
MyCanvas.draw();
九、渐变和阴影
createLinearGradient(x0, y0, x1, y1)
:创建线性渐变对象。createRadialGradient(x0, y0, r0, x1, y1, r1)
:创建径向渐变对象。createPattern(image, repeat)
:创建图案。shadowColor
、shadowBlur
和shadowOffsetX/Y
:设置阴影效果。
ts
import { fromEvent } from 'rxjs';
import { filter, take, switchMap } from 'rxjs/operators';
class MyCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
createLinearGradient() {
return this.context.createLinearGradient(50, 50, 250, 150);
}
createRadialGradient() {
return this.context.createRadialGradient(300, 100, 10, 350, 100, 60);
}
createPattern() {
return new Promise((resolve) => {
const img = new Image();
img.src = 'your_images.png'; // tips: 替换为实际图案的路径
img.onload = () => {
resolve(this.context.createPattern(img, 'repeat'));
};
});
}
drawLinearGradient(linearGradient) {
linearGradient.addColorStop(0, 'red');
linearGradient.addColorStop(1, 'blue');
this.context.fillStyle = linearGradient;
this.context.fillRect(50, 50, 200, 100);
}
drawRadialGradient(radialGradient) {
radialGradient.addColorStop(0, 'green');
radialGradient.addColorStop(1, 'yellow');
this.context.fillStyle = radialGradient;
this.context.beginPath();
this.context.arc(300, 100, 50, 0, Math.PI * 2);
this.context.fill();
}
async drawPattern() {
const pattern = await this.createPattern();
this.context.fillStyle = pattern;
this.context.fillRect(50, 150, 300, 50);
}
drawShadow() {
this.context.shadowColor = 'rgba(0, 0, 0, 0.5)';
this.context.shadowBlur = 5;
this.context.shadowOffsetX = 5;
this.context.shadowOffsetY = 5;
this.context.fillStyle = 'purple';
this.context.fillRect(50, 50, 100, 100);
}
initializeCanvas() {
fromEvent(document, 'DOMContentLoaded')
.pipe(
take(1),
filter(() => this.canvas),
switchMap(() => {
this.drawLinearGradient(this.createLinearGradient());
this.drawRadialGradient(this.createRadialGradient());
return this.drawPattern();
})
)
.subscribe(() => {
this.drawShadow();
});
}
}
const MyCanvas = new MyCanvas('myCanvas');
MyCanvas.initializeCanvas();
十、图像操作
drawImage(image, x, y)
:绘制图像。getImageData(x, y, width, height)
:获取像素数据。putImageData(imageData, x, y)
:放置像素数据。
ts
import { fromEvent } from 'rxjs';
import { filter, take, switchMap } from 'rxjs/operators';
class MyCanvas {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
loadAndDrawImage(imagePath) {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = imagePath;
img.onload = () => {
this.context.drawImage(img, 50, 50);
resolve();
};
img.onerror = (error) => {
reject(error);
};
});
}
getImageData(x, y, width, height) {
return this.context.getImageData(x, y, width, height);
}
putImageData(imageData, x, y) {
this.context.putImageData(imageData, x, y);
}
async initializeCanvas() {
fromEvent(document, 'DOMContentLoaded')
.pipe(
take(1),
filter(() => this.canvas),
switchMap(async () => {
await this.loadAndDrawImage('example-image.jpg'); // 替换为实际图像的路径
const imageData = this.getImageData(50, 50, 200, 100);
this.putImageData(imageData, 200, 100);
})
)
.subscribe(() => {
console.log('Canvas操作完成');
});
}
}
const MyCanvas = new MyCanvas('myCanvas');
MyCanvas.initializeCanvas();
十一、转换操作
scale(x, y)
:缩放。rotate(angle)
:旋转。translate(x, y)
:平移。transform(a, b, c, d, e, f)
:自定义变换矩阵。
ts
class CanvasTransform {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
}
applyTransform(transformFn) {
this.context.save();
transformFn(this.context);
}
resetTransform() {
this.context.restore();
}
drawRectangle(color, x, y, width, height) {
this.context.fillStyle = color;
this.context.fillRect(x, y, width, height);
}
async initializeCanvas() {
this.drawRectangle('blue', 50, 50, 100, 50);
this.applyTransform((context) => {
context.scale(2, 1);
});
this.drawRectangle('red', 50, 50, 100, 50);
this.applyTransform((context) => {
context.rotate(Math.PI / 4);
});
this.drawRectangle('green', 50, 50, 100, 50);
this.applyTransform((context) => {
context.translate(100, 0);
});
this.drawRectangle('orange', 50, 50, 100, 50);
this.context.setTransform(1, 0.5, 0.5, 1, 50, 50);
this.drawRectangle('purple', 50, 50, 100, 50);
this.resetTransform();
}
}
const MyCanvas = new CanvasTransform('myCanvas');
MyCanvas.initializeCanvas();
十三、清除 canvas
使用 clearRect()
或 fillRect()
可以清除整个 <canvas>
或特定区域。
13.1) 整个 canvas
ts
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
// 清除整个 canvas
context.clearRect(0, 0, canvas.width, canvas.height);
13.2) 特定区域
ts
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
// 清除特定区域,例如清除一个矩形区域
context.clearRect(50, 50, 100, 50);
十四、事件处理
Canvas 允许添加事件监听器来处理用户交互,如点击、鼠标移动等。
14.1) 点击事件
ts
import { fromEvent } from 'rxjs';
import { map } from 'rxjs/operators';
const canvas = document.getElementById('myCanvas');
const canvasClick$ = fromEvent(canvas, 'click').pipe(
map((event) => {
const { left, top } = canvas.getBoundingClientRect();
const x = event.clientX - left;
const y = event.clientY - top;
return { x, y };
})
);
canvasClick$.subscribe(({ x, y }) => {
console.log(`Clicked at x: ${x}, y: ${y}`);
// 在这里执行您的操作,例如根据坐标执行Canvas绘制操作
});
14.2) 鼠标移动事件
ts
import { fromEvent } from 'rxjs';
import { map } from 'rxjs/operators';
const canvas = document.getElementById('myCanvas');
const canvasMouseMove$ = fromEvent(canvas, 'mousemove').pipe(
map((event) => {
const { left, top } = canvas.getBoundingClientRect();
const x = event.clientX - left;
const y = event.clientY - top;
return { x, y };
})
);
canvasMouseMove$.subscribe(({ x, y }) => {
console.log(`Mouse moved to x: ${x}, y: ${y}`);
// 在这里执行您的操作,例如根据坐标执行Canvas绘制操作
});
14.3) 键盘事件
ts
import { fromEvent } from 'rxjs';
const keyDown$ = fromEvent(document, 'keydown');
const keyUp$ = fromEvent(document, 'keyup');
keyDown$.subscribe((event) => {
console.log(`Key down: ${event.key}`);
});
keyUp$.subscribe((event) => {
console.log(`Key up: ${event.key}`);
});
14.4) 触摸事件
ts
import { fromEvent } from 'rxjs';
const touchStart$ = fromEvent(document, 'touchstart');
const touchMove$ = fromEvent(document, 'touchmove');
const touchEnd$ = fromEvent(document, 'touchend');
touchStart$.subscribe((event) => {
console.log('Touch start');
});
touchMove$.subscribe((event) => {
console.log('Touch move');
});
touchEnd$.subscribe((event) => {
console.log('Touch end');
});
十五、动画
通过定时器或 requestAnimationFrame
可以实现动画效果。
ts
import { interval } from 'rxjs';
import { map, takeWhile, startWith } from 'rxjs/operators';
class CanvasAnimation {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.context = this.canvas.getContext('2d');
this.x = 50;
this.y = 50;
this.speed = 2;
this.animationFrameId = null;
}
animate() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.fillStyle = 'blue';
this.context.fillRect(this.x, this.y, 50, 50);
this.x += this.speed;
if (this.x + 50 > this.canvas.width || this.x < 0) {
this.speed = -this.speed;
}
this.animationFrameId = requestAnimationFrame(() => this.animate());
}
start() {
const animation$ = interval(16).pipe( // 每 16 ms 触发一次
map(() => {
this.animate();
return this.x;
}),
startWith(this.x),
takeWhile(() => this.animationFrameId !== null)
);
this.subscription = animation$.subscribe();
}
stop() {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
}
}
const canvasAnimation = new CanvasAnimation('myCanvas');
canvasAnimation.start();
// 如果需要停止动画,可以调用 canvasAnimation.stop();
十六、像素处理
可以直接访问和修改 <canvas>
上的像素数据,用于图像处理、滤镜等。
ts
import { interval, fromEvent } from 'rxjs';
import { map, startWith, switchMap } from 'rxjs/operators';
class CanvasAnimation {
constructor() {
this.canvas = document.getElementById('myCanvas');
this.context = this.canvas.getContext('2d');
this.x = 50;
this.speed = 2;
}
start() {
const canvasMouseMove$ = fromEvent(this.canvas, 'mousemove').pipe(
map((event) => {
const { left, top } = this.canvas.getBoundingClientRect();
return event.clientX - left;
})
);
// 使用 RxJS 来创建一个动画
const animation$ = canvasMouseMove$.pipe(
switchMap((mouseX) => {
return interval(16).pipe(
map(() => {
// 清除画布
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制方块
this.context.fillStyle = 'blue';
this.context.fillRect(this.x, 50, 50, 50);
// 更新方块位置
if (this.x < mouseX) {
this.x += this.speed;
} else if (this.x > mouseX) {
this.x -= this.speed;
}
// 通过 requestAnimationFrame 请求下一帧动画
requestAnimationFrame(() => this.animate());
return this.x;
}),
startWith(this.x)
);
})
);
animation$.subscribe();
}
animate() {
// 空函数,用于 requestAnimationFrame 的回调
}
}
const canvasAnimation = new CanvasAnimation();
canvasAnimation.start();
十七、Canvas 库类推荐
库/框架名称 | 主要特点和用途 |
---|---|
Fabric.js | Javascript Canvas 库、SVG 到 Canvas(和 canvas 到 SVG)解析器。 |
Konva.js | Konva.js 是一个 HTML5 Canvas JavaScript 框架,它通过为桌面和移动应用程序启用画布交互性来扩展 2d 上下文。 |
p5.js | p5.js 是一个客户端 JS 平台,使艺术家、设计师、学生和任何人都能学习编码并在网络上创造性地表达自己。 |
Three.js | 用于创建3D的库 |
EaselJS | 用于创建交互性和动画的库,提供易于理解的显示列表模型。 |
Phaser | 流行的HTML5游戏引擎,包括Canvas支持和游戏开发功能。 |
十八、nodejs canvas
- node-canvas Node canvas 是 Cairo 支持的 NodeJS Canvas 实现。
十九、知名项目
- canvas-confetti 🎉 浏览器中的高性能五彩纸屑动画
- excalidraw 用于绘制手绘图的虚拟白板。
- Chart.js 使用
<canvas>
标签的简单 HTML5 图表。 - echarts Apache ECharts 是一个强大的、交互式的浏览器图表和数据可视化库。
- d3 使用 SVG、 Canvas*和 HTML让数据栩栩如生。
二十、小结
本文主要讲解的 Web API Canvas API, 为了避免枯燥的学习,配合 RxJS 与 ES Class 来组合成 API 适用示例。学习本文除了有 Canvas 基础外还需要有 RxJS 基础和 ES class 面向对象基础。