# Physics 2D - Matter.js Motor de física 2D para experiencias interactivas estilo Awwwards. ## Table of Contents 1. [Setup](#setup) 2. [React Integration](#react-integration) 3. [Bodies (Cuerpos)](#bodies-cuerpos) 4. [World & Engine](#world--engine) 5. [Constraints (Restricciones)](#constraints-restricciones) 6. [Mouse Interaction](#mouse-interaction) 7. [Collision Events](#collision-events) 8. [Scroll Physics](#scroll-physics) 9. [Creative Patterns](#creative-patterns) ## Setup ### Instalación ```bash npm install matter-js npm install --save-dev @types/matter-js ``` ### Importación ```tsx import Matter from 'matter-js' // O destructurar const { Engine, Render, Runner, Bodies, World, Mouse, MouseConstraint } = Matter ``` ## React Integration ### Setup Básico con Hooks ```tsx 'use client' import { useRef, useEffect } from 'react' import Matter from 'matter-js' export function PhysicsScene() { const containerRef = useRef(null) const canvasRef = useRef(null) useEffect(() => { const { Engine, Render, Runner, Bodies, World, Composite } = Matter // Crear engine const engine = Engine.create() const world = engine.world // Configurar gravedad engine.gravity.y = 1 engine.gravity.x = 0 // Crear renderer const render = Render.create({ element: containerRef.current!, canvas: canvasRef.current!, engine: engine, options: { width: 800, height: 600, wireframes: false, // Renderizado sólido background: '#0a0a0a', }, }) // Crear suelo y paredes const ground = Bodies.rectangle(400, 590, 800, 20, { isStatic: true, render: { fillStyle: '#333' }, }) const leftWall = Bodies.rectangle(10, 300, 20, 600, { isStatic: true, render: { fillStyle: '#333' }, }) const rightWall = Bodies.rectangle(790, 300, 20, 600, { isStatic: true, render: { fillStyle: '#333' }, }) // Crear cuerpos dinámicos const boxes = Array.from({ length: 10 }, (_, i) => Bodies.rectangle(100 + i * 60, 100, 50, 50, { render: { fillStyle: `hsl(${i * 36}, 70%, 60%)`, }, }) ) // Agregar al mundo Composite.add(world, [ground, leftWall, rightWall, ...boxes]) // Ejecutar const runner = Runner.create() Runner.run(runner, engine) Render.run(render) // Cleanup return () => { Render.stop(render) Runner.stop(runner) World.clear(world, false) Engine.clear(engine) render.canvas.remove() } }, []) return (
) } ``` ### Hook Reutilizable: useMatter ```tsx 'use client' import { useRef, useEffect, useCallback } from 'react' import Matter from 'matter-js' interface UseMatterOptions { width?: number height?: number gravity?: { x: number; y: number } wireframes?: boolean background?: string } export function useMatter(options: UseMatterOptions = {}) { const { width = 800, height = 600, gravity = { x: 0, y: 1 }, wireframes = false, background = '#0a0a0a', } = options const containerRef = useRef(null) const canvasRef = useRef(null) const engineRef = useRef(null) const renderRef = useRef(null) const runnerRef = useRef(null) useEffect(() => { const { Engine, Render, Runner } = Matter engineRef.current = Engine.create() engineRef.current.gravity.x = gravity.x engineRef.current.gravity.y = gravity.y renderRef.current = Render.create({ element: containerRef.current!, canvas: canvasRef.current!, engine: engineRef.current, options: { width, height, wireframes, background }, }) runnerRef.current = Runner.create() Runner.run(runnerRef.current, engineRef.current) Render.run(renderRef.current) return () => { if (renderRef.current) Render.stop(renderRef.current) if (runnerRef.current) Runner.stop(runnerRef.current) if (engineRef.current) { Matter.World.clear(engineRef.current.world, false) Engine.clear(engineRef.current) } } }, [width, height, gravity.x, gravity.y, wireframes, background]) const addBody = useCallback((body: Matter.Body) => { if (engineRef.current) { Matter.Composite.add(engineRef.current.world, body) } }, []) const removeBody = useCallback((body: Matter.Body) => { if (engineRef.current) { Matter.Composite.remove(engineRef.current.world, body) } }, []) const addBodies = useCallback((bodies: Matter.Body[]) => { if (engineRef.current) { Matter.Composite.add(engineRef.current.world, bodies) } }, []) return { containerRef, canvasRef, engine: engineRef, addBody, removeBody, addBodies, Matter, } } ``` ## Bodies (Cuerpos) ### Formas Básicas ```tsx const { Bodies } = Matter // Rectángulo const box = Bodies.rectangle(x, y, width, height, { render: { fillStyle: '#ff6b6b' }, }) // Círculo const ball = Bodies.circle(x, y, radius, { restitution: 0.8, // Rebote friction: 0.1, render: { fillStyle: '#4ecdc4' }, }) // Polígono regular const hexagon = Bodies.polygon(x, y, 6, radius, { render: { fillStyle: '#45b7d1' }, }) // Trapecio const trapezoid = Bodies.trapezoid(x, y, width, height, slope, { render: { fillStyle: '#96ceb4' }, }) ``` ### Cuerpos desde Vértices (Custom Shapes) ```tsx // Forma L (Tetris) const lShape = Bodies.fromVertices(400, 300, [ { x: 0, y: 0 }, { x: 30, y: 0 }, { x: 30, y: 60 }, { x: 60, y: 60 }, { x: 60, y: 90 }, { x: 0, y: 90 }, ], { render: { fillStyle: '#ff6b6b' }, }) // Forma T const tShape = Bodies.fromVertices(400, 300, [ { x: 0, y: 0 }, { x: 90, y: 0 }, { x: 90, y: 30 }, { x: 60, y: 30 }, { x: 60, y: 60 }, { x: 30, y: 60 }, { x: 30, y: 30 }, { x: 0, y: 30 }, ], { render: { fillStyle: '#4ecdc4' }, }) ``` ### Propiedades de Bodies ```tsx const body = Bodies.circle(400, 300, 50, { // Física mass: 10, // Masa density: 0.001, // Densidad friction: 0.1, // Fricción (0-1) frictionAir: 0.01, // Resistencia del aire frictionStatic: 0.5, // Fricción estática restitution: 0.8, // Rebote (0-1) // Estado isStatic: false, // Estático (no se mueve) isSensor: false, // Solo detecta colisiones, no físicas // Render render: { visible: true, fillStyle: '#ff6b6b', strokeStyle: '#fff', lineWidth: 2, opacity: 1, }, // Identificación label: 'ball', // Colisiones collisionFilter: { category: 0x0001, mask: 0xFFFFFFFF, group: 0, }, }) ``` ### Manipular Bodies ```tsx const { Body } = Matter // Aplicar fuerza Body.applyForce(body, body.position, { x: 0.05, y: -0.1 }) // Establecer velocidad Body.setVelocity(body, { x: 10, y: -5 }) // Establecer velocidad angular Body.setAngularVelocity(body, 0.1) // Mover a posición Body.setPosition(body, { x: 400, y: 300 }) // Rotar Body.setAngle(body, Math.PI / 4) Body.rotate(body, 0.1) // Escalar Body.scale(body, 1.5, 1.5) ``` ## World & Engine ### Configuración del Engine ```tsx const engine = Engine.create({ gravity: { x: 0, y: 1, scale: 0.001, }, timing: { timeScale: 1, // Slow motion: < 1, Fast forward: > 1 }, }) // Cambiar gravedad dinámicamente engine.gravity.y = 0 // Sin gravedad engine.gravity.y = -1 // Gravedad invertida engine.gravity.x = 0.5 // Gravedad lateral ``` ### Bounds del World ```tsx // Crear contenedor con paredes function createBounds(width: number, height: number, thickness = 50) { return [ // Suelo Bodies.rectangle(width / 2, height + thickness / 2, width, thickness, { isStatic: true, label: 'ground', }), // Techo Bodies.rectangle(width / 2, -thickness / 2, width, thickness, { isStatic: true, label: 'ceiling', }), // Pared izquierda Bodies.rectangle(-thickness / 2, height / 2, thickness, height, { isStatic: true, label: 'leftWall', }), // Pared derecha Bodies.rectangle(width + thickness / 2, height / 2, thickness, height, { isStatic: true, label: 'rightWall', }), ] } ``` ## Constraints (Restricciones) ### Conectar Bodies ```tsx const { Constraint } = Matter // Constraint punto a punto const rope = Constraint.create({ bodyA: ball1, bodyB: ball2, length: 100, stiffness: 0.9, render: { strokeStyle: '#fff', lineWidth: 2, }, }) // Constraint a punto fijo const pendulum = Constraint.create({ pointA: { x: 400, y: 0 }, // Punto fijo en el mundo bodyB: ball, length: 200, stiffness: 1, }) // Spring (resorte) const spring = Constraint.create({ bodyA: ball1, bodyB: ball2, stiffness: 0.01, // Más bajo = más elástico damping: 0.1, }) Composite.add(world, [rope, pendulum, spring]) ``` ### Cadena de Bodies ```tsx function createChain(x: number, y: number, links: number, linkSize: number) { const bodies: Matter.Body[] = [] const constraints: Matter.Constraint[] = [] // Punto de anclaje const anchor = Bodies.circle(x, y, 5, { isStatic: true }) bodies.push(anchor) let prevBody = anchor for (let i = 0; i < links; i++) { const link = Bodies.rectangle( x, y + (i + 1) * linkSize, linkSize * 0.8, linkSize * 0.3, { render: { fillStyle: '#4ecdc4' } } ) bodies.push(link) constraints.push( Constraint.create({ bodyA: prevBody, bodyB: link, length: linkSize * 0.5, stiffness: 0.9, }) ) prevBody = link } return { bodies, constraints } } ``` ## Mouse Interaction ### Mouse Constraint ```tsx 'use client' import { useEffect } from 'react' import Matter from 'matter-js' export function DraggablePhysics() { useEffect(() => { const { Engine, Render, Runner, Bodies, Composite, Mouse, MouseConstraint } = Matter const engine = Engine.create() const render = Render.create({ element: document.body, engine: engine, options: { width: 800, height: 600, wireframes: false }, }) // Crear bodies arrastrables const boxes = Array.from({ length: 5 }, (_, i) => Bodies.rectangle(200 + i * 100, 200, 60, 60, { render: { fillStyle: `hsl(${i * 60}, 70%, 60%)` }, }) ) const ground = Bodies.rectangle(400, 580, 800, 40, { isStatic: true }) // Configurar mouse const mouse = Mouse.create(render.canvas) const mouseConstraint = MouseConstraint.create(engine, { mouse: mouse, constraint: { stiffness: 0.2, render: { visible: false }, }, }) // Sincronizar mouse con scroll render.mouse = mouse Composite.add(engine.world, [...boxes, ground, mouseConstraint]) Runner.run(Runner.create(), engine) Render.run(render) }, []) return null } ``` ### Hover Detection ```tsx import Matter from 'matter-js' const { Query } = Matter function getBodyAtPosition(engine: Matter.Engine, x: number, y: number) { const bodies = Matter.Composite.allBodies(engine.world) const point = { x, y } // Encontrar body bajo el punto const found = Query.point(bodies, point) return found[0] || null } // Uso en event listener canvas.addEventListener('mousemove', (e) => { const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top const body = getBodyAtPosition(engine, x, y) if (body) { // Hover sobre body body.render.fillStyle = '#ff0000' } }) ``` ## Collision Events ### Detectar Colisiones ```tsx import Matter from 'matter-js' const { Events } = Matter // Collision start Events.on(engine, 'collisionStart', (event) => { event.pairs.forEach((pair) => { const { bodyA, bodyB } = pair // Verificar labels if (bodyA.label === 'ball' && bodyB.label === 'ground') { console.log('Ball hit ground!') // Reproducir sonido, cambiar color, etc. bodyA.render.fillStyle = '#ff0000' } }) }) // Collision active (mientras colisionan) Events.on(engine, 'collisionActive', (event) => { // ... }) // Collision end Events.on(engine, 'collisionEnd', (event) => { event.pairs.forEach((pair) => { pair.bodyA.render.fillStyle = '#4ecdc4' pair.bodyB.render.fillStyle = '#4ecdc4' }) }) ``` ### Colisiones con Audio ```tsx 'use client' import { useEffect, useRef } from 'react' import Matter from 'matter-js' import * as Tone from 'tone' export function PhysicsWithSound() { const synthRef = useRef(null) useEffect(() => { synthRef.current = new Tone.MembraneSynth().toDestination() synthRef.current.volume.value = -10 const engine = Matter.Engine.create() // ... setup render, bodies Matter.Events.on(engine, 'collisionStart', async (event) => { await Tone.start() event.pairs.forEach((pair) => { // Calcular intensidad del impacto const velocity = Math.sqrt( Math.pow(pair.bodyA.velocity.x - pair.bodyB.velocity.x, 2) + Math.pow(pair.bodyA.velocity.y - pair.bodyB.velocity.y, 2) ) if (velocity > 2) { // Mapear velocidad a nota const note = Math.min(Math.floor(velocity * 10) + 30, 80) const freq = Tone.Frequency(note, 'midi').toFrequency() synthRef.current?.triggerAttackRelease(freq, '16n') } }) }) return () => synthRef.current?.dispose() }, []) return null } ``` ## Scroll Physics ### Gravedad Reactiva al Scroll ```tsx 'use client' import { useEffect, useRef } from 'react' import Matter from 'matter-js' import { gsap, ScrollTrigger } from '@/lib/gsap' export function ScrollGravity() { const engineRef = useRef(null) useEffect(() => { engineRef.current = Matter.Engine.create() // ... setup // Cambiar gravedad con scroll ScrollTrigger.create({ trigger: 'body', start: 'top top', end: 'bottom bottom', onUpdate: (self) => { if (engineRef.current) { // Gravedad basada en dirección del scroll const direction = self.direction engineRef.current.gravity.y = direction > 0 ? 1 : -1 } }, }) // O gravedad basada en posición del scroll ScrollTrigger.create({ trigger: 'body', start: 'top top', end: 'bottom bottom', onUpdate: (self) => { if (engineRef.current) { // Rotar gravedad 360° durante el scroll const angle = self.progress * Math.PI * 2 engineRef.current.gravity.x = Math.sin(angle) engineRef.current.gravity.y = Math.cos(angle) } }, }) }, []) return null } ``` ### Spawn Bodies on Scroll ```tsx 'use client' import { useEffect, useRef } from 'react' import Matter from 'matter-js' import { ScrollTrigger } from 'gsap/ScrollTrigger' export function ScrollSpawnBodies() { const engineRef = useRef(null) const lastSpawnRef = useRef(0) useEffect(() => { // ... setup engine ScrollTrigger.create({ trigger: 'body', start: 'top top', end: 'bottom bottom', onUpdate: (self) => { // Spawn body cada 5% de scroll const spawnPoint = Math.floor(self.progress * 20) if (spawnPoint > lastSpawnRef.current) { lastSpawnRef.current = spawnPoint const body = Matter.Bodies.circle( Math.random() * 600 + 100, 50, 20 + Math.random() * 30, { restitution: 0.7, render: { fillStyle: `hsl(${Math.random() * 360}, 70%, 60%)`, }, } ) Matter.Composite.add(engineRef.current!.world, body) } }, }) }, []) return null } ``` ## Creative Patterns ### Particle Explosion on Click ```tsx 'use client' import { useEffect, useRef } from 'react' import Matter from 'matter-js' export function ClickExplosion() { const engineRef = useRef(null) const renderRef = useRef(null) useEffect(() => { const engine = Matter.Engine.create() engineRef.current = engine const render = Matter.Render.create({ element: document.getElementById('physics-container')!, engine: engine, options: { width: 800, height: 600, wireframes: false, background: '#0a0a0a' }, }) renderRef.current = render // Ground const ground = Matter.Bodies.rectangle(400, 590, 800, 20, { isStatic: true }) Matter.Composite.add(engine.world, ground) // Click handler const handleClick = (e: MouseEvent) => { const rect = render.canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Crear explosión de partículas const particles = Array.from({ length: 20 }, () => { const angle = Math.random() * Math.PI * 2 const speed = 5 + Math.random() * 10 const particle = Matter.Bodies.circle(x, y, 5 + Math.random() * 10, { restitution: 0.6, friction: 0.1, render: { fillStyle: `hsl(${Math.random() * 60 + 10}, 100%, 60%)`, }, }) Matter.Body.setVelocity(particle, { x: Math.cos(angle) * speed, y: Math.sin(angle) * speed, }) return particle }) Matter.Composite.add(engine.world, particles) // Remover después de 3 segundos setTimeout(() => { particles.forEach((p) => Matter.Composite.remove(engine.world, p)) }, 3000) } render.canvas.addEventListener('click', handleClick) Matter.Runner.run(Matter.Runner.create(), engine) Matter.Render.run(render) return () => { render.canvas.removeEventListener('click', handleClick) } }, []) return
} ``` ### Soft Body (Blob) ```tsx function createSoftBody(x: number, y: number, columns: number, rows: number, spacing: number) { const particles: Matter.Body[] = [] const constraints: Matter.Constraint[] = [] // Crear partículas en grid for (let row = 0; row < rows; row++) { for (let col = 0; col < columns; col++) { const particle = Matter.Bodies.circle( x + col * spacing, y + row * spacing, spacing * 0.3, { render: { fillStyle: '#4ecdc4' }, } ) particles.push(particle) } } // Conectar partículas adyacentes for (let row = 0; row < rows; row++) { for (let col = 0; col < columns; col++) { const index = row * columns + col // Horizontal if (col < columns - 1) { constraints.push( Matter.Constraint.create({ bodyA: particles[index], bodyB: particles[index + 1], stiffness: 0.1, render: { visible: false }, }) ) } // Vertical if (row < rows - 1) { constraints.push( Matter.Constraint.create({ bodyA: particles[index], bodyB: particles[index + columns], stiffness: 0.1, render: { visible: false }, }) ) } // Diagonal if (col < columns - 1 && row < rows - 1) { constraints.push( Matter.Constraint.create({ bodyA: particles[index], bodyB: particles[index + columns + 1], stiffness: 0.05, render: { visible: false }, }) ) } } } return { particles, constraints } } ``` ### Newton's Cradle ```tsx function createNewtonsCradle(x: number, y: number, ballCount: number, ballRadius: number) { const balls: Matter.Body[] = [] const constraints: Matter.Constraint[] = [] for (let i = 0; i < ballCount; i++) { const ball = Matter.Bodies.circle( x + i * ballRadius * 2.1, y + 200, ballRadius, { restitution: 1, friction: 0, frictionAir: 0, render: { fillStyle: '#c0c0c0' }, } ) const constraint = Matter.Constraint.create({ pointA: { x: x + i * ballRadius * 2.1, y }, bodyB: ball, length: 200, stiffness: 1, render: { strokeStyle: '#666' }, }) balls.push(ball) constraints.push(constraint) } // Impulso inicial al primer ball Matter.Body.setPosition(balls[0], { x: balls[0].position.x - 100, y: balls[0].position.y - 50, }) return { balls, constraints } } ``` ## Recursos - [Matter.js](https://brm.io/matter-js/) - [Matter.js Docs](https://brm.io/matter-js/docs/) - [Matter.js GitHub](https://github.com/liabru/matter-js) - [Matter.js Demos](https://brm.io/matter-js/demo/)