先说结论
图形变换没什么玄学。所有效果都是同一个套路:
save() 保存当前上下文 → 改一次变换 → 重画图形 → restore() 还原。
平移、旋转、缩放、矩阵变换,区别只在于中间那步调的是 translate、rotate、scale 还是 transform。想明白这一点,后面全是细节。
一、效果
一块紫色画布,上面一个黄色描边的 100×100 矩形,下面一排绿色按钮:
- 平移:矩形整体往右下挪 75px,标题变成「平移图形」
- 旋转:以坐标原点为中心顺时针转 15°,标题变成「旋转图形」
- 缩放:水平和垂直都放大 2 倍,标题变成「缩放图形」
- 矩阵变换:水平拉伸 1.25、倾斜 30°、垂直压缩到 0.5,再位移 (50, 50)
- 还原:清空后重新画出初始矩形
- 矩形叠加椭圆:先画一个转 30° 的椭圆,再把矩形压在它上面
每次点击都会先把画布清空,所以看到的永远是「一次变换后的结果」,不会越叠越乱。
二、先把画布立起来
新建小程序项目,不勾云服务、不勾模板。画布用新版 2D 类型,CSS 上给宽高,别在标签里写死像素:
<canvas type="2d" id="my-canvas" />
canvas {
width: 100%;
height: 700rpx;
background-color: rebeccapurple;
}
拿绘图上下文要用 createSelectorQuery,老式的 canvas-id 是拿不到 node 的:
const query = wx.createSelectorQuery()
query.select('#my-canvas')
.fields({ node: true, size: true })
.exec((res) => {
const canvas = res[0].node
const ctx = canvas.getContext('2d')
// 后面所有绘制都靠这个 ctx
})
三、三行代码解决高清屏发虚
这是最容易翻车的地方。canvas.width 是物理像素,CSS 里写的是逻辑像素,两者之间差一个设备像素比:
const dpr = wx.getWindowInfo().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
前两行把画布的像素缓冲区放大,第三行把绘图坐标系缩回来。少了 ctx.scale 这一步,图形会缩在左上角;少了前两行,图形边缘就会糊。
四、五个按钮,其实是一个函数
以平移为例:
translate() {
const ctx = this.ctx
ctx.save() // 保存现场
this.clear() // 清空画布
ctx.translate(75, 75) // 坐标原点右移下移
this.drawing() // 重画矩形
ctx.restore() // 恢复现场
this.setData({ title: '平移图形' })
}
剩下三个换成对应的变换即可:ctx.rotate(15 * Math.PI / 180)、ctx.scale(2, 2),以及矩阵变换:
// transform(a, b, c, d, e, f)
// a 水平缩放 | b 水平倾斜 | c 垂直倾斜 | d 垂直缩放 | e 水平位移 | f 垂直位移
ctx.transform(1.25, 30 * Math.PI / 180, 0, 0.5, 50, 50)
save 和 restore 一定要成对出现。它们包住的是「这次变换」,一旦漏掉 restore,变换会污染后面所有的绘制——你会看到第二次点击时图形跑到画布外面去。
清屏也别偷懒:
clear() {
const ctx = this.ctx
const canvas = this.canvas
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
这里传的是 canvas.width(物理像素),不是 CSS 宽度。按物理像素清,才清得干净。
五、扩展一个「矩形叠加椭圆」
椭圆是这次扩展唯一的难点。Canvas 2D 有现成的 ellipse(),七个参数:
// ellipse(圆心x, 圆心y, 横半径, 纵半径, 旋转弧度, 起始角, 结束角)
ctx.beginPath()
ctx.ellipse(100, 105, 88, 58, 30 * Math.PI / 180, 0, 2 * Math.PI)
ctx.stroke()
ctx.strokeRect(50, 50, 100, 100) // 矩形叠在椭圆之上
三个要点,记住就不会画歪:
- 必须先 beginPath()。不调用它,椭圆会和上一次的路径连成一笔,描出来的线乱七八糟。
- 画完整的椭圆,起止角是 0 到 2 * Math.PI,不是 0 到 360。
- 旋转要传弧度,30 * Math.PI / 180 才是 30°。
椭圆画在下面、矩形画在上面,两者的描边颜色和线宽都一致,看起来就是「矩形叠加椭圆」的叠加效果。如果想让椭圆更扁一点,调小纵半径;想更倾斜,把旋转角加大。
六、踩过的三个坑
拼错的属性名。 课件里写的是 ctx.linewidth = 3,小写 w。这个属性根本不存在,赋值不会报错,只是静默失效,线宽一直是默认的 1 像素。正确的是 ctx.lineWidth,驼峰。
画布发虚。 只设 CSS 宽高、不设 canvas.width/height,在 iPhone 这种高倍屏上一定糊。开头那三行不能省。
变换残留。 中途 return 或者抛异常,restore 就漏了,下一次绘制会带着上一次的矩阵。写的时候把 save 和 restore 贴在一起写,中间再填内容,能少踩很多。
七、最后
整个工程一共 9 个文件,真正有逻辑的只有 pages/index/index.js 里那六个方法:drawing、clear、translate、rotate、scale、transform、reset、rectEllipse。wxml 和 wxss 负责排版,画布部分只有三行样式。
279



被折叠的 条评论
为什么被折叠?



