使用MatterJs物理2D引擎实现重力和鼠标交互等功能,有点击事件(盒子堆叠效果)

使用MatterJs物理2D引擎实现重力和鼠标交互等功能,有点击事件(盒子堆叠效果)

效果图:

直接上代码,我是用的是html,使用了MatterJs的cdn,直接复制到html文件中然后在浏览器打开即可

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Matter.js Mixed Effects Demo</title>
    <style>
      body {
        margin: 0;
        padding: 20px;
        font-family: Arial, sans-serif;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        min-height: 100vh;
        box-sizing: border-box;
      }

      .container {
        max-width: 1200px;
        margin: 0 auto;
      }

      h1 {
        text-align: center;
        color: white;
        margin-bottom: 20px;
        text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
        font-size: 2.2rem;
      }

      .controls {
        text-align: center;
        margin-bottom: 20px;
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 10px;
      }

      button {
        background: #4caf50;
        color: white;
        border: none;
        padding: 10px 18px;
        margin: 5px 0;
        border-radius: 5px;
        cursor: pointer;
        font-size: 1rem;
        transition: background 0.3s;
        min-width: 90px;
      }

      button:hover {
        background: #45a049;
      }

      button:active {
        transform: scale(0.95);
      }

      .canvas-container {
        text-align: center;
        margin-top: 20px;
      }

      #canvas {
        border: 3px solid #333;
        border-radius: 10px;
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
        background: #f0f0f0;
        width: 100%;
        max-width: 800px;
        height: auto;
        aspect-ratio: 4/3;
        display: block;
        margin: 0 auto;
      }

      .info {
        background: rgba(255, 255, 255, 0.9);
        padding: 15px;
        border-radius: 10px;
        margin-top: 20px;
        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        font-size: 1rem;
      }

      .info h3 {
        margin-top: 0;
        color: #333;
      }

      .info p {
        margin: 5px 0;
        color: #666;
      }
      @media (max-width: 900px) {
        .container {
          padding: 0 10px;
        }
        h1 {
          font-size: 1.5rem;
        }
        .info {
          font-size: 0.95rem;
        }
      }
      @media (max-width: 600px) {
        body {
          padding: 8px;
        }
        .container {
          padding: 0 2px;
        }
        .controls {
          gap: 6px;
        }
        button {
          font-size: 0.95rem;
          padding: 8px 10px;
          min-width: 70px;
        }
        #canvas {
          max-width: 100vw;
          min-width: 0;
          border-width: 2px;
        }
        .info {
          font-size: 0.9rem;
          padding: 10px;
        }
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>Matter.js Mixed Effects Demo</h1>

      <div class="controls">
        <button onclick="addBox()">添加方块</button>
        <button onclick="addCircle()">添加圆形</button>
        <button onclick="addPolygon()">添加多边形</button>
        <button onclick="addText()">添加文字</button>
        <button onclick="addConstraint()">添加约束</button>
        <button onclick="addExplosion()">爆炸效果</button>
        <button onclick="addWind()">风力效果</button>
        <button onclick="clearAll()">清除所有</button>
        <button onclick="toggleGravity()">切换重力</button>
      </div>

      <div class="canvas-container">
        <canvas id="canvas" width="800" height="600"></canvas>
      </div>

      <div class="info">
        <h3>功能说明:</h3>
        <p>• <strong>添加方块/圆形/多边形</strong>:创建不同形状的物体</p>
        <p>• <strong>添加约束</strong>:在物体之间创建连接</p>
        <p>• <strong>爆炸效果</strong>:在鼠标位置创建爆炸力</p>
        <p>• <strong>风力效果</strong>:模拟风力对物体的影响</p>
        <p>• <strong>切换重力</strong>:开启/关闭重力效果</p>
        <p>• <strong>鼠标交互</strong>:点击并拖拽物体</p>
      </div>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
    <script>
      // 初始化Matter.js模块
      const {
        Engine,
        Render,
        World,
        Bodies,
        Body,
        Composite,
        Constraint,
        Mouse,
        MouseConstraint,
        Events,
      } = Matter;

      // 获取canvas实际宽高
      function getCanvasSize() {
        const container = document.querySelector(".canvas-container");
        let width = container.offsetWidth;
        let height = width * 0.75; // 4:3比例
        if (width > 800) {
          width = 800;
          height = 600;
        }
        return { width, height };
      }

      // 初始化canvas尺寸
      const canvas = document.getElementById("canvas");
      const { width: initWidth, height: initHeight } = getCanvasSize();
      canvas.width = initWidth;
      canvas.height = initHeight;

      // 创建引擎和渲染器,宽高为自适应canvas宽高
      const engine = Engine.create();
      const render = Render.create({
        canvas: canvas,
        engine: engine,
        options: {
          width: initWidth,
          height: initHeight,
          wireframes: false,
          background: "#f0f0f0",
        },
      });

      // 创建地面和墙体的函数
      function createBounds(width, height) {
        const ground = Bodies.rectangle(width / 2, height - 10, width, 20, {
          isStatic: true,
          render: { fillStyle: "#2c3e50" },
        });
        const leftWall = Bodies.rectangle(10, height / 2, 20, height, {
          isStatic: true,
          render: { fillStyle: "#2c3e50" },
        });
        const rightWall = Bodies.rectangle(width - 10, height / 2, 20, height, {
          isStatic: true,
          render: { fillStyle: "#2c3e50" },
        });
        const ceiling = Bodies.rectangle(width / 2, 10, width, 20, {
          isStatic: true,
          render: { fillStyle: "#2c3e50" },
        });
        return [ground, leftWall, rightWall, ceiling];
      }

      // 初始边界
      let bounds = createBounds(initWidth, initHeight);
      World.add(engine.world, bounds);

      // 创建鼠标约束
      const mouse = Mouse.create(render.canvas);
      const mouseConstraint = MouseConstraint.create(engine, {
        mouse: mouse,
        constraint: {
          stiffness: 0.2,
          render: {
            visible: false,
          },
        },
      });
      World.add(engine.world, mouseConstraint);

      // 启动引擎和渲染器
      Engine.run(engine);
      Render.run(render);

      // 全局变量
      let gravityEnabled = true;
      let windForce = 0;
      let constraints = [];

      // 工具函数:生成随机数字
      function getRandomNumber() {
        return Math.floor(Math.random() * 100) + 1;
      }

      // 添加方块函数
      function addBox() {
        const margin = 50;
        const width = render.options.width;
        const height = render.options.height;
        const number = getRandomNumber();
        const box = Bodies.rectangle(
          Math.random() * (width - 2 * margin) + margin,
          Math.random() * (height / 3 - margin) + margin,
          40,
          40,
          {
            render: {
              fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`,
              number: number,
            },
            restitution: 0.8,
            friction: 0.1,
          }
        );
        box.customNumber = number;
        World.add(engine.world, box);
      }

      // 添加圆形函数
      function addCircle() {
        const margin = 50;
        const width = render.options.width;
        const height = render.options.height;
        const number = getRandomNumber();
        const circle = Bodies.circle(
          Math.random() * (width - 2 * margin) + margin,
          Math.random() * (height / 3 - margin) + margin,
          20,
          {
            render: {
              fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`,
              number: number,
            },
            restitution: 0.9,
            friction: 0.05,
          }
        );
        circle.customNumber = number;
        World.add(engine.world, circle);
      }

      // 添加多边形函数
      function addPolygon() {
        const margin = 50;
        const width = render.options.width;
        const height = render.options.height;
        const number = getRandomNumber();
        const sides = Math.floor(Math.random() * 4) + 3; // 3-6边形
        const vertices = [];
        for (let i = 0; i < sides; i++) {
          const angle = (i / sides) * Math.PI * 2;
          const radius = 15 + Math.random() * 10;
          vertices.push({
            x: Math.cos(angle) * radius,
            y: Math.sin(angle) * radius,
          });
        }
        const polygon = Bodies.fromVertices(
          Math.random() * (width - 2 * margin) + margin,
          Math.random() * (height / 3 - margin) + margin,
          [vertices],
          {
            render: {
              fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`,
              number: number,
            },
            restitution: 0.7,
            friction: 0.2,
          }
        );
        polygon.customNumber = number;
        World.add(engine.world, polygon);
      }

      // 添加文字函数
      function addText() {
        const margin = 50;
        const width = render.options.width;
        const height = render.options.height;
        const number = getRandomNumber();
        const text = Bodies.rectangle(
          Math.random() * (width - 2 * margin) + margin,
          Math.random() * (height / 3 - margin) + margin,
          80,
          30,
          {
            render: {
              fillStyle: `#ffffff00`,
              number: number,
              isText: true,
            },
            restitution: 0.8,
            friction: 0.1,
          }
        );
        text.customNumber = number;
        text.isText = true;
        World.add(engine.world, text);
      }

      // 添加约束函数
      function addConstraint() {
        const bodies = Composite.allBodies(engine.world).filter(
          (body) => !body.isStatic
        );
        if (bodies.length >= 2) {
          const bodyA = bodies[Math.floor(Math.random() * bodies.length)];
          const bodyB = bodies[Math.floor(Math.random() * bodies.length)];

          if (bodyA !== bodyB) {
            const constraint = Constraint.create({
              bodyA: bodyA,
              bodyB: bodyB,
              pointA: { x: 0, y: 0 },
              pointB: { x: 0, y: 0 },
              stiffness: 0.1,
              render: {
                strokeStyle: "#e74c3c",
                lineWidth: 2,
              },
            });
            constraints.push(constraint);
            World.add(engine.world, constraint);
          }
        }
      }

      // 爆炸效果函数
      function addExplosion() {
        const bodies = Composite.allBodies(engine.world).filter(
          (body) => !body.isStatic
        );
        const explosionPoint = { x: 400, y: 300 };
        const explosionForce = 0.05;

        bodies.forEach((body) => {
          const distance = Math.sqrt(
            Math.pow(body.position.x - explosionPoint.x, 2) +
              Math.pow(body.position.y - explosionPoint.y, 2)
          );

          if (distance < 200) {
            const force = explosionForce * (1 - distance / 200);
            const angle = Math.atan2(
              body.position.y - explosionPoint.y,
              body.position.x - explosionPoint.x
            );

            Body.applyForce(body, body.position, {
              x: Math.cos(angle) * force,
              y: Math.sin(angle) * force,
            });
          }
        });
      }

      // 风力效果函数
      function addWind() {
        windForce = windForce === 0 ? 0.001 : 0;
      }

      // 清除所有物体函数
      function clearAll() {
        const bodies = Composite.allBodies(engine.world).filter(
          (body) => !body.isStatic
        );
        bodies.forEach((body) => {
          World.remove(engine.world, body);
        });

        constraints.forEach((constraint) => {
          World.remove(engine.world, constraint);
        });
        constraints = [];
      }

      // 切换重力函数
      function toggleGravity() {
        gravityEnabled = !gravityEnabled;
        engine.world.gravity.y = gravityEnabled ? 1 : 0;
      }

      // 应用风力效果
      Events.on(engine, "beforeUpdate", function () {
        if (windForce !== 0) {
          const bodies = Composite.allBodies(engine.world).filter(
            (body) => !body.isStatic
          );
          bodies.forEach((body) => {
            Body.applyForce(body, body.position, {
              x: windForce,
              y: 0,
            });
          });
        }
      });

      // 自定义渲染数字
      (function patchRender() {
        const originalBodies = Render.bodies;
        Render.bodies = function (render, bodies, context) {
          // 先调用原始的 Render.bodies
          originalBodies.call(this, render, bodies, context);
          const ctx = context || render.context;
          for (let i = 0; i < bodies.length; i++) {
            const body = bodies[i];
            if (body.customNumber) {
              ctx.save();
              if (body.isText) {
                // 文字图形:只显示文字,不显示背景
                ctx.font = "20px Arial";
                ctx.fillStyle = "#222";
                ctx.textAlign = "center";
                ctx.textBaseline = "middle";
                ctx.globalAlpha = 0.9;
                ctx.fillText(
                  body.customNumber,
                  body.position.x,
                  body.position.y
                );
              } else {
                // 普通图形:显示数字
                ctx.font = `${Math.max(
                  16,
                  Math.floor(
                    body.circleRadius
                      ? body.circleRadius
                      : body.bounds.max.x - body.bounds.min.x
                  ) * 0.8
                )}px Arial`;
                ctx.fillStyle = "#222";
                ctx.textAlign = "center";
                ctx.textBaseline = "middle";
                ctx.globalAlpha = 0.9;
                ctx.fillText(
                  body.customNumber,
                  body.position.x,
                  body.position.y
                );
              }
              ctx.restore();
            }
          }
        };
      })();

      // 点击事件,打印数字
      let touchStartPos = null;
      let touchStartTime = null;

      function handleClick(e) {
        const rect = render.canvas.getBoundingClientRect();
        let mouseX, mouseY;

        if (e.type === "touchstart" || e.type === "touchmove") {
          const touch = e.touches[0] || e.changedTouches[0];
          mouseX =
            (touch.clientX - rect.left) *
            (render.options.width / render.canvas.width);
          mouseY =
            (touch.clientY - rect.top) *
            (render.options.height / render.canvas.height);
        } else {
          mouseX =
            (e.clientX - rect.left) *
            (render.options.width / render.canvas.width);
          mouseY =
            (e.clientY - rect.top) *
            (render.options.height / render.canvas.height);
        }

        const bodies = Composite.allBodies(engine.world).filter(
          (body) => !body.isStatic
        );
        for (let body of bodies) {
          if (Matter.Bounds.contains(body.bounds, { x: mouseX, y: mouseY })) {
            // 更精确判断(圆形/多边形)
            if (
              Matter.Vertices.contains(body.vertices, { x: mouseX, y: mouseY })
            ) {
              if (body.customNumber) {
                console.log("点击数字:", body.customNumber);
              }
              break;
            }
          }
        }
      }

      function handleTouchStart(e) {
        const touch = e.touches[0];
        const rect = render.canvas.getBoundingClientRect();
        touchStartPos = {
          x: touch.clientX - rect.left,
          y: touch.clientY - rect.top,
        };
        touchStartTime = Date.now();
      }

      function handleTouchEnd(e) {
        if (!touchStartPos || !touchStartTime) return;

        const touch = e.changedTouches[0];
        const rect = render.canvas.getBoundingClientRect();
        const touchEndPos = {
          x: touch.clientX - rect.left,
          y: touch.clientY - rect.top,
        };

        // 计算移动距离和时间
        const distance = Math.sqrt(
          Math.pow(touchEndPos.x - touchStartPos.x, 2) +
            Math.pow(touchEndPos.y - touchStartPos.y, 2)
        );
        const duration = Date.now() - touchStartTime;

        // 只有移动距离小于10px且时间小于300ms才认为是点击
        if (distance < 10 && duration < 300) {
          const mouseX =
            touchEndPos.x * (render.options.width / render.canvas.width);
          const mouseY =
            touchEndPos.y * (render.options.height / render.canvas.height);

          const bodies = Composite.allBodies(engine.world).filter(
            (body) => !body.isStatic
          );
          for (let body of bodies) {
            if (Matter.Bounds.contains(body.bounds, { x: mouseX, y: mouseY })) {
              if (
                Matter.Vertices.contains(body.vertices, {
                  x: mouseX,
                  y: mouseY,
                })
              ) {
                if (body.customNumber) {
                  console.log("点击数字:", body.customNumber);
                }
                break;
              }
            }
          }
        }

        // 重置触摸状态
        touchStartPos = null;
        touchStartTime = null;
      }

      render.canvas.addEventListener("click", handleClick);
      render.canvas.addEventListener("touchstart", handleTouchStart);
      render.canvas.addEventListener("touchend", handleTouchEnd);

      // 鼠标点击事件
      Events.on(mouseConstraint, "mousedown", function (event) {
        const bodies = event.source.body;
        if (bodies) {
          Body.setAngularVelocity(bodies, 0);
        }
      });

      // 键盘控制
      document.addEventListener("keydown", function (event) {
        switch (event.key) {
          case "b":
          case "B":
            addBox();
            break;
          case "c":
          case "C":
            addCircle();
            break;
          case "p":
          case "P":
            addPolygon();
            break;
          case "e":
          case "E":
            addExplosion();
            break;
          case "w":
          case "W":
            addWind();
            break;
          case "g":
          case "G":
            toggleGravity();
            break;
          case " ":
            clearAll();
            break;
        }
      });

      // 让canvas自适应屏幕宽度和高宽比
      function resizeCanvas() {
        const prevWidth = render.options.width;
        const prevHeight = render.options.height;
        const { width, height } = getCanvasSize();
        canvas.width = width;
        canvas.height = height;
        render.options.width = width;
        render.options.height = height;
        render.canvas.width = width;
        render.canvas.height = height;
        // 缩放所有非静态物体的位置和大小
        const scaleX = width / prevWidth;
        const scaleY = height / prevHeight;
        const bodies = Composite.allBodies(engine.world).filter(
          (body) => !body.isStatic
        );
        bodies.forEach((body) => {
          // 缩放位置
          Body.setPosition(body, {
            x: body.position.x * scaleX,
            y: body.position.y * scaleY,
          });
          // 缩放大小(仅对矩形和圆形,复杂多边形可选)
          if (body.circleRadius) {
            Body.scale(body, scaleX, scaleY);
          } else if (body.vertices.length === 4) {
            Body.scale(body, scaleX, scaleY);
          }
        });
        // 移除旧边界
        if (bounds) {
          bounds.forEach((b) => World.remove(engine.world, b));
        }
        // 添加新边界
        bounds = createBounds(width, height);
        World.add(engine.world, bounds);
      }
      window.addEventListener("resize", resizeCanvas);
      resizeCanvas();

      // 初始添加一些物体
      setTimeout(() => {
        for (let i = 0; i < 5; i++) {
          addBox();
          addCircle();
        }
      }, 1000);
    </script>
  </body>
</html>
相关推荐
梓贤Vigo9 分钟前
【Axure视频教程】动态折线图
交互·产品经理·axure·原型·教程
一叶怎知秋1 小时前
【openlayers框架学习】九:openlayers中的交互类(select和draw)
前端·javascript·笔记·学习·交互
allenlluo2 小时前
浅谈Web Components
前端·javascript
Mintopia2 小时前
把猫咪装进 public/ 文件夹:Next.js 静态资源管理的魔幻漂流
前端·javascript·next.js
用户1409508112802 小时前
如何在JavaScript中更好地使用闭包?
javascript
flashlight_hi2 小时前
LeetCode 分类刷题:16. 最接近的三数之和
javascript·数据结构·算法·leetcode
汪子熙2 小时前
如何使用 Node.js 代码下载 Github issue 到本地
javascript·后端
前端Hardy2 小时前
HTML&CSS&JS:有趣的练手小案例-开关灯效果
前端·javascript·css
我想说一句2 小时前
超酷HTML5的网页拖拽功能!!
前端·javascript