几个月以前,有人问了我一个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 = 400this.height = this.width * img.height / img.width} else {this.height = 400this.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.offsetLeftlet y = e.clientY - this.canvas.offsetTopthis.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.offsetLeftlet y = e.clientY - this.canvas.offsetTop// 鼠标在图像外部不处理if (!this.isMouseInImage(x, y)) {return}let dx = x-this.mousePos.xlet dy = y-this.mousePos.y// 更新鼠标的位置this.mousePos.x = xthis.mousePos.y = yif (this.isUserMove) {// 移动操作// 更新顶点位置this.vertexPos.x = this.vertexPos.x + dxthis.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 = falsethis.isInImage = false// 如果是在移动操作中,则清空canvas,重新绘制if (this.isUserMove) {this.redraw()}}// 鼠标松开和鼠标离开this.canvas.onmouseup = mouseUpAndOutthis.canvas.onmouseout = mouseUpAndOut},moveOrDraw: function() {this.isUserMove = !this.isUserMoveif (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 = 1this.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 * zoomthis.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 / zoomthis.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/2let vy = this.vertexPos.y+(1-zoom)*this.height/2let xInImage = vx <= x && x <= vx+this.width*zoomlet yInImage = vy <= y && y <= vy+this.height*zoomif (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/2let vy = this.vertexPos.y+(1-zoom)*this.height/2return {x: (x-vx) / zoom,y: (y-vy) / zoom}},run: function() {this.init()this.loadImage()this.addMouseEvent()}}editor.run()</script>
</body>
</html>