代码之家  ›  专栏  ›  技术社区  ›  John Ruddell

缩放、旋转和平移后,画布绘制点定位到单击的区域

  •  0
  • John Ruddell  · 技术社区  · 7 年前

    我有一个有点复杂的问题(我做了很多研究,但没能找到我要找的),基本上我正在建立一个标签工具,在那里我得到一组图像,我想能够点击对象的角落,并创建一个用户点击的点。

    没什么要注意的(我已经做了这些)

    • 图像可以是任何方向,我需要旋转它们(从一个方向旋转)
    • 图像开始时应按比例缩放以适应画布(从图像和画布大小将比例设置为“缩小”)。
    • 用户可以“平移”(基于 方向键 )
    • 用户可以放大和缩小图像(缩放 上移+箭头/下移 )
    • 用户可以将图像重置回中心( 空格键 中心, 移位+空格键 重置缩放初始和重新居中)

    我现在的问题是我正在构建单击部分(我在光标位置绘制一个点)。我尝试了多种方法将鼠标坐标放置在正确的位置(考虑到缩放、平移和旋转),但我很难将自己的头绕在它周围。希望得到一些帮助或指导 反转旋转、缩放和平移 我已经申请在正确的地方得到这一点。


    为了提供一些真实的背景,我制作了一个代码笔来显示发生了什么。

    Codepen to see it live with the arrow keys / clicks on the canvas

    const red = '#ff0000';
    
    class App extends React.Component<{}, {}> {
      private canvas: HTMLCanvasElement
      private image = new Image
      private ctx: CanvasRenderingContext2D | null
      private data: any
      private orientation: number = 270
    
      private moveKeys: {[key: number]: number} = {}
      private cw: number
      private ch: number
      private scaleFactor: number = 1.00
      private startX: number
      private startY: number
      private panX: number
      private panY: number
      private isShiftPressed: boolean
      private defaultScaleFactor: number = 1.00
    
      private imagePoints: number[][] = []
    
      loadImage = (url: string) => {
        this.image.onload = () => {
          const iw = this.orientation === 0 || this.orientation === 180 ? this.image.width : this.image.height
          const ih = this.orientation === 0 || this.orientation === 180 ? this.image.height : this.image.width
          const smaller = Math.min(this.canvas.width / iw, this.canvas.height / ih)
          this.defaultScaleFactor = smaller
          this.scaleFactor = smaller  
        }
        this.image.src = 'https://i.stack.imgur.com/EYvnM.jpg'
      }
      componentWillUnmount() {
        document.removeEventListener('keyup', this.handleKeyUp)
        document.removeEventListener('keydown', this.handleKeyDown)
        // window.removeEventListener('resize', this.resizeCanvas)
        this.canvas.removeEventListener('click', this.handleCanvasClick)
      }
      componentDidMount() {
        this.isShiftPressed = false
        document.addEventListener('keyup', this.handleKeyUp)
        document.addEventListener('keydown', this.handleKeyDown)
        // window.addEventListener('resize', this.resizeCanvas) // dont need for this example
        requestAnimationFrame(this.animate)
        const elem = document.getElementById('canvasContainer')
        if (!elem) return
    
        const rect = elem.getBoundingClientRect()
    
        this.canvas.addEventListener('click', this.handleCanvasClick)
        this.canvas.width = rect.width
        this.canvas.height = rect.height
        this.ctx = this.canvas.getContext('2d')
        this.cw = this.canvas.width
        this.ch = this.canvas.height
    
        this.startX = -(this.cw / 2)
        this.startY = -(this.ch / 2)
        this.panX = this.startX
        this.panY = this.startY
    
        this.loadImage()
    
      }
      handleCanvasClick = (e) => {
        let rect = this.canvas.getBoundingClientRect()
        let x = e.clientX - rect.left
        let y = e.clientY - rect.top
        this.imagePoints.push([x, y])
      }
    
      animate = () => {
        Object.keys(this.moveKeys).forEach( key => {
          this.handleMovement(key, this.moveKeys[key])
        })
        this.drawTranslated()
        requestAnimationFrame(this.animate)
      }
      handleMovement = (key, quantity) => {
        const moveUnit = 20
        switch (parseInt(key)) {
          case 32: // spacebar
            this.panX = this.startX
            this.panY = this.startY
            if (this.isShiftPressed) {
              this.scaleFactor = this.defaultScaleFactor
            }
            break
          case 37: // left
            if (this.orientation === 90 || this.orientation === 270) {
              this.panY -= moveUnit
            } else {
              this.panX -= moveUnit
            }
            break
          case 38: // up
            if (this.isShiftPressed) {
              this.scaleFactor *= 1.1
            } else {
              if (this.orientation === 90 || this.orientation === 270) {
                this.panX += moveUnit
              } else {
                this.panY += moveUnit
              }
            }
            break
          case 39: // right
            if (this.orientation === 90 || this.orientation === 270) {
              this.panY += moveUnit
            } else {
              this.panX += moveUnit
            }
            break
          case 40: // down
            if (this.isShiftPressed) {
              this.scaleFactor /= 1.1
            } else {
              if (this.orientation === 90 || this.orientation === 270) {
                this.panX -= moveUnit
              } else {
                this.panY -= moveUnit
              }
            }
            break
          default:
            break
        }
      }
    
      handleKeyUp = (e) => {
        if (e.shiftKey || e.keyCode === 16) {
          this.isShiftPressed = false
        }
        delete this.moveKeys[e.keyCode]
      }
      handleKeyDown = (e) => {
        e.preventDefault()
        if (e.shiftKey || e.keyCode === 16) {
          this.isShiftPressed = true
        }
        e.keyCode in this.moveKeys ? this.moveKeys[e.keyCode] += 1 : this.moveKeys[e.keyCode] = 1
      }
    
      drawTranslated = () => {
        if (!this.ctx) return
        const ctx = this.ctx
        ctx.clearRect(0, 0, this.cw, this.ch)
        ctx.save()
        ctx.translate(this.cw / 2, this.ch / 2)
        ctx.rotate(this.orientation * Math.PI / 180)
        ctx.scale(this.scaleFactor, this.scaleFactor)
        ctx.translate(this.panX, this.panY)
    
        const transformedWidth = this.canvas.width / 2 - this.image.width / 2
        const transformedHeight = this.canvas.height / 2 - this.image.height / 2
        ctx.drawImage(
          this.image,
          transformedWidth,
          transformedHeight
        )
    
        const pointSize = 10
        if (this.imagePoints.length > 0) {
          this.imagePoints.forEach( ([x, y]) => {
            ctx.fillStyle = red
            ctx.beginPath()
            // Obviously the x and y here need to be transformed to work with the current scale, rotation and translation. But I'm stuck here!
            ctx.arc(x, y, pointSize, 0, Math.PI * 2, true)
            ctx.closePath()
            ctx.fill()
          })
        }
        ctx.restore()
      }
      handleResetUserClicks = () => {
        this.imagePoints = []
      }
      render() {
        return (
          <div id="container">
            <div>Use arrow keys to pan the canvas, shift + up / down to zoom, spacebar to reset</div>
            <div id="canvasContainer">
              <canvas ref={this.assignCameraRef} id="canvasElement" style={{ position: 'absolute' }} ref={this.assignCameraRef} />
            </div>
            <div>
              <button onClick={this.handleResetUserClicks}>Reset Clicks</button>
            </div>
          </div>
        )
      }
      assignCameraRef = (canvas: HTMLCanvasElement) => this.canvas = canvas
    }
    

    请忽略缺少已定义的道具和少数硬编码值(如方向)。我删除了一些代码,并将其抽象为更通用的代码,其中一部分意味着将图像url硬编码为一个我在网上找到的虚拟url,并为该图像设置一些参数。

    1 回复  |  直到 7 年前
        1
  •  1
  •   Blindman67    7 年前

    反转变换

    Inverse transform 找到世界坐标。

    在本例中,世界坐标是图像像素坐标,函数 toWorld 将从画布坐标转换为世界坐标。

    不管你怎么翻译 cx , cy ,旋转,缩放,然后平移。在计算反变换之前,需要将平移坐标乘以上述3个变换的矩阵,并将其添加到矩阵的最后两个值。

    注释 你一次淘了两次 this.panX , this.panY 然后你走过 transformedWidth transformedHeight 下面的函数需要完整的pan this.panX + transformedWidth this.panY + transformedHeight 最后两个论点。

    来自 linked answer

    // NOTE rotate is in radians
    // with panX, and panY added
    var matrix = [1,0,0,1,0,0];
    var invMatrix = [1,0,0,1];
    function createMatrix(x, y, scale, rotate, panX, panY){
        var m = matrix; // just to make it easier to type and read
        var im = invMatrix; // just to make it easier to type and read
    
        // create the rotation and scale parts of the matrix
        m[3] =   m[0] = Math.cos(rotate) * scale;
        m[2] = -(m[1] = Math.sin(rotate) * scale);
    
        // add the translation
        m[4] = x;
        m[5] = y;
    
        // transform pan and add to the position part of the matrix
        m[4] += panX * m[0] + panY * m[2];
        m[5] += panX * m[1] + panY * m[3];    
    
        //=====================================
        // calculate the inverse transformation
    
        // first get the cross product of x axis and y axis
        cross = m[0] * m[3] - m[1] * m[2];
    
        // now get the inverted axis
        im[0] =  m[3] / cross;
        im[1] = -m[1] / cross;
        im[2] = -m[2] / cross;
        im[3] =  m[0] / cross;
     }  
    

    然后你可以使用 世界 功能来自 链接答案 获取世界坐标(图像空间中的坐标)