几个月以前,有人问了我一个canvass怎么实现缩放和涂鸦的问题,我基于当时的想法写了一篇博客,但是后来发现当时做的不完善,所以实现上其实还是有一些其他问题的。但是因为前段时间太忙了,也就一直没有机会去改进它。现在总算是有时间了,正好抽空把这个问题解决一下。因为不太熟悉 JS,感觉代码写多了(200行以上),复杂性上来之后,我的能力就无法来维护这个代码了,所以这次换一个面向对象的写法,感觉是好了一点。
演示效果
左边是展示的 Canvas,右边是缓存 Canvas(这个通常是不显示的),这里进行显示是为了让你更好的理解我的思路。移动图片是很好理解的,主要是缩放和绘制,绘制时,需要计算当前点在图片上面的位置,然后计算对应的在缓存 Canvas图片上的位置,然后在对应的位置进行绘制。今天的状态不好,不想写那么详细了,可以去看上一篇博客了解怎么做的,这里主要是代码上面的改进,总体的思路是不变的:
canvas实现图片缩放+涂鸦
- 拖拽图片进行移动
- 在图片上面进行绘制
- 拖拽绘制的图片
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image-Editor</title>
<style type="text/css">
body, div {
margin: 0;
padding: 0;
}
#cs {
float: left;
}
canvas {
border: red 1px solid;
}
</style>
</head>
<body>
<div class="cas">
<canvas id="cs" width="800" height="600"></canvas>
</div>
<div class="cas">
<canvas id="cache_cs" width="800" height="600"></canvas>
</div>
<button id="draw_move" onclick="editor.moveOrDraw()">移动</button>
<script type="text/javascript">
let ZOOM = [0.5, 0.6, 0.7, 1.0, 1.1, 1.2, 1.5, 2.0] // 缩放数组
let editor = {
isUserMove: true, // 用户是否在移动,否则就是绘制
isMouseDown: false, // 用户鼠标是否按下,按下才处理移动和绘制
isInImage: false,
zoomIndex: 3, // 缩放下标
lineWidth: 10, // 默认线宽
unit: 400, // 宽高的最大值
width: 0, // 图像的宽
height: 0, // 图像的高
canvas: null,
ctx: null,
cacheCanvas: null,
cacheCtx: null,
vertexPos: {x:0, y:0}, // 左上顶点的位置
mousePos: {x:0, y:0}, // 鼠标当前位置
init: function() {
this.canvas = document.getElementById("cs")
this.ctx = this.canvas.getContext("2d")
this.ctx.lineWidth = this.lineWidth * ZOOM[this.zoomIndex]
this.ctx.strokeStyle = "red"
this.cacheCanvas = document.getElementById("cache_cs")
this.cacheCtx = this.cacheCanvas.getContext("2d")
this.cacheCtx.lineWidth = this.lineWidth * ZOOM[this.zoomIndex]
this.cacheCtx.strokeStyle = "black"
},
loadImage: function() {
let img = new Image()
img.src = "./husky.png"
img.onload = () => {
// 缩放图片
if (img.width > img.height) {
this.width = 400
this.height = this.width * img.height / img.width
} else {
this.height = 400
this.width = this.height * img.width / img.height
}
// 计算左上顶点的位置
this.vertexPos = {
x: (this.canvas.width-this.width)/2,
y: (this.canvas.height-this.height)/2
}
let zoom = ZOOM[this.zoomIndex]
this.ctx.drawImage(img, this.vertexPos.x, this.vertexPos.y, this.width*zoom, this.height*zoom)
this.cacheCtx.drawImage(img, 0, 0, this.width, this.height)
}
},
addMouseEvent: function() {
// 鼠标按下
this.canvas.onmousedown = e => {
let x = e.clientX - this.canvas.offsetLeft
let y = e.clientY - this.canvas.offsetTop
this.isMouseDown = true
// 每次按下鼠标时更新顶点的位置
let zoom = ZOOM[this.zoomIndex]
console.log("vertex: ", this.vertexPos)
this.mousePos = {x: x, y: y}
console.log("On (%d, %d)", x, y)
// 判断是否点击在图像上, 否则不做处理
if (this.isMouseInImage(x, y)) {
console.log("In image")
this.isInImage = true
// 这里加一个选中提示框
if (this.isUserMove) {
this.drawChooseRect()
} else {
// 把画笔移动到鼠标点击处
this.ctx.beginPath()
this.ctx.moveTo(x, y)
console.log("move: ", x, y)
// 计算相对位置
let cachePos = this.computeRelevantPos(x, y)
this.cacheCtx.beginPath()
this.cacheCtx.moveTo(cachePos.x, cachePos.y)
}
} else {
console.log("Out image")
}
}
// 鼠标移动
this.canvas.onmousemove = e => {
// 鼠标按下才处理
if (!this.isMouseDown) {
return
}
let x = e.clientX - this.canvas.offsetLeft
let y = e.clientY - this.canvas.offsetTop
// 鼠标在图像外部不处理
if (!this.isMouseInImage(x, y)) {
return
}
let dx = x-this.mousePos.x
let dy = y-this.mousePos.y
// 更新鼠标的位置
this.mousePos.x = x
this.mousePos.y = y
if (this.isUserMove) {
// 移动操作
// 更新顶点位置
this.vertexPos.x = this.vertexPos.x + dx
this.vertexPos.y = this.vertexPos.y + dy
// 重新绘制
this.redraw()
this.drawChooseRect()
} else {
// 绘制操作
this.draw(x, y)
}
}
// 鼠标滚轮
this.canvas.onmousewheel = e => {
// 禁止移动和缩放一起操作
if (this.isMouseDown) {
return
}
let x = e.clientX - this.canvas.offsetLeft;
let y = e.clientY - this.canvas.offsetTop;
delta = e.wheelDelta;
if (delta > 0) {
if (this.zoomIndex + 1 < ZOOM.length) {
this.zoomIndex += 1;
} else {
this.zoomIndex = ZOOM.length - 1;
}
} else {
if (this.zoomIndex - 1 >= 0) {
this.zoomIndex -= 1;
} else {
this.zoomIndex = 0;
}
}
// 图像缩放
this.redraw()
}
let mouseUpAndOut =e => {
this.isMouseDown = false
this.isInImage = false
// 如果是在移动操作中,则清空canvas,重新绘制
if (this.isUserMove) {
this.redraw()
}
}
// 鼠标松开和鼠标离开
this.canvas.onmouseup = mouseUpAndOut
this.canvas.onmouseout = mouseUpAndOut
},
moveOrDraw: function() {
this.isUserMove = !this.isUserMove
if (this.isUserMove) {
document.getElementById("draw_move").innerText = "移动";
} else {
document.getElementById("draw_move").innerText = "绘制";
}
},
redraw: function() {
let zoom = ZOOM[this.zoomIndex]
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
// 如果从顶点处进行缩放,我感觉不是很好,所以考虑从图像的中心处开始缩放,
// 所以这里的顶点的计算要多思考一下
this.ctx.save()
this.ctx.drawImage(this.cacheCanvas, 0, 0,
this.width, this.height,
this.vertexPos.x + (1-zoom)*this.width/2,
this.vertexPos.y + (1-zoom)*this.height/2,
this.width*zoom, this.height*zoom)
this.ctx.restore()
console.log("zoom vertex: ", this.vertexPos)
},
drawChooseRect: function() {
let zoom = ZOOM[this.zoomIndex]
this.ctx.save()
this.ctx.lineWidth = 1
this.ctx.strokeStyle = "red"
this.ctx.strokeRect(
this.vertexPos.x + (1-zoom)*this.width/2,
this.vertexPos.y + (1-zoom)*this.height/2,
this.width*zoom,
this.height*zoom
)
this.ctx.restore()
},
draw: function(x, y) {
// 在显示canvas中绘制图像,在缓存canvas中绘制
let zoom = ZOOM[this.zoomIndex]
this.ctx.save()
this.ctx.lineWidth = this.ctx.lineWidth * zoom
this.ctx.lineTo(x, y)
this.ctx.stroke()
this.ctx.restore()
// 计算在缓存canvas中的相对位置,并进行绘制
let cachePos = this.computeRelevantPos(x, y)
this.cacheCtx.save()
// 缩小对应放大的尺寸
this.cacheCtx.lineWidth = this.ctx.lineWidth / zoom
this.cacheCtx.lineTo(cachePos.x, cachePos.y)
this.cacheCtx.stroke()
this.cacheCtx.restore()
},
isMouseInImage: function(x, y) {
let zoom = ZOOM[this.zoomIndex]
let vx = this.vertexPos.x+(1-zoom)*this.width/2
let vy = this.vertexPos.y+(1-zoom)*this.height/2
let xInImage = vx <= x && x <= vx+this.width*zoom
let yInImage = vy <= y && y <= vy+this.height*zoom
if (xInImage && yInImage) {
return true
}
return false
},
computeRelevantPos: function(x, y) {
// 对应的缓存画布坐标需要做一个转换
// 计算相对位置,这里是这个程序最复杂的一部分了
// 这里需要考虑到显示canvas中图像的顶点位置,缩放尺寸,
// 然后来计算在对应的缓存canvas上的相对位置
let zoom = ZOOM[this.zoomIndex]
let vx = this.vertexPos.x+(1-zoom)*this.width/2
let vy = this.vertexPos.y+(1-zoom)*this.height/2
return {
x: (x-vx) / zoom,
y: (y-vy) / zoom
}
},
run: function() {
this.init()
this.loadImage()
this.addMouseEvent()
}
}
editor.run()
</script>
</body>
</html>