【问题标题】:Canvas draw points localized to clicked area after scale, rotate and translation缩放、旋转和平移后定位到单击区域的画布绘制点
【发布时间】:2018-11-10 00:07:41
【问题描述】:

我有一个复杂的问题(我做了很多研究,但无法找到我正在寻找的东西),基本上我正在构建一个标签工具,我可以在其中获取一组图像并且我希望能够单击对象的角并创建用户单击的点。

需要注意的几点(我已经做过这些)

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

我现在遇到的问题是我正在构建点击部分(我在光标位置绘制一个点)。我已经尝试了多种方法将鼠标坐标放在正确的位置(考虑比例、平移和旋转),但我很难把头绕在它周围。希望得到一些关于如何基本上反转旋转、缩放和平移的帮助或指示,我已申请将点放在正确的位置。


为了提供一些真实的背景信息,我制作了一个 Codepen 来展示正在发生的事情。

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,并为该图像设置一些参数。

【问题讨论】:

    标签: javascript reactjs typescript canvas


    【解决方案1】:

    反转变换

    Inverse transform 查找世界坐标。

    在这种情况下,世界坐标是图像像素坐标,toWorld 函数会将画布坐标转换为世界坐标。

    但是,您可以转换为cx,cy,旋转、缩放然后平移。在计算逆变换之前,您需要将平移坐标乘以上述 3 个变换的矩阵,并将其添加到矩阵的最后两个值。

    注意你已经为this.panXthis.panY平移了两次,然后你平移了transformedWidthtransformedHeight下面的函数需要完整平移this.panX + transformedWidththis.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;
     }  
    

    然后您可以使用linked answer 中的toWorld 函数来获取世界坐标(图像空间中的坐标)

    【讨论】:

    • 感谢您的帮助/回答!我在集成它时遇到了一些麻烦,我将继续努力,当我完成它时,我会接受你的回答:)
    • 想通了! :) 只需要花一些时间了解矩阵中的每一块都在做什么,以及如何考虑图像偏移。您介意我用最终解决方案为其他路人编辑您的答案吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-06-11
    • 1970-01-01
    • 2018-05-13
    • 1970-01-01
    • 1970-01-01
    • 2019-03-09
    • 2015-07-14
    相关资源
    最近更新 更多