网格
Canvas 的默认尺寸为 300 像素 × 150 像素 (宽 × 高 ,像素的单位是 px)
arduino
<canvas width="200" height="420"></canvas>
- Canvas 元素的尺寸由其 HTML 属性
width
和height
定义,而不是 CSS !!! - 绘制时图像会伸缩以适应它的框架尺寸(也就是 HTML 属性 width、height)
- 如果 CSS 的尺寸与初始画布的比例不一致,它会出现扭曲变形
- 如果你绘制出来的图像是扭曲的,尝试用 width 和 height 属性为明确规定宽高 ,而不是使用 CSS
- HTML 属性定义了画布的像素网格大小,避免拉伸
- CSS 的
width
和height
仅用于视觉调整
canvas 元素默认被网格所覆盖。通常来说网格中的一个单元相当于 canvas 元素中的一像素。
canvas 2D 栅格的起点为左上角坐标为 (0, 0),所有元素的位置都相对于原点定位
图形
不同于 SVG, 只支持两种形式的 图形绘制 :*矩形、路径, *所有其他图形都是基于这种图形绘制而来
矩形 Rect
上面提供的方法之中每一个都包含了相同的参数:
- x 与 y 指定了在 canvas 画布上所绘制的矩形的左上角(相对于原点)的坐标
- width 和 height 设置矩形的尺寸
xml
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
ctx.fillRect(25, 25, 100, 100); // 绘制了一个边长为 100px 的黑色正方形
ctx.clearRect(45, 45, 60, 60); // 从正方形的中心开始擦除了一个 60*60px 的正方形
ctx.strokeRect(50, 50, 50, 50); // 在清除区域内生成一个 50*50 的正方形边框
}
draw()
</script>
路径 Path
图形 的基本元素是 路径。
路径 是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。
使用 路径 Path 绘制图形 需要一些额外的步骤:
- 首先,你需要创建路径起始点。
- 然后你使用 画图命令 ****去画出路径。
- 之后你把路径封闭。
- 一旦路径生成,你就能通过描边或填充路径区域来渲染图形。
xml
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
ctx.beginPath(); // 开始绘制路径
ctx.moveTo(75, 50); // 【画图命令】指定起始位置点
ctx.lineTo(100, 75); // 【画图命令】连线
ctx.lineTo(100, 25); // 【画图命令】连线
ctx.fill(); // 填充
}
draw()
</script>
xml
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
ctx.beginPath(); // 开始绘制路径
ctx.arc(75, 75, 50, 0, Math.PI * 2, true); // 绘制
ctx.moveTo(110, 75);
ctx.arc(75, 75, 35, 0, Math.PI, false); // 口 (顺时针)
ctx.moveTo(65, 65);
ctx.arc(60, 65, 5, 0, Math.PI * 2, true); // 左眼
ctx.moveTo(95, 65);
ctx.arc(90, 65, 5, 0, Math.PI * 2, true); // 右眼
ctx.stroke();
// 如果你想看到连续的线,你可以移除调用的 moveTo()
}
draw()
</script>
线 lineTo
lineTo(x, y) :绘制一条从当前位置到指定 x 以及 y 位置的直线。
该方法有两个参数:x 以及 y,代表坐标系中直线结束的点。
开始点和之前的绘制路径有关,之前路径的结束点就是接下来的开始点,以此类推。
开始点也可以通过 moveTo()
函数改变。
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// 填充三角形
ctx.beginPath();
ctx.moveTo(25, 25);
ctx.lineTo(105, 25);
ctx.lineTo(25, 105);
ctx.fill();
// 描边三角形
ctx.beginPath();
ctx.moveTo(125, 125);
ctx.lineTo(125, 45);
ctx.lineTo(45, 125);
ctx.closePath();
ctx.stroke();
}
draw()
</script>
圆弧 arc
arc(x, y, radius, startAngle, endAngle, anticlockwise) 绘制 圆弧 或 圆
画一个以 (x, y) 为圆心,以 radius 为半径的圆弧(圆)
从 startAngle 开始到 endAngle 结束,按照 anticlockwise 给定的方向(默认为顺时针)来生成
注:arc 函数中表示角的单位是 弧度,不是角度。
注:角度 与 弧度 的计算公式: 弧度 = (Math.PI/180) * 角度
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas")
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 3; j++) {
ctx.beginPath();
const x = 25 + j * 50; // x 坐标值
const y = 25 + i * 50; // y 坐标值
const radius = 20; // 圆弧半径
const startAngle = 0; // 开始点
const endAngle = Math.PI + (Math.PI * j) / 2; // 结束点
const anticlockwise = i % 2 == 0 ? false : true; // 顺时针或逆时针
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);
if (i > 1) {
ctx.fill();
} else {
ctx.stroke();
}
}
}
}
draw()
</script>
贝塞尔曲线
本节涉及的路径类型就是 贝塞尔曲线。
二次及三次贝塞尔曲线都十分有用,一般用来绘制复杂有规律的图形。
二次贝塞尔
quadraticCurveTo(cp1x, cp1y, x, y) 绘制二次贝塞尔曲线
cp1x, cp1y
为 1 个控制点,x, y
为结束点。
二次贝塞尔曲线 有 **1 个开始点(蓝色) **、**1 个结束点(蓝色) **以及 1 个控制点(红色)
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// 二次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(75, 25);
ctx.quadraticCurveTo(25, 25, 25, 62.5);
ctx.quadraticCurveTo(25, 100, 50, 100);
ctx.quadraticCurveTo(50, 120, 30, 125);
ctx.quadraticCurveTo(60, 120, 65, 100);
ctx.quadraticCurveTo(125, 100, 125, 62.5);
ctx.quadraticCurveTo(125, 25, 75, 25);
ctx.stroke();
}
draw()
</script>
三次贝塞尔
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) 绘制三次贝塞尔曲线
cp1x, cp1y
为控制点一,cp2x, cp2y
为控制点二,x, y
为结束点。
三次贝塞尔曲线 有 **1 个开始点(蓝色) **、**1 个结束点(蓝色) **以及 2 个控制点(红色)
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// 三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(75, 40);
ctx.bezierCurveTo(75, 37, 70, 25, 50, 25);
ctx.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
ctx.bezierCurveTo(20, 80, 40, 102, 75, 120);
ctx.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
ctx.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
ctx.bezierCurveTo(85, 25, 75, 37, 75, 40);
ctx.fill();
}
draw()
</script>
矩形
rect(x, y, width, height) 绘制矩形
绘制一个左上角坐标为 **(x, y) **,宽高为 width 以及 height 的矩形
xml
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// 使用 rect 方法绘制矩形路径
ctx.rect(0.5, 0.5, 200, 100);
// 为啥是 .5 呢?
// 参考:https://njtf.yuque.com/tqgxlr/oussfl/eztfg3dmluzelqqq
// 填充矩形
ctx.strokeStyle = 'red';
ctx.stroke();
}
draw()
</script>
文本
canvas 提供了 2 种方法来渲染文本:fillText 和 strokeText
fillText
fillText(text, x, y [, maxWidth])
在指定的 (x, y) 位置填充指定的文本,绘制的最大宽度是可选的。
xml
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
ctx.font = "48px serif"; // 指定字体
ctx.fillText("Hello world", 10, 50);
}
draw()
</script>
strokeText
strokeText(text, x, y [, maxWidth])
在指定的 (x, y) 位置绘制文本边框,绘制的最大宽度是可选的。
xml
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
ctx.font = "48px serif"; // 指定字体
ctx.strokeText("Hello world", 10, 50);
}
draw()
</script>
图片
canvas 更有意思的一项能力,就是图片操作能力
drawImage
引入图片到 canvas 中,需要 两个基本操作:
- 获取一个指向 HTMLImageElement (new Image())的对象,或者另一个 canvas 元素的引用作为源,也可以通过提供一个 URL 的方式来使用图片(参见 例子)
- 使用 drawImage() 函数,将图片绘制到画布上
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = function () {
ctx.drawImage(img, 0, 0); // 绘制图片
ctx.beginPath();
ctx.moveTo(30, 96);
ctx.lineTo(70, 66);
ctx.lineTo(103, 76);
ctx.lineTo(170, 15);
ctx.stroke();
};
img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAACCCAMAAADYMBKwAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAELUExURUdwTPb29v39/fv7+8rKyszMzM3NzczMzMzMzMzMzPn5+fr6+vj4+P7+/vz8/P////f3983NzUJCQurq6uvr6+np6efn5+bm5js7Ozw8PHx8fL+/v39/f4uLiyoqKru7u46Ojujo6FxcXH5+fl1dXd/f3319fcrKykxMTD09Pc7Ozqurq42Njby8vK2trbq6uikpKa+vrysrK/Pz8/Hx8ePj44yMjJ6enmxsbNra2s/Pz97e3l5eXpubm76+vszMzJ2dnaysrJ+fn19fXxkZGb29vezs7E1NTdnZ2cnJyd3d3dzc3G1tbcvLy0tLSxgYGHBwcG9vb+7u7u3t7U5OTpycnK6urmtra9vb2zkVlaEAAAAKdFJOUwD///8dcDnqt7CBu8DRAAAC40lEQVR42u3c2VLiQBSA4UCIgSwYwjLdgWwIhB2RzR3cd5195v2fZDoKlOOQGy+m+uD5KdKaq69OHaG8iSAICSkpZ4AkJ6WEwNpIVbODGJAG2Wpqg805VYiBqpBKCFI1BqyqJCSz0NDZpCAPoKEHspCJgSuDaES/F23yj755IiQW+0XIDf9oY5HdCi+24W8v7pgGl71GbxPLN8hCDwTdM+zyM7pnwUGziBGuRmuJ/sRTq9B+uCHG47VhlAMwk962eo9HRkB6bEnAoP0jq8eOa+vIMEDt9JsQjWhEIxrRiH4fWovO1LhsHdA+QHRga9ouIQEotGNptq35DiT07q6lEXbaLThot+y+oNkBBe07PtOGq9Faogs8tQpdr1tl4tb3NK0cQJm077quXd4LCLuC+vTYc9nb+oHfiIiGjVaiMxUuQzSiEY1oRINB5wlxHhSF/Y/4AAd9cqL8rivjsZJ3QK1Hvq4QdoxPuEery2ZPU5Wys1Ob3zBVLvsLrXZrqvNyzNF5nlqFnrE3Vet3qlqZgpl0rdKpdNUp7YwrKpz16NQ67HpXm6kqpJ1+G6IRjWhErxtajM4UuQzRiF5jdI5Sh/1yT2kGDrpywdxiuy3mHDjo8AfKXqLY7kLa6cnkGX2xBQg9uRfFcDW6S3SWp1aiMxV2ubpl253hftLpec3zn8fH6SY9bh8ubplpLnuFbm+x0unbrUkaDnpFiEY0ohGNaER/IHQ8OjPOZeDR3nmRXb9Q2oQ06RJDe1782w40NA1H3geILpaAocPV6C/ROZ6KRl8dxOOHTWCTbtKidwjpD7HIphw/KJ3ilwuiEf3f0Xp0ps5liEY0ohH9odGfG419aGjP07/uQEM3QngfILpYAoYOV6M/R5t8FY3+XtX1s31gk95vFL0zne/+/cirlk51/tGb4EI0otcPLYvQzKIsJIfQ0MOkII2goUeSkEhdwjJfphLho5xHQzB7LQ5H4aOcIT40+w8Cj+zDQXkYpQAAAABJRU5ErkJggg==";
}
draw()
</script>
变换 Transform
状态 State
当我们开始绘制复杂图形时,对状态的保存和恢复,是必不可少的操作!
Canvas 的状态 就是当前画面应用的所有样式和变形的一个快照!
Canvas 状态 存储在栈中,每当save()
方法被调用后,当前的状态就被推送到栈中保存!
save() 保存画布(canvas)的所有状态
restore() 恢复画布(canvas)上一个状态
一个绘画的状态包括:
- 当前应用的变换(平移、旋转、缩放)
- 以下属性设置::strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation, font, textAlign, textBaseline, direction, imageSmoothingEnabled
- 当前的裁切路径(clipping path)
你可以调用任意多次 save
方法。
每一次调用 restore
方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
save 和 restore 往往成对儿出现
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
ctx.fillRect(0, 0, 150, 150); // 使用默认设置绘制一个矩形
ctx.save(); // 保存默认状态
ctx.fillStyle = "#09F"; // 在原有配置基础上对颜色做改变
ctx.fillRect(15, 15, 120, 120); // 使用新的设置绘制一个矩形
ctx.save(); // 保存当前状态
ctx.fillStyle = "#FFF"; // 再次改变颜色配置
ctx.globalAlpha = 0.5;
ctx.fillRect(30, 30, 90, 90); // 使用新的配置绘制一个矩形
ctx.restore(); // 重新加载之前的颜色状态
ctx.fillRect(45, 45, 60, 60); // 使用上一次的配置绘制一个矩形
ctx.restore(); // 加载默认颜色配置
ctx.fillRect(60, 60, 30, 30); // 使用加载的配置绘制一个矩形
}
draw()
</script>
平移 Translate
translate(x, y) 用来移动 canvas 和它的原点到一个不同的位置
接受两个参数:x 是左右偏移量,y ****是上下偏移量
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
ctx.save(); // 状态保存
ctx.fillStyle = "rgb(" + 51 * i + ", " + (255 - 51 * i) + ", 255)";
ctx.translate(10 + j * 50, 10 + i * 50); // 平移
ctx.fillRect(0, 0, 25, 25); // 画图形
ctx.restore(); // 恢复状态
}
}
}
draw()
</script>
旋转 Rotate
rotate(angle) 用于以原点为中心旋转 canvas
只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值。
旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate
方法。
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// left rectangles, rotate from canvas origin
ctx.save();
// 蓝色矩形
ctx.fillStyle = "#0095DD";
ctx.fillRect(30, 30, 100, 100);
ctx.rotate((Math.PI / 180) * 25);
// 灰色矩形
ctx.fillStyle = "#4D4E53";
ctx.fillRect(30, 30, 100, 100);
ctx.restore();
// right rectangles, rotate from rectangle center
// draw blue rect
ctx.fillStyle = "#0095DD";
ctx.fillRect(150, 30, 100, 100);
ctx.translate(200, 80); // translate to rectangle center
ctx.rotate((Math.PI / 180) * 25); // rotate
ctx.translate(-200, -80); // translate back
// draw grey rect
ctx.fillStyle = "#4D4E53";
ctx.fillRect(150, 30, 100, 100);
}
draw()
</script>
缩放 Scale
scale(x, y) 用 来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大
两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子
ini
<canvas id="canvas"></canvas>
<script>
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// draw a simple rectangle, but scale it.
ctx.save();
ctx.scale(10, 3);
ctx.fillRect(1, 10, 10, 10);
ctx.restore();
// mirror horizontally 水平镜像
ctx.scale(-1, 1);
ctx.font = "48px serif";
ctx.fillText("MDN", -135, 120);
}
draw()
</script>
Composite and Clip
在此之前,我们总是将一个图形画在另一个之上,然而这远远是不够的。比如对于一些合成的图形来讲,绘制顺序就会有限制!
本节为了解决这种限制,采用下面的办法:
- 利用
globalCompositeOperation
属性来改变绘制顺序限制 - 利用
clip
属性允许我们隐藏不想看到的部分图形
遮盖 Composite
globalCompositeOperation = type
这个属性设定了在画新图形时采用的遮盖策略,其值是一个标识 ******12 ******种遮盖方式的字符串。
裁切 clip
clip() 将当前正在构建的路径转换为当前的裁剪路径
裁切路径也属于 canvas 状态的一部分
裁切路径和普通的 canvas 图形差不多,不同的是它的作用是遮罩,用来隐藏不需要的部分
与 globalCompositeOperation
相比,两者最重要的区别是裁切路径不会在 canvas 上绘制东西,而且它永远不受新图形的影响。这些特性使得它在特定区域里绘制图形时相当好用。
ini
<canvas id="canvas"></canvas>
<script>
// 绘制星星
function drawStar(ctx, r) {
ctx.save();
ctx.beginPath();
ctx.moveTo(r, 0);
for (let i = 0; i < 9; i++) {
ctx.rotate(Math.PI / 5);
if (i % 2 === 0) {
ctx.lineTo((r / 0.525731) * 0.200811, 0);
} else {
ctx.lineTo(r, 0);
}
}
ctx.closePath();
ctx.fill();
ctx.restore();
}
function draw() {
const canvas = document.getElementById("canvas");
if (!canvas.getContext) return
const ctx = canvas.getContext("2d");
// 画了一个与 canvas 一样大小的黑色方形作为背景,并移动原点至中心点
ctx.fillRect(0, 0, 150, 150);
ctx.translate(75, 75);
// 创建一个弧形的裁切路径
ctx.beginPath();
ctx.arc(0, 0, 60, 0, Math.PI * 2, true);
ctx.clip();
// draw background
const lingrad = ctx.createLinearGradient(0, -75, 0, 75);
lingrad.addColorStop(0, "#232256");
lingrad.addColorStop(1, "#143778");
ctx.fillStyle = lingrad;
ctx.fillRect(-75, -75, 150, 150);
// draw stars
for (let j = 1; j < 50; j++) {
ctx.save();
ctx.fillStyle = "#fff";
ctx.translate(
75 - Math.floor(Math.random() * 150),
75 - Math.floor(Math.random() * 150),
);
drawStar(ctx, Math.floor(Math.random() * 4) + 2);
ctx.restore();
}
}
draw()
</script>
动画
使用 2D canvas 动画的基本步骤是:
- 清空 canvas . *除非接下来要画的内容会完全充满 canvas(例如背景图),否则你需要清空所有。
*最简单的做法就是用clearRect
方法 - 保存 canvas 状态 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。用
save
** 方法 - 绘制动画图形(animated shapes) 这一步才是重绘动画帧
- 恢复 canvas 状态 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。用
restore
方法
2D 太阳星系
xml
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import sunSrc from '@/assets/imgs/canvas_sun.png'
import moonSrc from '@/assets/imgs/canvas_moon.png'
import earthSrc from '@/assets/imgs/canvas_earth.png'
let rafId
const canvasEl = ref()
const sun = new Image()
const moon = new Image()
const earth = new Image()
function init2DSolar() {
sun.src = sunSrc
moon.src = moonSrc
earth.src = earthSrc
setSize()
rafId = requestAnimationFrame(draw)
}
function draw() {
const ctx = canvasEl.value.getContext('2d')
ctx.globalCompositeOperation = 'destination-over'
ctx.clearRect(0, 0, canvasEl.value.width, canvasEl.value.height) // 清除画布
ctx.fillStyle = 'rgb(0 0 0 / 40%)'
ctx.strokeStyle = 'rgb(0 153 255 / 40%)'
ctx.save() // 保存状态
ctx.translate(canvasEl.value.width / 2, canvasEl.value.height / 2)
// 地球
const time = new Date()
ctx.rotate(
((2 * Math.PI) / 60) * time.getSeconds() +
((2 * Math.PI) / 60000) * time.getMilliseconds(),
)
ctx.translate(105, 0)
ctx.fillRect(0, -12, 40, 24) // 阴影
ctx.drawImage(earth, -12, -12)
// 月亮
ctx.save()
ctx.rotate(
((2 * Math.PI) / 6) * time.getSeconds() +
((2 * Math.PI) / 6000) * time.getMilliseconds(),
)
ctx.translate(0, 28.5)
ctx.drawImage(moon, -3.5, -3.5)
ctx.restore()
ctx.restore()
ctx.beginPath()
ctx.arc(
canvasEl.value.width / 2,
canvasEl.value.height / 2,
105,
0,
Math.PI * 2,
false
) // 地球轨道
ctx.stroke()
ctx.drawImage(sun, 0, 0, canvasEl.value.width, canvasEl.value.height)
rafId = requestAnimationFrame(draw)
}
function setSize() {
canvasEl.value.width = 600
canvasEl.value.height = 600
}
onMounted(() => {
init2DSolar()
window.onresize = () => setSize()
})
onUnmounted(() => {
cancelAnimationFrame(rafId)
})
</script>
<template>
<canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>
<style scoped>
.paint-2d-canvas {
position: fixed;
}
</style>
2D 时钟表盘
ini
<script setup>
import { zeroPadding } from '@/utils/index.js'
import { ref, onMounted, onUnmounted } from 'vue'
let rafId
const canvasEl = ref()
const isSweepSecond = ref(false) // 是否为扫秒式
function init2DClock() {
setSize()
rafId = requestAnimationFrame(animate)
}
function animate() {
const now = new Date()
const canvas = canvasEl.value
const context = canvas.getContext('2d')
context.save()
context.clearRect(0, 0, canvasEl.value.width, canvasEl.value.height)
context.translate(canvasEl.value.width / 2, canvasEl.value.height / 2 - 60)
context.rotate(-Math.PI / 2)
context.strokeStyle = 'black'
context.fillStyle = 'white'
context.lineWidth = 4
context.lineCap = 'round'
// 小时刻度
context.save()
for (let i = 0; i < 12; i++) {
context.beginPath()
context.rotate(Math.PI / 6)
context.moveTo(108, 0)
context.lineTo(120, 0)
context.stroke()
}
context.restore()
// 分钟刻度
context.save()
context.lineWidth = 1
for (let i = 0; i < 60; i++) {
if (i % 5 !== 0) {
context.beginPath()
context.moveTo(117, 0)
context.lineTo(120, 0)
context.stroke()
}
context.rotate(Math.PI / 30)
}
context.restore()
let sec = now.getSeconds() // 钟摆式
if (isSweepSecond.value) {
sec = now.getSeconds() + now.getMilliseconds() / 1000
} // 扫秒式
const min = now.getMinutes()
const hr = now.getHours() % 12
context.fillStyle = 'black'
// 时针
const hourRadian = (Math.PI / 6) * hr + (Math.PI / 360) * min + (Math.PI / 21600) * sec
context.save()
context.rotate(hourRadian)
context.lineWidth = 14
context.beginPath()
context.moveTo(-20, 0)
context.lineTo(80, 0)
context.stroke()
context.restore()
// 分针
const minuteRadian = (Math.PI / 30) * min + (Math.PI / 1800) * sec
context.save()
context.rotate(minuteRadian)
context.lineWidth = 10
context.beginPath()
context.moveTo(-28, 0)
context.lineTo(100, 0)
context.stroke()
context.restore()
// 秒针
const secondRadian = (sec * Math.PI) / 30
context.save()
context.rotate(secondRadian)
context.strokeStyle = '#D40000'
context.fillStyle = '#D40000'
context.lineWidth = 6
context.beginPath()
context.moveTo(-30, 0)
context.lineTo(120, 0)
context.stroke()
context.beginPath()
context.arc(0, 0, 10, 0, Math.PI * 2, true)
context.fill()
context.restore()
context.beginPath()
context.lineWidth = 14
context.strokeStyle = '#325FA2'
context.arc(0, 0, 142, 0, Math.PI * 2, true)
context.stroke()
context.restore()
const hours = zeroPadding(now.getHours())
const minutes = zeroPadding(now.getMinutes())
const seconds = zeroPadding(now.getSeconds())
const time = `${hours}:${minutes}:${seconds}`
context.font = '48px serif' // 指定字体
context.fillText(time, 210, canvasEl.value.height / 2 + 150)
context.strokeText('双击切换钟摆方式', 108, canvasEl.value.height / 2 + 220)
rafId = requestAnimationFrame(animate)
}
function setSize() {
canvasEl.value.width = 600
canvasEl.value.height = 600
}
onMounted(() => {
init2DClock()
window.onresize = () => setSize()
// 双击切换钟摆方式
canvasEl.value.ondblclick = () => isSweepSecond.value = !isSweepSecond.value
})
onUnmounted(() => {
cancelAnimationFrame(rafId)
})
</script>
<template>
<canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>
<style scoped>
.paint-2d-canvas {
position: fixed;
margin: 10px;
border-radius: var(--el-border-radius-base);
border: solid 1px var(--el-border-color);
}
</style>
2D 自由小球
xml
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
let rafId, context
const canvasEl = ref()
const ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: 'blue',
drawFrame() {
context.beginPath()
context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true)
context.closePath()
context.fillStyle = this.color
context.fill()
} // 绘制单帧
}
function init2DBall() {
setSize()
context = canvasEl.value.getContext('2d')
canvasEl.value.onmouseover = () => rafId = window.requestAnimationFrame(animate)
canvasEl.value.onmouseout = () => window.cancelAnimationFrame(rafId)
ball.drawFrame()
}
function animate() {
context.clearRect(0, 0, canvasEl.value.width, canvasEl.value.height)
ball.drawFrame()
ball.x += ball.vx
ball.y += ball.vy
ball.vy *= 0.99
ball.vy += 0.25
if (
ball.y + ball.vy > canvasEl.value.height - ball.radius ||
ball.y + ball.vy < ball.radius
) {
ball.vy = -ball.vy
}
if (
ball.x + ball.vx > canvasEl.value.width - ball.radius ||
ball.x + ball.vx < ball.radius
) {
ball.vx = -ball.vx
}
rafId = requestAnimationFrame(animate)
}
function setSize() {
canvasEl.value.width = 600
canvasEl.value.height = 600
}
onMounted(() => {
init2DBall()
window.onresize = () => setSize()
})
onUnmounted(() => {
cancelAnimationFrame(rafId)
})
</script>
<template>
<canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>
<style scoped>
.paint-2d-canvas {
position: fixed;
margin: 10px;
border-radius: var(--el-border-radius-base);
border: solid 1px var(--el-border-color);
}
</style>
2D 鼠标追踪
ini
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { generateColor } from '@/utils/index.js'
let context, rafId, particlesArray = []
const canvasEl = ref() // 画布节点
const cursor = { x: 0, y: 0 } // 鼠标位置
function init2DMouse() {
context = canvasEl.value.getContext('2d')
context.lineWidth = '2'
context.globalAlpha = 0.5
setSize()
animate()
generateParticles(101)
}
function generateParticles(amount) {
for (let i = 0; i < amount; i++) {
particlesArray[i] = new Particle(
canvasEl.value.width / 2,
canvasEl.value.height / 2,
4,
generateColor(),
Math.PI / 360,
)
}
}
function setSize() {
canvasEl.value.width = 600
canvasEl.value.height = 600
cursor.x = canvasEl.value.width / 2
cursor.y = canvasEl.value.height / 2
}
function Particle(x, y, particleTrailWidth, strokeColor, rotateSpeed) {
this.x = x
this.y = y
this.rotateSpeed = rotateSpeed
this.strokeColor = strokeColor
this.particleTrailWidth = particleTrailWidth
this.t = Math.random() * 150
this.theta = Math.random() * Math.PI * 2
this.rotate = () => {
const ls = {
x: this.x,
y: this.y,
}
this.theta += this.rotateSpeed
this.x = cursor.x + Math.cos(this.theta) * this.t
this.y = cursor.y + Math.sin(this.theta) * this.t
context.beginPath()
context.lineWidth = this.particleTrailWidth
context.strokeStyle = this.strokeColor
context.moveTo(ls.x, ls.y)
context.lineTo(this.x, this.y)
context.stroke()
} // 旋转
}
function animate() {
rafId = requestAnimationFrame(animate)
context.fillStyle = "rgb(0 0 0 / 5%)"
context.fillRect(0, 0, canvasEl.value.width, canvasEl.value.height)
particlesArray.forEach((particle) => particle.rotate())
}
onMounted(() => {
init2DMouse()
window.onresize = () => setSize()
canvasEl.value.onmousemove = (e) => {
cursor.x = e.clientX - (e.x - e.layerX)
cursor.y = e.clientY - (e.y - e.layerY)
}
canvasEl.value.ontouchmove = (e) => {
e.preventDefault()
cursor.x = e.touches[0].clientX
cursor.y = e.touches[0].clientY
}
})
onUnmounted(() => {
cancelAnimationFrame(rafId)
})
</script>
<template>
<canvas ref="canvasEl" class="paint-2d-canvas"></canvas>
</template>
<style scoped>
.paint-2d-canvas {
position: fixed;
margin: 10px;
border-radius: var(--el-border-radius-base);
border: solid 1px var(--el-border-color);
}
</style>