uniapp+canvas实现逐字手写效果

在移动端使用 UniApp 进行逐字手写的功能。用户可以在一个 inputCanvas 上书写单个字,然后在特定时间后将这个字添加到 outputCanvas 上,形成一个逐字的手写效果。用户还可以保存整幅图像或者撤销上一个添加的字。

 

  1. 初始化 Canvas

    • 使用 uni.createCanvasContext 创建画布上下文,设置笔触样式和线条属性。
  2. 触摸事件处理

    • handleTouchStart:捕获触摸开始事件,初始化绘图状态。
    • handleTouchMove:捕获触摸移动事件,实时绘制路径。
    • handleTouchEnd:捕获触摸结束事件,启动定时器准备添加字。
  3. 添加字符

    • addChar 方法将 inputCanvas 的内容绘制到 outputCanvas 上,同时保存字符的路径。
  4. 撤销功能

    • undoChar 方法删除上一个字符,并重新绘制 outputCanvas
  5. 保存和上传图像

    • 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>

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个简单的示例代码,实现了使用uniappcanvas组件实现手写签名效果,并提供了保存和清空的功能。 template: ```html <template> <view> <canvas canvas-id="myCanvas" style="width:100%;height:400rpx;background-color: #fff;" bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd"></canvas> <view class="btn-group"> <button type="primary" class="btn" @tap="save">保存</button> <button type="default" class="btn" @tap="clear">清空</button> </view> </view> </template> ``` script: ```javascript <script> export default { data() { return { ctx: null, // canvas绘图上下文 lastX: 0, // 上一次触摸的X坐标 lastY: 0, // 上一次触摸的Y坐标 isDrawing: false // 是否正在绘制 } }, mounted() { // 获取canvas绘图上下文 this.ctx = uni.createCanvasContext('myCanvas', this); }, methods: { touchStart(e) { // 记录起始点坐标 this.lastX = e.changedTouches[0].x; this.lastY = e.changedTouches[0].y; // 开始绘制 this.isDrawing = true; }, touchMove(e) { if (this.isDrawing) { // 获取当前坐标 const currentX = e.changedTouches[0].x; const currentY = e.changedTouches[0].y; // 绘制线条 this.ctx.beginPath(); this.ctx.moveTo(this.lastX, this.lastY); this.ctx.lineTo(currentX, currentY); this.ctx.stroke(); // 更新上一次坐标 this.lastX = currentX; this.lastY = currentY; } }, touchEnd() { // 结束绘制 this.isDrawing = false; }, save() { // 保存签名 uni.canvasToTempFilePath({ canvasId: 'myCanvas', success: (res) => { uni.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { uni.showToast({ title: '保存成功' }) } }) } }, this); }, clear() { // 清空画布 this.ctx.clearRect(0, 0, uni.upx2px(750), uni.upx2px(400)); this.ctx.draw(); } } } </script> ``` 需要注意的是,canvas组件的宽度和高度应该使用rpx或者upx作为单位,并且在保存签名时,需要先使用canvasToTempFilePath方法将canvas转换成临时文件路径,再使用saveImageToPhotosAlbum方法保存到相册中。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值