获取鼠标坐标

学习Canvas中获取鼠标坐标的方法,掌握坐标转换和变换后的坐标计算。正确获取鼠标在Canvas中的坐标是实现交互的基础,需要考虑元素位置、滚动、缩放等因素。

基本方法

使用offsetX/offsetY

canvas.addEventListener('click', (e) => {
  console.log('鼠标位置:', e.offsetX, e.offsetY)
})

使用clientX/clientY

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top
  console.log('鼠标位置:', x, y)
})

使用pageX/pageY

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = e.pageX - rect.left - window.scrollX
  const y = e.pageY - rect.top - window.scrollY
  console.log('鼠标位置:', x, y)
})

坐标属性对比

属性说明参考点
offsetX/offsetY相对于目标元素目标元素左上角
clientX/clientY相对于视口浏览器视口左上角
pageX/pageY相对于文档文档左上角
screenX/screenY相对于屏幕屏幕左上角

坐标获取演示

鼠标坐标显示

封装坐标获取

基础函数

function getMousePos(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  return {
    x: e.clientX - rect.left,
    y: e.clientY - rect.top
  }
}

canvas.addEventListener('click', (e) => {
  const pos = getMousePos(canvas, e)
  console.log('点击位置:', pos.x, pos.y)
})

考虑边框

function getMousePosWithBorder(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const style = getComputedStyle(canvas)
  
  const borderLeft = parseInt(style.borderLeftWidth) || 0
  const borderTop = parseInt(style.borderTopWidth) || 0
  
  return {
    x: e.clientX - rect.left - borderLeft,
    y: e.clientY - rect.top - borderTop
  }
}

考虑内边距

function getMousePosWithPadding(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const style = getComputedStyle(canvas)
  
  const paddingLeft = parseInt(style.paddingLeft) || 0
  const paddingTop = parseInt(style.paddingTop) || 0
  
  return {
    x: e.clientX - rect.left - paddingLeft,
    y: e.clientY - rect.top - paddingTop
  }
}

CSS变换影响

处理缩放

function getMousePosWithScale(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const scaleX = canvas.width / rect.width
  const scaleY = canvas.height / rect.height
  
  return {
    x: (e.clientX - rect.left) * scaleX,
    y: (e.clientY - rect.top) * scaleY
  }
}

处理变换

function getMousePosWithTransform(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const style = getComputedStyle(canvas)
  const transform = style.transform
  
  let scaleX = 1
  let scaleY = 1
  let offsetX = 0
  let offsetY = 0
  
  if (transform && transform !== 'none') {
    const matrix = transform.match(/matrix\(([^)]+)\)/)
    if (matrix) {
      const values = matrix[1].split(',').map(v => parseFloat(v.trim()))
      scaleX = values[0]
      scaleY = values[3]
      offsetX = values[4]
      offsetY = values[5]
    }
  }
  
  return {
    x: (e.clientX - rect.left - offsetX) / scaleX,
    y: (e.clientY - rect.top - offsetY) / scaleY
  }
}

Canvas变换影响

平移变换

let translateX = 100
let translateY = 50

ctx.translate(translateX, translateY)

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const canvasX = e.clientX - rect.left
  const canvasY = e.clientY - rect.top
  
  const worldX = canvasX - translateX
  const worldY = canvasY - translateY
  
  console.log('世界坐标:', worldX, worldY)
})

缩放变换

let scale = 2

ctx.scale(scale, scale)

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const canvasX = e.clientX - rect.left
  const canvasY = e.clientY - rect.top
  
  const worldX = canvasX / scale
  const worldY = canvasY / scale
  
  console.log('世界坐标:', worldX, worldY)
})

旋转变换

let rotation = Math.PI / 4
let centerX = 200
let centerY = 150

ctx.translate(centerX, centerY)
ctx.rotate(rotation)
ctx.translate(-centerX, -centerY)

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const canvasX = e.clientX - rect.left
  const canvasY = e.clientY - rect.top
  
  const dx = canvasX - centerX
  const dy = canvasY - centerY
  
  const worldX = centerX + dx * Math.cos(-rotation) - dy * Math.sin(-rotation)
  const worldY = centerY + dx * Math.sin(-rotation) + dy * Math.cos(-rotation)
  
  console.log('世界坐标:', worldX, worldY)
})

逆变换矩阵

使用当前变换矩阵

function getTransformedPoint(canvas, e) {
  const rect = canvas.getBoundingClientRect()
  const canvasX = e.clientX - rect.left
  const canvasY = e.clientY - rect.top
  
  const matrix = ctx.getTransform()
  const inverse = matrix.inverse()
  
  if (inverse) {
    const point = new DOMPoint(canvasX, canvasY)
    const transformed = point.matrixTransform(inverse)
    return { x: transformed.x, y: transformed.y }
  }
  
  return { x: canvasX, y: canvasY }
}

手动计算逆矩阵

function invertMatrix(a, b, c, d, e, f) {
  const det = a * d - b * c
  if (det === 0) return null
  
  return {
    a: d / det,
    b: -b / det,
    c: -c / det,
    d: a / det,
    e: (b * f - d * e) / det,
    f: (c * e - a * f) / det
  }
}

function applyInverseTransform(x, y, matrix) {
  if (!matrix) return { x, y }
  return {
    x: matrix.a * x + matrix.c * y + matrix.e,
    y: matrix.b * x + matrix.d * y + matrix.f
  }
}

视口与相机

相机系统

class Camera {
  constructor() {
    this.x = 0
    this.y = 0
    this.zoom = 1
  }
  
  screenToWorld(screenX, screenY) {
    return {
      x: (screenX - this.x) / this.zoom,
      y: (screenY - this.y) / this.zoom
    }
  }
  
  worldToScreen(worldX, worldY) {
    return {
      x: worldX * this.zoom + this.x,
      y: worldY * this.zoom + this.y
    }
  }
  
  apply(ctx) {
    ctx.setTransform(this.zoom, 0, 0, this.zoom, this.x, this.y)
  }
}

const camera = new Camera()

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const screenX = e.clientX - rect.left
  const screenY = e.clientY - rect.top
  
  const worldPos = camera.screenToWorld(screenX, screenY)
  console.log('世界坐标:', worldPos)
})

相机演示

相机坐标系统(滚轮缩放,拖拽平移)

坐标工具类

class CoordinateSystem {
  constructor(canvas) {
    this.canvas = canvas
    this.transforms = []
  }
  
  pushTransform(transform) {
    this.transforms.push(transform)
  }
  
  popTransform() {
    this.transforms.pop()
  }
  
  screenToCanvas(e) {
    const rect = this.canvas.getBoundingClientRect()
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    }
  }
  
  canvasToWorld(canvasX, canvasY) {
    let x = canvasX
    let y = canvasY
    
    for (let i = this.transforms.length - 1; i >= 0; i--) {
      const t = this.transforms[i]
      
      if (t.type === 'translate') {
        x -= t.x
        y -= t.y
      } else if (t.type === 'scale') {
        x /= t.x
        y /= t.y
      } else if (t.type === 'rotate') {
        const cos = Math.cos(-t.angle)
        const sin = Math.sin(-t.angle)
        const nx = x * cos - y * sin
        const ny = x * sin + y * cos
        x = nx
        y = ny
      }
    }
    
    return { x, y }
  }
  
  screenToWorld(e) {
    const canvas = this.screenToCanvas(e)
    return this.canvasToWorld(canvas.x, canvas.y)
  }
}