使用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>