在移动端使用 UniApp 进行逐字手写的功能。用户可以在一个 inputCanvas
上书写单个字,然后在特定时间后将这个字添加到 outputCanvas
上,形成一个逐字的手写效果。用户还可以保存整幅图像或者撤销上一个添加的字。
-
初始化 Canvas:
- 使用
uni.createCanvasContext
创建画布上下文,设置笔触样式和线条属性。
- 使用
-
触摸事件处理:
handleTouchStart
:捕获触摸开始事件,初始化绘图状态。handleTouchMove
:捕获触摸移动事件,实时绘制路径。handleTouchEnd
:捕获触摸结束事件,启动定时器准备添加字。
-
添加字符:
addChar
方法将inputCanvas
的内容绘制到outputCanvas
上,同时保存字符的路径。
-
撤销功能:
undoChar
方法删除上一个字符,并重新绘制outputCanvas
。
-
保存和上传图像:
saveImage
方法将outputCanvas
的内容保存为图片,并调用upload
方法上传。
完整代码:
<template>
<view class="container">
<view class="tip">
<view class="">
请您在区域内逐字手写以下文字,全部写完后点击保存!
</view>
<u-alert style="margin-bottom: 20upx;" :description="ruleForm.sqcn" type = "primary" ></u-alert>
</view>
<view class="canvas-container">
<canvas canvas-id="inputCanvas" class="input-canvas" @touchstart="handleTouchStart"
@touchmove="handleTouchMove" @touchend="handleTouchEnd"></canvas>
</view>
<view class="buttons">
<u-button text="撤销上一个字" size="normal" type="error" @click="undoChar"></u-button>
<u-button text="保存" size="normal" type="primary" @click="saveImage"></u-button>
</view>
<canvas :style="{ height: outputHeight }" canvas-id="outputCanvas" class="output-canvas"></canvas>
</view>
</template>
<script>
import fileService from "@/api/file/fileService.js";
import knsService from "@/api/kns/knsService"
export default {
data() {
return {
isDrawing: false,
startX: 0,
startY: 0,
strokes: [],
canvasWidth: 300,
canvasHeight: 300,
charObjects: [],
timer: null,
delay: 1000, // 1秒延迟
fj: '',
outputHeight: '50px',
label: '',
ruleForm: {}
};
},
mounted() {
this.getData()
this.initCanvas('inputCanvas');
this.initCanvas('outputCanvas');
},
onLoad(option) {
this.label = option.label;
},
methods: {
// 获取承诺
async getData() {
const res = await knsService.getSettingData();
this.ruleForm = res[0];
},
initCanvas(canvasId) {
const context = uni.createCanvasContext(canvasId, this);
context.setStrokeStyle('#000');
context.setLineWidth(4);
context.setLineCap('round');
context.setLineJoin('round');
context.draw();
},
handleTouchStart(e) {
e.preventDefault(); // 阻止默认滚动行为
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const touch = e.touches[0];
this.isDrawing = true;
this.startX = touch.x;
this.startY = touch.y;
this.strokes.push({
x: touch.x,
y: touch.y
});
},
handleTouchMove(e) {
e.preventDefault(); // 阻止默认滚动行为
if (!this.isDrawing) return;
const touch = e.touches[0];
const context = uni.createCanvasContext('inputCanvas', this);
context.moveTo(this.startX, this.startY);
context.lineTo(touch.x, touch.y);
context.stroke();
context.draw(true);
this.startX = touch.x;
this.startY = touch.y;
this.strokes.push({
x: touch.x,
y: touch.y
});
},
handleTouchEnd(e) {
e.preventDefault(); // 阻止默认滚动行为
this.isDrawing = false;
this.timer = setTimeout(this.addChar, this.delay);
},
addChar() {
const inputContext = uni.createCanvasContext('inputCanvas', this);
uni.canvasToTempFilePath({
canvasId: 'inputCanvas',
success: (res) => {
// 保存这个字符的路径
this.charObjects.push(res.tempFilePath);
// 清空 inputCanvas 上的内容
inputContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
inputContext.draw();
this.redrawOutputCanvas()
},
});
},
undoChar() {
if (this.charObjects.length > 0) {
this.charObjects.pop();
this.redrawOutputCanvas();
if (this.charObjects.length === 0) {
this.outputHeight = 50; // 如果字符对象为空,则将输出画布高度设置为 50
}
}
},
redrawOutputCanvas() {
const context = uni.createCanvasContext('outputCanvas', this);
const charSize = 50; // 调整字符大小
const charSpacing = 48; // 调整字符间距
const maxCharsPerRow = Math.floor(this.canvasWidth / charSpacing); // 每行最大字符数
// 动态设置高度
const numRows = Math.ceil(this.charObjects.length / maxCharsPerRow); // 计算行数
this.outputHeight = `${numRows * charSize}px`; // 动态计算输出画布的高度
console.log(this.outputHeight, this.charObjects.length, 'outputHeight');
// 清除画布并设置高度
context.clearRect(0, 0, this.canvasWidth, this.outputHeight);
// 绘制字符
this.charObjects.forEach((charPath, index) => {
const rowIndex = Math.floor(index / maxCharsPerRow); // 当前字符的行索引
const colIndex = index % maxCharsPerRow; // 当前字符的列索引
context.drawImage(charPath, 10 + colIndex * charSpacing, 10 + rowIndex * charSpacing, charSize,
charSize);
});
this.$nextTick(() => {
// 一次性绘制所有字符
context.draw();
})
},
saveImage() {
if (this.charObjects.length === 0) {
uni.showToast({
icon: "error",
title: '请手写文字!'
})
return false;
}
uni.canvasToTempFilePath({
canvasId: 'outputCanvas',
success: (res) => {
// 保存图片
console.log(res.tempFilePath, 'res.tempFilePath');
this.upload(res.tempFilePath);
},
});
},
upload(img) {
fileService.upload(img).then((res) => {
let pages = getCurrentPages()
let currPage = pages[pages.length - 1]; //当前页面
let prevPage = pages[pages.length - 2]; //上一个页面
//修改前一页数据
if (prevPage.inputForm) {
prevPage.inputForm[this.label] = res
}
console.log(res, 'res');
//返回上一页
uni.navigateBack({
delta: 1,
})
});
},
},
};
</script>
<style scoped lang="scss">
.container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 40upx;
.canvas-container {
position: relative;
width: 600upx;
height: 600upx;
.input-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 10upx;
border: 4upx dashed #dddee1;
touch-action: none;
/* 禁止默认触摸动作 */
}
}
.output-canvas {
width: 600upx;
/* 设置高度为原来的一半 */
border: 2upx solid #dddee1;
margin-top: 40upx;
}
.buttons {
display: flex;
justify-content: space-around;
width: 100%;
padding: 0upx 50upx;
}
button {
margin: 20upx;
}
.tip {
view:nth-child(1){
color: #FF6F77;
font-size: 24upx;
margin-bottom: 20upx;
}
}
}
</style>