当前位置: 首页 > news >正文

Canvas—从入门到案例实现

文章目录

  • Canvas—从入门到案例实现
    • 一、设置canvas环境
      • 1.1 `<canvas>`元素
      • 1.2 渲染上下文context
    • 二、形状与路径的绘制
      • 2.1 形状绘制
      • 2.2 路径绘制
      • 2.3 绘制一个笑脸
    • 三、使用样式和颜色
    • 四、绘制文本
    • 五、使用图像
      • 5.1 图片源
      • 5.2 获取页面内的图片
      • 5.3 缩放Scaling
      • 5.4 切片Slicing
      • 5.5 控制图像的缩放行为
    • 六、状态的保存与恢复
    • 七、变形 Transformations
      • 7.1 移动 Translating
      • 7.2 旋转 Rotating
      • 7.3 缩放 Scaling
    • 八、综合案例:多功能画板
    • 九、综合案例:时钟
    • 十、更多内容

Canvas—从入门到案例实现

<canvas>是一个可以使用脚本(通常是JavaScript)来绘制图形的HTML元素,例如,它可以用于绘制图表、制作图片构图或者制作简单的动画

本篇博客就基础开始,描述了如何使用<canvas>元素来绘制2D图形。

并通过多功能画板时钟两个综合案例带大家练习canvas

多功能画板展示:

在这里插入图片描述

时钟展示:
在这里插入图片描述

一、设置canvas环境

1.1 <canvas>元素

<canvas>看起来和<img>元素很相像,与<img>的不同就是它没有srcalt属性,并且<canvas>元素需要结束标签</canvas>

实际上,<canvas>标签只有两个属性widthheight,这两个属性是可选的,当没有设置宽高时,<canvas>初始化宽度为300像素和高度为150像素,另外<canvas>也可以使用CSS来定义宽高,但在绘制时图像会伸缩以适应它的框架尺寸:如果CSS的尺寸与初始化画布的比例不一致,它会出现扭曲。

注意:如果你绘制出来的图像是扭曲的,尝试用widthheight属性为<canvas>明确规定宽高,而不是使用CSS

1.2 渲染上下文context

<canvas>元素创造了一个固定大小的画布,它公开了一个或多个渲染上下文,其可以用来绘制和处理要展示的内容。

<canvas>起初是空白的,为了展示,首先脚本需要找到渲染上下文,然后在它的上面绘制,<canvas>元素有一个叫做**getContext()的方法,这个方法用来获取渲染上下文和它的绘画功能,getContext()**接受一个参数,即上下文的类型,对于2D图像而言,传'2d'即可。

下面我们来看一个实例:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>canvas</title><style>#canvas {border: 1px solid #000;/* 这里使用css定义canvas宽高经常会出现画出的图形被压缩的问题 可以删掉canvas dom元素上的宽高属性试试 *//* width: 150px;height: 150px; */position: relative;top: 150px;left: 50%;transform: translate(-50%);}</style>
</head><body><canvas id="canvas" width="150" height="150"><!-- 如果当前浏览器不支持canvas,将会显示盒子内的内容作为兜底提示 -->current browoser not support canvas</canvas><script>function draw () {// 拿到canvas的dom对象let canvas = document.getElementById('canvas');// 拿到canvas的上下文对象let ctx = canvas.getContext('2d');if(ctx) {  // 判断上下文对象是否存在 即检查支持性// canvas-supported code herectx.fillStyle = 'rgba(200, 0, 0)';ctx.fillRect(10, 10, 50, 50);ctx.fillStyle = 'rgba(0, 0, 200, 0.5)';ctx.fillRect(30, 30, 50, 50);}else {// canvas-unsupported code herealert('当前浏览器不支持canvas')return}}draw()</script>
</body></html>

上面代码通过拿到<canvas>的dom对象,然后根据上下文是否存在来检查canvas的支持性以做不同处理,如果支持则进行绘制,不支持则给出提示【现今,所有主流浏览器都支持canvas】,上面代码运行结果如下:

在这里插入图片描述

二、形状与路径的绘制

在开始画图之前,我们需要了解一下画布栅格以及坐标空间

在这里插入图片描述

如上图所示,<canvas>元素默认被网格所覆盖,通过来说网格中的一个单元相当于canvas元素中的一像素,栅格的起点为左上角,坐标为(0,0)。所有元素的位置都相对于原点定位。

2.1 形状绘制

这里我们主要介绍一下矩形的绘制,<canvas>提供了三种方法绘制矩形:

  • fillRect(x, y, width, height)绘制一个填充的矩形
  • strokeRect(x, y, width, height)绘制一个矩形的边框
  • clearRect(x, y, width, height)清除指定矩形区域,让清除部分完全透明

x,y分别表示矩形的起始点(即左上角的位置)相对于坐标轴原点的距离,widthheight分别表示矩形的宽高。

实例如下:

function draw () {let canvas = document.getElementById('canvas');let ctx = canvas.getContext('2d');if(ctx) {  // 判断上下文对象是否存在 即检查支持性// canvas-supported code herectx.fillRect(25, 25, 100, 100)  // 填充矩形ctx.clearRect(45, 45, 60, 60)  // 透明/空心矩形ctx.strokeRect(50, 50, 50, 50)  // 轮廓矩形}else {// canvas-unsupported code herealert('当前浏览器不支持canvas')return}
}
draw()

输出如下:

在这里插入图片描述

这里再介绍一下圆弧的绘制语法:ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)

圆弧路径的圆心在(x,y)位置,半径为radius,根据anticlockwise默认为顺时针指定的方向从startAngle开始绘制,到endAngle结束。

startAngle:圆弧的起始点,x轴方向开始计算,单位以弧度表示。

endAngle:圆弧的终点,单位以弧度表示。

anticlockwise:可选的Boolean值,如果为true,逆时针制圆弧,反之,顺时针绘制,默认为false

2.2 路径绘制

图形的基本元素是路径。路径是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合。一个路径,甚至一个子路径,都是闭合的。使用路径绘制图形需要一些额外的步骤。

  1. 首先,你需要创建路径起始点
  2. 然后使用画图命令去画出路径
  3. 之后把路径封闭
  4. 一旦路径生成,你就能通过描边或填充路径区域来渲染图形

以下是所要用到的函数:

beginPath():新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。

closePath():闭合路径之后图形绘制命令又重新指向到上下文中。

stroke():通过线条来绘制图形轮廓。

fill():通过填充路径的内容区域生成实心的图形。

moveTo():移动笔触,将笔触移动到指定的坐标x以及y上,就类似手写笔记时将笔尖移动到A4纸上的某个位置,这个方法跟python的海龟作图一样。

关于beiginPath():生成路径的第一步,本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线,弧形,等等)构成图形,而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形了

注意:前路径为空,即调用 beginPath() 之后,或者 canvas 刚建的时候,第一条路径构造命令通常被视为是 moveTo(),无论实际上是什么。出于这个原因,你几乎总是要在设置路径之后专门指定你的起始位置。

关于closePath():非必需,这个方法会通过绘制一条从当前点到开始点的直线来闭合图形。如果图形是已经闭合了的,即当前点为开始点,该函数什么也不做。当调用fill()函数之后,形状会自动闭合,所以不需要调用此函数、

下面我们通过使用canvas绘制一个三角形来体会前面讲的函数:

function draw() {let canvas = document.getElementById('canvas');let ctx = canvas.getContext('2d');ctx.beginPath();  // 新建一条路径ctx.moveTo(75, 50);  // 将路径的起始点定在(75, 50)位置ctx.lineTo(100, 75);  // 从路径起始点到(100, 75)绘制一条线ctx.lineTo(75, 100);ctx.fill();  // 自动填充图形 如果图形没闭合会自动闭合
}
draw()

结果如下:

在这里插入图片描述

2.3 绘制一个笑脸

下面,我们结合图形绘制和路径绘制实现一个简单的笑脸。

function draw() {let canvas = document.getElementById('canvas');let ctx = canvas.getContext('2d');ctx.beginPath();  // 开始绘制ctx.arc(75, 75, 50, 0, 2 * Math.PI)ctx.moveTo(110, 75)ctx.arc(75, 75, 35, 0, Math.PI)ctx.moveTo(65, 65)ctx.arc(60, 65, 5, 0, Math.PI * 2)ctx.moveTo(95, 65)ctx.arc(90, 65, 5, 0, Math.PI * 2)ctx.stroke();  // 展示线条
}
draw()

结果如下:

在这里插入图片描述

三、使用样式和颜色

可配置颜色、透明度、线型样式、线型渐变、径向渐变、图案样式、阴影等。

api比较多,详细可看MDN——使用样式和颜色

四、绘制文本

canvas提供了两种方法来渲染文本:

  1. fillText(text, x, y, [, maxWidth]):在指定的(x,y)位置填充指定的文本,绘制的最大宽度是可选的。
  2. strokeText(text, x, y, [, maxWidth]):在指定的(x,y)位置绘制文本边框,绘制的最大宽度是可选的。

文本也可以通过一些样式属性来调整文本样式:

  • ctx.font = value:这个属性和CSS的font属性相同语法,默认的字体是10px sans-serif
  • textAlign = value:文本对齐选项,可选的值包括:startendleftrightorcenter,默认值是start
  • textBaseline = value:基线对齐选项,可选的值包括:tophangingmiddlealphabeticideographicbottom。默认值是alphabetic
  • direction = value:文本方向。可能的值包括:ltrrtlinherit。默认值是inherit

下面我们看实例:

function draw() {let canvas = document.getElementById('canvas');let ctx = canvas.getContext('2d');ctx.font = '10px serif';ctx.fillText('Hello World', 0, 10);ctx.font = '30px serif';ctx.strokeText('Hello World', 0, 150);let text = ctx.measureText('Hello World');console.log(text.width);  // 165
}
draw()

结果如下:

在这里插入图片描述

需要注意的是:canvas绘制图形和路径往往是以左上角的位置为参考,而canvas使用fillText()strokeText()设置文字位置时,是以文字左下角的位置为相对位置。

上面的代码段中还使用了measureText()对象获取文本的更多细节,它将返回一个TextMetrics对象的宽度、所在像素,这些体现文本特性的属性,其中输出文本宽度可以使用.width

五、使用图像

canvas更有意思的一项特性就是图像操作能力。可以用于动态的图像合成或者作为图形的背景,以及游戏界面(Sprites)等等。浏览器支持的任意格式的外部图片都可以使用,比如PNG、GIF或者JPEG。你甚至可以将同一个页面中其他canvas元素生成的图片作为图片源。

引入图形到canvas里需要以下两步基本操作

  1. 获得一个指向HTMLImageElement的对象或者另一个canvas元素的引用作为源,也可以通过提供一个URL的方式来使用图片。
  2. 使用drawImage()函数将图片绘制到画布上。

drawImage()函数的常规语法如下

drawImage(image, dx, dy)

其中dx表示图片相对canvas左上角的x轴位置,dy表示图片相对canvas左上角的y轴位置。

更高阶的用法如缩放Scaling,切片Slicing,会在后面详细讲解。

5.1 图片源

canvas的API可以使用下面这些类型中的一个作为图片的源:

  • HTMLImageElement:这种类型是由Image()函数构造出来的,或者任何的<img>元素。
  • HTMLVideoElement:用一个HTML的<video>元素作为你的图片源,可以从视频中抓取当前帧作为一个图像。
  • HTMLCanvasElement:可以使用另一个<canvas>元素作为你的图片源。
  • ImageBitmap:这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其他几种源中生成。

5.2 获取页面内的图片

我们可以通过下列方法的一种来获得与canvas相同页面内的图片的引用:

  • document.images集合
  • document.getElementsByTagName()方法
  • 如果你知道你想使用的指定图片的ID,你可以用document.getElementById()获得这个图片

也可以通过new Image()创建<img>元素,自定义图片。

下面我们来看实例:

let img = new Image();
img.src = './canvas_drawimage2.jpg';
ctx.drawImage(img, 0, 0);

当脚本执行后,图片开始装载,若调用drawImage时,图片没装载完,那什么都不会发生(在一些旧浏览器中可能会抛出异常)。因此你应该用load事件来保证不会在加载完毕之前使用这个图片:

let img = new Image();
img.onload = function () {// 执行drawImage语句ctx.drawImage(img, 0, 0);
}
img.src = './canvas_drawimage2.jpg';

结果如下:

在这里插入图片描述

5.3 缩放Scaling

drawImage()的缩放用法比常规用法多了两个参数,语法如下:

drawImage(image, x, y, width, height)

其中:

  • x, y:表示图片位置
  • width, height:表示图片缩放的大小

下面我们通过缩放功能实现在canvas上平铺图像:

let img = new Image();
img.onload = function () {for (let i = 0; i < 4; i++) {for (let j = 0; j < 3; j++) {ctx.drawImage(frame, j * 50, i * 38, 50, 38);}}
}
img.src = './canvas_drawimage2.jpg';

结果如下:

在这里插入图片描述

5.4 切片Slicing

drawImage()的切片方法需要传递9个参数,除了第一个参数和常规方法和缩放方法一样,其他参数都不一样,下面我们来详细看看它的语法:

drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

其余八个参数中,前4个定义图片源的切片位置和大小,后4个则是定义切片的目标显示位置和大小。

切片是做图片合成的强大工具。假设有一张包含了所有元素的图像,那么你可以用这个方法来合成一个完整图像。例如,你想画一张图表,而手上有一个包含所有必需的文字的 PNG 文件,那么你可以很轻易的根据实际数据的需要来改变最终显示的图表。这方法的另一个好处就是你不需要单独装载每一个图像。

下面我们来实现用另一张图片来替换前面章节的相框内的犀牛头。

<body><canvas id="canvas" width="300" height="300"><!-- 如果当前浏览器不支持canvas,将会显示盒子内的内容作为兜底提示 -->current browoser not support canvas</canvas><div style="display: none;"><img id="source" src="./img22_min.png" width="132" height="150" alt="资源"><img id="frame" src="./canvas_drawimage2.jpg" width="190" height="190" alt="相框"></div><script>function draw() {// 获取canvas元素const canvas = document.getElementById('canvas');// 获取绘图环境const ctx = canvas.getContext('2d');// 获取图片元素const source = document.getElementById('source');const frame = document.getElementById('frame');// 绘制相框ctx.drawImage(frame, 0, 0);// 绘制图片ctx.drawImage(source, 10, 10, 250, 120, 52, 43, 124, 102);}draw();</script>
</body>

结果如下:

在这里插入图片描述

5.5 控制图像的缩放行为

过度缩放图像可能会导致图像模糊或像素化。你可以通过使用绘图环境的imageSmoothingEnabled属性来控制是否在缩放图像时使用平滑算法。默认值为true,即启用平滑缩放。你也可以像这样禁用此功能:

ctx.mozImageSmoothingEnabled = false;
ctx.webkitImageSmoothingEnabled = false;
ctx.msImageSmoothingEnabled = false;
ctx.imageSmoothingEnabled = false;

六、状态的保存与恢复

在了解变形之前,我们先了解两个你在开始绘制复杂图形时必不可少的方法。

  • save():保存画布(canvas)的所有状态
  • restore()saverestore方法是用来保存和恢复canvas状态的,都没有参数。

canvas的状态就是当前画面应用的所有样式和变形的一个快照。

canvas状态存储在栈中,每当save()方法被调用后,当前的状态就被推送到栈中保存。一个绘画状态包括:

  • 当前应用的变形(即移动,旋转和缩放)
  • 以及下面这些属性:strokeStyle,fillStyle,globalAppha,lineWidth,lineCap
  • 当前的裁剪路径(clipping path)

你可以调用任意次save方法。每一次调用restore方法,上一个保存的状态就从栈中弹出,所有设定都恢复。

下面我们看实例:

function draw() {const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');// 使用默认状态画一个矩形 背景色是黑色的ctx.fillRect(0, 0, 150, 150)ctx.save()  // 保存默认状态ctx.fillStyle = '#ff0000'// 使用新状态绘制一个矩形 背景色红色ctx.fillRect(15, 15, 120, 120)ctx.save()  // 保存新状态ctx.fillStyle = '#0000ff'ctx.globalAlpha = 0.5// 使用新状态绘制一个矩形 背景色蓝色 透明度0.5ctx.fillRect(30, 30, 90, 90)// 恢复到之前保存的状态 根据栈后进先出原则 弹出的状态应该是红色背景的状态ctx.restore()ctx.fillRect(45, 45, 60, 60)// 再次弹出的应该是默认状态ctx.restore()ctx.fillRect(60, 60, 30, 30)
}
draw();

上面这段代码实现了两次保存状态,第一次是默认状态,第二次是红色背景状态,在restore()恢复状态时,根据栈后进先出原则,第一次恢复弹出的状态是红色背景状态,第二次弹窗的是默认状态,结果展示如下:

在这里插入图片描述

七、变形 Transformations

canvas的变形和css的变形比较类似,变形可以将canvas原点移动到另一点、对网格进行旋转和缩放。

这里主要讲解移动 Translating旋转 Rotating缩放 Scaling,有关变形Transforms的内容建议查看MDN——Transformations。

7.1 移动 Translating

使用translate(x, y)方法实现移动canvas和它的原点到一个不同的位置。

其中x表示左右偏移量,y表示上下偏移量。

在做变形之前先保存状态是一个良好的习惯,大多数情况下,调用restore方法比手动恢复原先的状态要简单得多。又,如果你是在一个循环中做位移但没有保存和恢复canvas的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出canvas范围以外了。

下面我们看实例:

function draw() {const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');for (let i = 0; i < 3; i++) {for (let j = 0; j < 3; j++) {// 保存默认状态ctx.save()// 改变填充颜色ctx.fillStyle = 'rgb(' + 80 * i + ',' + 80 * j + ', 255)'// 移动原点ctx.translate(10 + 50 * j, 10 + 50 * i)ctx.fillRect(0, 0, 30, 30)// 恢复默认状态ctx.restore()}}
}
draw();

上面的代码段使用循环实现了通过tranlate()改变原点位置,从而绘制不同颜色的矩形,结果如下:

在这里插入图片描述

7.2 旋转 Rotating

使用rotate(angle)方法实现旋转canvas,这个方法只接收一个参数:旋转的角度(angle),它是顺时针方向的,以弧度为单位的值,并且是以原点为中心旋转canvas

旋转的中心始终是canvas的原点,如果要改变它,我们需要用到translate(x, y)方法。

下面我们看实例:

function draw() {const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');ctx.save()ctx.fillStyle = '#88f';ctx.fillRect(10, 10, 60, 60);// 绕默认原点旋转ctx.rotate(Math.PI / 180 * 25);ctx.fillStyle = '#f88';ctx.fillRect(10, 10, 60, 60);ctx.restore();ctx.fillStyle = '#8f8'ctx.fillRect(80, 10, 60, 60);ctx.translate(110, 40)// 改变原点 绕新的原点旋转  这里就相当于设置了一个旋转用的圆心ctx.rotate(Math.PI / 180 * 45);ctx.translate(-110, -40)ctx.fillStyle = '#88f';ctx.fillRect(80, 10, 60, 60);
}
draw();

上面的代码段实现了矩形绕默认原点旋转以及绕通过translate(x, y)新设置的原点旋转,结果如下:

在这里插入图片描述

7.3 缩放 Scaling

使用scale(x, y)实现缩放,我们用它来增减图形在canvas中的像素数目,对形状,位图进行缩小或者放大。

scale方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x为水平缩放因子,y为垂直缩放因子,如果比1小,会缩放图形,如果比1大会放大图形。默认值为1,为实际大小。

画布初始情况下,是以左上角坐标为原点的第一象限。如果参数为负实数,相当于以 x 或 y 轴作为对称轴镜像反转(例如,使用translate(0,canvas.height); scale(1,-1); 以 y 轴作为对称轴镜像反转,就可得到著名的笛卡尔坐标系,左下角为原点)。

默认情况下,canvas 的 1 个单位为 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍

下面我们看案例:

function draw() {const canvas = document.getElementById('canvas');const ctx = canvas.getContext('2d');ctx.save()// 参照矩阵ctx.fillRect(1, 10, 10, 10)// 设置缩放因子x为10 y为3 则canvas的一个单位[默认是一个像素]就会变成x方向上10像素,y方向上3像素ctx.scale(10, 3)// 因此第二个矩形相对于为缩放前的canvas的参数应该是(10, 30, 100, 30)ctx.fillRect(1, 10, 10, 10)ctx.restore()ctx.font = '20px serif'ctx.save()// 参照文字ctx.fillText('canvas', 80, 80)// 水平镜像ctx.scale(-1, 1)ctx.fillText('canvas', -80, 100)ctx.restore()// 垂直镜像ctx.scale(1, -1)ctx.fillText('canvas', 80, -120)
}
draw();

上面代码段画了一个初识盒子并按照x方向10倍、y方向3倍缩放盒子,以及实现了文字的水平镜像和垂直镜像,结果如下:

在这里插入图片描述

八、综合案例:多功能画板

接下来,我们结合前面章节所学的知识,使用原生html和js实现一个画板,画板要具有保存图片、清除画板、设置画笔粗细功能。

结果界面展示:

在这里插入图片描述

功能演示:
在这里插入图片描述

保存的图片展示:

在这里插入图片描述

下面我们看代码:

drawBoard.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>canvas</title><style>* {padding: 0;margin: 0;box-sizing: border-box;}.main-box {width: 100%;height: 100vh;position: relative;overflow: hidden;user-select: none;.bgc {z-index: -1;position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);object-fit: cover;}.operate-box {height: 32px;line-height: 30px;display: flex;/* column-gap: 20px; */padding-left: 10px;font-size: 20px;background-color: rgba(255, 255, 255, .9);.title {font-weight: 600;padding: 0 10px;margin-right: 20px;}.btn {cursor: pointer;padding: 0 5px;}.btn:hover {background-color: rgba(204, 204, 204, 0.8);}}.canvas-box {position: absolute;top: calc(50% + 16px);left: 50%;transform: translate(-50%, -50%);border-radius: 10px;background-color: #fff;padding: 10px;box-shadow: 4px 5px 10px rgba(0, 0, 0, .7);}}.dialog {width: 100vw;height: 100vh;position: absolute;top: 0;z-index: 2;display: none;/* 文字无法选中 */user-select: none;.setting-box {width: 400px;height: 290px;position: relative;left: 50%;top: 50%;transform: translate(-50%, -50%);border: 1px solid #ccc;border-radius: 10px;padding: 10px;display: flex;flex-direction: column;background-color: #fff;.header {display: flex;align-items: center;justify-content: space-between;font-size: 20px;.close {width: 24px;height: 24px;cursor: pointer;}}.content {display: flex;flex-direction: column;align-items: center;justify-content: center;flex: 1;.item {width: 100%;display: flex;align-items: center;.item-name {width: 80px;text-align: right;font-size: 18px;}.item-value {display: flex;align-items: center;.sub,.add {font-size: 26px;height: 20px;line-height: 18px;cursor: pointer;}.sub {margin: 0 6px 0 10px;}.add {margin: 0 10px 0 6px;}.progress-box {height: 20px;display: flex;align-items: center;width: 180px;cursor: pointer;.horizontal-line {position: relative;width: 100%;height: 4px;border: 1px solid #000;.vertical-line {position: absolute;top: 50%;transform: translateY(-50%);height: 20px;width: 4px;background-color: #000;border: 1px solid #000;}}}}}}.footer {display: flex;align-items: center;padding: 0 100px;justify-content: space-between;font-size: 20px;.setting-confirm,.setting-reset {text-align: center;width: 60px;border: 1px solid #ccc;border-radius: 4px;cursor: pointer;}}}}</style>
</head><body><div class="main-box"><!-- 背景图 --><img class="bgc" src="./bgc_1.jpg" alt=""><!-- 操作栏 --><div class="operate-box"><div class="title">这是一块画板</div><div class="btn" id="setting">设置</div><div class="btn" id="clear">清除</div><div class="btn" id="save">保存</div></div><div class="canvas-box"><!-- 画布 --><canvas id="canvas"><!-- 如果当前浏览器不支持canvas,将会显示盒子内的内容作为兜底提示 -->current browoser not support canvas</canvas></div></div><div class="dialog" id="dialog"><div class="setting-box"><div class="header"><div class="theme">设置</div><img class="close" id="close" src="./close_icon.png" alt="关闭按钮"></img></div><div class="content"><div class="item"><div class="item-name">线粗:</div><div class="item-value"><div class="sub" id="sub">-</div><div class="progress-box" id="line"><div class="horizontal-line"><div class="vertical-line" id="progress"></div></div></div><div class="add" id="add">+</div><div class="value" id="line_width_value"></div></div></div></div><div class="footer"><div class="setting-confirm" id="setting_save">保存</div><div class="setting-reset" id="setting_reset">重置</div></div></div></div><script src="./drawBoard.js"></script>
</body></html>

drawBoard.js

let canvasWidth = window.innerWidth * 0.8;
let canvasHeight = window.innerHeight * 0.88;// 拿到画板、重置按钮、保存按钮的dom元素
let canvas = document.getElementById("canvas");
let clear = document.getElementById("clear");
let save = document.getElementById("save");
// 设置相关数据
let setting = document.getElementById("setting"); // 设置按钮dom元素
let close = document.getElementById("close"); // 关闭设置按钮dom元素
let line_width_value = document.getElementById("line_width_value"); // 当前值dom元素
let add = document.getElementById("add"); // 加按钮dom元素
let sub = document.getElementById("sub"); // 减按钮dom元素
let line = document.getElementById("line"); // 进度线dom元素
let progress = document.getElementById("progress"); // 进度线标签dom元素
let setting_save = document.getElementById("setting_save"); // 保存按钮dom元素
let setting_reset = document.getElementById("setting_reset"); // 重置按钮dom元素
let defaultConfig = {// 默认设置数据lineWidth: 3,
};let tempConfig = {// 临时存的设置数据lineWidth: defaultConfig.lineWidth,
};
// 初始化画板宽高
canvas.width = canvasWidth;
canvas.height = canvasHeight;// 监听窗口大小变化 实现canvas大小自适应
window.addEventListener("resize", () => {canvasWidth = window.innerWidth * 0.9;canvasHeight = window.innerHeight * 0.88;canvas.width = canvasWidth;canvas.height = canvasHeight;
});let startTag = false; // 开始绘画的标识 鼠标按下设为true 鼠标弹起设为false
let ctx = canvas.getContext("2d");// 初始化
ctx.lineWidth = defaultConfig.lineWidth; // 初始化canvas路径宽度
line_width_value.innerText = ctx.lineWidth;
progress.style.left = (ctx.lineWidth / 10) * 100 + "%";
ctx.fillStyle = "#fff"; // 初始化canvas背景色
ctx.fillRect(0, 0, canvasWidth, canvasHeight); // 填个矩形 避免下载下来的图片背景是镂空的
// 监听鼠标按下事件
canvas.addEventListener("mousedown", (e) => {// 新建一条路径ctx.beginPath();// 设置路径原点为鼠标按下出ctx.moveTo(e.offsetX, e.offsetY);startTag = true;
});// 监听鼠标移动事件
canvas.addEventListener("mousemove", (e) => {if (!startTag) return;// 生成路径ctx.lineTo(e.offsetX, e.offsetY);ctx.stroke(); // 绘制当前或已经存在的路径的方法 也就是让路径看得到 默认是黑色的
});canvas.addEventListener("mouseup", (e) => {ctx.closePath();startTag = false;
});// 清除事件
clear.onclick = () => {ctx.fillStyle = "#fff";ctx.fillRect(0, 0, canvasWidth, canvasHeight);
};// 保存图片事件
save.onclick = () => {let img = canvas.toDataURL("image/jpg");let a = document.createElement("a");a.href = img;a.download = "画板.png";a.target = "_blank"; // a标签的属性target _blank表示在新标签页打开a.click();
};// 控制设置弹窗的显示
setting.onclick = () => {dialog.style.display = "block";
};
// 控制设置弹窗的隐藏
close.onclick = () => {ctx.lineWidth = tempConfig.lineWidth;line_width_value.innerText = ctx.lineWidth;progress.style.left = (ctx.lineWidth / 10) * 100 + "%";dialog.style.display = "none";
};// +
add.onclick = () => {if (ctx.lineWidth < 10) {ctx.lineWidth = ctx.lineWidth + 1;line_width_value.innerText = ctx.lineWidth;progress.style.left = (ctx.lineWidth / 10) * 100 + "%";}
};// -
sub.onclick = () => {if (ctx.lineWidth > 1) {ctx.lineWidth = ctx.lineWidth - 1;line_width_value.innerText = ctx.lineWidth;progress.style.left = (ctx.lineWidth / 10) * 100 + "%";}
};// 重置 恢复默认数据
setting_reset.onclick = () => {ctx.lineWidth = defaultConfig.lineWidth;line_width_value.innerText = ctx.lineWidth;progress.style.left = (ctx.lineWidth / 10) * 100 + "%";
};// 保存 设置数据
setting_save.onclick = () => {tempConfig.lineWidth = ctx.lineWidth;dialog.style.display = "none";
};// 点击进度条更改值
line.addEventListener("click", (e) => {ctx.lineWidth = ((e.offsetX / 180) * 10).toFixed()? ((e.offsetX / 180) * 10).toFixed(): 1;line_width_value.innerText = ctx.lineWidth;progress.style.left = (ctx.lineWidth / 10) * 100 + "%";
});

代码上传github了:canvas实现画板,欢迎大家光临我的空间!

九、综合案例:时钟

下面我们再实现一个案例——时钟

界面展示如下:

在这里插入图片描述

效果展示如下:
在这里插入图片描述

下面我们看代码:

clock.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>clock</title><style>* {margin: 0;padding: 0;}.main-box {width: 100%;height: 100vh;display: flex;align-items: center;justify-content: center;overflow: hidden;#canvas {border: 1px solid #000;}}</style>
</head><body><div class="main-box"><canvas id="canvas" width="600" height="600"></canvas></div><script src="./clock.js"></script>
</body></html>

clock.js

/*** window.requestAnimationFrame()告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画* 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行*/
let animate = (time) => {let now = new Date();let hour = now.getHours();let minus = now.getMinutes();let second = now.getSeconds();hour = hour % 12;let ctx = document.getElementById("canvas").getContext("2d");ctx.clearRect(0, 0, 600, 600);// 保存默认状态ctx.save();ctx.lineWidth = 10;// 画个圆ctx.beginPath();ctx.arc(300, 300, 250, 0, Math.PI * 2);ctx.stroke();// 画完圆后恢复到默认状态ctx.restore();// 画小时数字ctx.save();ctx.lineWidth = 2;ctx.translate(300, 300);ctx.beginPath();ctx.font = "48px serif";for (let i = 0; i < 12; i++) {let x = Math.sin((Math.PI / 6) * (i + 1)) * 180;let y = Math.cos((Math.PI / 6) * (i + 1)) * 180 * -1;  // 注意 这里一定要乘以负一// console.log(x, "x", y, "y");ctx.textAlign = "center";ctx.textBaseline = "middle";ctx.strokeText((i + 1).toString(), x, y);}ctx.restore();// 保存默认状态ctx.save();// canvas初始化:原点移到canvas中心,旋转-90度,因为默认选择是从左上角原点y轴方向开始旋转的ctx.translate(300, 300);ctx.rotate(-Math.PI / 2);ctx.lineCap = "round";// 画小时刻度// 保存初始化ctx.save();ctx.lineWidth = 8;ctx.beginPath();for (let i = 0; i < 12; i++) {ctx.rotate((Math.PI / 180) * 30);ctx.moveTo(210, 0);ctx.lineTo(250, 0);ctx.stroke();}ctx.restore();// 画分钟刻度ctx.save();ctx.lineWidth = 4;ctx.beginPath();for (let i = 0; i < 60; i++) {ctx.rotate((Math.PI / 180) * 6);ctx.moveTo(230, 0);ctx.lineTo(250, 0);ctx.stroke();}ctx.restore();// 画时针ctx.save();ctx.beginPath();ctx.lineWidth = 20;ctx.rotate((Math.PI / 6) * (hour + minus / 60 + second / 3600));ctx.moveTo(-60, 0);ctx.lineTo(140, 0);ctx.stroke();ctx.restore();// 画分针ctx.save();ctx.beginPath();ctx.lineWidth = 14;ctx.rotate((Math.PI / 30) * (minus + second / 60));ctx.moveTo(-60, 0);ctx.lineTo(190, 0);ctx.stroke();ctx.restore();// 画秒针ctx.save();ctx.beginPath();ctx.lineWidth = 10;ctx.rotate((Math.PI / 30) * second);ctx.moveTo(-60, 0);ctx.lineTo(220, 0);ctx.strokeStyle = "#f00";ctx.stroke();ctx.restore();// 画圆心ctx.save();ctx.beginPath();ctx.fillStyle = "#f00";ctx.arc(0, 0, 15, 0, Math.PI * 2);ctx.fill();ctx.restore();ctx.restore();window.requestAnimationFrame(animate);
};window.requestAnimationFrame(animate);

需要注意的是:

  1. 在修改默认状态前一定要先save状态
  2. 每画一部分内容记得restore状态,并且使用beiginPath重新开始绘制,否则画出的结果将会出现奇怪的内容,比如页面无限闪动、偏移等问题

源码同样上传了github:canvas实现clock。欢迎访问!

十、更多内容

以上是我所介绍的canvas所有基础内容。

更多内容建议查看MDN——canvas教程。

相关文章:

Canvas—从入门到案例实现

文章目录 Canvas—从入门到案例实现一、设置canvas环境1.1 <canvas>元素1.2 渲染上下文context 二、形状与路径的绘制2.1 形状绘制2.2 路径绘制2.3 绘制一个笑脸 三、使用样式和颜色四、绘制文本五、使用图像5.1 图片源5.2 获取页面内的图片5.3 缩放Scaling5.4 切片Slici…...

飞书开发学习笔记(六)-网页应用免登

飞书开发学习笔记(六)-网页应用免登 一.上一例的问题修正 在上一例中&#xff0c;飞书登录查看网页的界面显示是有误的&#xff0c;看了代码&#xff0c;理论上登录成功之后&#xff0c;应该显示用户名等信息。 最后的res.nickName是用户名&#xff0c;res.i18nName.en_us是英…...

【ROS】Nav2源码下载、编译、运行

【ROS】郭老二博文之:ROS目录 1、源码下载 1.1 源码地址 https://github.com/ros-planning/navigation2 1.2 创建工程目录 ROS2使用目录结果来管理项目,因此在下载前需要创建好目录结构: mkdir -p ~/git/nav2/src1.3 下载 git中默认版本是main。本人的开发环境为Ubun…...

微信小程序 30分钟倒计时功能

ps:凑个数 getTimeDiff(date) {let _this = this;let curTime = new Date(date)_this.countDown(_this.timeFormatConvert(new Date(curTime.setMinutes(curTime.getMinutes() + 30))))},timeFormatConvert(e) {const Y = e.getFullYear(); // 年const M = this.prefixZero(e.…...

JAVA判断指定日期是否在指定的时间段内

参考文献: Java语言判断当前时间在时间范围内_java判断时间区间-CSDN博客 package com.itheima.method2;import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date;public class DateTest {public static voi…...

关于晋升与跳槽的一些思考

内部晋升 内部晋升是我优先考虑的&#xff0c;原因有很多。首先这是一个新业务&#xff0c;相对而言容易拿到结果。其次我想体验不同的晋升路径&#xff0c;内部晋升答辩&#xff0c;是挑战也是一次成长的机会&#xff0c;是一次他人帮助自己review的机会。作为从校园出来的校…...

url找不到404的问题,url被拼接

今天遇到一个测试feign调用的功能&#xff0c;如图所示 先说结论 Controller换成RestController 将日志设置为debug模式 被DispatcherServlet FORWARD了 找到路径 对属性设置断点&#xff0c;看下是哪注进来的 我们再去找encodedPath 此处是undertow的源码&#xff0c;但是und…...

如何解决golang开发中遇到的报错:checksum mismatch downloaded

问题描述 如题&#xff0c;项目开发中遇到如下报错&#xff08;你的报错信息可能与我的有一点区别&#xff0c;如verifying的包名&#xff0c;但是问题本质都是一样的&#xff09;&#xff1a; verifying github.com/algorand/go-codec/codecv1.1.8/go.mod: checksum mismatc…...

4.以docker容器生成镜像推送到阿里云镜像仓库

1.开通阿里云镜像仓库 1.1 登录阿里云&#xff0c;访问容器镜像服务。地址如下&#xff1a; https://cr.console.aliyun.com/cn-shanghai/instances 1.2 个人学习为例&#xff0c;创建个人版实例 1.2.1 点击个人实例 1.2.2 .创建个人实例 1.2.3 创建完成后&#xff0c;设置…...

CSS Form表单布局

效果图 <Tab IsCard"true"><TabItem Text"表单信息-DIV版本"><div class"row"><div class"col"><label for"field1">工程名称:</label><input class"form-control" type&…...

c++ shared_mutex 读写锁使用详解

c 读写锁使用详解 std::shared_mutex c17 头文件 #include <shared_mutex>。用于实现共享和独占访问的互斥锁。提供了一种更加灵活的机制&#xff0c;允许多个线程在共享模式下读取数据&#xff0c;但只允许单个线程在独占模式下写入或修改数据。与 std::mutex 相比&am…...

淘宝商品详情接口,淘宝详情页接口,宝贝详情页接口,商品属性接口,商品信息查询,商品详细信息接口,h5详情,淘宝API接口演示案例

淘宝详情接口API可以帮助简化运营流程&#xff0c;更加专注于产品本身。通过调用API&#xff0c;可以快速获取到商品的标题、图片、价格等信息&#xff0c;省去了手动编写和编辑的繁琐过程。这样就可以更快地上架新品、更新商品信息&#xff0c;提高运营的效率。 taobao.item_…...

python爬取网站数据,作为后端数据

一. 内容简介 python爬取网站数据&#xff0c;作为后端数据 二. 软件环境 2.1vsCode 2.2Anaconda version: conda 22.9.0 2.3代码 链接&#xff1a; 三.主要流程 3.1 通过urllib请求网站 里面用的所有的包 ! pip install lxml ! pip install selenium ! pip install…...

【机器学习】K近邻算法:原理、实例应用(红酒分类预测)

案例简介&#xff1a;有178个红酒样本&#xff0c;每一款红酒含有13项特征参数&#xff0c;如镁、脯氨酸含量&#xff0c;红酒根据这些特征参数被分成3类。要求是任意输入一组红酒的特征参数&#xff0c;模型需预测出该红酒属于哪一类。 1. K近邻算法介绍 1.1 算法原理 原理&a…...

基于安卓android微信小程序的快递取件及上门服务系统

项目介绍 本文从管理员、用户的功能要求出发&#xff0c;快递取件及上门服务中的功能模块主要是实现管理员服务端&#xff1b;首页、个人中心、用户管理、快递下单管理、预约管理、管理员管理、系统管理、订单管理&#xff0c;用户客户端&#xff1b;首页、快递下单、预约管理…...

leetCode 92.反转链表 II + 图解

92. 反转链表 II - 力扣&#xff08;LeetCode&#xff09; 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 206. 反转链表 - 力扣&#xff08;LeetCode&am…...

【MongoDB】索引 – 通配符索引

一、准备工作 这里准备一些数据 db.books.drop();db.books.insert({_id: 1, name: "Java", alias: "java 入门", description: "入门图书" }); db.books.insert({_id: 2, name: "C", alias: "c", description: "C 入…...

python安装pip install报错Could not fetch URL https://pypi.org/simple/pip/...更换镜像源

更换镜像源 一. 现象pycharm使用 pip install xxx安装包时&#xff0c;一直报错&#xff1a; 二. 原因&#xff1a;三. 解决办法&#xff1a;一. 临时使用二. 永久更改三. 永久更改1. Windowswindows环境下Windows&#xff08;示例win10&#xff09; 2. Linux or Mac3. Pycharm…...

C++ 算数运算符 学习资料

C 算数运算符 在 C 中&#xff0c;算数运算符用于执行各种数学运算。以下是常用的算数运算符&#xff1a; &#xff1a;加法运算符&#xff0c;用于将两个表达式相加。-&#xff1a;减法运算符&#xff0c;用于从一个表达式中减去另一个表达式。*&#xff1a;乘法运算符&…...

问题 H: 棋盘游戏(二分图变式)

题意&#xff1a;要求找到 不放车就无法达到最大数的点 的个数 题解&#xff1a;1.以行列绘制二分图 2.先算出最大二分匹配数 3.依次遍历所有边 删除该边&#xff0c;并计算二分匹配最大值 &#xff08;若小于原最大值即为重要点&#xff09;&#xff0…...

web vue 项目 Docker化部署

Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段&#xff1a; 构建阶段&#xff08;Build Stage&#xff09;&#xff1a…...

DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径

目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

rknn优化教程(二)

文章目录 1. 前述2. 三方库的封装2.1 xrepo中的库2.2 xrepo之外的库2.2.1 opencv2.2.2 rknnrt2.2.3 spdlog 3. rknn_engine库 1. 前述 OK&#xff0c;开始写第二篇的内容了。这篇博客主要能写一下&#xff1a; 如何给一些三方库按照xmake方式进行封装&#xff0c;供调用如何按…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

工程地质软件市场:发展现状、趋势与策略建议

一、引言 在工程建设领域&#xff0c;准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具&#xff0c;正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3

一&#xff0c;概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本&#xff1a;2014.07&#xff1b; Kernel版本&#xff1a;Linux-3.10&#xff1b; 二&#xff0c;Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01)&#xff0c;并让boo…...

sipsak:SIP瑞士军刀!全参数详细教程!Kali Linux教程!

简介 sipsak 是一个面向会话初始协议 (SIP) 应用程序开发人员和管理员的小型命令行工具。它可以用于对 SIP 应用程序和设备进行一些简单的测试。 sipsak 是一款 SIP 压力和诊断实用程序。它通过 sip-uri 向服务器发送 SIP 请求&#xff0c;并检查收到的响应。它以以下模式之一…...

C#中的CLR属性、依赖属性与附加属性

CLR属性的主要特征 封装性&#xff1a; 隐藏字段的实现细节 提供对字段的受控访问 访问控制&#xff1a; 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性&#xff1a; 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑&#xff1a; 可以…...

uniapp手机号一键登录保姆级教程(包含前端和后端)

目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号&#xff08;第三种&#xff09;后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...