功能
先列出目前小程序已完成了功能:
- 笔记绘制;
- 颜色和宽度;
- 背景;
- 撤销;
- 恢复撤销;
- 清空;
- 保存本地;
- 笔记播放;
- 分享/口令分享;
微信搜索【涂图了】即可体验,下面简单介绍几个比较重要的功能实现。
画布的实现
由于一开始使用了uni + vite + vue3
来进行小程序的开发,遇到的第一个坑就是当前版本的uni不支持canvas响应touch事件,从而直接导致无法进行正常的绘制操作。于是就给uni-app提了一个issue,为了不影响开发进度,于是先自己搞了一个解决方案:
其实也比较简单,就是在canvas
上面覆盖了一层view
,将touch
事件绑定在view
上。
然后就是创建上下文,由于目前uni
没有跟上微信官方的api,所以就直接使用了微信官方提供 的api来获取上下文:
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画布上,但这个操作相对于更换背景应该是很少的。
<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的调用,没啥可说的,直接上代码:
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端,可使用以下方法:
export function download(url: string, name = String(Date.now())) {
const a = document.createElement('a');
a.download = name;
a.href = url;
a.click();
}
未完待续
篇幅有限,先分享到这里,撤销和播放功能比较复杂,后面再详细说。欢迎大家前来体验我的小程序【涂图了】,提出的你的宝贵意见