Canvas里,所有图形都绘制在帧上,绘制方法不会将绘制好的图形元素作为一个返回值输出,js也无法获取到已经绘制好的图形元素,在Canvas中绘制的图形都是一个整体。
假设用Canvas绘制了一个图形,要判断一个事件是否发生在该图形上,有个isPointInPath方法,但是该方法仅判断当前上下文环境中的路径,所以当Canvas里已经绘制了多个图形时,仅能以最后一个图形的上下文环境来判断事件。这种问题的解决方法是:当事件发生时,重绘所有图形,每绘制一个就使用isPointInPath方法,判断事件坐标是否在该图形覆盖范围内。当图形过多时,为了在性能和视觉效果上达到更好的效果,我们可以采取一种数学方法。
多个矩形框
如图,红色部分是由很多个矩形框拼接而成的平滑区域。需求是当鼠标移至矩形框上时出现文本提示框来显示当前是第几个矩形。下图是这些矩形框的坐标:
坐标
接下来绘制矩形框:
绘制
判断一个点是否在矩形框内的数学方法为:假设点为o,矩形为abcd,如果Soab+Sobc+Socd+Soda=Sabcd,则该点在矩形框内,否则不在。
判断
涉及到的函数有:计算两点之间距离、计算三角形面积、判断点是否在矩形内。当鼠标移动的时候,循环判断鼠标坐标点是否在矩形框内,在的话就出现提示框。为了减少判断、提高性能,创建一个函数用于计算矩形框的水平跨度,即X轴范围,当目标点不在该范围内就不做是否在矩形框内的判断。
事件处理程序
其中minmax是一个二维数组,保存着所有矩形框的X轴跨度。
当条件成立时,效果如下,即显示文本:
效果
家好! 欢迎来到本教程,我们将深入了解使用 HTML 画布和 JavaScript 在代码中创建有趣的气泡的世界。 最好的部分? 我们将只使用一点 HTML 和所有 JavaScript,而不是 CSS 来实现所有这一切。
今天,我们要掌握以下几个概念:
使用画布上下文的 arc 方法创建圆。
利用 requestAnimationFrame 函数实现平滑的圆形动画。
利用 JavaScript 类的强大功能来创建多个圆圈,而无需重复代码。
向我们的圆圈添加描边样式和填充样式以获得 3D 气泡效果。
你可以跟着我一起看,或者如果你想看源代码,可以使用最终的codepen
首先,我们需要一个 HTML5 Canvas 元素。 Canvas 是创建形状、图像和图形的强大元素。 这就是气泡将产生的地方。 让我们来设置一下 -
<canvas id="canvas"></canvas>
为了使用画布做任何有意义的事情,我们需要访问它的上下文。 Context 提供了在画布上渲染对象和绘制形状的接口。
让我们访问画布及其上下文。
const canvas=document.getElementById('canvas');
const context=canvas.getContext('2d');
我们将设置画布以使用整个窗口的高度和宽度 -
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
让我们通过添加一些 css 为画布提供一个漂亮的舒缓浅蓝色背景。 这是我们要使用的唯一 CSS。 如果您愿意,也可以使用 JavaScript 来完成此操作。
#canvas {
background: #00b4ff;
}
让我们进入有趣的部分。 我们将通过单击画布来创建气泡。 为了实现这一点,我们首先创建一个点击事件处理程序:
canvas.addEventListener('click', handleDrawCircle);
由于我们需要知道在画布上单击的位置,因此我们将在句柄 DrawCircle 函数中跟踪它并使用事件的坐标 -
//We are adding x and y here because we will need it later.
let x, y
const handleDrawCircle=(event)=> {
x=event.pageX;
y=event.pageY;
// Draw a bubble!
drawCircle(x, y);
};
为了创建圆圈,我们将利用画布上下文中可用的 arc 方法。 Arc 方法接受 x 和 y - 圆心、半径、起始角和结束角,对于我们来说,这将是 0 和 2* Math.PI,因为我们正在创建一个完整的圆。
const drawCircle=(x, y)=> {
context.beginPath();
context.arc(x, y, 50, 0, 2 * Math.PI);
context.strokeStyle='white';
context.stroke();
};
现在我们有了圆圈,让我们让它们移动,因为……
GIF
请记住,当我们创建圆时,我们使用了 arc 方法,它接受 x 和 y 坐标 - 圆的中心。 如果我们快速移动圆的 x 和 y 坐标,就会给人一种圆在移动的印象。 让我们试试吧!
//Define a speed by which to increment to the x and y coordinates
const dx=Math.random() * 3;
const dy=Math.random() * 7;//Increment the center of the circle with this speed
x=x + dx;
y=y - dy;
我们可以将其移至函数内 -
let x, y;
const move=()=> {
const dx=Math.random() * 3;
const dy=Math.random() * 7; x=x + dx;
y=y - dy;
};
为了让我们的圆圈无缝移动,我们将创建一个动画函数并使用浏览器的 requestAnimationFrame 方法来创建一个移动的圆圈。
const animate=()=> {
context.clearRect(0, 0, canvas.width, canvas.height);
move();
drawCircle(x,y); requestAnimationFrame(animate);
};//Don't forget to call animate at the bottom
animate();
现在我们已经创建了一个圆圈,是时候创建多个圆圈了!
但在我们创建多个圆圈之前,让我们准备一下我们的代码。为了避免重复我们的代码,我们将使用类并引入 Particle 类。 粒子是我们动态艺术作品和动画的构建块。 每个气泡都是一个粒子,具有自己的位置、大小、运动和颜色属性。 让我们定义一个 Particle 类来封装这些属性:
class Particle {
constructor(x=0, y=0) {}
draw() {
// Drawing the particle as a colored circle
// ...
} move() {
// Implementing particle movement
// ...
}
}
让我们将一些已设置的常量移至 Particle 类 -
class Particle {
constructor(x=0, y=0) {
this.x=x;
this.y=y;
this.radius=Math.random() * 50;
this.dx=Math.random() * 3;
this.dy=Math.random() * 7;
}
draw() {
// Drawing the particle as a colored circle
// ...
} move() {
// Implementing particle movement
// ...
}
}
draw 方法将负责在画布上渲染粒子。 我们已经在drawCircle中实现了这个功能,所以让我们将它移动到我们的类中并将变量更新为类变量
class Particle {
constructor(x=0, y=0) {
this.x=x;
this.y=y;
this.radius=Math.random() * 50;
this.dx=Math.random() * 3;
this.dy=Math.random() * 7;
this.color='white';
}
draw() {
context.beginPath();
context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
context.strokeStyle=this.color;
context.stroke(); context.fillStyle=this.color;
context.fill();
} move() {}
}
同样,让我们在类中移动 move 函数 -
move() {
this.x=this.x + this.dx;
this.y=this.y - this.dy;
}
现在,我们需要确保在事件处理程序中调用 Particle 类。
const handleDrawCircle=(event)=> {
const x=event.pageX;
const y=event.pageY;
const particle=new Particle(x, y);
};canvas.addEventListener('click', handleDrawCircle);
由于我们需要在 animate 函数中访问该粒子,以便调用其 move 方法,因此我们将该粒子存储在一个名为 molecularArray 的数组中。 当创建大量粒子时,这个数组也会很有帮助。 这是反映这一点的更新代码 -
const particleArray=[];
const handleDrawCircle=(event)=> {
const x=event.pageX;
const y=event.pageY; const particle=new Particle(x, y);
particleArray.push(particle);
};canvas.addEventListener('click', handleDrawCircle);
记得也要更新动画功能 -
此时,您将在屏幕上看到这个粒子 -
惊人的! 现在,到了有趣的部分! 让我们创建很多圆圈并设计它们的样式,使它们看起来像气泡。
为了创建大量气泡,我们将使用 for 循环创建粒子并将它们添加到我们在此处创建的粒子数组中。
const handleDrawCircle=(event)=> {
const x=event.pageX;
const y=event.pageY;
for (let i=0; i < 50; i++) {
const particle=new Particle(x, y);
particleArray.push(particle);
}
};canvas.addEventListener('click', handleDrawCircle);
在动画函数中,我们将通过清除画布并在新位置重新绘制粒子来不断更新画布。 这会给人一种圆圈在移动的错觉。
const animate=()=> {
context.clearRect(0, 0, canvas.width, canvas.height);
particleArray.forEach((particle)=> {
particle?.move();
particle?.draw();
}); requestAnimationFrame(animate);
};animate();
现在我们有了移动的气泡,是时候给它们添加颜色,使它们看起来像气泡了!
我们将通过向气泡添加渐变填充来实现此目的。 这可以使用 context.createRadialGradient 方法来完成。
const gradient=context.createRadialGradient(
this.x,
this.y,
1,
this.x + 0.5,
this.y + 0.5,
this.radius
);
gradient.addColorStop(0.3, 'rgba(255, 255, 255, 0.3)');
gradient.addColorStop(0.95, '#e7feff');context.fillStyle=gradient;
恭喜! 您刚刚仅使用 HTML Canvas 和 JavaScript 创建了一些超级有趣的东西。 您已经学习了如何使用 arc 方法、利用 requestAnimationFrame、利用 JavaScript 类的强大功能以及使用渐变设计气泡以实现 3D 气泡效果。
请随意尝试颜色、速度和大小,使您的动画真正独一无二。
请随意尝试颜色、速度和大小,使您的动画真正独一无二。
我希望您在学习本教程时能像我在创建它时一样获得乐趣。 现在,轮到你进行实验了。 我很想看看你是否尝试过这个以及你创造了什么。 与我分享您的代码链接,我很乐意查看。
点赞 + 关注 + 收藏=学会了
在前端领域,如果只是懂 Vue 或者 React ,未来在职场的竞争力可能会比较弱。
根据我多年在家待业经验来看,前端未来在 数据可视化 和 AI 这两个领域会比较香,而 Canvas 是数据可视化在前端方面的基础技术。
本文就用光的速度将 canvas 给入门了。
01.gif
要入门一个技术,前期最重要是快!所以本文只讲入门内容,能应付简单项目。深入的知识点会在其他文章讲解。
文章同时收录于小程序-互联网小兵,不止于前端,收各平台优质热门的技术文章(后端、移动端、算法、人工智能...),大家支持支持,点击前往体验!
CanvasSVG用JS动态生成元素(一个HTML元素)用XML描述元素(类似HTML元素那样,可用多个元素来描述一个图形)位图(受屏幕分辨率影响)矢量图(不受屏幕分辨率影响)不支持事件支持事件数据发生变化需要重绘不需要重绘
就上面的描述而言可能有点难懂,你可以打开 AntV 旗下的图形编辑引擎做对比。G6[1] 是使用 canvas 开发的,X6[2] 是使用 svg 开发的。
我的建议是:如果要展示的数据量比较大,比如一条数据就是一个元素节点,那使用 canvas 会比较合适;如果用户操作的交互比较多,而且对清晰度有要求(矢量图),那么使用 svg 会比较合适。
学习前端一定要动手敲代码,然后看效果展示。
起步阶段会用几句代码说明 canvas 如何使用,本例会画一条直线。
02.png
<!-- 1、创建 canvas 元素 -->
<canvas
id="c"
width="300"
height="200"
style="border: 1px solid #ccc;"
></canvas>
<script>
// 2、获取 canvas 对象
const cnv=document.getElementById('c')
// 3、获取 canvas 上下文环境对象
const cxt=cnv.getContext('2d')
// 4、绘制图形
cxt.moveTo(100, 100) // 起点坐标 (x, y)
cxt.lineTo(200, 100) // 终点坐标 (x, y)
cxt.stroke() // 将起点和终点连接起来
</script>
复制代码
moveTo 、 lineTo 和 stroke 方法暂时可以不用管,它们的作用是绘制图形,这些方法在后面会讲到~
canvas 有 默认的 宽度(300px) 和 高度(150px)
如果不在 canvas 上设置宽高,那 canvas 元素的默认宽度是300px,默认高度是150px。
canvas 元素提供了 width 和 height 两个属性,可设置它的宽高。
需要注意的是,这两个属性只需传入数值,不需要传入单位(比如 px 等)。
<canvas width="600" height="400"></canvas>
复制代码
使用 css 设置 canvas 的宽高,会出现 内容被拉伸 的后果!!!
03.png
<style>
#c {
width: 400px;
height: 400px;
border: 1px solid #ccc;
}
</style>
<canvas id="c"></canvas>
<script>
// 1、获取canvas对象
const cnv=document.getElementById('c')
// 2、获取canvas上下文环境对象
const cxt=cnv.getContext('2d')
// 3、绘制图形
cxt.moveTo(100, 100) // 起点
cxt.lineTo(200, 100) // 终点
cxt.stroke() // 将起点和终点连接起来
console.log(cnv.width) // 获取 canvas 的宽度,输出:300
console.log(cnv.height) // 获取 canvas 的高度,输出:150
</script>
复制代码
canvas 的默认宽度是300px,默认高度是150px。
最后出现的效果如上图所示。
线条的默认宽度是 1px ,默认颜色是黑色。
但由于默认情况下 canvas 会将线条的中心点和像素的底部对齐,所以会导致显示效果是 2px 和非纯黑色问题。
暂时只有 IE 9 以上才支持 canvas 。但好消息是 IE 已经有自己的墓碑了。
如需兼容 IE 7 和 8 ,可以使用 ExplorerCanvas [3]。但即使是使用了 ExplorerCanvas 仍然会有所限制,比如无法使用 fillText() 方法等。
在绘制基础图形之前,需要先搞清除 Canvas 使用的坐标系。
Canvas 使用的是 W3C 坐标系 ,也就是遵循我们屏幕、报纸的阅读习惯,从上往下,从左往右。
04.jpg
W3C 坐标系 和 数学直角坐标系 的 X轴 是一样的,只是 Y轴 的反向相反。
W3C 坐标系 的 Y轴 正方向向下。
最简单的起步方式是画一条直线。这里所说的 “直线” 是几何学里的 “线段” 的意思。
需要用到这3个方法:
起步阶段可以先这样理解。
05.png
<canvas id="c" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 绘制直线
cxt.moveTo(50, 100) // 起点坐标
cxt.lineTo(200, 50) // 下一个点的坐标
cxt.stroke() // 将上面的坐标用一条线连接起来
</script>
复制代码
上面的代码所呈现的效果,可以看下图解释(手不太聪明,画得不是很标准,希望能看懂)
06.jpg
如需画多条直线,可以用会上面那几个方法。
07.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(20, 100)
cxt.lineTo(200, 100)
cxt.stroke()
cxt.moveTo(20, 120.5)
cxt.lineTo(200, 120.5)
cxt.stroke()
</script>
复制代码
仔细观察一下,为什么两条线的粗细不一样的?
明明使用的方法都是一样的,只是第二条直线的 Y轴 的值是有小数点。
答:默认情况下 canvas 会将线条的中心点和像素的底部对齐,所以会导致显示效果是 2px 和非纯黑色问题。
08.jpg
上图每个格子代表 1px。
线的中心点会和画布像素点的底部对齐,所以会线中间是黑色的,但由于一个像素就不能再切割了,所以会有半个像素被染色,就变成了浅灰色。
所以如果你设置的 Y轴 值是一个整数,就会出现上面那种情况。
09.png
<canvas id="c" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 绘制直线
cxt.moveTo(50, 50)
cxt.lineTo(200, 50)
// 修改直线的宽度
cxt.lineWidth=20
// 修改直线的颜色
cxt.strokeStyle='pink'
// 修改直线两端样式
cxt.lineCap='round' // 默认: butt; 圆形: round; 方形: square
cxt.stroke()
</script>
复制代码
开辟新路径的方法:
在绘制多条线段的同时,还要设置线段样式,通常需要开辟新路径。
要不然样式之间会相互污染。
比如这样
10.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 第一条线
cxt.moveTo(20, 100)
cxt.lineTo(200, 100)
cxt.lineWidth=10
cxt.strokeStyle='pink'
cxt.stroke()
// 第二条线
cxt.moveTo(20, 120.5)
cxt.lineTo(200, 120.5)
cxt.stroke()
</script>
复制代码
如果不想相互污染,需要做2件事:
如果上面2步却了其中1步都会有影响。
11.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 第一条线
cxt.moveTo(20, 100)
cxt.lineTo(200, 100)
cxt.lineWidth=10
cxt.strokeStyle='pink'
cxt.stroke()
// 第二条线
cxt.beginPath() // 重新开启一个路径
cxt.moveTo(20, 120.5)
cxt.lineTo(200, 120.5)
cxt.stroke()
</script>
复制代码
第一条线的样式会影响之后的线。
但如果使用了 beginPath() ,后面的线段不会影响前面的线段。
12.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 第一条线
cxt.moveTo(20, 100)
cxt.lineTo(200, 100)
cxt.stroke()
// 第二条线
cxt.beginPath() // 重新开启一个路径
cxt.moveTo(20, 120.5)
cxt.lineTo(200, 120.5)
cxt.lineWidth=4
cxt.strokeStyle='red'
cxt.stroke()
</script>
复制代码
这个情况会反过来,后面的线能影响前面的线。
13.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 第一条线
cxt.moveTo(20, 100)
cxt.lineTo(200, 100)
cxt.lineWidth=10
cxt.strokeStyle='pink'
cxt.stroke()
// 第二条线
cxt.moveTo(20, 120.5)
cxt.lineTo(200, 120.5)
cxt.lineWidth=4
cxt.strokeStyle='red'
cxt.stroke()
</script>
复制代码
在设置 beginPath() 的同时,也各自设置样式。这样就能做到相互不影响了。
14.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(20, 100)
cxt.lineTo(200, 100)
cxt.lineWidth=10
cxt.strokeStyle='pink'
cxt.stroke()
cxt.beginPath() // 重新开启一个路径
cxt.moveTo(20, 120.5)
cxt.lineTo(200, 120.5)
cxt.lineWidth=4
cxt.strokeStyle='red'
cxt.stroke()
</script>
复制代码
和 直线 差不多,都是使用 moveTo() 、lineTo() 和 stroke() 方法可以绘制折线。
15.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(50, 200)
cxt.lineTo(100, 50)
cxt.lineTo(200, 200)
cxt.lineTo(250, 50)
cxt.stroke()
</script>
复制代码
画这种折线,最好在草稿纸上画一个坐标系,自己计算并描绘一下每个点大概在什么什么位置,最后在 canvas 中看看效果。
根据前面的基础,我们可以 使用线段来描绘矩形,但 canvas 也提供了 rect() 等方法可以直接生成矩形。
可以使用前面画线段的方法来绘制矩形
16.png
canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 绘制矩形
cxt.moveTo(50, 50)
cxt.lineTo(200, 50)
cxt.lineTo(200, 120)
cxt.lineTo(50, 120)
cxt.lineTo(50, 50) // 需要闭合,又或者使用 closePath() 方法进行闭合,推荐使用 closePath()
cxt.stroke()
</script>
复制代码
上面的代码几个点分别对应下图。
17.jpg
18.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// strokeStyle 属性
// strokeRect(x, y, width, height) 方法
cxt.strokeStyle='pink'
cxt.strokeRect(50, 50, 200, 100)
</script>
复制代码
上面的代码可以这样理解
19.jpg
fillRect() 和 strokeRect() 方法差不多,但 fillRect() 的作用是填充。
需要注意的是,fillStyle 必须写在 fillRect() 之前,不然样式不生效。
20.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// fillStyle 属性
// fillRect(x, y, width, height) 方法
cxt.fillStyle='pink'
cxt.fillRect(50, 50, 200, 100) // fillRect(x, y, width, height)
</script>
复制代码
同时使用 strokeRect() 和 fillRect() 会产生描边和填充的效果
21.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.strokeStyle='red'
cxt.strokeRect(50, 50, 200, 100) // strokeRect(x, y, width, height)
cxt.fillStyle='yellow'
cxt.fillRect(50, 50, 200, 100) // fillRect(x, y, width, height)
</script>
复制代码
rect() 和 fillRect() 、strokeRect() 的用法差不多,唯一的区别是:
strokeRect() 和 fillRect() 这两个方法调用后会立即绘制;rect() 方法被调用后,不会立刻绘制矩形,而是需要调用 stroke() 或 fill() 辅助渲染。
22.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.strokeStyle='red'
cxt.fillStyle='pink'
cxt.rect(50, 50, 200, 100) // rect(x, y, width, height)
cxt.stroke()
cxt.fill()
</script>
复制代码
等价公式:
cxt.strokeStyle='red',
cxt.rect(50, 50, 200, 100)
cxt.stroke()
// 等价于
cxt.strokeStyle='red'
cxt.strokerect(50, 50, 200, 100)
// -----------------------------
cxt.fillStyle='hotpink'
cxt.rect(50, 50, 200, 100)
cxt.fill()
// 等价于
cxt.fillStyle='yellowgreen'
cxt.fillRect(50, 50, 200, 100)
复制代码
使用 clearRect() 方法可以清空指定区域。
clearRect(x, y, width, height)
复制代码
其语法和创建 cxt.rect() 差不多。
23.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.fillStyle='pink' // 设置填充颜色
cxt.fillRect(50, 50, 200, 200) // 填充矩形
cxt.clearRect(60, 60, 180, 90) // 清空矩形
</script>
复制代码
canvas 画布元素是矩形,所以可以通过下面的代码把整个画布清空掉。
// 省略部分代码
cxt.clearRect(0, 0, cnv.width, cnv.height)
复制代码
要清空的区域:从画布左上角开始,直到画布的宽和画布的高为止。
\
Canvas 要画多边形,需要使用 moveTo() 、 lineTo() 和 closePath() 。
虽然三角形是常见图形,但 canvas 并没有提供类似 rect() 的方法来绘制三角形。
需要确定三角形3个点的坐标位置,然后使用 stroke() 或者 fill() 方法生成三角形。
24.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(50, 50)
cxt.lineTo(200, 50)
cxt.lineTo(200, 200)
// 注意点:如果使用 lineTo 闭合图形,是不能很好闭合拐角位的。
cxt.lineTo(50, 50) // 闭合
cxt.stroke()
</script>
复制代码
注意,默认情况下不会自动从最后一个点连接到起点。最后一步需要设置一下 cxt.lineTo(50, 50) ,让它与 cxt.moveTo(50, 50) 一样。这样可以让路径回到起点,形成一个闭合效果。
但这样做其实是有点问题的,而且也比较麻烦,要记住起始点坐标。
上面的闭合操作,如果遇到设置了 lineWidth 或者 lineJoin 就会有问题,比如:
25.png
// 省略部分代码
cxt.lineWidth=20
复制代码
当线段变粗后,起始点和结束点的链接处,拐角就出现“不正常”现象。
如果需要真正闭合,可以使用 closePath() 方法。
26.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(50, 50)
cxt.lineTo(200, 50)
cxt.lineTo(200, 200)
// 手动闭合
cxt.closePath()
cxt.lineJoin='miter' // 线条连接的样式。miter: 默认; bevel: 斜面; round: 圆角
cxt.lineWidth=20
cxt.stroke()
</script>
复制代码
使用 cxt.closePath() 可以自动将终点和起始点连接起来,此时看上去就正常多了。
有一组邻边相等的平行四边形是菱形
27.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(150, 50)
cxt.lineTo(250, 100)
cxt.lineTo(150, 150)
cxt.lineTo(50, 100)
cxt.closePath()
cxt.stroke()
</script>
复制代码
要绘制直线类型的图形,在草稿纸上标记出起始点和每个拐角的点,然后再连线即可。相对曲线图形来说,直线图形是比较容易的。
绘制圆形的方法是 arc()。
语法:
arc(x, y, r, sAngle, eAngle,counterclockwise)
复制代码
28.jpg
开始角度和结束角度,都是以弧度为单位。例如 180°就写成 Math.PI ,360°写成 Math.PI * 2 ,以此类推。
在实际开发中,为了让自己或者别的开发者更容易看懂弧度的数值,1°应该写成 Math.PI / 180。
注意:绘制圆形之前,必须先调用 beginPath() 方法!!!在绘制完成之后,还需要调用 closePath() 方法!!!
29.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.beginPath()
cxt.arc(150, 150, 80, 0, 360 * Math.PI / 180)
cxt.closePath()
cxt.stroke()
</script>
复制代码
如果使用 arc() 方法画圆时,没做到刚好绕完一周(360°)就直接闭合路径,就会出现半圆的状态。
30.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.beginPath()
cxt.arc(150, 150, 100, 0, 180 * Math.PI / 180) // 顺时针
cxt.closePath()
cxt.stroke()
</script>
复制代码
上面的代码中,cxt.arc 最后一个参数没传,默认是 false ,所以是顺时针绘制。
31.jpg
如果希望半圆的弧面在上方,可以将 cxt.arc 最后一个参数设置成 true
32.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.beginPath()
cxt.arc(150, 150, 100, 0, 180 * Math.PI / 180, true)
cxt.closePath()
cxt.stroke()
</script>
复制代码
使用 arc() 方法画半圆时,如果最后不调用 closePath() 方法,就不会出现闭合路径。也就是说,那是一条弧线。
在 canvas 中,画弧线有2中方法:arc() 和 arcTo() 。
如果想画一条 0° ~ 30° 的弧线,可以这样写
33.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.beginPath()
cxt.arc(150, 150, 100, 0, 30 * Math.PI / 180)
cxt.stroke()
</script>
复制代码
原理如下图所示,红线代表画出来的那条弧线。
34.jpg
arcTo() 的使用方法会更加复杂,如果初学看不太懂的话可以先跳过,看完后面的再回来补补。
语法:
arcTo(cx, cy, x2, y2, radius)
复制代码
其中,(cx, cy) 也叫控制点,(x2, y2) 也叫结束点。
是不是有点奇怪,为什么没有 x1 和 y1 ?
(x1, y1) 是开始点,通常是由 moveTo() 或者 lineTo() 提供。
arcTo() 方法利用 开始点、控制点和结束点形成的家教,绘制一段与家教的两边相切并且半径为 radius 的圆弧。
35.jpg
举个例子
36.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(40, 40)
cxt.arcTo(120, 40, 120, 80, 80)
cxt.stroke()
</script>
复制代码
前面学完基础图形,接下来可以开始了解一下如何设置元素的基础样式。
前面的案例中,其实已经知道使用 stroke() 方法进行描边了。这里就不再多讲这个方法。
lineWidth 默认值是 1 ,默认单位是 px。
语法:
lineWidth=线宽
复制代码
37.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 线宽 10
cxt.beginPath()
cxt.moveTo(50, 50)
cxt.lineTo(250, 50)
cxt.lineWidth=10 // 设置线宽
cxt.stroke()
// 线宽 20
cxt.beginPath()
cxt.moveTo(50, 150)
cxt.lineTo(250, 150)
cxt.lineWidth=20 // 设置线宽
cxt.stroke()
// 线宽 30
cxt.beginPath()
cxt.moveTo(50, 250)
cxt.lineTo(250, 250)
cxt.lineWidth=30 // 设置线宽
cxt.stroke()
</script>
复制代码
使用 strokeStyle 可以设置线条颜色
语法:
strokeStyle=颜色值
复制代码
38.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.moveTo(50, 50)
cxt.lineTo(250, 50)
cxt.lineWidth=20
cxt.strokeStyle='pink' // 设置颜色
cxt.stroke()
</script>
复制代码
为了展示方便,我将 lineWidth 设为 20。
线帽指的是线段的开始和结尾处的样式,使用 lineCap 可以设置
语法:
lineCap='属性值'
复制代码
属性值包括:
39.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 设置线宽,方便演示
cxt.lineWidth=16
// 默认线帽 butt
cxt.beginPath()
cxt.moveTo(50, 60)
cxt.lineTo(250, 60)
cxt.stroke()
// 方形线帽 square
cxt.beginPath()
cxt.lineCap='square'
cxt.moveTo(50, 150)
cxt.lineTo(250, 150)
cxt.stroke()
// 圆形线帽 round
cxt.beginPath()
cxt.lineCap='round'
cxt.moveTo(50, 250)
cxt.lineTo(250, 250)
cxt.stroke()
</script>
复制代码
使用 square 和 round 的话,会使线条变得稍微长一点点,这是给线条增加线帽的部分,这个长度在日常开发中需要注意。
线帽只对线条的开始和结尾处产生作用,对拐角不会产生任何作用。
如果需要设置拐角样式,可以使用 lineJoin 。
语法:
lineJoin='属性值'
复制代码
属性值包括:
40.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.lineWidth=20
// 默认,尖角
cxt.moveTo(50, 40)
cxt.lineTo(200, 40)
cxt.lineTo(200, 90)
cxt.stroke()
// 斜角 bevel
cxt.beginPath()
cxt.moveTo(50, 140)
cxt.lineTo(200, 140)
cxt.lineTo(200, 190)
cxt.lineJoin='bevel'
cxt.stroke()
// 圆角 round
cxt.beginPath()
cxt.moveTo(50, 240)
cxt.lineTo(200, 240)
cxt.lineTo(200, 290)
cxt.lineJoin='round'
cxt.stroke()
</script>
复制代码
使用 setLineDash() 方法可以将描边设置成虚线。
语法:
setLineDash([])
复制代码
需要传入一个数组,且元素是数值型。
虚线分3种情况
41.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.lineWidth=20
cxt.strokeStyle='pink'
cxt.moveTo(50, 50)
cxt.lineTo(200, 50)
cxt.setLineDash([10]) // 只传1个参数,实线与空白都是 10px
cxt.stroke()
cxt.beginPath()
cxt.moveTo(50, 100)
cxt.lineTo(200, 100)
cxt.setLineDash([10, 20]) // 2个参数,此时,实线是 10px, 空白 20px
cxt.stroke()
cxt.beginPath()
cxt.moveTo(50, 150)
cxt.lineTo(200, 150)
cxt.setLineDash([10, 20, 5]) // 传3个以上的参数,此例:10px实线,20px空白,5px实线,10px空白,20px实线,5px空白 ……
cxt.stroke()
</script>
复制代码
此外,还可以始终 cxt.getLineDash() 获取虚线不重复的距离;
用 cxt.lineDashOffset 设置虚线的偏移位。
使用 fill() 可以填充图形,根据前面的例子应该掌握了如何使用 fill()
42.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.fillStyle='pink'
cxt.rect(50, 50, 200, 100)
cxt.fill()
</script>
复制代码
可以使用 fillStyle 设置填充颜色,默认是黑色。
在使用 fill() 方法填充时,需要注意一个规则:非零环绕填充。
在使用 moveTo 和 lineTo 描述图形时,如果是按顺时针绘制,计数器会加1;如果是逆时针,计数器会减1。
当图形所处的位置,计数器的结果为0时,它就不会被填充。
这样说有点复杂,先看看例子
43.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 外层矩形
cxt.moveTo(50, 50)
cxt.lineTo(250, 50)
cxt.lineTo(250, 250)
cxt.lineTo(50, 250)
cxt.closePath()
// 内层矩形
cxt.moveTo(200, 100)
cxt.lineTo(100, 100)
cxt.lineTo(100, 200)
cxt.lineTo(200, 200)
cxt.closePath()
cxt.fill()
</script>
复制代码
请看看上面的代码,我画了2个矩形,它们都没有用 beginPath() 方法开辟新路径。
44.png
内层矩形是逆时针绘制的,所以内层的值是 -1 ,它又经过外层矩形,而外层矩形是顺时针绘制,所以经过外层时值 +1,最终内层的值为 0 ,所以不会被填充。
Canvas 提供了一些操作文本的方法。
为了方便演示,我们先了解一下在 Canvas 中如何给本文设置样式。
和 CSS 设置 font 差不多,Canvas 也可以通过 font 设置样式。
语法:
cxt.font='font-style font-variant font-weight font-size/line-height font-family'
复制代码
如果需要设置字号 font-size,需要同事设置 font-family。
cxt.font='30px 宋体'
复制代码
使用 strokeText() 方法进行文本描边
语法:
strokeText(text, x, y, maxWidth)
复制代码
45.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.font='60px Arial' // 将字号设置成 60px,方便观察
cxt.strokeText('雷猴', 30, 90)
</script>
复制代码
使用 strokeStyle 设置描边颜色。
46.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.font='60px Arial' // 将字号设置成 60px,方便观察
cxt.strokeStyle='pink' // 设置文本描边颜色
cxt.strokeText('雷猴', 30, 90)
</script>
复制代码
使用 fillText() 可填充文本。
语法和 strokeText() 一样。
fillText(text, x, y, maxWidth)
复制代码
47.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.font='60px Arial'
cxt.fillText('雷猴', 30, 90)
</script>
复制代码
使用 fillStyle 可以设置文本填充颜色。
48.png
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
cxt.font='60px Arial'
cxt.fillStyle='pink'
cxt.fillText('雷猴', 30, 90)
</script>
复制代码
measureText().width 方法可以获取文本的长度,单位是 px 。
<canvas id="c" width="300" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
let text='雷猴'
cxt.font='bold 40px Arial'
cxt.fillText(text, 40, 80)
console.log(cxt.measureText(text).width) // 80
</script>
复制代码
使用 textAlign 属性可以设置文字的水平对齐方式,一共有5个值可选
49.png
红线是辅助参考线。
<canvas id="c" width="400" height="400" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 竖向的辅助线(参考线,在画布中间)
cxt.moveTo(200, 0)
cxt.lineTo(200, 400)
cxt.strokeStyle='red'
cxt.stroke()
cxt.font='30px Arial'
// 横坐标开始位对齐
cxt.textAlign='start' // 默认值,
cxt.fillText('雷猴 start', 200, 40)
// 横坐标结束位对齐
cxt.textAlign='end' // 结束对齐
cxt.fillText('雷猴 end', 200, 100)
// 左对齐
cxt.textAlign='left' // 左对齐
cxt.fillText('雷猴 left', 200, 160)
// 右对齐
cxt.textAlign='right' // 右对齐
cxt.fillText('雷猴 right', 200, 220)
// 居中对齐
cxt.textAlign='center' // 右对齐
cxt.fillText('雷猴 center', 200, 280)
</script>
复制代码
从上面的例子看,start 和 left 的效果好像是一样的,end 和 right 也好像是一样的。
在大多数情况下,它们的确一样。但在某些国家或者某些场合,阅读文字的习惯是 从右往左 时,start 就和 right 一样了,end 和 left 也一样。这是需要注意的地方。
使用 textBaseline 属性可以设置文字的垂直对齐方式。
在使用 textBaseline 前,需要自行了解 css 的文本基线。
50.png
用一张网图解释一下基线
textBaseline 可选属性:
51.png
红线是辅助参考线。
<canvas id="c" width="800" height="300" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 横向的辅助线(参考线,在画布中间)
cxt.moveTo(0, 150)
cxt.lineTo(800, 150)
cxt.strokeStyle='red'
cxt.stroke()
cxt.font='20px Arial'
// 默认 alphabetic
cxt.textBaseline='alphabetic'
cxt.fillText('雷猴 alphabetic', 10, 150)
// 默认 top
cxt.textBaseline='top'
cxt.fillText('雷猴 top', 200, 150)
// 默认 bottom
cxt.textBaseline='bottom'
cxt.fillText('雷猴 bottom', 320, 150)
// 默认 middle
cxt.textBaseline='middle'
cxt.fillText('雷猴 middle', 480, 150)
// 默认 hanging
cxt.textBaseline='hanging'
cxt.fillText('雷猴 hanging', 640, 150)
</script>
复制代码
注意:在绘制文字的时候,默认是以文字的左下角作为参考点进行绘制
在 Canvas 中可以使用 drawImage() 方法绘制图片。
渲染图片的方式有2中,一种是在JS里加载图片再渲染,另一种是把DOM里的图片拿到 canvas 里渲染。
渲染的语法:
drawImage(image, dx, dy)
复制代码
在 JS 里加载图片并渲染,有以下几个步骤:
52.png
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
// 1 创建 Image 对象
const image=new Image()
// 2 引入图片
image.src='./images/dog.jpg'
// 3 等待图片加载完成
image.onload=()=> {
// 4 使用 drawImage() 方法渲染图片
cxt.drawImage(image, 30, 30)
}
</script>
复制代码
53.png
<style>
#dogImg {
display: none;
}
</style>
<img src="./images/dog.jpg" id="dogImg"/>
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
const image=document.getElementById('dogImg')
cxt.drawImage(image, 70, 70)
</script>
复制代码
因为图片是从 DOM 里获取到的,所以一般来说,只要在 window.onload 这个生命周期内使用 drawImage 都可以正常渲染图片。
本例使用了 css 的方式,把图片的 display 设置成 none 。因为我不想被 <img> 影响到本例讲解。
实际开发过程中按照实际情况设置即可。
前面的例子都是直接加载图片,图片默认的宽高是多少就加载多少。
如果需要指定图片宽高,可以在前面的基础上再添加两个参数:
drawImage(image, dx, dy, dw, dh)
复制代码
image、 dx、 dy 的用法和前面一样。
dw 用来定义图片的宽度,dy 定义图片的高度。
54.png
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
const image=new Image()
image.src='./images/dog.jpg'
image.onload=()=> {
cxt.drawImage(image, 30, 30, 100, 100)
}
</script>
复制代码
我把图片的尺寸设为 100px * 100px,图片看上去比之前就小了很多。
截图图片同样使用drawImage() 方法,只不过传入的参数数量比之前都多,而且顺序也有点不一样了。
drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
复制代码
以上参数缺一不可
55.png
<canvas id="c" width="500" height="500" style="border: 1px solid #ccc;"></canvas>
<script>
const cnv=document.getElementById('c')
const cxt=cnv.getContext('2d')
const image=new Image()
image.src='./images/dog.jpg'
image.onload=()=> {
cxt.drawImage(image, 0, 0, 100, 100, 30, 30, 200, 200)
}
</script>
复制代码
本文主要讲解了在 Canvas 中绘制一些基础图形,还有一些基础样式设置。
还有更多高级的玩法会在之后的文章中讲到,比如渐变、投影、滤镜等等。
德育处主任,https://juejin.cn/post/7116784455561248775
*请认真填写需求信息,我们会在24小时内与您取得联系。