学习Canvas拖拽交互技术,掌握拖拽实现、拖放操作和交互反馈方法。拖拽交互是指用户按住图形并移动到新位置,是Canvas交互的重要功能。
let isDragging = false
let dragShape = null
let dragOffsetX = 0
let dragOffsetY = 0
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(canvas, e)
dragShape = findShapeAt(pos.x, pos.y)
if (dragShape) {
isDragging = true
dragOffsetX = pos.x - dragShape.x
dragOffsetY = pos.y - dragShape.y
}
})
canvas.addEventListener('mousemove', (e) => {
if (!isDragging || !dragShape) return
const pos = getMousePos(canvas, e)
dragShape.x = pos.x - dragOffsetX
dragShape.y = pos.y - dragOffsetY
redraw()
})
canvas.addEventListener('mouseup', () => {
isDragging = false
dragShape = null
})
canvas.addEventListener('mouseleave', () => {
isDragging = false
dragShape = null
})
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value))
}
canvas.addEventListener('mousemove', (e) => {
if (!isDragging || !dragShape) return
const pos = getMousePos(canvas, e)
dragShape.x = clamp(pos.x - dragOffsetX, 0, canvas.width - dragShape.width)
dragShape.y = clamp(pos.y - dragOffsetY, 0, canvas.height - dragShape.height)
redraw()
})
const gridSize = 20
function snapToGrid(value) {
return Math.round(value / gridSize) * gridSize
}
canvas.addEventListener('mousemove', (e) => {
if (!isDragging || !dragShape) return
const pos = getMousePos(canvas, e)
dragShape.x = snapToGrid(pos.x - dragOffsetX)
dragShape.y = snapToGrid(pos.y - dragOffsetY)
redraw()
})
function constrainToAxis(shape, axis) {
if (axis === 'x') {
shape.y = shape.originalY
} else if (axis === 'y') {
shape.x = shape.originalX
}
}
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(canvas, e)
dragShape = findShapeAt(pos.x, pos.y)
if (dragShape) {
isDragging = true
dragShape.originalX = dragShape.x
dragShape.originalY = dragShape.y
dragOffsetX = pos.x - dragShape.x
dragOffsetY = pos.y - dragShape.y
dragAxis = e.shiftKey ? 'y' : (e.altKey ? 'x' : null)
}
})
canvas.addEventListener('mousemove', (e) => {
if (!isDragging || !dragShape) return
const pos = getMousePos(canvas, e)
dragShape.x = pos.x - dragOffsetX
dragShape.y = pos.y - dragOffsetY
if (dragAxis) {
constrainToAxis(dragShape, dragAxis)
}
redraw()
})
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(canvas, e)
dragShape = findShapeAt(pos.x, pos.y)
if (dragShape) {
isDragging = true
dragShape.originalColor = dragShape.color
dragShape.color = lightenColor(dragShape.color, 0.2)
dragShape.zIndex = 100
bringToFront(dragShape)
redraw()
}
})
canvas.addEventListener('mouseup', () => {
if (dragShape) {
dragShape.color = dragShape.originalColor
dragShape.zIndex = 0
}
isDragging = false
dragShape = null
redraw()
})
function drawShadow(ctx, shape) {
ctx.save()
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'
if (shape.type === 'rect') {
ctx.fillRect(shape.x + 5, shape.y + 5, shape.width, shape.height)
} else if (shape.type === 'circle') {
ctx.beginPath()
ctx.arc(shape.x + 5, shape.y + 5, shape.radius, 0, Math.PI * 2)
ctx.fill()
}
ctx.restore()
}
function redraw() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
shapes.forEach(shape => {
if (shape === dragShape) {
drawShadow(ctx, shape)
}
drawShape(ctx, shape)
})
}
class Draggable {
constructor(shape) {
this.shape = shape
this.isDragging = false
this.dragOffsetX = 0
this.dragOffsetY = 0
this.constraints = []
this.onDragStart = null
this.onDrag = null
this.onDragEnd = null
}
startDrag(x, y) {
this.isDragging = true
this.dragOffsetX = x - this.shape.x
this.dragOffsetY = y - this.shape.y
if (this.onDragStart) {
this.onDragStart(this.shape)
}
}
drag(x, y) {
if (!this.isDragging) return
this.shape.x = x - this.dragOffsetX
this.shape.y = y - this.dragOffsetY
this.applyConstraints()
if (this.onDrag) {
this.onDrag(this.shape)
}
}
endDrag() {
this.isDragging = false
if (this.onDragEnd) {
this.onDragEnd(this.shape)
}
}
addConstraint(constraint) {
this.constraints.push(constraint)
return this
}
applyConstraints() {
this.constraints.forEach(constraint => {
constraint(this.shape)
})
}
}
const draggable = new Draggable(shape)
.addConstraint(shape => {
shape.x = Math.max(0, Math.min(canvas.width - shape.width, shape.x))
shape.y = Math.max(0, Math.min(canvas.height - shape.height, shape.y))
})
class DragManager {
constructor(canvas) {
this.canvas = canvas
this.shapes = []
this.draggables = new Map()
this.activeDraggable = null
this.bindEvents()
}
addShape(shape) {
this.shapes.push(shape)
const draggable = new Draggable(shape)
this.draggables.set(shape, draggable)
return draggable
}
removeShape(shape) {
const index = this.shapes.indexOf(shape)
if (index !== -1) {
this.shapes.splice(index, 1)
}
this.draggables.delete(shape)
}
bindEvents() {
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this))
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this))
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this))
this.canvas.addEventListener('mouseleave', this.onMouseLeave.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
}
onMouseDown(e) {
const pos = this.getMousePos(e)
const shape = this.findShapeAt(pos.x, pos.y)
if (shape) {
const draggable = this.draggables.get(shape)
this.activeDraggable = draggable
draggable.startDrag(pos.x, pos.y)
}
}
onMouseMove(e) {
const pos = this.getMousePos(e)
if (this.activeDraggable) {
this.activeDraggable.drag(pos.x, pos.y)
this.redraw()
}
}
onMouseUp() {
if (this.activeDraggable) {
this.activeDraggable.endDrag()
this.activeDraggable = null
}
}
onMouseLeave() {
if (this.activeDraggable) {
this.activeDraggable.endDrag()
this.activeDraggable = null
}
}
redraw() {
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.shapes.forEach(shape => drawShape(ctx, shape))
}
}
const dragManager = new DragManager(canvas)
dragManager.addShape(new RectShape(50, 50, 100, 80))
dragManager.addShape(new CircleShape(200, 100, 40))
class DropZone {
constructor(x, y, width, height, options = {}) {
this.x = x
this.y = y
this.width = width
this.height = height
this.accepts = options.accepts || []
this.onDrop = options.onDrop || null
this.highlighted = false
}
containsPoint(px, py) {
return px >= this.x && px <= this.x + this.width &&
py >= this.y && py <= this.y + this.height
}
canAccept(shape) {
if (this.accepts.length === 0) return true
return this.accepts.includes(shape.type)
}
highlight(enabled) {
this.highlighted = enabled
}
draw(ctx) {
ctx.fillStyle = this.highlighted ? 'rgba(52, 152, 219, 0.2)' : 'rgba(52, 152, 219, 0.1)'
ctx.fillRect(this.x, this.y, this.width, this.height)
ctx.strokeStyle = '#3498db'
ctx.lineWidth = 2
ctx.strokeRect(this.x, this.y, this.width, this.height)
}
}
const dropZone = new DropZone(300, 50, 80, 80, {
accepts: ['circle'],
onDrop: (shape) => {
console.log('放入:', shape)
}
})
let draggedShape = null
let dropZones = []
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(canvas, e)
draggedShape = findShapeAt(pos.x, pos.y)
})
canvas.addEventListener('mousemove', (e) => {
const pos = getMousePos(canvas, e)
if (draggedShape) {
draggedShape.x = pos.x - dragOffsetX
draggedShape.y = pos.y - dragOffsetY
dropZones.forEach(zone => {
zone.highlight(zone.containsPoint(pos.x, pos.y) && zone.canAccept(draggedShape))
})
redraw()
}
})
canvas.addEventListener('mouseup', (e) => {
if (draggedShape) {
const pos = getMousePos(canvas, e)
dropZones.forEach(zone => {
if (zone.containsPoint(pos.x, pos.y) && zone.canAccept(draggedShape)) {
if (zone.onDrop) {
zone.onDrop(draggedShape)
}
}
zone.highlight(false)
})
draggedShape = null
redraw()
}
})
class UnifiedDrag {
constructor(canvas) {
this.canvas = canvas
this.shapes = []
this.activeShape = null
this.dragOffset = { x: 0, y: 0 }
this.activePointer = null
this.bindEvents()
}
bindEvents() {
this.canvas.addEventListener('mousedown', this.onStart.bind(this))
this.canvas.addEventListener('mousemove', this.onMove.bind(this))
this.canvas.addEventListener('mouseup', this.onEnd.bind(this))
this.canvas.addEventListener('touchstart', this.onStart.bind(this))
this.canvas.addEventListener('touchmove', this.onMove.bind(this))
this.canvas.addEventListener('touchend', this.onEnd.bind(this))
}
getPos(e) {
const rect = this.canvas.getBoundingClientRect()
const touch = e.touches ? e.touches[0] : e
return {
x: (touch.clientX || touch.offsetX) - rect.left,
y: (touch.clientY || touch.offsetY) - rect.top,
id: touch.identifier || 0
}
}
onStart(e) {
if (e.touches) e.preventDefault()
const pos = this.getPos(e)
const shape = { ...this.findShapeAt(pos.x, pos.y) }
if (shape) {
this.activePointer = pos.id
this.activeShape = shape
this.dragOffset = {
x: pos.x - shape.x,
y: pos.y - shape.y
}
}
}
onMove(e) {
if (e.touches) e.preventDefault()
if (!this.activeShape) return
const pos = this.getPos(e)
if (pos.id !== this.activePointer) return
this.activeShape.x = pos.x - this.dragOffset.x
this.activeShape.y = pos.y - this.dragOffset.y
this.redraw()
}
onEnd(e) {
const pos = this.getPos(e)
if (pos.id === this.activePointer) {
this.activeShape = null
this.activePointer = null
}
}
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
}
redraw() {
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
this.shapes.forEach(shape => drawShape(ctx, shape))
}
}