canvas绘制路线可撤销+放大镜跟随效果,uniapp+vue2

效果如图

每次绘制都重新开始,可实现上一步,下一步撤回。控制台输出绘制的图片。

思路:

1、首先绘制区域的主要的bigCanvas=需要一个装有背景图的firstCanvas+再需要一个绘制路线的secondCanvas,这几个canvas的尺寸都应一致,尺寸通过图片加载完成后getImg函数获取尺寸进行赋值。

2、放大镜mark-img-big=背景图mark-img-big-bg+跟随绘制showScaleCanvas组成,主要是限制放大镜的可视范围,超出hidden。背景用图片放大两倍,位置移动用transform-origin修改坐标即可。

.mark-img-big-bg {
						transform: scale(2);
						position: absolute;
						top: 0;
					}
	<canvas id="showScaleCanvas" class="mark-img-big-bg w-inherit"
						:style="{'transform-origin':moveX +'px '+ moveY +'px','width':`${canvasw+'px'}`,'height':`${canvash+'px'}`}"
						canvas-id="showScaleCanvas" disable-scroll='true'>
					</canvas>

实现过程

1、全局设置,在app.vue中设置rem比例,375宽度下,1rem=10px

body {
		font-size: calc(100vw / 37.5);
	}

body {
		width: 100vw;
		height: 100vh;
		background-size: 100%;
		overflow: scroll;
		background-color: #031129;
}

2、页面代码直接拷贝就行

<template>
	<view class="mark">

		<view class="tit"
			style="color:#fff;font-size: 20px;text-align: center;height: 50px;margin: 20px;background-color: #aa7d89;">
			标题
		</view>
		<view class="content w-inherit" >

			<view class=" mark-img padding-box w-inherit flex flex-c" style="height: 400px;">
				<image ref='ctxbg' class="s-inherit" style="overflow: hidden;" :src="bigImgUrl" @load="getImg($event)"
					mode="aspectFit">
				</image>
				<!-- 画布 -->

				<view class="canvas-wrap s-inherit flex flex-c">
					<view class="" :style="{'height':`${canvash+'px'}`,'width':`${canvasw+'px'}`}">
						<canvas id="bigCanvas" canvas-id="bigCanvas" class="s-inherit" disable-scroll='true'
							style="position: relative;">
							<canvas id="firstCanvas"
								:style="{'background-image':'url('+bigImgUrl+')','position':'absolute','top':'0px','background-size':'contain','background-repeat':'no-repeat','background-position':'center'}"
								class="s-inherit" canvas-id="firstCanvas" disable-scroll='true'>

							</canvas>
							<canvas class="s-inherit" id="secondCanvas" style="position:absolute;top:0px"
								canvas-id="secondCanvas" disable-scroll='true' @touchmove='touctMove'
								@touchstart='touchStart($event)' @touchend='touchEnd(($event))'>

							</canvas>
						</canvas>
					</view>
				</view>


				<!-- 放大镜 -->
				<view class="mark-img-big ">
					<image
						:style="{'transform-origin':moveX +'px '+ moveY +'px','width':`${canvasw+'px'}`,'height':`${canvash+'px'}`}"
						class="mark-img-big-bg w-inherit" :src="bigImgUrl" mode="aspectFill"></image>

					<canvas id="showScaleCanvas" class="mark-img-big-bg w-inherit"
						:style="{'transform-origin':moveX +'px '+ moveY +'px','width':`${canvasw+'px'}`,'height':`${canvash+'px'}`}"
						canvas-id="showScaleCanvas" disable-scroll='true'>
					</canvas>
				</view>
			</view>

			<view ref="pinter" class="content-pinter w-inherit">
				<view class="back-step flex flex-c">
					<view class="back-step-item">
					
						<view class="" style="color: #fff;font-size: 20px;" v-if="moveArr.length>0&&nowStep>0" @click="lastStep()">
							<
						</view>
						<view class=""  v-else style="color: #858585;font-size: 20px;">
							<
						</view>
					</view>
					<view class="back-step-item">
				
						<view class="" style="color: #fff;font-size: 20px;" v-if="nowStep<moveArr.length" @click="nextStep()">
							>
						</view>
						<view class=""  v-else style="color: #858585;font-size: 20px;">
							>
						</view>
					</view>
				</view>
				<view class="content-pinter-top padding-box w-inherit flex flex-ai-c flex-jc-sb">
					<view class="content-pinter-top-icon">
						<view class="" style="color: #fff;font-size: 20px;">
							X
						</view>
					</view>
					<view class="content-pinter-top-txt flex flex-c">
						Lable
					</view>
					<view class="content-pinter-top-icon" @click="overSign">
						
						<view class="" style="color: #fff;font-size: 20px;">
							√
						</view>
					</view>
				</view>
				<view class="content-pinter-btm w-inherit ">

					<view class="content-pinter-btm-inner  w-inherit flex flex-jc-c padding-box">
						<view class="content-pinter-btm-txt">
							Thickness
						</view>
						<view class="content-pinter-btm-point flex flex-ai-c flex-jc-sb">

							<view class="content-pinter-btm-point-item"
								:class="index==penThickness?'content-pinter-btm-point-item-choosed':''"
								v-for="i,index in 4" @click="changMarkPoint(index)">

							</view>
							<view class="content-pinter-btm-point-line">

							</view>
						</view>
					</view>

				</view>
			</view>
		</view>

	</view>
</template>

<script>
	export default {
		data() {
			return {
				
				startX: 0,//绘制开始的位置
				startY: 0,

				moveX: 0, //放大窗口移动位置
				moveY: 0,

				content: null, //图片canvas
				contentSecond: null, //划线canvas
				canvasBig: null, //总canvas=图+线
				scaleCanvas: null,//放大镜绘制线条canvas
			
				touchs: [], //绘制点记录
				canvasw: 0,//canvas绘制尺寸
				canvash: 0,
				bigImgUrl:
				 //'https://n.sinaimg.cn/sinacn10/765/w564h1001/20180423/a2d4-fzqvvrz6612779.jpg',//竖图
				'https://img0.baidu.com/it/u=887040227,1380035048&fm=253&fmt=auto&app=138&f=JPEG?w=641&h=384', //图片地址 横图
				penThickness: 1, //0123 选择笔画粗细
				moveArr: [], //记录所有路线步数
				nowStep: 0, //当前步数
				
			}
		},

		onLoad(e) {
			// 若页面加载的时候传入base64位图片,转换
			if (e.scene) {
				this.bigImgUrl = e.scene.replace(new RegExp(" ", "gm"), "+")
			}
		},

		mounted() {
			// 各canvas初始化;
			this.content = uni.createCanvasContext('firstCanvas')//背景
			this.contentSecond = uni.createCanvasContext('secondCanvas')//线条
			this.canvasBig = uni.createCanvasContext('bigCanvas')//包含背景和线条,用于最终合成背景和线条
			this.scaleCanvas = uni.createCanvasContext('showScaleCanvas')//放大镜绘制线条
		},
		methods: {
			
			// 上一步
			lastStep() {
				
				this.clearClick()
				this.nowStep--
				if (this.nowStep == 0) {
					return
				}

				let needDrawArrs = this.moveArr[this.nowStep - 1]
				this.touchs = []

				this.touchs.push(needDrawArrs[0])
				for (let i = 1; i < needDrawArrs.length; i++) {
					this.touchs.push(needDrawArrs[i])
					if (this.touchs.length >= 2) {
						this.drawLine(this.touchs)
					}
				}
				this.getMoveData(needDrawArrs[needDrawArrs.length - 1].x, needDrawArrs[needDrawArrs.length - 1].y)
				this.drawArc(needDrawArrs[0].x, needDrawArrs[0].y)
				this.drawArc(needDrawArrs[needDrawArrs.length - 1].x, needDrawArrs[needDrawArrs.length - 1].y)
				this.touchs.length = 0
			},
			// 下一步
			nextStep() {
				this.clearClick()
				this.nowStep++
				let needDrawArrs = this.moveArr[this.nowStep - 1]
				this.touchs = []
				this.touchs.push(needDrawArrs[0])
				for (let i = 1; i < needDrawArrs.length; i++) {
					this.touchs.push(needDrawArrs[i])
					if (this.touchs.length >= 2) {
						this.drawLine(this.touchs)
					}
				}
				this.getMoveData(needDrawArrs[needDrawArrs.length - 1].x, needDrawArrs[needDrawArrs.length - 1].y)
				this.drawArc(needDrawArrs[0].x, needDrawArrs[0].y)
				this.drawArc(needDrawArrs[needDrawArrs.length - 1].x, needDrawArrs[needDrawArrs.length - 1].y)
				this.touchs.length = 0
			},
			changMarkPoint(index) {
				this.penThickness = index
				switch (index) {
					case 0: {
						this.contentSecond.setLineWidth(2)
						this.scaleCanvas.setLineWidth(3)
						break
					}
					case 1: {
						this.contentSecond.setLineWidth(4)
						this.scaleCanvas.setLineWidth(4)
						break
					}
					case 2: {
						this.contentSecond.setLineWidth(6)
						this.scaleCanvas.setLineWidth(6)
						break
					}
					case 3: {
						this.contentSecond.setLineWidth(8)
						this.scaleCanvas.setLineWidth(6)
						break
					}
				}
			},
			back() {
				uni.navigateBack();
			},
			getImg(e) {
				let picw = e.detail.width
				let pich = e.detail.height
				let scaleData = 1
				// 横
				if (picw >= pich) {
					scaleData = this.$refs.ctxbg.$el.offsetWidth / picw
				} else { // 竖
					scaleData = this.$refs.ctxbg.$el.offsetHeight / pich
				}
				// 将绘制canvas尺寸限制成跟图片一样大
				this.canvash = pich * scaleData
				this.canvasw = picw * scaleData
				
				this.drawCanvas()
			},
			initDraw() {

				//设置图片绘制线条
				this.contentSecond.setStrokeStyle("#5565F2")
				//设置线的宽度
				this.contentSecond.setLineWidth(5)
				//设置线两端端点样式更加圆润
				this.contentSecond.setLineCap('round')
				//设置两条线连接处更加圆润
				this.contentSecond.setLineJoin('round')

				//设置放大镜线条
				this.scaleCanvas.setStrokeStyle("#5565F2")
				this.scaleCanvas.setLineWidth(5)
				this.scaleCanvas.setLineCap('round')
				this.scaleCanvas.setLineJoin('round')
			},

			touchStart(e) {
				this.touchs.length = 0
				this.clearClick()

				let point = {
					x: e.changedTouches[0].x,
					y: e.changedTouches[0].y,
				}
				if (this.startX == 0) {
					this.startX = e.changedTouches[0].x
					this.startY = e.changedTouches[0].y
				}
				this.drawArc(e.changedTouches[0].x, e.changedTouches[0].y)

				this.touchs.push(point)
				let a = []
				a.push(point)
				this.moveArr.push(a)
				this.nowStep = this.moveArr.length
				this.changMarkPoint(this.penThickness)
			},
			touctMove(e) {
				let point = {
					x: e.touches[0].x,
					y: e.touches[0].y,
				}

				point = this.getMoveData(e.touches[0].x, e.touches[0].y)

				this.touchs.push(point)
				this.moveArr[this.moveArr.length - 1].push(point)
				if (this.touchs.length >= 2) {
					this.drawLine(this.touchs)
				}
			},
			touchEnd(e) {
				var that = this
				this.drawArc(this.touchs[0].x, this.touchs[0].y)

				setTimeout(function() {
					that.touchs.length = 0
				}, 20)

			},
			// 移动的时候计算边界和放大镜位置
			getMoveData(x, y) {

				// 放大窗口的尺寸120*120 ,60是一半,画布放大2倍this.canvasw*2
				let tx = (x / this.canvasw) * this.canvasw * 2 - 60
				let ty = (y / this.canvash) * this.canvash * 2 - 60

				if (tx < 0) {//左边界
					tx = 0
				} else if (tx > this.canvasw * 2 - 120) {//右边界
					tx = this.canvasw * 2 - 120
				}
				this.moveX = tx // 放大镜移动数值

				if (ty < 0) {//上边界
					ty = 0
				} else if (ty > this.canvash * 2 - 120) {//下边界
					ty = this.canvash * 2 - 120
				}
				this.moveY = ty // 放大镜移动数值

				// 线条绘制边界
				let obj = {
					x: 0,
					y: 0
				}
				obj.x = x
				obj.y = y
				if (x < 0) {
					obj.x = 0
				}
				if (x > this.canvasw) {
					obj.x = this.canvasw
				}
				if (y < 0) {
					obj.y = 0
				}
				if (y > this.canvash) {
					obj.y = this.canvash
				}

				return obj
			},
			//路线起止圆圈
			drawArc(x, y) {
				this.contentSecond.beginPath();
				this.contentSecond.arc(x, y, 5, 0, 2 * Math.PI); // 绘制圆形
				this.contentSecond.setFillStyle('#5565F2'); // 设置填充颜色
				this.contentSecond.setLineWidth(2)
				this.contentSecond.fill(); // 填充
				this.contentSecond.setStrokeStyle('#fff');
				this.contentSecond.stroke(); //对当前路径进行描边
				this.contentSecond.save(); //保存
				this.contentSecond.draw(true)


				this.scaleCanvas.beginPath();
				this.scaleCanvas.arc(x, y, 5, 0, 2 * Math.PI); // 绘制圆形
				this.scaleCanvas.setFillStyle('#5565F2'); // 设置填充颜色
				this.scaleCanvas.setLineWidth(2)
				this.scaleCanvas.fill(); // 填充
				this.scaleCanvas.setStrokeStyle('#fff');
				this.scaleCanvas.stroke(); //对当前路径进行描边
				this.scaleCanvas.save(); //保存
				this.scaleCanvas.draw(true)

			},
			drawLine(touchs) {

				let point1 = touchs[0]
				let point2 = touchs[1]
				this.touchs.shift()

				// 保持直线
				let flagX = point1.x - point2.x
				let flagY = point1.y - point2.y
				if (Math.abs(flagX) < 15) {
					point2.x = point1.x
				} else {
					point2.x = point2.x
				}
				if (Math.abs(flagY) < 15) {
					point2.y = point1.y
				} else {
					point2.y = point2.y
				}

				this.contentSecond.setStrokeStyle('#5565F2');
				this.contentSecond.moveTo(point1.x, point1.y)
				this.contentSecond.lineTo(point2.x, point2.y)
				this.contentSecond.stroke()
				this.contentSecond.draw(true)

				this.scaleCanvas.setStrokeStyle('#5565F2');
				this.scaleCanvas.moveTo(point1.x, point1.y)
				this.scaleCanvas.lineTo(point2.x, point2.y)
				this.scaleCanvas.stroke()
				this.scaleCanvas.draw(true)
			},

			//清除操作
			clearClick() {
				//清除画布
				this.contentSecond.clearRect(0, 0, this.canvasw, this.canvash)
				this.contentSecond.draw(true)

				// //清除画布
				this.scaleCanvas.clearRect(0, 0, this.canvasw, this.canvash)
				this.scaleCanvas.draw(true)
			},

			drawCanvas() {
				var that = this
				uni.getImageInfo({
					src: that.bigImgUrl,
					success(res) {
						// 画布背景
						that.content.drawImage(res.path, 0, 0, that.canvasw, that
							.canvash) // 设置图片坐标及大小,括号里面的分别是(图片路径,x坐标,y坐标,width,height)
						that.content.save(); //保存
						that.content.draw(true) //绘制	
						that.initDraw()
					}
				})
			},

			// 签名完成,将canvas转为图片,同时获得图片的临时路径
			overSign() {
				var that = this
				that.content.restore();
				let url = ''
				// 主canvas先绘制图片canvas即firstCanvas
				// 然后绘制线条canvas即secondCanvas
				// 然后生成图片canvasToTempFilePath
				that.canvasBig.draw(true, () => {// 若绘制失败,给予一定的时间确保绘制完成
					uni.canvasToTempFilePath({
						canvasId: 'firstCanvas',
						x: 0,
						y: 0,
						destWidth: that.canvasw,
						destHeight: that.canvash,
						quality: .7,
						success: function(res) {
							// 得到 图片的临时路径
							const srcUrl = res.tempFilePath
							that.canvasBig.drawImage(srcUrl, 0, 0, that.canvasw, that
								.canvash) // 设置图片坐标及大小,括号里面的分别是(图片路径,x坐标,y坐标,width,height)
							that.canvasBig.save(); //保存

							that.canvasBig.draw(true, () => {
								uni.canvasToTempFilePath({
									canvasId: 'secondCanvas',
									x: 0,
									y: 0,
									destWidth: that.canvasw,
									destHeight: that.canvash,
									quality: .7,
									success: function(res) {
										// 得到 图片的临时路径
										const srcUrl = res.tempFilePath
										that.canvasBig.drawImage(srcUrl, 0, 0, that
											.canvasw, that.canvash
										) // 设置图片坐标及大小,括号里面的分别是(图片路径,x坐标,y坐标,width,height)
										that.canvasBig.save(); //保存

										that.canvasBig.draw(true, () => {
											uni.canvasToTempFilePath({
												canvasId: 'bigCanvas',
												x: 0,
												y: 0,
												destWidth: that
													.canvasw,
												destHeight: that
													.canvash,
												quality: .7,
												success: function(
													res) {
													// 得到 图片的临时路径
													const
														srcUrl =
														res
														.tempFilePath
													that.canvasBig
														.save(); //保存
													console.log(srcUrl)
												},
												fail: function(
													err) {
												
												}
											});
										}); 
									},
									fail: function(err) {
										
									}
								});
							}); 
						},
						fail: function(err) {
							
						}
					});
				});

			},

		},
	}
</script>
<style lang="scss">
	.w-inherit {
		width: 100%;
	}

	.s-inherit {
		width: 100%;
		height: 100%;
	}

	.h-inherit {
		height: 100%;
	}

	.padding-box {
		box-sizing: border-box;
	}

	.flex {
		display: flex;
	}

	.flex-c {
		align-items: center;
		justify-content: center;
	}

	.flex-jc-sb {
		justify-content: space-between;
	}

	.flex-ai-c {
		align-items: center;
	}

	img {
		width: 100%;
		height: 100%;
	}

	.zoomed-image {
		position: absolute;
		background-color: #fff;
		border: 1px solid #ccc;
		overflow: hidden;
	}

	.large-image {
		position: relative;
		width: 100%;
		height: 100%;
		transform: scale(1);
		transition: transform 0.3s ease-in-out;
	}

	/* 当放大时的样式 */
	.zoomed-image:hover .large-image {
		transform: scale(2);
		transform-origin: center;
	}

	.mark {

		.content {
			background-color: rgba(0, 0, 0, .7);

			.canvas-wrap {
				top: 0;
				// height: 400px;
				// background-color: #ccc;
				// width: 300px;
				position: absolute;

				uni-canvas {

					width: 100%;
					height: 100%;
				}

				#canvas-wrap {
					background-repeat: no-repeat;
					background-position: center;
					background-size: contain;
				}
			}

			.mark-img {
				padding: 1.2rem;
				position: relative;

				.mark-img-big {
					width: 120px;
					height: 120px;
					border: .4rem solid rgba(0, 0, 0, 0.58);

					border-radius: .8rem;

					position: absolute;
					right: 1.2rem;
					top: 0rem;
					overflow: hidden;

					.mark-img-big-bg {
						transform: scale(2);
						position: absolute;
						top: 0;
					}
				}
			}

			.content-pinter {
				height: 27.6rem;
				// height: 24.4rem;
				position: fixed;
				bottom: 0;

				.back-step {
					.back-step-item {
						display: flex;
						align-items: center;
						justify-content: center;
						width: 3.2rem;
						height: 3.2rem;
						margin: 0 1.2rem 1.6rem;
					}
				}

				.content-pinter-top {
					height: 4.4rem;
					border-radius: .8rem .8rem 0 0;
					background-color: #232526;
					position: relative;
					padding: 0 1.2rem;

					.content-pinter-top-icon {
						width: 3.2rem;
						height: 3.2rem;
					}

					.content-pinter-top-txt {
						position: absolute;
						bottom: 0;
						width: 9.4rem;
						left: calc(50% - 4.7rem);
						height: 3.9rem;
						font-weight: 500;
						font-size: 1.7rem;
						color: #FFFFFF;
						background-color: #1B1B1B;
						border-radius: .8rem .8rem 0 0;
					}
				}

				.content-pinter-btm {
					background-color: #1B1B1B;
					padding: 4.6rem .24rem 0;
					height: 20rem;

					.content-pinter-btm-inner {
						height: 2rem;
					}

					.content-pinter-btm-txt {
						font-weight: 500;
						font-size: 1.5rem;
						color: #FFFFFF;
						margin-right: .4rem;
					}

					.content-pinter-btm-point {
						width: 24rem;
						position: relative;


						:nth-child(1) {
							width: .8rem;
							height: .8rem;
						}

						:nth-child(2) {
							width: 1.2rem;
							height: 1.2rem;
						}

						:nth-child(3) {
							width: 1.6rem;
							height: 1.6rem;
						}

						:nth-child(4) {
							width: 2rem;
							height: 2rem;
						}

					}

					.content-pinter-btm-point-item {

						background-color: #000000;
						border-radius: 50%;
						z-index: 11;

					}

					.content-pinter-btm-point-item-choosed {

						background: linear-gradient(144deg, #4070ED 0%, #6D39FF 47%, #0F31E5 100%);
						border: 2px solid #fff;

					}

					.content-pinter-btm-point-line {
						background-color: #000000;
						width: 23.6rem;
						position: absolute;
						height: 2px;
						top: calc(50% - 1px);
						left: calc(50% - 11.8rem);
					}
				}
			}
		}
	}
</style>

3、其他需求

3.1 假如不想每次都清空画布,保留原先路径,注释clearClick函数即可。

3.2 假如想路径跟随保持一直,非刻意直线可以注释drawLine函数中的代码。

// 保持直线
				let flagX = point1.x - point2.x
				let flagY = point1.y - point2.y
				if (Math.abs(flagX) < 15) {
					point2.x = point1.x
				} else {
					point2.x = point2.x
				}
				if (Math.abs(flagY) < 15) {
					point2.y = point1.y
				} else {
					point2.y = point2.y
				}

3.3假如绘制不成功,可以刷新重试或者给予一定的时间确保绘制完成.

3.4目前放大镜尺寸是120*120px,假如要修改为m*m,也要修改getMoveData函数中120和60的数值为m和m/2。

3.5假如修改放大倍数为x,getMoveData函数中this.canvasw * 2的2就是放大倍数也要修改为x,css中的 transform: scale(x);也修改

  • 10
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值