实现了以下基本功能:
1. 用户自定义画笔大小。
2. 更换涂鸦颜色。
3. 橡皮擦功能。
4. 回退和清空功能。
5. 保存和预览。
实现思路:
将canvas背景颜色设置成透明,底下渲染一张定位好的背景图。每次手指离开手机屏幕当作一次操作,然后保存成临时文件,用作回退。预览时先将背景图渲染上去,然后再将最后一张保存的临时文件渲染上去,就是带背景图片的涂鸦了,不需要背景图片的话将画布颜色设置成白色即可。橡皮檫功能使用CanvasContext.clearRecthttps://developers.weixin.qq.com/miniprogram/dev/api/canvas/CanvasContext.clearRect.html清空画布上的内容。
效果预览:
实现效果:
涂鸦小程序实现效果
实现代码:
let penType = 'drawPen';
Page({
/**
* 页面的初始数据
*/
data: {
scale: 1,
imageList: [],
showBars: false,
selectSize: wx.getStorageSync('selectSize') || 5,
selectColor: wx.getStorageSync('selectColor') || '#FFFFFF',
colors: ["#FFFFFF", "#000000", "#ff0000", "#ffff00", "#00CC00", "#99CCFF", "#0000ff", "#ff00ff"],
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.setData({
cover: options["cover"] || "/static/image/2.jpg"
})
this.initCanvas();
},
// 页面卸载 把字号选择的颜色和透明度保存
onUnload() {
const This = this.data;
penType = 'drawPen';
wx.setStorageSync('selectSize', This.selectSize);
wx.setStorageSync('selectColor', This.selectColor);
},
colorChange(e) {
const color = e.currentTarget.dataset.color;
this.setData({
selectColor: color
})
penType = 'drawPen';
},
sizeHandler(e) {
const size = e.detail.value;
this.setData({
selectSize: size
})
},
// 使用橡皮檫
rubberHandler() {
penType = 'clearPen';
this.setData({
selectColor: ""
})
},
//初始化画布
initCanvas() {
const This = this.data;
const query = wx.createSelectorQuery("#myCanvas");
query.select('#myCanvas').fields({
node: true,
size: true,
context: true
}).exec(res => {
const canvas = res[0].node;
const context = canvas.getContext('2d');
// 获取设备像素比
const dpr = wx.getSystemInfoSync().pixelRatio;
const width = res[0].width * dpr;
const height = res[0].height * dpr;
canvas.width = width;
canvas.height = height;
// 填充背景颜色
context.fillStyle = "transparent";
context.fillRect(0, 0, width, height);
// 缩放
context.scale(dpr, dpr);
// 设置默认属性
context.strokeStyle = This.selectColor;
context.lineWidth = This.selectSize;
this.setData({
canvasElement: canvas,
canvasContext: context,
})
})
},
// 开始
startTouchClick(e) {
var that = this;
const x = e.touches[0].x;
const y = e.touches[0].y;
that.setData({
oldPosition: {
x: x,
y: y
},
}, () => {
that.setData({
isDraw: true,
})
})
},
// 移动
moveClick(e) {
if (this.data.isDraw) {
let positionItem = e.touches[0]
if (this.data.canvasContext) {
this.drawCanvas(positionItem, true)
} else {
this.initCanvas(() => {
this.drawCanvas(positionItem, true)
})
}
}
},
// 描绘canvas
drawCanvas(position) {
const ctx = this.data.canvasContext;
const size = this.data.selectSize;
const color = this.data.selectColor;
const This = this.data;
if (ctx) {
ctx.beginPath();
ctx.lineWidth = size;
ctx.strokeStyle = color;
ctx.lineCap = 'round';
if (penType == 'clearPen') {
const radius = size + 1;
ctx.clearRect(position.x - (radius / 2), position.y - (radius / 2), radius, radius);
} else {
ctx.moveTo(This.oldPosition.x, This.oldPosition.y);
ctx.lineTo(position.x, position.y);
ctx.stroke()
};
ctx.closePath();
this.setData({
oldPosition: {
x: position.x,
y: position.y,
}
})
}
},
//触摸结束
endTouchClick(e) {
this.setData({
isDraw: false
})
this.saveImage();
},
//误触事件
errorClick(e) {
console.log("误触事件:", e);
},
// 是否展示 操作栏
showBarsHandler() {
this.setData({
showBars: !this.data.showBars
})
},
hideBarsHandler() {
this.setData({
showBars: false
})
},
// 回退一步 || 重绘
restore() {
// 实际上的回退就是取存储的最后一张图片 渲染出来
// 所以会有抖动 暂未想到其他方案解决
const ctx = this.data.canvasContext;
const canvas = this.data.canvasElement;
const dpr = wx.getSystemInfoSync().pixelRatio;
let imgs = this.data.imageList;
if (!imgs || imgs.length == 0) return false;
if (imgs.length == 1) return this.clearRect();
ctx.clearRect(0, 0, canvas.width, canvas.height);
// -2 是因为当前的也储存了
const cover = imgs[imgs.length - 2];
imgs.splice(imgs.length - 1, 1);
let bg = canvas.createImage();
bg.src = cover;
bg.onload = () => {
// 缩放【放大还原】
ctx.scale(1 / dpr, 1 / dpr);
ctx.drawImage(bg, 0, 0, canvas.width, canvas.height);
// 再缩放
ctx.scale(dpr, dpr);
}
},
// 清空画布
clearRect() {
const ctx = this.data.canvasContext;
const canvas = this.data.canvasElement;
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.setData({
imageList: []
})
},
// 保存图片
saveImage() {
const that = this;
wx.canvasToTempFilePath({
canvasId: 'myCanvas',
canvas: this.data.canvasElement,
success: function (res) {
that.data.imageList.push(res.tempFilePath);
},
fail: function (err) {}
})
},
// 图片预览 这边的思路是 首先将背景图片画上去 再将最后的涂鸦展示上去
preview() {
const that = this;
wx.showLoading({
title: '打包中...',
})
const images = that.data.imageList;
if (!images && images.length == 0) return false;
const img = images[images.length - 1];
// 将背景图片画上去
const ctx = this.data.canvasContext;
const canvas = this.data.canvasElement;
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cover = this.data.cover;
wx.getImageInfo({
src: cover,
success: e => {
let realWidth = canvas.width;
let realHeight = canvas.height;
// 动态计算图片宽高
if (e.width > e.height) {
const ratio = canvas.height / e.height;
realWidth = e.width * ratio;
} else {
const ratio = canvas.width / e.width;
realHeight = e.height * ratio;
}
let bg = canvas.createImage();
bg.src = cover;
bg.onload = () => {
const dpr = wx.getSystemInfoSync().pixelRatio;
ctx.scale(1 / dpr, 1 / dpr);
ctx.drawImage(bg, 0, (canvas.height - realHeight) / 2, realWidth, realHeight);
let trajectory = canvas.createImage();
trajectory.src = img;
trajectory.onload = _ => {
ctx.drawImage(trajectory, 0, 0, canvas.width, canvas.height);
wx.canvasToTempFilePath({
canvasId: 'myCanvas',
canvas: that.data.canvasElement,
success: function (res) {
wx.previewImage({
urls: [res.tempFilePath],
showmenu: true,
current: res.tempFilePath,
complete: _ => {
wx.hideLoading();
ctx.scale(dpr, dpr);
}
})
},
fail: function (err) {}
})
}
}
}
})
},
})
<view style="height: 100vh;" class="flex_column" catch:tap="hideBarsHandler">
<!-- 涂鸦区 -->
<view style="flex: 1; position: relative;" catch:touchstart="hideBarsHandler">
<image src="{{cover}}" mode="aspectFit" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: -1; width: 100%; height: 100%;" />
<canvas style="width: 100%; height: 100%;;" id="myCanvas" canvas-id="myCanvas" type="2d" bindtouchstart="startTouchClick" bindtouchmove="moveClick" bindtouchend="endTouchClick" binderror="errorClick"></canvas>
</view>
<!-- 涂鸦工具区 -->
<view style="padding: 30rpx 32rpx 50rpx;">
<view class="space" style="padding-bottom: 20rpx; color: #FFF; font-size: 30rpx; line-height: 56rpx;">
<view catch:tap="restore">回退</view>
<view style="width: 30rpx;"></view>
<view catch:tap="preview">预览</view>
<view style="flex: 1;"></view>
<view catch:tap="showBarsHandler">笔力</view>
<view style="width: 30rpx;"></view>
<view catch:tap="rubberHandler">橡皮擦</view>
<view style="width: 30rpx;"></view>
<view catch:tap="clearRect">清除</view>
</view>
<scroll-view scroll-x style="height: 90rpx;">
<view style="white-space: nowrap;">
<block wx:for="{{colors}}" wx:key="index">
<view style="background-color: {{item}};" class="colorBtn {{selectColor == item && 'select'}}" catch:tap="colorChange" data-color="{{item}}"></view>
</block>
</view>
</scroll-view>
</view>
</view>
<view class="bars {{showBars && 'show'}}" wx:if="{{showBars}}">
<view class="space vertical_center">
<view style="width: 136rpx;">字号:</view>
<slider style="flex: 1;" value="{{selectSize}}" step="1" min="1" max="20" block-size="12" activeColor="#a88cf8" bindchange="sizeHandler" />
<view style="width: 50rpx; text-align: right;">{{selectSize}}</view>
</view>
</view>
page {
height: 100vh;
overflow: hidden;
box-sizing: border-box;
padding-bottom: 0 !important;
background-color: #242424 !important;
}
.colorBtn {
width: 50rpx;
height: 50rpx;
border-radius: 50%;
margin-right: 20rpx;
display: inline-block;
border: 6rpx solid #242424;
}
.colorBtn.select {
border: 6rpx solid #FFF;
}
.bars {
right: 1rem;
width: 400rpx;
padding: 20rpx;
bottom: 7.5rem;
color: #a88cf8;
font-size: 32rpx;
font-weight: bold;
position: absolute;
border-radius: 8rpx;
background-color: #242424;
box-shadow: 0 0 1.5rem 0 #FFFFFF20;
}
slider {
margin: 20rpx 20rpx 20rpx 0 !important;
}
.flex_column {
display: flex;
flex-direction: column !important;
}
.vertical_center {
display: flex;
align-items: center;
}
.space {
display: flex;
justify-content: space-between;
}