Canvas 绘制基础图形详解
Canvas 是 HTML5 核心绘图 API,支持在网页中动态绘制矢量图形。本文将系统讲解 Canvas 基础图形(线条、三角形、矩形、圆形)及组合图形(笑脸)的绘制方法,并附带完整代码与关键说明。
一、基础环境搭建(HTML + CSS + 初始化)
首先创建 Canvas 容器与绘图上下文,设置基础样式确保绘图区域清晰可见。
<style>
/* 容器样式:优化布局与视觉效果 */
.canvas-container {
background-color: #f8fafc; /* 浅灰背景,区分页面其他区域 */
padding: 20px;
max-width: 600px;
margin: 20px auto; /* 水平居中 */
border-radius: 8px; /* 圆角优化 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 轻微阴影增强层次感 */
}
/* Canvas 样式:明确绘图边界 */
#basic-canvas {
border: 4px dashed #cbd5e1; /* 虚线边框,区分画布区域 */
background-color: #ffffff; /* 白色画布,便于观察图形 */
border-radius: 4px;
}
</style>
<!-- 画布容器 -->
<div class="canvas-container">
<!-- Canvas 核心元素:width/height 需直接设置(非CSS),确保图形不失真 -->
<canvas id="basic-canvas" width="500" height="200"></canvas>
</div>
<script>
// 1. 获取 Canvas 元素与 2D 绘图上下文(核心对象)
const canvas = document.getElementById('basic-canvas')
const ctx = canvas.getContext('2d') // 所有绘图操作都通过 ctx 实现
// 2. 设置公共样式(避免重复代码)
ctx.lineWidth = 2 // 线条宽度(所有图形通用)
ctx.strokeStyle = '#2d3748' // 线条颜色(深灰,比黑色更柔和)
// 3. 页面加载完成后执行绘图(确保 Canvas 已渲染)
window.addEventListener('load', () => {
drawLine() // 绘制线条
drawTriangle() // 绘制三角形
drawRectangle() // 绘制矩形(原 Square 更准确的命名)
drawCircle() // 绘制圆形
drawSmilingFace() // 绘制笑脸(组合图形)
})
</script>
二、Canvas 路径绘制核心 API
在绘制路径之前先介绍几个常用的canvas的api。
- beginPath() 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
- closePath() 闭合路径之后图形绘制命令又重新指向到上下文中。
- stroke() 通过线条来绘制图形轮廓。
- fill() 通过填充路径的内容区域生成实心的图形。
- moveTo(x, y) 将笔触移动到指定的坐标 x 以及 y 上。
- lineTo(x, y) 绘制一条从当前位置到指定 x 以及 y 位置的直线。
三、具体图形绘制实现
1. 绘制直线(基础入门)
通过 moveTo() 定位起点,lineTo() 绘制线段,最后用 stroke() 渲染轮廓。
function drawLine() {
ctx.beginPath() // 开启新路径(避免与其他图形混淆)
ctx.moveTo(25, 25) // 起点:(25,25)(Canvas 左上角为原点 (0,0))
ctx.lineTo(105, 25) // 终点:(105,25)(水平向右绘制)
ctx.stroke() // 渲染直线轮廓
}
2. 绘制三角形(空心 + 实心)
三角形由三条线段组成,空心需手动闭合路径,实心可直接填充(自动闭合)。
function drawTriangle() {
// 1. 绘制空心三角形
ctx.beginPath()
ctx.moveTo(150, 25) // 顶点1
ctx.lineTo(200, 25) // 顶点2(水平向右)
ctx.lineTo(150, 75) // 顶点3(向左下方)
ctx.closePath() // 闭合路径(连接顶点3与顶点1)
ctx.stroke() // 渲染空心轮廓
// 2. 绘制实心三角形(位置偏移,避免与空心重叠)
ctx.beginPath()
ctx.moveTo(155, 30) // 顶点1(右移5px,下移5px)
ctx.lineTo(185, 30) // 顶点2(缩短宽度,更美观)
ctx.lineTo(155, 60) // 顶点3(上移15px,避免超出范围)
ctx.fillStyle = '#4299e1' // 单独设置填充色(蓝色)
ctx.fill() // 填充实心(无需 closePath(),自动闭合)
}
3. 绘制矩形(专用 API,更高效)
Canvas 为矩形提供了专用方法,无需手动写路径,直接指定位置与尺寸即可。
function drawRectangle() {
// 1. 空心矩形:strokeRect(x, y, 宽度, 高度)
ctx.strokeRect(10, 100, 50, 50) // 位置(10,100),尺寸50x50
// 2. 实心矩形:fillRect(x, y, 宽度, 高度)(偏移避免重叠)
ctx.fillStyle = '#48bb78' // 填充色(绿色)
ctx.fillRect(15, 105, 40, 40) // 位置(15,105),尺寸40x40
// 3. 清除矩形区域:clearRect(x, y, 宽度, 高度)(生成“镂空”效果)
ctx.clearRect(25, 115, 20, 20) // 清除中间20x20区域,变为透明
}
4. 绘制圆形(arc () 方法详解)
圆形通过 arc() 方法绘制,核心是理解「弧度制」与「绘制方向」。
arc () 方法语法: arc(x, y, radius, startAngle, endAngle, anticlockwise)
- x, y:圆心坐标
- radius:圆的半径
- startAngle/endAngle:起始 / 结束角度(必须用弧度制,公式:
弧度 = (Math.PI / 180) * 角度)
- anticlockwise:是否逆时针绘制(布尔值,默认 false 顺时针)
function drawCircle() {
// 1. 绘制完整圆形(360° = 2π 弧度)
ctx.beginPath()
ctx.arc(100, 125, 25, 0, Math.PI * 2, false) // 圆心(100,125),半径25
ctx.stroke()
// 2. 绘制上半圆(逆时针,180° = π 弧度)
ctx.beginPath()
ctx.arc(100, 125, 15, 0, Math.PI, true) // 半径15,逆时针绘制上半圆
ctx.stroke()
// 3. 绘制实心下半圆(顺时针)
ctx.beginPath()
ctx.arc(100, 130, 10, 0, Math.PI, false) // 圆心下移5px,半径10
ctx.fillStyle = '#f6ad55' // 填充色(橙色)
ctx.fill()
}
注意事项:为了保证新的圆弧不会追加到上一次的路径中,在每一次绘制圆弧的过程中都需要使用beginPath()方法。
5. 绘制组合图形(笑脸)
通过组合「圆形(脸)+ 小圆(眼睛)+ 半圆(嘴巴)」,实现复杂图形。
function drawSmilingFace() {
// 1. 绘制脸部轮廓(圆形)
ctx.beginPath()
ctx.arc(170, 125, 25, 0, Math.PI * 2, false) // 圆心(170,125),半径25
ctx.stroke()
// 2. 绘制左眼(小圆)
ctx.beginPath()
ctx.arc(163, 120, 3, 0, Math.PI * 2, false) // 左眼位置:左移7px,上移5px
ctx.fillStyle = '#2d3748' // 眼睛颜色(深灰)
ctx.fill() // 实心眼睛,无需 stroke()
// 3. 绘制右眼(小圆,与左眼对称)
ctx.beginPath()
ctx.arc(178, 120, 3, 0, Math.PI * 2, false) // 右眼位置:右移8px,上移5px
ctx.fill()
// 4. 绘制微笑嘴巴(下半圆,顺时针)
ctx.beginPath()
ctx.arc(170, 123, 18, 0, Math.PI, false) // 圆心(170,123),半径18,180°
ctx.stroke()
}
完整效果展示:

四、常见问题与注意事项
-
Canvas 尺寸设置: width 和 height 必须直接在 Canvas 标签上设置,若用 CSS 设置会导致图形拉伸失真。
-
路径隔离: 每次绘制新图形前,务必调用 beginPath(),否则新图形会与上一次路径叠加。
-
弧度与角度转换: arc() 方法仅支持弧度制,需用
(Math.PI / 180) * 角度
转换(如 90° = Math.PI/ 2)。
-
样式优先级: 若单个图形需要特殊样式(如不同颜色),需在 stroke()/fill() 前单独设置(如 ctx.fillStyle),否则会继承公共样式。
Canvas 实现电子签名功能
电子签名功能在现代 Web 应用中非常常见,从在线合同签署到表单确认都有广泛应用。本文将带你从零开始,使用 Canvas API 实现一个功能完备的电子签名组件。
一、实现思路与核心技术点
实现电子签名的核心思路是追踪用户的鼠标或触摸轨迹,并在 Canvas 上将这些轨迹绘制出来。
核心技术点:
-
Canvas API:用于在网页上动态绘制图形
-
事件监听:监听鼠标 / 触摸的按下、移动和松开事件
-
坐标转换:将鼠标 / 触摸事件的坐标转换为 Canvas 元素内的相对坐标
-
线条优化:通过设置线条属性实现平滑的签名效果
二、HTML 结构设计
这是一份简单到爆的html结构,没错,就是这样简单...
<div class="container">
<p>电子签名</p>
<canvas id="signatureCanvas" class="signature-border"></canvas>
</div>
三、CSS 样式设置
为 Canvas 添加一些基础样式,使其看起来像一个签名板。
.container {
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
}
.signature-border {
width: 98%;
height: 300px;
border: 4px dashed #cbd5e1;
border-radius: 10px;
cursor: crosshair;
}
四、JavaScript 核心实现
这是实现签名功能的关键部分,主要包含以下几个步骤:
- 获取 Canvas 元素和上下文
- 设置 Canvas 的实际绘制尺寸
- 定义变量存储签名状态和坐标
- 实现坐标转换函数
- 编写事件处理函数
- 绑定事件监听器
// 获取Canvas元素和上下文
const canvas = document.getElementById('signatureCanvas')
const ctx = canvas.getContext('2d', { willReadFrequently: true })
// 签名状态变量
let isDrawing = false
let lastX = 0
let lastY = 0
let lineColor = '#000000'
let lineWidth = 2
// 初始化Canvas
function initCanvas() {
// 设置Canvas样式
ctx.strokeStyle = lineColor
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
}
// 响应窗口大小变化
function resizeCanvas() {
const rect = canvas.getBoundingClientRect()
const { width, height } = rect
// 保存当前画布内容
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// 调整Canvas尺寸
canvas.width = width
canvas.height = height
// 恢复画布内容
ctx.putImageData(imageData, 0, 0)
// 重新设置绘图样式
ctx.strokeStyle = lineColor
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
}
// 获取坐标(适配鼠标和触摸事件)
function getCoordinates(e) {
const rect = canvas.getBoundingClientRect()
if (e.type.includes('mouse')) {
return [e.clientX - rect.left, e.clientY - rect.top]
} else if (e.type.includes('touch')) {
return [e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top]
}
}
// 开始绘制
function startDrawing(e) {
isDrawing = true
lastX = getCoordinates(e)[0]
lastY = getCoordinates(e)[1]
}
// 绘制中
function draw(e) {
if (!isDrawing) return
const [currentX, currentY] = getCoordinates(e)
ctx.beginPath()
ctx.moveTo(lastX, lastY)
ctx.lineTo(currentX, currentY)
ctx.stroke()
// 解释: 这里是将当前移动的坐标赋值给下一次绘制的起点,实现线条的流畅。
;[lastX, lastY] = [currentX, currentY]
}
// 结束绘制
function stopDrawing() {
isDrawing = false
}
// 绑定事件监听
function bindEvents() {
canvas.addEventListener('mousedown', startDrawing)
canvas.addEventListener('mousemove', draw)
canvas.addEventListener('mouseup', stopDrawing)
canvas.addEventListener('mouseout', stopDrawing)
// 触摸事件(移动设备)
canvas.addEventListener('touchstart', e => {
e.preventDefault() // 防止触摸事件被浏览器默认处理
startDrawing(e)
})
canvas.addEventListener('touchmove', e => {
e.preventDefault()
draw(e)
})
canvas.addEventListener('touchend', e => {
e.preventDefault()
stopDrawing()
})
}
// 初始化
window.addEventListener('load', () => {
initCanvas()
bindEvents()
})
五、功能亮点与设计思路
-
流畅的绘制体验:通过设置
lineCap: 'round'
和lineJoin: 'round'
让线条更加平滑自然。
-
响应式设计:监听窗口
resize
事件,动态调整 Canvas 尺寸,确保在不同设备和屏幕尺寸下都能正常工作。
-
跨设备支持:同时支持鼠标和触摸事件,兼容桌面和移动设备。
六、完整的代码

七、下一步可以探索的方向
-
颜色和粗细选择:增加 UI 控件让用户自定义签名的颜色和笔触粗细。
-
清空签名和保存签名:增加 UI 控件让用户清空当前的签名,同时支持保存和下载签名。
canvas 实现滚动序列帧动画
前言
在现代网页设计中,滚动触发的动画能极大增强用户体验,其中 Apple 官网的 AirPods Pro 产品页动画堪称经典 —— 通过滚动进度控制序列帧播放,营造出流畅的产品展示效果。本文将简单的实现一下这个动画效果。
一、动画核心逻辑
- 页面分为 3 个楼层:楼层 1(灰色背景)、楼层 2(黑色背景,核心动画区)、楼层 3(灰色背景)
- 楼层 2 高度为200vh(2 倍视口高度),内部有一个sticky定位的容器,包含文字和 Canvas
- 当用户滚动页面时,仅在楼层 2 进入并完全离开视口的过程中,Canvas 会根据滚动进度播放 147 帧 AirPods 序列图
- 窗口尺寸变化时,Canvas 会自动适配,保证动画显示比例正确
二、核心技术栈及原理拆解
要实现滚动序列帧动画,需要解决 3 个核心问题:序列帧加载与管理、滚动进度计算、Canvas 渲染与适配。
- HTML 部分的核心是三层 section 结构和Canvas 动画容器,结构清晰且语义化:
<!-- 楼层1:引导区 -->
<section class="floor1-container floor-container">
<p>楼层一</p>
</section>
<!-- 楼层2:核心动画区(目标楼层) -->
<section class="floor2-container floor-container" id="targetFloor">
<!-- sticky容器:滚动时"粘住"视口 -->
<div class="sticky">
<p>楼层二</p>
<!-- Canvas:用于渲染序列帧 -->
<canvas class="canvas" id="hero-lightpass"></canvas>
</div>
</section>
<!-- 楼层3:结束区 -->
<section class="floor3-container floor-container">
<p>楼层三</p>
</section>
- CSS 的核心作用是控制三层布局、实现 sticky 定位、保证 Canvas 适配,代码注释已标注关键逻辑:
/* 重置默认margin,避免布局偏移 */
body,
p {
margin: 0;
}
/* 楼层1和楼层3样式:灰色背景+居中文字 */
.floor1-container,
.floor3-container {
background-color: #474646; /* 深灰色背景 */
height: 500px; /* 固定高度,模拟常规内容区 */
display: flex; /* Flex布局:实现文字水平+垂直居中 */
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
/* 楼层1/3文字样式:响应式字体 */
.floor3-container p,
.floor1-container p {
font-size: 5vw; /* 5vw:相对于视口宽度的5%,实现响应式字体 */
color: #fff; /* 白色文字,与深色背景对比 */
}
/* 楼层2样式:黑色背景+高高度(动画触发区) */
.floor2-container {
height: 200vh; /* 200vh:2倍视口高度,保证有足够滚动空间触发动画 */
background-color: black; /* 黑色背景,突出产品图片 */
color: #fff; /* 白色文字 */
}
/* 楼层2文字:水平居中 */
.floor2-container p {
text-align: center;
}
/* 核心:sticky定位容器 */
.sticky {
position: sticky; /* 粘性定位:滚动到top:0时固定 */
top: 0; /* 固定在视口顶部 */
height: 500px; /* 与楼层1/3高度一致,保证视觉连贯 */
width: 100%; /* 占满视口宽度 */
}
/* Canvas样式:宽度自适应 */
.canvas {
width: 100%; /* 宽度占满容器 */
height: auto; /* 高度自动,保持图片比例 */
}
- JS 部分是整个动画的核心,负责预加载序列帧、计算滚动进度、控制 Canvas 渲染和窗口适配,我们分模块解析:
模块 1:初始化变量与 DOM 元素
首先定义动画所需的核心变量,包括序列帧数量、图片数组、Canvas 上下文等:
// 1. 动画核心配置
const frameCount = 147 // 序列帧总数(根据实际图片数量调整)
const images = [] // 存储所有预加载的序列帧图片
const canvas = document.getElementById('hero-lightpass') // 获取Canvas元素
const context = canvas.getContext('2d') // 获取Canvas 2D渲染上下文
const airpods = { frame: 0 } // 存储当前播放的帧序号(用对象便于修改)
// 2. 获取目标楼层(楼层2)的DOM元素,用于后续计算滚动位置
const targetFloor = document.getElementById('targetFloor')
// 3. 序列帧图片地址模板(Apple官网的AirPods序列帧地址)
// 作用:通过索引生成每帧图片的URL(如0001.jpg、0002.jpg...)
const currentFrame = index =>
`https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${(index + 1).toString().padStart(4, '0')}.jpg`
模块 2:预加载所有序列帧图片
序列帧动画需要所有图片加载完成后才能流畅播放,因此必须先预加载图片:
// 循环生成147帧图片,存入images数组
for (let i = 0; i < frameCount; i++) {
const img = new Image() // 创建Image对象
img.src = currentFrame(i) // 给图片设置URL(通过模板生成)
images.push(img) // 将图片存入数组
}
// 当第一张图片加载完成后,执行首次渲染(避免页面空白)
images[0].onload = render
为什么要预加载:
- 如果不预加载,用户滚动时图片可能还在加载,导致动画卡顿或跳帧
- 监听第一张图片的onload事件:保证页面初始化时至少有一张图显示,提升首屏体验
模块 3:Canvas 渲染函数
定义render()函数,负责将当前帧图片绘制到 Canvas 上:
function render() {
// 1. 清除Canvas画布(避免上一帧残留)
context.clearRect(0, 0, canvas.width, canvas.height)
// 2. 绘制当前帧图片
// 参数:图片对象、绘制起点X、Y、绘制宽度、绘制高度
context.drawImage(images[airpods.frame], 0, 0, canvas.width, canvas.height)
}
模块 4:Canvas 窗口适配函数
当窗口尺寸变化时,需要重新调整 Canvas 的宽高,避免图片拉伸或变形:
function resizeCanvas() {
// 1. 获取Canvas元素的实际位置和尺寸(包含CSS样式的影响)
const rect = canvas.getBoundingClientRect()
// 2. 设置Canvas的实际宽高(Canvas的width/height是像素尺寸,而非CSS样式)
canvas.width = rect.width
canvas.height = rect.height
// 3. 重新渲染当前帧(避免尺寸变化后画布空白)
render()
}
易错点提醒:
- Canvas 有两个 "尺寸":一个是 HTML 属性width/height(实际像素尺寸),另一个是 CSS 样式width/height(显示尺寸)
- 如果只改 CSS 样式而不改canvas.width/height,图片会拉伸变形;因此必须通过getBoundingClientRect()获取实际显示尺寸,同步设置 Canvas >的像素尺寸
模块 5:滚动进度计算与帧控制(核心中的核心)
这是整个动画的逻辑核心 —— 根据用户的滚动位置,计算当前应播放的帧序号,实现 "滚动控制动画":
function handleScroll() {
// 1. 获取关键尺寸数据
const viewportHeight = window.innerHeight // 视口高度(浏览器可见区域高度)
const floorTop = targetFloor.offsetTop // 目标楼层(楼层2)距离页面顶部的距离
const floorHeight = targetFloor.offsetHeight // 目标楼层自身的高度(200vh)
const currentScrollY = window.scrollY // 当前滚动位置(页面顶部到视口顶部的距离)
// 2. 计算"滚动结束点":当目标楼层底部进入视口时,动画应播放到最后一帧
const scrollEnd = floorTop + floorHeight - viewportHeight
// 3. 计算滚动进度(0~1):0=未进入楼层2,1=完全离开楼层2
let scrollProgress = 0
if (currentScrollY < floorTop) {
// 情况1:滚动位置在楼层2上方→进度0(显示第一帧)
scrollProgress = 0
} else if (currentScrollY > scrollEnd) {
// 情况2:滚动位置在楼层2下方→进度1(显示最后一帧)
scrollProgress = 1
} else {
// 情况3:滚动位置在楼层2内部→计算相对进度
const scrollDistanceInFloor = currentScrollY - floorTop // 进入楼层2后滚动的距离
const totalScrollNeeded = scrollEnd - floorTop // 楼层2内需要滚动的总距离(触发完整动画的距离)
scrollProgress = scrollDistanceInFloor / totalScrollNeeded // 进度=已滚动距离/总距离
}
// 4. 根据进度计算当前应显示的帧序号
// 公式:目标帧 = 进度 × (总帧数-1) → 保证进度1时显示最后一帧(避免数组越界)
const targetFrame = Math.floor(scrollProgress * (frameCount - 1))
// 5. 优化性能:仅当帧序号变化时才重新渲染
if (targetFrame !== airpods.frame) {
airpods.frame = targetFrame
render() // 重新绘制当前帧
}
}
模块 6:事件监听与初始化
最后,通过事件监听触发上述逻辑,完成动画初始化:
window.addEventListener('load', () => {
// 1. 监听滚动事件:用户滚动时触发进度计算
window.addEventListener('scroll', handleScroll)
// 2. 监听窗口 resize 事件:窗口尺寸变化时适配Canvas
window.addEventListener('resize', resizeCanvas)
// 3. 初始化Canvas尺寸(页面加载完成后首次适配)
resizeCanvas()
})
三、完成代码展示

更多canvas功能敬请期待...