做小程序开发时,想实现个性化绘图、生成海报、做互动动画,canvas是绕不开的工具,但很多新手刚接触时,要么画不出东西,要么性能崩了,甚至跨端显示不一样……今天用问答形式,把小程序canvas绘图从入门到实战的关键问题掰碎了讲,帮你少走弯路。
网页H5里canvas是单线程渲染,逻辑和渲染在同一个上下文;但小程序是**逻辑层与渲染层分离**的架构,canvas的绘制命令要从逻辑层传递到渲染层执行,这就导致绘制时机、数据同步容易出问题,比如H5里写完`ctx.fillRect()`立刻显示,小程序里旧版canvas要调用`ctx.draw()`才会真正渲染,2d模式虽然接近H5,但也要注意上下文创建时机。
不同小程序平台(微信、支付宝、抖音)的canvas API也有差异,比如微信小程序分“旧版canvas”(用canvas-id,上下文是wx.createCanvasContext)和“2d模式”(用type="2d",上下文是wx.createSelectorQuery().select('#canvas').fields({ node: true }).exec()获取);支付宝小程序则是my.createCanvasContext,参数和方法名有细微差别。
还有尺寸适配的坑:H5用CSS控制canvas大小,小程序里要注意rpx转px(比如设计稿750rpx宽,实际要转成wx.getSystemInfoSync().windowWidth像素),否则绘制的图形会模糊或位置错乱。
第一步就卡壳!canvas初始化到底要注意啥?
很多人开局就栽在“canvas上下文拿不到”“画了没反应”,核心是把握初始化时机和API版本:
API版本选择:优先用2d模式(微信小程序
type="2d"),它更接近H5标准,支持requestAnimationFrame、离屏canvas等,性能和开发体验更好,旧版canvas(canvas-id)已逐渐被替代,且存在绘制异步、API繁琐的问题。获取上下文的时机:canvas是组件,要等页面渲染完成后才能获取节点,所以初始化代码要写在
onReady生命周期(而不是onLoad),以微信小程序2d模式为例:Page({ onReady() { const query = wx.createSelectorQuery() query.select('#myCanvas') // <canvas type="2d" id="myCanvas" /> .fields({ node: true, size: true }) .exec(res => { const canvas = res[0].node this.ctx = canvas.getContext('2d') // 现在可以用this.ctx.drawRect()等命令了 }) } })尺寸与设备像素比:canvas的实际绘制分辨率由
width和height属性决定(不是CSS样式),为了高清显示,要根据设备像素比(devicePixelRatio)调整:const { windowWidth, devicePixelRatio } = wx.getSystemInfoSync() canvas.width = windowWidth * devicePixelRatio canvas.height = 500 * devicePixelRatio // 假设设计稿高度500rpx this.ctx.scale(devicePixelRatio, devicePixelRatio) // 缩放上下文,后续用rpx单位绘制
基础图形咋画?矩形、圆形、文字、图片各有啥坑?
先把基础图形的“必踩雷区”讲透,再练手就顺了:
矩形:strokeRect vs fillRect
用
strokeRect(x, y, w, h)画边框,必须先设置strokeStyle(颜色/渐变),否则默认透明看不到,同理,fillRect要先设fillStyle。坑点:如果先画了fillRect再画strokeRect,stroke会覆盖fill的边缘,要注意绘制顺序。
圆形:arc的角度陷阱
画圆用arc(x, y, r, startAngle, endAngle, anticlockwise),要画完整的圆,startAngle设0,endAngle设2 * Math.PI(不是360!因为canvas用弧度制),画完要调用fill()或stroke()才会显示。
错误案例:ctx.arc(100,100,50,0,360) → 角度用了度数,导致只画了一条线。
文字:换行和字体兼容
fillText(text, x, y)默认不换行,要自己分割字符串:计算每个字的宽度(measureText(text).width),超过canvas宽度就换行。字体样式
font要写全,比如ctx.font = '28rpx PingFang SC',小程序里部分手机字体渲染有差异,建议用系统默认字体(如PingFang),避免自定义字体因加载慢导致绘制失败。
图片:加载完成再绘制
drawImage(img, x, y)的img必须是已加载完成的图片对象,所以要先通过wx.getImageInfo或<image>组件的onLoad获取图片路径:
wx.getImageInfo({
src: 'https://xxx.com/logo.png',
success: (res) => {
const img = canvas.createImage()
img.src = res.path
img.onload = () => {
this.ctx.drawImage(img, 0, 0, 100, 100)
}
}
})坑点:没等onload就调用drawImage,图片会画不出来;网络图片要配置download域名,否则跨域报错。
画着画着性能崩了!canvas绘图怎么优化?
小程序canvas性能差,常见表现是绘制卡顿、内存溢出、动画掉帧,这几个优化技巧能救命:
离屏canvas(OffscreenCanvas)
把复杂绘制(比如大量图形、动画帧)放到离屏canvas,最后再合并到主canvas,比如生成海报时,先在离屏canvas画背景、文字、图片,再把离屏canvas的内容draw到主canvas,减少主canvas的重绘次数。
合并绘制操作
canvas的draw()(旧版)或上下文操作(2d模式)是异步的,频繁调用会导致性能爆炸,尽量把多个绘制命令合并:
// 坏例子:多次draw ctx.fillRect(0,0,100,100) ctx.draw() ctx.fillRect(100,100,100,100) ctx.draw() // 好例子:合并命令 ctx.beginPath() ctx.fillRect(0,0,100,100) ctx.fillRect(100,100,100,100) ctx.fill() // 或调用一次draw(旧版)
避免重复创建对象
比如绘制动画时,不要每次帧都新建路径、渐变对象,复用已有的Path2D、LinearGradient:
// 坏例子:每次新建渐变
ctx.fillStyle = ctx.createLinearGradient(0,0,100,0)
// 好例子:复用渐变对象
if (!this.gradient) {
this.gradient = ctx.createLinearGradient(0,0,100,0)
this.gradient.addColorStop(0, 'red')
this.gradient.addColorStop(1, 'blue')
}
ctx.fillStyle = this.gradient动画用requestAnimationFrame
做动画时,用canvas.requestAnimationFrame代替setInterval,它能和屏幕刷新率同步,减少丢帧,旧版canvas可以用wx.nextTick配合,但2d模式优先用标准API:
animate() {
this.ctx.clearRect(0,0,canvas.width,canvas.height)
// 更新图形位置
this.ball.y += this.speed
this.ctx.drawImage(this.ball.img, this.ball.x, this.ball.y)
this.raf = this.canvas.requestAnimationFrame(this.animate.bind(this))
}跨端(微信/支付宝/抖音)适配,canvas有哪些雷区?
如果小程序要多端发布,canvas的API差异能把人搞疯,这些细节要盯紧:
API命名差异:微信小程序2d模式是
canvas.getContext('2d'),支付宝是my.createCanvasContext('id'),抖音是tt.createCanvasContext('id'),可以封装跨端工具函数:function createCtx(canvasId) { #ifdef MP-WEIXIN const query = wx.createSelectorQuery() return query.select(`#${canvasId}`).fields({ node: true }).exec(res => res[0].node.getContext('2d')) #elif MP-ALIPAY return my.createCanvasContext(canvasId) #elif MP-TOUTIAO return tt.createCanvasContext(canvasId) #endif }绘制回调差异:微信旧版canvas用
ctx.draw(false, () => { /* 回调 */ }),2d模式没有draw()回调,要自己用requestAnimationFrame控制顺序;支付宝的draw回调是ctx.draw(() => {})。组件属性差异:微信小程序2d模式用
type="2d",支付宝没有type属性,直接用canvas-id,开发时要多端真机调试,比如在微信开发者工具、支付宝IDE、抖音IDE分别测试。
想做互动效果,比如手势擦除、动画,canvas咋结合事件?
canvas本身没有“点击某个图形触发事件”的API,得自己实现坐标计算+事件绑定:
手势擦除:模拟“刮刮乐”
监听touchstart、touchmove事件,在canvas上绘制透明线条(用globalCompositeOperation = 'destination-out'),露出下层背景:
onTouchMove(e) {
const { x, y } = e.touches[0]
this.ctx.lineTo(x, y)
this.ctx.strokeStyle = 'rgba(0,0,0,0)' // 透明笔触
this.ctx.globalCompositeOperation = 'destination-out'
this.ctx.stroke()
}点击图形判断:数学碰撞检测
比如判断点击是否在圆形内,要计算点击坐标与圆心的距离是否≤半径:
onTap(e) {
const { x, y } = e.detail // 点击坐标
const centerX = 100, centerY = 100, radius = 50
const distance = Math.sqrt((x - centerX)**2 + (y - centerY)**2)
if (distance <= radius) {
// 点击到圆形内
}
}动画:帧循环更新状态
用requestAnimationFrame循环更新图形的位置、角度,每次重绘前清空画布:
drawBall() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.ball.y += this.ball.speed
if (this.ball.y > this.canvas.height) {
this.ball.y = 0 // 边界反弹
}
this.ctx.drawImage(this.ball.img, this.ball.x, this.ball.y)
this.raf = this.canvas.requestAnimationFrame(this.drawBall.bind(this))
}实战案例:生成带用户信息的分享海报,canvas咋落地?
很多小程序需要“生成海报保存到相册”,这是canvas的典型实战场景,拆解步骤+避坑:
需求拆解
海报包含:用户头像(圆形)、昵称(自动换行)、活动二维码、背景图、品牌slogan。
资源准备
用户头像:通过
wx.getUserInfo获取,或调用后端接口。二维码:后端生成带参数的二维码图片,或用小程序API(如微信的
wx.createQRCode)。背景图:本地图片(减少加载时间)或CDN图片(需配置download域名)。
绘制流程(关键步骤)
// 1. 加载所有图片(Promise.all确保全加载完再绘制)
const [bgImg, avatarImg, qrImg] = await Promise.all([
this.loadImage('bg.png'), // 封装的加载函数
this.loadImage(avatarUrl),
this.loadImage(qrUrl)
])
// 2. 绘制背景
this.ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height)
// 3. 绘制圆形头像(裁剪)
this.ctx.save() // 保存上下文状态
this.ctx.beginPath()
this.ctx.arc(100, 100, 50, 0, 2 * Math.PI)
this.ctx.clip() // 裁剪成圆形
this.ctx.drawImage(avatarImg, 50, 50, 100, 100) // 注意坐标要让图片居中
this.ctx.restore() // 恢复上下文
// 4. 绘制昵称(自动换行)
const nickName = '前端开发狮'
const lineHeight = 32
const maxWidth = 200
let y = 200
nickName.split('').forEach((char, index) => {
const textWidth = this.ctx.measureText(char).width
if (x + textWidth > maxWidth) {
x = 0
y += lineHeight
}
this.ctx.fillText(char, x, y)
x += textWidth
})
// 5. 绘制二维码
this.ctx.drawImage(qrImg, 300, 200, 100, 100)
// 6. 保存到临时文件,再保存到相册
wx.canvasToTempFilePath({
canvas: this.canvas,
success: (res) => {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath
})
}
})避坑指南
图片跨域:提前配置download域名,或用本地图片。
绘制顺序:后画的元素会覆盖先画的,比如二维码要在背景之后画。
高清适配:canvas宽高要乘以
devicePixelRatio,否则生成的海报模糊。
常见报错“canvas未找到”“绘制不显示”,怎么排查?
遇到问题别慌,按这个流程查:
组件是否渲染:canvas代码写在
onLoad里?要移到onReady,因为onLoad时组件还没渲染,拿不到节点。上下文是否正确:
2d模式:检查
select('#id')的id是否和旧版canvas:canvas-id是否匹配,wx.createCanvasContext(canvasId)的canvasId是否正确。绘制命令是否完整:
画形状后没调
fill()/stroke()?比如arc后要fill()才显示。drawImage的图片没加载完?检查onload回调是否执行。样式是否为空:
strokeStyle、fillStyle是不是默认的null?比如想画红色矩形,要先ctx.fillStyle = 'red'。尺寸是否为0:canvas的
width和height属性是不是设为0了?比如用rpx没转px,导致实际尺寸为0,图形画在“看不见的地方”。
掌握这些问题的解法,小程序canvas绘图从基础到实战就通了,记住核心逻辑:理解小程序的双线程架构,把握绘制时机,优化性能,多端适配时封装API,实战中拆分步骤避坑,下次做海报生成、互动动画,就能稳稳落地啦~


网友回答文明上网理性发言 已有0人参与
发表评论: