我的第一个小程序:Canvas画布的使用

先放出小程序码:

功能

先列出目前小程序已完成了功能:

  • 笔记绘制;
  • 颜色和宽度;
  • 背景;
  • 撤销;
  • 恢复撤销;
  • 清空;
  • 保存本地;
  • 笔记播放;
  • 分享/口令分享;

下面简单介绍几个重要的功能实现

画布的实现

由于一开始使用了uni + vite + vue3来进行小程序的开发,遇到的第一个坑就是当前版本的uni不支持canvas响应touch事件,从而直接导致无法进行正常的绘制操作。于是就给uni-app提了一个issue,为了不影响开发进度,于是先自己搞了一个解决方案:

其实也比较简单,就是在canvas上面覆盖了一层view,将touch事件绑定在view上。

然后就是创建上下文,由于目前uni没有跟上微信官方的api,所以就直接使用了微信官方提供 的api来获取上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
export default function usePaint(selector: string) {
const paint = ref<Paint>();

const initCanvas = (canvas: any) => {
const { windowWidth, windowHeight, pixelRatio } = uni.getSystemInfoSync();
/**
* 解决绘图路径锯齿问题
* 1. 尺寸取物理像素 windowWidth * pixelRatio
* 2. 画布缩放像素比 ctx.scale
*/
canvas.width = windowWidth * pixelRatio;
canvas.height = windowHeight * pixelRatio;

const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
ctx.translate(windowWidth * pixelRatio / 2, windowHeight * pixelRatio / 2);

// #ifndef MP-TOUTIAO
ctx.scale(pixelRatio, pixelRatio);
// #endif

paint.value = new Paint(ctx);
};

onReady(() => {
// #ifdef MP
uni
.createSelectorQuery()
.select('#' + selector)
// #ifdef MP-TOUTIAO
// @ts-ignore
.node()
.exec(([{ node: canvas }]) => {
initCanvas(canvas);
})
// #endif
// #ifndef MP-TOUTIAO
.fields(
{
// @ts-ignore
node: true,
size: true,
},
({ node: canvas }: any) => {
initCanvas(canvas);
}
).exec();
// #endif
// #endif
// #ifndef MP
const { windowWidth, windowHeight } = uni.getSystemInfoSync();
const ctx = uni.createCanvasContext(selector, getCurrentInstance());
ctx.translate(windowWidth / 2, windowHeight / 2);
paint.value = new Paint(ctx as unknown as CanvasRenderingContext2D);
// #endif
});

return paint;
}

其中比较关键的一个点就是ctx.scale(pixelRatio, pixelRatio);,我们通过css样式设置的大小,只是canvas展示的大小,事件绘图时画布的大小是通过canvas.width = windowWidth * pixelRatio;来确定的,这是设置是画布大小是设备屏幕的物理像素大小,为了保持视觉的一致性,所以就需要.scale方法进行缩放。

对于canvas的api,我就不介绍了,与Web端的canvas完全保持一致。

背景的实现

一开始我以为背景很简单,其实就是在画布上绘制一个宽高100%的矩形,然后填充颜色就可以了,但是实际上是行不通的,就比如这样一个场景:

当目前画布上已经绘制了很多笔记了,如果直接矩形填充,就会把当前的画布上的笔记全部覆盖了。如果绘制矩形之前,先将当前绘制的笔记保存起来,然后等背景绘制完成之后再将保存的笔记重新绘制在画布上,是否可行呢?答案当然的可行的,但不是最优的

我们只需要在设置背景之前,先通过ctx.getImageData将当前画布保存起来,然后设置玩背景之后,再同各国ctx.putImageData将画布还原就可以了。为什么说不是最优的方案呢?因为设置背景是一个用户的自由操作,可能会存在反复更换的情况,这是不可预料的,但绝对是可行的。我的解决方案是在canvas的底部搞了一个view,然后设置背景的时候只需要改变底部view的背景颜色就行了,不需要对画布进行任何操作,画布永远都是透明的。但也存在一个小问题,就是当用户将画布保存在本地的时候,还是需要将背景绘制到canvas画布上,但这个操作相对于更换背景应该是很少的。

1
2
3
4
5
6
7
8
9
10
11
12
13
<view class="canvas canvas-bg" :style="{ backgroundColor: state.backgroundColor }"></view>
<canvas
id="drawCanvas"
type="2d"
class="canvas"
></canvas>
<view
class="canvas canvas-cover"
@touchstart.stop="handleTouchStart"
@touchmove.stop="handleTouchMove"
@touchend.stop="handleTouchEnd"
@touchcancel.stop="handleTouchEnd"
></view>

将画布保存在本地

这个功能其实就是api的调用,没啥可说的,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export const useGenerateImage = async (selector: string): Promise<string> => {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
uni
.createSelectorQuery()
.select('#' + selector)
.fields(
{
// @ts-ignore
node: true,
size: true,
},
({ node: canvas }: any) => {
uni.canvasToTempFilePath({
// @ts-ignore
canvas,
success: ({ tempFilePath }) => {
resolve(tempFilePath);
},
fail: reject,
});
}
)
.exec();
// #endif
// #ifndef MP-WEIXIN
uni.canvasToTempFilePath({
canvasId: selector,
success: ({ tempFilePath }) => {
resolve(tempFilePath);
},
fail: reject,
});
// #endif
});
}

然后通过uni.saveImageToPhotosAlbum将生成的图片链接保存在本地,如果是h5端,可使用以下方法:

1
2
3
4
5
6
export function download(url: string, name = String(Date.now())) {
const a = document.createElement('a');
a.download = name;
a.href = url;
a.click();
}

未完待续

篇幅有限,先分享到这里,撤销播放请参考下一篇文章。

如果觉得不错,可以关注我的公众号【末日码农】,我会将开发中遇到的实际问题和一些好的技术知识分享给大家。


我的第一个小程序:Canvas画布的使用
https://codingmo.com/article/20220228/17df73d3a780/
作者
颜漠笑年
发布于
2022年2月28日
许可协议