学习Canvas图形拾取技术,掌握点击检测、图形选择和碰撞检测方法。图形拾取是指检测用户点击了哪个图形,是Canvas交互的核心技术。
// 从后往前遍历,因为后绘制的图形在上层
for (let i = shapes.length - 1; i >= 0; i--) {
if (shapes[i].containsPoint(x, y)) {
return shapes[i]
}
}
function pointInRect(px, py, rx, ry, rw, rh) {
return px >= rx && px <= rx + rw &&
py >= ry && py <= ry + rh
}
class RectShape {
constructor(x, y, width, height) {
this.x = x
this.y = y
this.width = width
this.height = height
}
containsPoint(px, py) {
return pointInRect(px, py, this.x, this.y, this.width, this.height)
}
}
function pointInCircle(px, py, cx, cy, radius) {
const dx = px - cx
const dy = py - cy
return dx * dx + dy * dy <= radius * radius
}
class CircleShape {
constructor(x, y, radius) {
this.x = x
this.y = y
this.radius = radius
}
containsPoint(px, py) {
return pointInCircle(px, py, this.x, this.y, this.radius)
}
}
function pointInEllipse(px, py, cx, cy, rx, ry) {
const dx = px - cx
const dy = py - cy
return (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1
}
class EllipseShape {
constructor(x, y, radiusX, radiusY) {
this.x = x
this.y = y
this.radiusX = radiusX
this.radiusY = radiusY
}
containsPoint(px, py) {
return pointInEllipse(px, py, this.x, this.y, this.radiusX, this.radiusY)
}
}
function pointInPolygon(px, py, vertices) {
let inside = false
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {
const xi = vertices[i].x
const yi = vertices[i].y
const xj = vertices[j].x
const yj = vertices[j].y
const intersect = ((yi > py) !== (yj > py)) &&
(px < (xj - xi) * (py - yi) / (yj - yi) + xi)
if (intersect) inside = !inside
}
return inside
}
class PolygonShape {
constructor(vertices) {
this.vertices = vertices
}
containsPoint(px, py) {
return pointInPolygon(px, py, this.vertices)
}
}
class PathShape {
constructor(path) {
this.path = path
this.path2D = new Path2D()
this.path2D.addPath(path)
}
containsPoint(px, py) {
return ctx.isPointInPath(px, py, this.path2D)
}
}
const path = new Path2D()
path.moveTo(100, 100)
path.lineTo(200, 100)
path.lineTo(150, 150)
path.closePath()
const shape = new PathShape(path)
class StrokeShape {
constructor(path, lineWidth) {
this.path = path
this.path2D = new Path2D()
this.path2D.addPath(path)
this.lineWidth = lineWidth
}
containsPoint(px, py) {
return ctx.isPointInStroke(px, py, this.path2D, this.lineWidth)
}
}
class TransformedShape {
constructor(x, y, width, height) {
this.x = x
this.y = y
this.width = width
this.height = height
this.rotation = 0
this.scale = 1
}
containsPoint(px, py) {
const cx = this.x + this.width / 2
const cy = this.y + this.height / 2
const dx = px - cx
const dy = py - cy
const cos = Math.cos(-this.rotation)
const sin = Math.sin(-this.rotation)
const localX = (dx * cos - dy * sin) / this.scale + this.width / 2
const localY = (dx * sin + dy * cos) / this.scale + this.height / 2
return localX >= 0 && localX <= this.width &&
localY >= 0 && localY <= this.height
}
}
function pickColor(canvas, x, y) {
const pixel = ctx.getImageData(x, y, 1, 1).data
return {
r: pixel[0],
g: pixel[1],
b: pixel[2],
a: pixel[3]
}
}
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect()
const x = Math.floor(e.clientX - rect.left)
const y = Math.floor(e.clientY - rect.top)
const color = pickColor(canvas, x, y)
console.log('点击颜色:', color)
})
const idCanvas = document.createElement('canvas')
const idCtx = idCanvas.getContext('2d')
idCanvas.width = canvas.width
idCanvas.height = canvas.height
function drawWithIDs() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
idCtx.clearRect(0, 0, idCanvas.width, idCanvas.height)
shapes.forEach((shape, index) => {
const id = index + 1
ctx.fillStyle = shape.color
drawShape(ctx, shape)
idCtx.fillStyle = `rgb(${id}, 0, 0)`
drawShape(idCtx, shape)
})
}
function pickShape(x, y) {
const pixel = idCtx.getImageData(x, y, 1, 1).data
const id = pixel[0]
if (id > 0) {
return shapes[id - 1]
}
return null
}
class SpatialGrid {
constructor(cellSize, width, height) {
this.cellSize = cellSize
this.cols = Math.ceil(width / cellSize)
this.rows = Math.ceil(height / cellSize)
this.grid = new Map()
}
clear() {
this.grid.clear()
}
getKey(x, y) {
const col = Math.floor(x / this.cellSize)
const row = Math.floor(y / this.cellSize)
return `${col},${row}`
}
insert(shape) {
const key = this.getKey(shape.x, shape.y)
if (!this.grid.has(key)) {
this.grid.set(key, [])
}
this.grid.get(key).push(shape)
}
query(x, y) {
const key = this.getKey(x, y)
return this.grid.get(key) || []
}
}
const grid = new SpatialGrid(50, 400, 200)
function findShapeAt(x, y) {
const candidates = grid.query(x, y)
for (let i = candidates.length - 1; i >= 0; i--) {
if (candidates[i].containsPoint(x, y)) {
return candidates[i]
}
}
return null
}
class QuadTree {
constructor(bounds, capacity = 4) {
this.bounds = bounds
this.capacity = capacity
this.objects = []
this.divided = false
this.children = null
}
insert(obj) {
if (!this.contains(obj)) return false
if (this.objects.length < this.capacity) {
this.objects.push(obj)
return true
}
if (!this.divided) {
this.subdivide()
}
return this.children.some(child => child.insert(obj))
}
query(range, found = []) {
if (!this.intersects(range)) return found
this.objects.forEach(obj => {
if (range.contains(obj)) {
found.push(obj)
}
})
if (this.divided) {
this.children.forEach(child => child.query(range, found))
}
return found
}
contains(obj) {
return obj.x >= this.bounds.x &&
obj.x < this.bounds.x + this.bounds.width &&
obj.y >= this.bounds.y &&
obj.y < this.bounds.y + this.bounds.height
}
intersects(range) {
return !(range.x > this.bounds.x + this.bounds.width ||
range.x + range.width < this.bounds.x ||
range.y > this.bounds.y + this.bounds.height ||
range.y + range.height < this.bounds.y)
}
}
class PickSystem {
constructor(canvas) {
this.canvas = canvas
this.shapes = []
this.selectedShape = null
this.hoveredShape = null
this.bindEvents()
}
addShape(shape) {
this.shapes.push(shape)
return this
}
removeShape(shape) {
const index = this.shapes.indexOf(shape)
if (index !== -1) {
this.shapes.splice(index, 1)
}
return this
}
bindEvents() {
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
this.canvas.addEventListener('click', this.onClick.bind(this))
}
getMousePos(e) {
const rect = this.canvas.getBoundingClientRect()
return {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
}
findShapeAt(x, y) {
for (let i = this.shapes.length - 1; i >= 0; i--) {
if (this.shapes[i].containsPoint(x, y)) {
return this.shapes[i]
}
}
return null
}
onMouseMove(e) {
const pos = this.getMousePos(e)
const shape = this.findShapeAt(pos.x, pos.y)
if (shape !== this.hoveredShape) {
if (this.hoveredShape && this.hoveredShape.onMouseLeave) {
this.hoveredShape.onMouseLeave()
}
this.hoveredShape = shape
if (shape && shape.onMouseEnter) {
shape.onMouseEnter()
}
}
}
onClick(e) {
const pos = this.getMousePos(e)
const shape = this.findShapeAt(pos.x, pos.y)
if (this.selectedShape && this.selectedShape.onDeselect) {
this.selectedShape.onDeselect()
}
this.selectedShape = shape
if (shape && shape.onSelect) {
shape.onSelect()
}
}
}
const pickSystem = new PickSystem(canvas)
pickSystem
.addShape(new RectShape(50, 50, 100, 80))
.addShape(new CircleShape(200, 100, 40))
.addShape(new PolygonShape([
{ x: 300, y: 50 },
{ x: 350, y: 100 },
{ x: 300, y: 150 },
{ x: 250, y: 100 }
]))