You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

当鼠标超出边界框时,如何释放被拖拽的元素?

解决Matter.js拖拽球体触碰边界自动释放及旋转问题

问题现象

将球体拖拽出蓝色框外时,球体开始快速旋转;在边界框外释放鼠标后,元素仍处于拖拽状态。需求是当球体触碰边界框时,自动释放拖拽。

解决方案

  1. 锁定拖拽时球体旋转:拖拽开始时临时将球体设为静态,避免旋转;拖拽结束后恢复动态状态。
  2. 完善边界检测逻辑:检测球体是否触碰容器上下左右四个边界,而非仅顶部。
  3. 正确释放鼠标约束:使用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;

修改说明

  • 旋转控制:拖拽开始时将球体设为静态,避免拖拽过程中产生旋转;拖拽结束后恢复动态状态,保证球体正常运动。
  • 边界检测:通过计算球体边缘位置,判断是否触碰容器四个边界,触发释放逻辑。
  • 约束释放:使用官方APIMouseConstraint.clear()解除拖拽绑定,确保球体不再跟随鼠标移动,同时避免内部属性修改导致的异常。

内容的提问来源于stack exchange,提问作者João Pedro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 03:08:12