效果如图
每次绘制都重新开始,可实现上一步,下一步撤回。控制台输出绘制的图片。
思路:
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);也修改