Canvas绘图--保姆级教程

Canvas介绍

Canvas 作为HTML5引入的核心组件之一,其角色就像一块数字化的画布,赋予 Web页面 前所未有的绘图与动画创作能力。在 Canvas 出现之前,实现复杂的图形与动画效果往往依赖于Flash插件,这不仅增加了页面加载的复杂性与安全风险,还迫使开发者在 JavaScript 与 Flash 之间进行繁琐的交互。Canvas 的诞生,如同一股清流,彻底革新了这一局面,使得 纯JavaScript 环境下的图形绘制与动态效果实现成为可能,彻底告别了 Flash 的依赖。

尽管 Canvas 相较于 SVG(可缩放矢量图形)而言,其历史更为悠久,这可能在一定程度上影响了其在某些老旧浏览器上的兼容性,但其强大的位图处理能力却不容忽视。Canvas 通过像素级别的操控,能够绘制出细腻且丰富的图形与动画,尤其在3D绘图领域展现出了卓越的性能,成为了大型游戏开发、复杂动画创作以及数据统计图表绘制的优选工具。诸如 three.js 等著名的3D图形库,正是基于 Canvas 构建,为开发者提供了便捷而强大的3D场景搭建能力。

相比之下,SVG 则以其矢量图形的特性著称,无论放大多少倍,都能保持图形的清晰度与细节,这一特点使得 SVG 在地图绘制、图标设计以及需要高精度的图形展示场景中占据了优势。 SVG 的XML 结构也赋予了其高度的可编辑性与可访问性,为数据可视化与交互设计带来了更多可能性。

因此,对于致力于数据可视化与前端开发的专业人士而言,深入掌握 Canvas 与 SVG 两大技术,无疑是提升专业技能与创新能力的重要途径。它们各自独特的优势与适用场景,为开发者提供了丰富的工具与手段,无论是出于个人兴趣的探索,还是业务需求的驱动,都值得投入时间与精力进行系统性的学习与实践。从Canvas的基础入门开始,逐步深入其高级特性与实战应用,再结合 SVG 的矢量绘图能力,将能够极大地拓宽你的创作边界,为 Web页面 带来前所未有的视觉体验与交互效果。

注意: 部分案例均来自 Canvas教程。该文提供 Canvas 基础用法以及用法示例,快速入门,简化官网教程,方便自己温故而知新 Canvas 基础以及一些 Canvas API。

Canvas 基础用法

宽高

<canvas> 和 <img> 类似,不同的是他没有 src 和 alt 属性,它只有 width 和 height 属性,这两个属性也是可选的,当没有设置时,默认宽为 300px、高为 150px。该元素也能用 CSS 定义大小,但绘制时可能出现伸缩,如果 CSS 定义的大小与默认尺寸不成比例,会发生扭曲。

<canvas id="draw" width="180" height="180"></canvas>

替代内容

前面说到 Canvas 针对老旧浏览器兼容性较差,因此当 <canvas> 运行在不兼容浏览器时会显示 <canvas> 中的内容,可在 <canvas> 中输出替代内容(根据需要定义文字描述、静态图片、其他 HTML 标签等)。

<canvas id="draw" width="180" height="180">当前浏览器不兼容</canvas>

原点

<canvas> 原点(0,0)在画布的左上角,坐标向右X轴越大,坐标向下Y轴越大。绘制图形时,给定的点也是绘制图形的左上角(可以开 F12 检查看看)。

渲染上下文 (context)

<canvas> 与 <svg> 类似,标签仅仅是创建一块画布,<svg> 通过 <text> <line> <path> <rect> 等绘制和处理图形,而 <canvas> 通过公开一个或多个渲染上下文来绘制和处理图形。

为了展示,脚本需要找到渲染上下文,再在其上进行绘制处理。<canvas> 有一个 getContext() 方法用于获取 context 和绘图功能,方法接收一个上下文类型的参数。本文介绍  '2d'  参数,即二维图形的属性和方法。

const canvas = document.getElementById('draw')
if (canvas.getContext) {
  // 当前浏览器兼容canvas
  const ctx = canvas.getContext('2d') // 获取canvas上下文
}

上下文类型参数

'2d': 返回 CanvasRenderingContext2D 对象,提供用于绘制二维图形的属性和方法。

'webgl': 返回 WebGLRenderingContext 对象,提供用于三维图形渲染的 OpenEL ES 2.0 API。

'webgl2':返回 WebGL2RenderingContext 对象,即 'webgl' 的扩展。

绘制图形

不同于 Svg ,Canvas 只支持矩形和路径。路径生成的众多方法可实现复杂图形。

矩形

fillRect(x,y,width,height): 填充矩形。

strokeRect(x,y,width,height): 矩形边框。

clearRect(x,y,width,height): 清除区域(橡皮擦)。

ctx.fillRect(0, 0, 50, 50)
ctx.strokeRect(100, 100, 100, 100)
ctx.clearRect(0, 0, 10, 10)

路径

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

closePath(): 闭合路径之后图形绘制命令又重新指向到上下文中,即闭合路径后脱离这一整条路径关系。

stroke(): 线条绘制路径。

fill(): 填充路径。

moveTo(x,y): 将画笔移向(x,y),不进行绘画,相当于现实提起笔转至某地方打算开始书写。

lineTo(x,y): 从上一点画直线到(x,y)。

ctx.beginPath()
ctx.moveTo(75, 50) // 将画笔移向该点(x,y)
ctx.lineTo(100, 75) // 从上点画直线到该点(x,y)
ctx.lineTo(100, 25)
ctx.fill()

ctx.beginPath()
ctx.moveTo(125, 125)
ctx.lineTo(125, 45)
ctx.lineTo(45, 125)
ctx.closePath()
ctx.stroke()

圆弧

arc(x,y,radius,startAngle,endAngle,anticlockwise): 以 (x,y) 为圆心,radius 为半径,从 startAngle 到 endAngle 的圆(圆弧),anticlockwise(默认顺时针,true时逆时针)。

弧度 = (Math.PI / 180) * 角度。

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()
  }
}

贝塞尔曲线

二次贝塞尔曲线: quadraticCurveTo(cp1x,cp1y,x,y)。

三次贝塞尔曲线: bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)。

cpx,cpy 为控制点,x,y 为结束点。二次贝塞尔每个点需一个控制点,三次贝塞尔需两个控制点。

Svg 绘图三次贝塞尔代码可转至 Svg三次贝塞尔曲线

// 二次贝塞尔曲线
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()

// 三次贝塞尔曲线
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()

Path2D对象

用于缓存或记录绘画命令,类似于 Vue 中组件抽离,当需要被多次使用时,抽离出来要用就调用一下。

注意: Path2D 对象可以使用 Svg path 初始化 canvas 上的对象。

const rectangle = new Path2D()
rectangle.rect(10, 10, 50, 50)
const circle = new Path2D()
circle.moveTo(125, 35)
circle.arc(100, 35, 25, 0, 2 * Math.PI)
ctx.stroke(rectangle)
ctx.fill(circle)

样式和色彩

颜色

fillStyle(16进制颜色、渐变对象、图案对象): 填充颜色。

strokeStyle(16进制颜色、渐变对象、图案对象): 轮廓颜色。

注意: 一旦设置 fillStyle 或 strokeStyle的值,那么新值就会成为新绘制的图形的默认值。如果要给每个图形上不同的颜色,需重新设置 fillStyle 或 strokeStyle 的值。16进制颜色包括 #FFFFFF,rgb(0,0,0),rgba(0,0,0,1)。

for (let i = 0; i < 6; i++) {
  for (let j = 0; j < 6; j++) {
    ctx.fillStyle = "rgb(" + Math.floor(255 - 42.5 * i) + "," + Math.floor(255 - 42.5 * j) + ",0)"
    ctx.fillRect(j * 25, i * 25, 25, 25)
  }
}

for (let i = 0; i < 6; i++) {
  for (let j = 0; j < 6; j++) {
    ctx.strokeStyle ="rgb(0," + Math.floor(255 - 42.5 * i) + "," + Math.floor(255 - 42.5 * j) + ")"
    ctx.beginPath()
    ctx.arc(12.5 + j * 25, 12.5 + i * 25, 10, 0, Math.PI * 2, true)
    ctx.stroke()
  }
}

透明度

见上'颜色'处,设置rgba()。

globalAlpha属性。

ctx.fillStyle = "#FD0"
ctx.fillRect(0, 0, 75, 75)
ctx.fillStyle = "#6C0"
ctx.fillRect(75, 0, 75, 75)
ctx.fillStyle = "#09F"
ctx.fillRect(0, 75, 75, 75)
ctx.fillStyle = "#F30"
ctx.fillRect(75, 75, 75, 75)
ctx.fillStyle = "#FFF"
ctx.globalAlpha = 0.2
for (let i = 0; i < 7; i++) {
  ctx.beginPath()
  ctx.arc(75, 75, 10 + 10 * i, 0, Math.PI * 2, true)
  ctx.fill()
}

线型

lineWidth = value: 设置线条宽度。

for (let i = 0; i < 10; i++) {
  ctx.lineWidth = 1 + i
  ctx.beginPath()
  ctx.moveTo(5 + i * 14, 5)
  ctx.lineTo(5 + i * 14, 140)
  ctx.stroke()
}

lineCap = type: 设置线条末端样式。 type: butt ,round,square。

        butt: 默认值,无处理。

        round: 端点处加上了半径为一半线宽的半圆。

        square: 端点处加上了等宽且高度为一半线宽的方块。

const lineCap = ["butt", "round", "square"]
ctx.strokeStyle = "#09f"
ctx.beginPath()
ctx.moveTo(10, 10)
ctx.lineTo(140, 10)
ctx.moveTo(10, 140)
ctx.lineTo(140, 140)
ctx.stroke()
ctx.strokeStyle = "black"
for (let i = 0; i < lineCap.length; i++) {
  ctx.lineWidth = 15
  ctx.lineCap = lineCap[i]
  ctx.beginPath()
  ctx.moveTo(25 + i * 50, 10)
  ctx.lineTo(25 + i * 50, 140)
  ctx.stroke()
}

lineJoin = type: 设定线条与线条间接合处的样式。 type: round,bevel,miter。

        bevel: 

        round: 边角处被磨圆,圆的半径等于线宽。

        miter:线段会在连接处外侧延伸直至交于一点,延伸效果受到下面将要介绍 minterLimit 属性的制约。

const lineJoin = ["round", "bevel", "miter"]
ctx.lineWidth = 10
for (let i = 0; i < lineJoin.length; i++) {
  ctx.lineJoin = lineJoin[i]
  ctx.beginPath()
  ctx.moveTo(-5, 5 + i * 40)
  ctx.lineTo(35, 45 + i * 40)
  ctx.lineTo(75, 5 + i * 40)
  ctx.lineTo(115, 45 + i * 40)
  ctx.lineTo(155, 5 + i * 40)
  ctx.stroke()
}

miterLimit: 限制当两条线相交时交接处最大长度;所谓交接处长度(斜接长度)是指线条交接处内角顶点到外角顶点的长度。线段的外侧边缘会被延伸交汇于一点上。线段之间夹角比较大时,交点不会太远,但随夹角变小,交点距离会呈指数级增大。该属性用来设定外延交点与连接点的最大距离,如果交点距离大于此值,连接效果会变成 bevel。

-------------------------------------------分割线(分割 miter 和 lindDash )--------------------------------------------

setLineDash(segments):  设置当前虚线样式。 接受一个数组,来指定线段与间隙的交替。

lineDashOffset = value:  设置虚线样式的起始偏移量。

getLineDash():返回一个包含当前虚线样式,长度为非负偶数的数组。

let offset = 0
function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.setLineDash([4, 2])
  ctx.lineDashOffset = -offset
  ctx.strokeRect(10, 10, 100, 100)
}
function march() {
  offset++
  if (offset > 16) 
    offset = 0
  draw()
  setTimeout(march, 20)
}
march()

注意: 这是一个动图,偏移量随时间变化,呈现一个边框顺时针转动的正方形。

渐变

createLinearGradient(x1,y1,x2,y2): 渐变起点 (x1,y1) ,渐变终点 (x2,y2) 。

createRadialGradient(x1,y1,r1,x2,y2,r2): 以 (x1,y1) 为原点、r1为半径的圆,以 (x2,y2) 为原点、r2为半径的圆。

grandient.addColorStop(position,color): 上色,position(0~1) 表示渐变中中颜色所在的相对位置。如0.5表示颜色出现在正中间。

const lingrad = ctx.createLinearGradient(0, 0, 0, 150)
lingrad.addColorStop(0, "#00ABEB")
lingrad.addColorStop(0.5, "#fff")
lingrad.addColorStop(0.5, "#26C000")
lingrad.addColorStop(1, "#fff")

const lingrad2 = ctx.createLinearGradient(0, 50, 0, 95)
lingrad2.addColorStop(0.5, "#000")
lingrad2.addColorStop(1, "rgba(0,0,0,0)")

ctx.fillStyle = lingrad
ctx.strokeStyle = lingrad2

ctx.fillRect(10, 10, 130, 130)
ctx.strokeRect(50, 50, 50, 50)

const radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30)
radgrad.addColorStop(0, "#A7D30C")
radgrad.addColorStop(0.9, "#019F62")
radgrad.addColorStop(1, "rgba(1,159,98,0)")

const radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50)
radgrad2.addColorStop(0, "#FF5F98")
radgrad2.addColorStop(0.75, "#FF0188")
radgrad2.addColorStop(1, "rgba(255,1,136,0)")

const radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40)
radgrad3.addColorStop(0, "#00C9FF")
radgrad3.addColorStop(0.8, "#00B5E2")
radgrad3.addColorStop(1, "rgba(0,201,255,0)")

const radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90)
radgrad4.addColorStop(0, "#F4F201")
radgrad4.addColorStop(0.8, "#E4C700")
radgrad4.addColorStop(1, "rgba(228,199,0,0)")

ctx.fillStyle = radgrad4
ctx.fillRect(0, 0, 150, 150)
ctx.fillStyle = radgrad3
ctx.fillRect(0, 0, 150, 150)
ctx.fillStyle = radgrad2
ctx.fillRect(0, 0, 150, 150)
ctx.fillStyle = radgrad
ctx.fillRect(0, 0, 150, 150)

图案样式 patterns

createPatterns(image,type): image可以是一个 image 对象的引用,或另一个 canvas 对象。type即对 image 的操作,可以是 repeat, repeat-x, repeat-y, no-repeat。

const img = new Image()
img.src = "someimage.png"
const ptrn = ctx.createPattern(img, "repeat")

 阴影

shadowOffsetX = float、shadowOffsetY = float: 用来设定阴影在 X 和 Y 轴的延伸距离,它们是不受变换矩阵所影响的。负值表示阴影会往上或左延伸,正值则表示会往下或右延伸,它们默认都为 0

shadowBlur = float: 用于设定阴影的模糊程度,其数值并不跟像素数量挂钩,也不受变换矩阵的影响,默认为 0

shadowColor = color: 标准的 CSS 颜色值,用于设定阴影颜色效果,默认是全透明的黑色。

ctx.shadowOffsetX = 2
ctx.shadowOffsetY = 2
ctx.shadowBlur = 2
ctx.shadowColor = "rgba(0, 0, 0, 0.5)"

ctx.font = "20px Times New Roman"
ctx.fillStyle = "Black"
ctx.fillText("Sample String", 5, 30)

Canvas 填充规则

当用到 fill 时,可以选择填充规则。

nonzero: 默认值。

evenodd。

ctx.beginPath()
ctx.arc(50, 50, 30, 0, Math.PI * 2, true)
ctx.arc(50, 50, 15, 0, Math.PI * 2, true)
ctx.fill("evenodd")

 

绘制文本

fillText(text,x,y,[maxwidth]): 填充文本。

strokeText(text,x,y,[maxwidth]): 轮廓文本。

ctx.font = "48px serif"
ctx.fillText("Hello world", 10, 50)
ctx.font = "48px serif"
ctx.strokeText("Hello world", 10, 100)

给文本加样式

font = value: 绘制文本的样式。默认的字体是 10px sans-serif 。

fontAlign = value: 文本对齐。可选的值包括 start , end , left , right , center , 默认值是 start 。

textBaseline = value: 基线对齐。可选的值包括: top , hanging , middle , alphabetic , ideographic , bottom 。默认值是 alphabetic 。

direction = value: 文本方向。可能的值包括 ltr , rtl , inherit 。默认值是 inherit 。

语法跟 CSS font 差不多。

获取文本细节

measureText(): 可获取文本对象的属性。

const text = ctx.measureText("foo")
text.width; // 16

 使用图像

获取需要绘制的图片

HTMLImageElement

这些图片是由 Image() 函数构造出来的,或者任何的 <img>。 

HTMLVideoElement

用一个 HTML 的 <video> 元素作为你的图片源,可以从视频中抓取当前帧作为一个图像。

HTMLCanvasElemnt

可以使用另一个 <canvas> 元素作为你的图片源。

ImageBitMap

这是一个高性能的位图,可以低延迟地绘制,它可以从上述的所有源以及其他几种源中生成。

使用相同内页面的图片

可用 document.images() , document.getElementByTagName , document.getElementById() 获取。

使用其他域名下的图片

可在 HTMLImageElement 上使用 crossOrigin 属性请求加载,如果图片的服务器允许跨域访问这个图片,那么可以使用这个图片而不污染 canvas,否则,使用这个图片会污染 canvas 。

使用其他 canvas 元素

可用 document.getElementByTagName , document.getElementById() 获取。

从零开始创建图像

const img = new Image() // 创建一个<img>元素
img.src = "myImage.png" // 设置图片源地址

 通过 data:url 方式嵌入对象

允许用 Base64 编码的字符串来定义图片,图片内容即时可用,不用发送请求到服务器。

img.src = "data:image/gif;base64,R0lGODlhCwALAIAAAAAA3pn/ZiH5BAEAAAEALAAAAAALAAsAAAIUhA+hkcuO4lmNVindo7qyrIXiGBYAOw=="

 使用视频帧

返回 HTMLVideoElement 对象。

function getMyVideo() {
  var canvas = document.getElementById("canvas")
  if (canvas.getContext) {
    var ctx = canvas.getContext("2d")
    return document.getElementById("myvideo")
  }
}

 绘制图片

获取到源图对象,就可使用 drawImage() 将其渲染到 canvas画布上。

drawImage(Image,x,y): Image 即 Image 或 Canvas 对象。x,y 即 起始坐标。

缩放

drawImage(Image,x,y,width,height): width , height 用来控制 当向 canvas 画入时应该缩放的大小。

const img = new Image();
img.onload = () => {
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 3; j++) 
      ctx.drawImage(img, j * 50, i * 38, 50, 38)
  }
}
img.src = "https://mdn.github.io/shared-assets/images/examples/rhino.jpg"

切片

drawImage(image,sx,sy,sWidth,Sheight,dx,dy,dWidth,dHeight): sx , sy , sWidth , sHeight 是在源图片里进行操作,dx, dy , dWidth , dHeight 是在 canvas 画布上进行操作。

<canvas id="draw" width="150" height="150"></canvas>
<div style="display: none;">
  <img
    id="source"
    src="https://mdn.github.io/shared-assets/images/examples/rhino.jpg"
    width="300"
    height="227" />
  <img id="frame" src="canvas_picture_frame.png" width="132" height="150" />
</div>
async function draw() {
  // 等待所有图片的加载。
  await Promise.all(
    Array.from(document.images).map(
      (image) => new Promise((resolve) => image.addEventListener("load", resolve))
    )
  )
  const canvas = document.getElementById("canvas")
  const ctx = canvas.getContext("2d")
  ctx.drawImage(
    document.getElementById("source"),
    33,
    71,
    104,
    124,
    21,
    20,
    87,
    104,
  )
  ctx.drawImage(document.getElementById("frame"), 0, 0)
}
draw()

 

变形

保存

 save(): 保存 canvas 上的所有状态。

restore(): 弹出栈顶的 canvas 状态。

移动

translate(x,y): 移动原点到 (x,y),即移动 canvas。

旋转

rotate(angle): 旋转angle角度。

缩放

scale(x,y): 缩放画布的水平和垂直单位。1为实际大小。为负值时,根据x轴或y轴做对称处理。

ctx.save()
ctx.scale(10, 3)
ctx.fillRect(10, 10, 50, 50)
ctx.restore()
ctx.scale(-1, 1)
ctx.font = "32px serif"
ctx.fillText("Newl", -135, 120)

 

变形

transform(a,b,c,d,e,f): a水平方向的缩放,b竖直方向的倾斜偏移,c水平方向的倾斜偏移,d竖直方向的缩放,e水平方向的移动,f竖直方向的移动。

setTransform(a,b,c,d,e,f): 将当前变阵矩阵变成单位矩阵,再以相同参数调用transform()。

resetTransform(): 将当前变阵矩阵变成单位矩阵。

const sin = Math.sin(Math.PI / 6)
const cos = Math.cos(Math.PI / 6)
ctx.translate(100, 100)
let c = 0
for (let i = 0; i <= 12; i++) {
  c = Math.floor((255 / 12) * i)
  ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")"
  ctx.fillRect(0, 0, 100, 10)
  ctx.transform(cos, sin, -sin, cos, 0, 0)
}
ctx.setTransform(-1, 0, 0, 1, 100, 100)
ctx.fillStyle = "rgba(255, 128, 255, 0.5)"
ctx.fillRect(0, 50, 100, 100)

裁剪路径

clip()

简单的基础内容就是这些。如需要学习更细致更高阶的 canvas 知识、技巧,Canvas 教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Newlz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值