前言
需要通过H5上实现一个涂抹消除的功能,包括放大镜,涂抹痕迹、放大缩小,平移等功能
实现
实现过程概述:
用到了四个图层,分别是原图图层、涂抹图层、导出涂抹的图层、放大镜图层,每个图层都是一个canvas。
将操作的图片放在canvas_1中,手指涂抹时,smearCanvas绘制出线条,绘制完成时,将涂抹层的图案复刻到canvas_2中并改变颜色导出。涂抹时,enlargeCanvas实时绘制手指区域的图案并放大。
涂抹和放大镜
let canvas_1 = document.createElement("canvas") //原图图层
let canvas_layer_1 = canvas_1.getContext("2d")
let smearCanvas = document.createElement('canvas') //涂抹图层
let smearLayer = smearCanvas.getContext('2d')
let canvas_2 = document.createElement("canvas") //导出涂抹的涂层
let canvas_layer_2 = canvas_2.getContext("2d")
let enlargeCanvas = document.getElementById('enlarge_canvas') //放大镜的图层
let enlargeCtx = enlargeCanvas.getContext('2d');
canvas布局,一个是主要操作区域,另一个是放大镜。
// 放大镜
<canvas ref={enlargeRef} style={{ marginTop: `${window.barHeight}px` }} id="enlarge_canvas" className={enlargePoi ? styles[`enlarge1`] : styles['enlarge0']}></canvas>
// 图片操作区
<canvas
id="canvas"
className={styles['canvas']}
style={{ width: `${canvasWrap.w}px`, height: `${canvasWrap.h}px`, transform: `scale(${scale}) translate(${translateData.x}px, ${translateData.y}px)` }}
onTouchStart={debounce(touchStart, 80)}
onTouchMove={touchMove}
onTouchEnd={debounce(touchEnd, 80)}
></canvas>
初始化时计算图片宽高,并设置画布的尺寸
//初始化处理
const initCanvas = async () => {
maxWidth = mainRef.current.clientWidth
maxHeight = mainRef.current.clientHeight
let image = new Image()
image.src = imageUrl
enlargeCanvas = document.getElementById('enlarge_canvas') //放大镜的图层
enlargeCtx = enlargeCanvas.getContext('2d');
console.log('enlargeRef', enlargeRef);
console.log(canvasRef);
image.onload = async () => {
let w, h
w = Math.round((image.width / image.height) * maxHeight)
h = Math.round(maxWidth / (image.width / image.height))
magicRadius = Math.round(image.width / 100) || 1 //计算魔法笔半径大小
if (image.width < image.height) {
magicRadius = Math.round(image.height / 100)
// 长图
if (w > maxWidth) {
w = maxWidth
} else {
h = maxHeight
}
} else if (image.width === image.height) {
// 等比例
w = h = maxWidth < maxHeight ? maxWidth : maxHeight
}
else {
// 宽图
if (h > maxHeight) {
h = maxHeight
} else {
w = maxWidth
h = Math.round(w / (image.width / image.height))
}
}
setCanvasWrap({ w, h })
smearCanvas.width = canvas.width = canvas_1.width = w * dpr
smearCanvas.height = canvas.height = canvas_1.height = h * dpr
canvas_2.width = image.width
canvas_2.height = image.height
enlargeCanvas.width = enlargeRef.current.clientWidth
enlargeCanvas.height = enlargeRef.current.clientHeight
magnifyRectangle.width = enlargeCanvas.width * dpr
magnifyRectangle.height = enlargeCanvas.height * dpr
canvas_layer_1.drawImage(image, 0, 0, canvas.width, canvas.height)
ctx.drawImage(canvas_1, 0, 0)
setLoading(false)
setShowModal(false)
setSmearCanvasInfo()
}
image.onerror = (e) => {
Toast.show({ desc: 'failed' })
setLoading(false)
setShowModal(false)
console.log('图片加载失败', imageUrl);
}
}
涂抹部分的实现
// 涂鸦--选择区域
function smearAction() {
smearLayer.clearRect(0, 0, smearCanvas.width, smearCanvas.height)
if (eventName === 'start') {
smearLayer.beginPath()
smearLayer.arc(magnifyRectangle.x, magnifyRectangle.y, Math.round((lineWidth * dpr) / scale / 2), 0, 2 * Math.PI, false);
smearLayer.fill();
// 在点击太快时 上面的不生效 所以这里处理点击事件
console.log('smearAction是否点击', isTap);
if (!isTap) {
smearLayer.beginPath()
smearLayer.moveTo(magnifyRectangle.x, magnifyRectangle.y)
}
} else if (eventName === 'move') {
smearLayer.lineTo(magnifyRectangle.x, magnifyRectangle.y)
} else {
smearLayer.closePath()
}
!isTap && smearLayer.stroke()
draw()
}
function draw() {
enlargeCtx.clearRect(0, 0, enlargeCanvas.width, enlargeCanvas.height)
canvas_layer_2.clearRect(0, 0, canvas_2.width, canvas_2.height)
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(smearCanvas, 0, 0)
ctx.globalCompositeOperation = "destination-over";
ctx.drawImage(canvas_1, 0, 0)
}
放大镜部分的逻辑,获取手指对应在画布的位置,取手指区域的图案,绘制在放大镜图层中
// 放大镜
function drawMagnifyGlass(mouse) {
magnifyRectangle.x = mouse.x;
magnifyRectangle.y = mouse.y;
// 涂鸦轨迹
smearAction()
// 放大图像
let mX = magnifyRectangle.x - magnifyRectangle.width / scale / 2;
let mY = magnifyRectangle.y - magnifyRectangle.height / scale / 2;
if (mX < 0) {
mX = 0
} else if (mX + magnifyRectangle.width / scale > canvas.width) {
mX = canvas.width - magnifyRectangle.width / scale
}
if (mY < 0) {
mY = 0
} else if (mY + magnifyRectangle.height / scale > canvas.height) {
mY = canvas.height - magnifyRectangle.height / scale
}
enlargeCtx.clearRect(0, 0, enlargeCanvas.width, enlargeCanvas.height)
enlargeCtx.drawImage(canvas,
mX, mY,
magnifyRectangle.width / scale, magnifyRectangle.height / scale,
0, 0,
enlargeCanvas.width,
enlargeCanvas.height
)
ctx.restore();
}
用户涂抹操作的逻辑
// 开始涂抹
const touchStart = (e) => {
console.log('touchStart');
eventName = 'start'
let touchData = e.touches[0]
let poiData = windowToCanvas({ x: touchData.clientX, y: touchData.clientY })
if (e.touches.length === 1) {
// 单手指画轨迹
canDrawLine = true;
if (!actionType) {
movePoiForMagic = poiData
return
}
enlargeCanvas.style.visibility = 'visible'
drawMagnifyGlass(poiData)
} else {
canDrawLine = false
}
}
//
const touchMove = (e) => {
let touchData = e.touches[0]
let poiData = windowToCanvas({ x: touchData.clientX, y: touchData.clientY })
eventName = 'move'
console.log('touchMove');
if (e.touches.length < 2 && canDrawLine) {
if (!actionType) {
movePoiForMagic = poiData
return
}
enlargeCanvas.style.visibility = 'visible'
// 单手指画轨迹
drawMagnifyGlass(poiData)
} else {
canDrawLine = false
}
}
const touchEnd = (e) => {
eventName = 'end'
enlargeCanvas.style.visibility = 'hidden'
setEnlargePoi(0)
if (!isPinch && canDrawLine) {
if (!actionType) {
console.log(e, 'touchEnd');
dealMagicMask()
canDrawLine = false
isTap = false
return
}
// 导出涂鸦图层
convertCanvasToImage(smearCanvas, smearLayer)
}
smearLayer.clearRect(0, 0, smearCanvas.width, smearCanvas.height)
draw()
canDrawLine = false
isTap = false
}
计算手与画布的对应位置
// 计算手指位置和canvas位置
function windowToCanvas({ x, y }) {
let box = canvas.getBoundingClientRect();
console.log(box, 'box');
const minX = enlargeRef.current.offsetLeft
const maxX = enlargeRef.current.offsetLeft + enlargeRef.current.offsetWidth
const minY = enlargeRef.current.offsetTop
const maxY = enlargeRef.current.offsetTop + enlargeRef.current.offsetHeight
if ((minX < x - 10) && (x - 10 < maxX) && (minY < y - 10) && (y - 10 < maxY)) {
// 如果碰到了放大框,放大镜的位置移到另一个角落
let v = +(!enlargePoi)
setEnlargePoi(v)
}
// 考虑放大缩小的因素,需要先除缩放的倍数
return {
x: ((x - box.left) / scaleNum) * dpr,
y: ((y - box.top) / scaleNum) * dpr
};
}
缩放和平移
借助any-touch实现监听
参考github
// 注册手势 --缩放--平移
const initAtPan = () => {
if (at) at.destroy();
at = new AnyTouch(canvas);
at.on(['tap'], e => {
isTap = true
enlargeCanvas.style.visibility = 'hidden'
console.log(e, 'TAPclick点击');
})
at.on('pinch', (e) => {
console.log('pinch');
isDouble = true
const { scale: currentScale, displacementX, displacementY, phase, isEnd } = e
switch (phase) {
case 'start':
enlargeCanvas.style.visibility = 'hidden'
break;
case 'move':
if (Math.abs(currentScale - 1) >= 0.2) {
isPinch = true
currentChangeNum = (scaleNum * currentScale).toFixed(1)
if (currentChangeNum <= 0.5) {
// 缩小最小为0.5倍
currentChangeNum = 0.5
}
setTranslateData({ x: 0, y: 0 })
setScale(currentChangeNum)
smearLayer.lineWidth = Math.round((lineWidth * dpr) / currentChangeNum)
} else {
// 平移
let { width, height, } = canvas.getBoundingClientRect();
// 计算当前图片与容器的宽高差值
let diffW = width - maxWidth
let diffH = height - maxHeight
if (diffW > 0 || diffH > 0) {
//图片宽度(高度)大于容器宽度(高度),可以平移
let x = Math.round(displacementX / scaleNum)
let y = Math.round(displacementY / scaleNum)
x = x + currentTrans.x
y = y + currentTrans.y
setTranslateData({ x, y })
}
}
break;
case 'end':
if (isPinch) {
scaleNum = currentChangeNum
} else {
let { left, top, width, height, } = canvas.getBoundingClientRect();
let x = displacementX
let y = displacementY
// 计算当前图片与容器的宽高差值
let diffW = width - maxWidth
let diffH = height - maxHeight
if (diffW > 0 || diffH > 0) {
//图片宽度(高度)大于容器宽度(高度),可以平移
if (x > 0) {
//x轴方向向右偏移时,left不能为负数
if (left > 0) x = (diffW / 2 - currentTrans.x * scaleNum) * (x / Math.abs(x))
} else {
//x轴方向向左偏移时,left+可视区域宽度<=width
if ((Math.abs(left) + maxWidth) > width) x = (diffW / 2 + currentTrans.x * scaleNum) * (x / Math.abs(x))
}
if (y > 0) {
// y轴方向向下偏移时,top不能为负数
if (top > 0) y = (diffH / 2 - currentTrans.y * scaleNum) * (y / Math.abs(y))
} else {
//y轴方向向上偏移时,top+可视区域高度<=height
if (Math.abs(top) + maxHeight > height) y = (diffH / 2 + currentTrans.y * scaleNum) * (y / Math.abs(y))
}
let getX = Math.round(x / scaleNum)
let getY = Math.round(y / scaleNum)
getX = currentTrans.x + getX
getY = currentTrans.y + getY
setTranslateData({ x: getX, y: getY })
currentTrans = { x: getX, y: getY }
}
}
isPinch = false
isDouble = false
isTap = false
enlargeCanvas.style.visibility = 'hidden'
console.log('pinchend', isEnd);
break;
}
// if (isEnd) isDouble = false
})
}