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 here
                ctx.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 here
                alert('当前浏览器不支持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 here
        ctx.fillRect(25, 25, 100, 100)  // 填充矩形
        ctx.clearRect(45, 45, 60, 60)  // 透明/空心矩形
        ctx.strokeRect(50, 50, 50, 50)  // 轮廓矩形
    }else {
        // canvas-unsupported code here
        alert('当前浏览器不支持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.5
    ctx.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教程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/148307.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深度学习+opencv+python实现车道线检测 - 自动驾驶 计算机竞赛

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 YOLOV56 数据集处理7 模型训练8 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &am…

Vue3使用i18n国际化

安装 npm install vue-i18nnext 创建i18n文件夹 我这个项目是中、俄语言切换 zh.ts里放中文语言下要显示的字段&#xff0c;rn.ts里放俄语要显示的字段 index.ts import { createI18n } from vue-i18n; import ZH from ./zh.js; import RN from ./rn.js; const messages {zh…

远程创建分支本地VScode看不到分支

在代码存放处右击&#xff0c;点击Git Bash Here 输入git fetch–从远程仓库中获取最新的分支代码和提交历史 就OK啦&#xff0c;现在分支可以正常查看了

【SpringBoot3+Vue3】二【实战篇】-后端

目录 一、环境搭建 1、数据库脚本 2、pom 3、yml 4、通过mybatis-X生成实体pojo等 4.1 Article 4.2 Category 4.3 User 5、 Mapper 5.1 ArticleMapper 5.2 CategoryMapper 5.3 UserMapper 6、service 6.1 ArticleService 6.2 CategoryService 6.3 UserService …

使用亚马逊鲲鹏系统有什么好处?

亚马逊鲲鹏系统是一款能绕过亚马逊智能检测&#xff0c;完全模拟人类真实行为&#xff0c;通过模拟真实的人流量来帮助你提升你的产品排名&#xff0c;让你的产品出现在搜索首页&#xff0c;从而快速帮助提高销售业绩的营销工具&#xff01; 好处1&#xff1a;自动化操作更节约…

Fabric多机部署启动节点与合约部署

这是我搭建的fabric的网络拓扑 3 个 orderer 节点&#xff1b;组织 org1 , org1 下有两个 peer 节点&#xff0c; peer0 和 peer1; 组织 org2 , org2 下有两个 peer 节点&#xff0c; peer0 和 peer1; 以上是我的多机环境的网络拓扑&#xff0c;使用的是docker搭建的。我的网络…

什么是数据泄露?泄露途径有哪些?企业如何免遭数据泄露?

数据泄露指将机密信息、私人信息或其他敏感信息发布到不安全的环境中。数据泄露可能由意外引起&#xff0c;也可能是蓄意攻击的结果。 每年都有数百万人卷入数据泄露&#xff0c;包括意外看错病人图表的医生&#xff0c;以及大规模尝试访问政府计算机以发现敏感信息。 因为敏…

向量矩阵范数pytorch

向量矩阵范数pytorch 矩阵按照某个维度求和&#xff08;dim就是shape数组的下标&#xff09;1. torch1.1 Tensors一些常用函数 一些安装问题cd进不去不去目录PyTorch里面_表示重写内容 在默认情况下&#xff0c;PyTorch会累积梯度&#xff0c;我们需要清除之前的值 范数是向量或…

企业级真实应用利用Mybatis-Plus进行分页查询处理

怎么导入依赖我在之前的文章里边有说过不理解的可以看看 你应该懂点Mybatis-plus&#xff0c;真的好用 1&#xff1a;了解Page<T>类的使用 首先我们需要使用到Page类 &#xff0c;建立一个Page类&#xff0c;泛式类型中放入我们需要输出的类&#xff0c;是列表的话就…

分享5款好用到爆的神仙软件

​ 最近陆陆续续收到好多小伙伴的咨询&#xff0c;这边也是抓紧时间整理出几个好用的软件&#xff0c;希望可以帮到大家。 1.全局鼠标手势——MouseInc ​ MouseInc是一款由shuax制作的全局鼠标手势软件&#xff0c;还支持很多增强辅助功能&#xff0c;如屏幕取色、窗口管理、…

前端学习笔记--Event-loop

定义 Event Loop&#xff1a;即事件循环&#xff0c;是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制&#xff0c;也就是我们经常使用异步的原理。 **进程&#xff1a;**进程是计算机中的程序关于某数据集合上的一次运行活动&#xff0c;是系统进行资源分…

vmware安装MacOS以及flutter遇到的问题

安装过程&#xff1a;参考下面的文章 链接&#xff1a; 虚拟机VMware安装苹果系统macOS&#xff0c;超级详细教程&#xff0c;附文件下载&#xff0c;真教程&#xff01;&#xff01; 无限重启情况&#xff1a; &#xff08;二&#xff09; 配置虚拟机找到你的虚拟机安装文件…

查询站点真实IP地址,绕过CDN

一.如何判断站点是否使用了CDN&#xff1f; 使用其他省市的电脑进行ping看返回的IP地址是否相同通过第三方网站查询 站长工具 3.nslookup命令 二. 如何绕过CDN获取真实IP 子域名查询&#xff0c;因为很多站点只对主域名进行了CDN加速网站邮件头信息微步在线DNS查询

[PyTorch][chapter 63][强化学习-QLearning]

前言&#xff1a; 这里结合走迷宫的例子,重点学习一下QLearning迭代更新算法 0,1,2,3,4 是房间&#xff0c;之间绿色的是代表可以走过去。 5为出口 可以用下图表示 目录&#xff1a; 策略评估 策略改进 迭代算法 走迷宫实现Python 一 策略评估 强化学习最终是为了…

算法通关村——数组中第K大的数字

数组中第K大的数字 1、题目描述 ​ LeetCode215. 数组中的第K个最大元素。给定整数数组nums和整数k&#xff0c;请返回数组中第k个最大的元素。请注意&#xff0c;你需要找的是数组排序后的第k个最大的元素&#xff0c;而不是第k个不同的元素。 示例1&#xff1a; 输入&#…

LLM prompt提示工程调试方法经验技巧汇总

现在接到一个LLM模型任务&#xff0c;第一反应就是能不能通过精调prompt来实现&#xff0c;因为使用prompt不需要训练模型&#xff0c;只需输入指令就可以实现和LLM的交互。按照以往经验&#xff0c;不同的prompt对模型输出影响非常大&#xff0c;如果能构造一个好的prompt&…

【23真题】厉害,这套竟有150分满分!

今天分享的是23年中国海洋大学946的信号与系统试题及解析。 本套试卷难度分析&#xff1a;22年中国海洋大学946考研真题&#xff0c;我也发布过&#xff0c;若有需要&#xff0c;戳这里自取!平均分为109-120分&#xff0c;最高分为150分满分&#xff01;本套试题内容难度中等&…

【vue】 实现 自定义 Calendar 日历

图例&#xff1a;自定义日历 一、标签自定义处理 <div class"date-box"><el-calendar v-model"state.currDate" ref"calendar"><template #header"{ date }"><div class"date-head flex"><div …

Golang获取月份的第一天和最后一天

package mainimport ("fmt""strconv""strings""time" )func main() {month : "2023-11"result : GetMonthStartAndEnd(month)fmt.Println(result["start"] " - " result["end"]) }// 获取月…

图形化探索:快速改造单实例为双主、MGR、读写分离等架

单机GreatSQL/MySQL调整架构为多副本复制的好处有哪些&#xff1f;为什么要调整&#xff1f; 性能优化&#xff1a;如果单个GreatSQL服务器的处理能力达到瓶颈&#xff0c;可能需要通过主从复制、双主复制或MGR&#xff0c;以及其他高可用方案等来提高整体性能。通过将读请求分…