Canvas实现缩放+涂鸦改进

几个月以前,有人问了我一个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>

补充一些细节

这里补充一些实现上的数学细节和效果展示, 便于更好的理解这个程序的功能和实现。

缩放之后图片的顶点的位置变化

这点我认为是最难的地方, 因为其他的坐标变化都是很直观的, 容易推导得出. 但是这里, 我当时也是饶了一个弯子, 感觉不是很好理解.

图片当前的顶点为:
( x 0 , y 0 ) (x_0, y_0) (x0,y0)

那么, 在缩放尺寸为 zoom 的情况下呢:

{ x 0 ′ = x 0 − ( z o o m − 1 ) ∗ w i d t h / 2 y 0 ′ = y 0 − ( z o o m − 1 ) ∗ h e i g h t / 2 \begin{cases} x_0' = x_0 - (zoom-1) * width / 2 \\ y_0' = y_0 - (zoom-1) * height / 2 \\ \end{cases} {x0=x0(zoom1)width/2y0=y0(zoom1)height/2

但是, 这里的意思是当前的顶点坐标依赖于缩放尺寸, 它是一个计算的值. 当前的顶点坐标还是不变的, 除非通过鼠标移动图片. 如果缩放以后使用这个计算的值更新了坐标, 后面再缩放就会造成坐标漂移了. 所以注意看第一个推导中的实际代码, 在比较的时候计算在缩放下的顶点坐标。这里可以举一个例子: 原地进行缩放, 它只是在当前顶点坐标的基础上进行变化, 而不是每次变化之后更新顶点坐标.

缩放和移动的坐标判断

首先是图片的移动, 当鼠标点击在图片中时, 可以触发图片移动操作, 否则不做处理. 所以需要首先判断鼠标是否点击在图片中, 注意这里我加了一个逻辑, 如果选中了会绘制一个红色的边框, 判断逻辑是: 在 canvas 中按下鼠标后, 获取鼠标当前的坐标(需要做和屏幕坐标做一个偏移转换)和图片左上顶点, 图片的宽高, 图片的缩放尺寸做一个比较:

图片左上顶点坐标:

( x 0 , y 0 ) (x_0, y_0) (x0,y0)

鼠标按下坐标:

( x 1 , y 1 ) (x_1, y_1) (x1,y1)

图片宽高以及缩放尺寸: width, height, zoom

{ x 0 < = x 1 < = x 0 + w i d t h ∗ z o o m y 0 < = y 1 < = y 0 + h e i g h t ∗ z o o m \begin{cases} x_0 <= x_1 <= x_0 + width * zoom \\ y_0 <= y_1 <= y_0 + height * zoom \\ \end{cases} {x0<=x1<=x0+widthzoomy0<=y1<=y0+heightzoom

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
}

在这里插入图片描述

绘制重定位

这里为什么需要两个 canvas, 图片是绘制在 canvas 中的像素, 放到和缩小会影响到显示的质量, 从一个小的像素放大, 是把当前的像素放大, 这样多次操作之后就会很模糊了. 所以采用这种方式, 在缓存canvas上绘制的是不会进行放大缩小的, 所以图片的显示质量是可以保证和刚开始一样.

在显示 canvas 中, 不论是放大还是缩小或者移动到别处, 我们在绘制时, 和图片左上顶点的相对位置是不变的. 即, 当前的鼠标按下的点(显然得在图片中)和其到图片左上顶点的位置的差值再除以当前的缩放尺寸, 即是在缓存canvas上对应的坐标了.
所以, 这里可以很容易写成这个坐标转换公式:

{ x = ( x 1 − x 0 ) / z o o m y = ( y 1 − y 0 ) / z o o m \begin{cases} x = (x_1 - x_0) / zoom \\ y = (y_1 - y_0) / zoom \\ \end{cases} {x=(x1x0)/zoomy=(y1y0)/zoom

不过这里需要注意, 虽然坐标的转换相对容易, 但是也不是绝对精确的, 因为浮点数的问题, 不过这里并不是太大的问题. 还有就是线条粗细的转换, 这也是一个不能精确计算的.

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
    }
}

在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
实现图片缩放、移动和旋转,可以利用HTML5中的canvas元素和JavaScript来实现。具体的步骤如下: 1. 在HTML中创建canvas元素,并设置它的宽度和高度,以及一个放置图片的容器元素。 ```html <canvas id="myCanvas" width="500" height="500"></canvas> <div id="image-container"></div> ``` 2. 在JavaScript中,获取canvas元素和容器元素,并创建一个Image对象来加载图片。 ```javascript var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); var img = new Image(); img.onload = function() { // 图片加载完成后执行以下代码 }; img.src = "path/to/image.jpg"; ``` 3. 在Image对象加载完成后,将它绘制到canvas上,并将canvas中的图像转换为一个可缩放、可旋转、可拖动的对象。 ```javascript var container = document.getElementById("image-container"); var imageObject = { x: canvas.width / 2, // 图片中心点的x坐标 y: canvas.height / 2, // 图片中心点的y坐标 width: img.width, // 图片的宽度 height: img.height, // 图片的高度 angle: 0, // 图片的旋转角度,单位为弧度 scale: 1 // 图片缩放比例 }; // 将图片绘制到canvas上 ctx.drawImage(img, 0, 0); // 将canvas中的图像转换为一个可缩放、可旋转、可拖动的对象 var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); container.style.width = canvas.width + "px"; container.style.height = canvas.height + "px"; container.style.backgroundImage = "url(" + canvas.toDataURL() + ")"; container.style.backgroundSize = "100% 100%"; ``` 4. 实现图片缩放、移动和旋转功能。对于缩放和旋转,可以利用CSS3的transform属性来实现;对于移动,可以通过修改图片对象的x和y属性来实现。 ```javascript // 缩放图片 function zoomImage(scale) { imageObject.scale *= scale; container.style.transform = "rotate(" + imageObject.angle + "rad) scale(" + imageObject.scale + ")"; } // 旋转图片 function rotateImage(angle) { imageObject.angle += angle; container.style.transform = "rotate(" + imageObject.angle + "rad) scale(" + imageObject.scale + ")"; } // 移动图片 function moveImage(dx, dy) { imageObject.x += dx; imageObject.y += dy; container.style.left = imageObject.x - imageObject.width / 2 + "px"; container.style.top = imageObject.y - imageObject.height / 2 + "px"; } ``` 以上代码仅为示例,具体实现方式可能会因具体需求而有所不同。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值