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

直接上代码,我是用的是html,使用了MatterJs的cdn,直接复制到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>const {Engine,Render,World,Bodies,Body,Composite,Constraint,Mouse,MouseConstraint,Events,} = Matter;function getCanvasSize() {const container = document.querySelector(".canvas-container");let width = container.offsetWidth;let height = width * 0.75; if (width > 800) {width = 800;height = 600;}return { width, height };}const canvas = document.getElementById("canvas");const { width: initWidth, height: initHeight } = getCanvasSize();canvas.width = initWidth;canvas.height = initHeight;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; 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) {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;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;}});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>