skills/awwwards-animations/references/physics-2d.md

923 lines
20 KiB
Markdown

# 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<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(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 (
<div ref={containerRef}>
<canvas ref={canvasRef} />
</div>
)
}
```
### 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<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const engineRef = useRef<Matter.Engine | null>(null)
const renderRef = useRef<Matter.Render | null>(null)
const runnerRef = useRef<Matter.Runner | null>(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<Tone.MembraneSynth | null>(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<Matter.Engine | null>(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<Matter.Engine | null>(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<Matter.Engine | null>(null)
const renderRef = useRef<Matter.Render | null>(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 <div id="physics-container" />
}
```
### 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/)