当鼠标超出边界框时,如何释放被拖拽的元素?
解决Matter.js拖拽球体触碰边界自动释放及旋转问题
问题现象
将球体拖拽出蓝色框外时,球体开始快速旋转;在边界框外释放鼠标后,元素仍处于拖拽状态。需求是当球体触碰边界框时,自动释放拖拽。
解决方案
- 锁定拖拽时球体旋转:拖拽开始时临时将球体设为静态,避免旋转;拖拽结束后恢复动态状态。
- 完善边界检测逻辑:检测球体是否触碰容器上下左右四个边界,而非仅顶部。
- 正确释放鼠标约束:使用Matter.js提供的
MouseConstraint.clear()方法解除拖拽绑定,替代直接修改内部属性的方式。
修改后的完整代码
import { Bodies, Body, Bounds, Composite, Engine, Events, Mouse, MouseConstraint, Render, Runner, } from "matter-js"; import { useEffect, useRef } from "react"; const Bubbles = () => { const boxRef = useRef<HTMLDivElement>(null); const engine = useRef(Engine.create()); const ballPositions = useRef<Map<number, { x: number; y: number }>>( new Map() ); const ballColors = useRef<Map<number, string>>(new Map()); const mouseConstraintRef = useRef<MouseConstraint | null>(null); useEffect(() => { if (!boxRef.current) return; // 清除已有画布避免重复渲染 const existingCanvas = boxRef.current.querySelector("canvas"); if (existingCanvas) { existingCanvas.remove(); } const engineRef = engine.current; const world = engineRef.world; const width = boxRef.current.clientWidth; const height = boxRef.current.clientHeight; // 创建容器边界 const boundsVertices = [ { x: 0, y: 0 }, { x: width, y: 0 }, { x: width, y: height }, { x: 0, y: height }, ]; const bounds = Bounds.create(boundsVertices); const render = Render.create({ element: boxRef.current, engine: engineRef, bounds, options: { width: width, height: height, wireframes: false, background: "transparent", hasBounds: true, }, }); let balls = []; for (let i = 0; i < 2; i++) { const xPos = Math.random() * (width - 100) + 50; const yPos = Math.random() * (height - 100) + 50; const color = "red"; const body = Bodies.circle(xPos, yPos, 50, { frictionAir: 0, friction: 0, restitution: 1, force: { x: 0.1, y: 0.1 }, render: { fillStyle: color, opacity: 0.5, }, collisionFilter: { group: -1, }, label: `bubble-${i}`, angle: 0, isStatic: false, }); balls.push(body); } // 创建与容器边缘对齐的静态墙 const walls = [ Bodies.rectangle(width / 2, -25, width, 50, { isStatic: true, restitution: 1 }), Bodies.rectangle(width / 2, height + 25, width, 50, { isStatic: true, restitution: 1 }), Bodies.rectangle(-25, height / 2, 50, height, { isStatic: true, restitution: 1 }), Bodies.rectangle(width + 25, height / 2, 50, height, { isStatic: true, restitution: 1 }), ]; Composite.add(world, [...walls, ...balls]); // 创建鼠标约束 const mouse = Mouse.create(render.canvas); const mouseConstraint = MouseConstraint.create(engineRef, { mouse: mouse, constraint: { stiffness: 0.2, render: { visible: false }, }, collisionFilter: { group: 1 }, }); mouseConstraintRef.current = mouseConstraint; Composite.add(world, mouseConstraint); render.mouse = mouse; let selectedBody: Body | null = null; // 拖拽开始:锁定球体避免旋转 Events.on(mouseConstraint, "mousedown", ({ source }) => { selectedBody = source.body; if (selectedBody) { Body.setStatic(selectedBody, true); } }); // 拖拽结束:恢复球体动态状态 Events.on(mouseConstraint, "mouseup", () => { if (selectedBody) { Body.setStatic(selectedBody, false); selectedBody = null; } }); // 拖拽移动:检测边界并释放约束 Events.on(mouseConstraint, "mousemove", ({ source }) => { const body = source.body; if (!body) return; const ballRadius = 50; // 检测球体边缘是否触碰容器边界 const isOutOfBounds = body.position.x - ballRadius <= 0 || body.position.x + ballRadius >= width || body.position.y - ballRadius <= 0 || body.position.y + ballRadius >= height; if (isOutOfBounds) { MouseConstraint.clear(mouseConstraint); if (selectedBody) { Body.setStatic(selectedBody, false); selectedBody = null; } } }); engineRef.gravity = { x: 0, y: 0, scale: 0 }; const runner = Runner.create(); Runner.run(runner, engineRef); Render.run(render); return () => { Render.stop(render); Runner.stop(runner); Composite.clear(world, false); Engine.clear(engineRef); render.canvas.remove(); }; }, []); return ( <div style={{ backgroundColor: "blue", width: "100%", height: "500px", overflow: "hidden", }} ref={boxRef} ></div> ); }; export default Bubbles;
修改说明
- 旋转控制:拖拽开始时将球体设为静态,避免拖拽过程中产生旋转;拖拽结束后恢复动态状态,保证球体正常运动。
- 边界检测:通过计算球体边缘位置,判断是否触碰容器四个边界,触发释放逻辑。
- 约束释放:使用官方API
MouseConstraint.clear()解除拖拽绑定,确保球体不再跟随鼠标移动,同时避免内部属性修改导致的异常。
内容的提问来源于stack exchange,提问作者João Pedro
相关产品推荐
相关产品推荐

