# Geometric Shapes & Creative Coding Patterns for creating geometric shapes, grids, and Tetris-style animations in React. ## Table of Contents 1. [Decision Matrix](#decision-matrix) 2. [SVG Programático](#svg-programático) 3. [Canvas 2D](#canvas-2d) 4. [CSS Grid + GSAP](#css-grid--gsap) 5. [Zdog (Pseudo-3D)](#zdog-pseudo-3d) 6. [p5.js Creative Coding](#p5js-creative-coding) 7. [Tetris-Style Patterns](#tetris-style-patterns) ## Decision Matrix | Necesidad | Herramienta | Por qué | |-----------|-------------|---------| | Formas vectoriales simples | SVG en JSX | Nativo, animable con GSAP/Motion | | Grids animados | CSS Grid + GSAP | Layout + animaciones | | Dibujo programático | Canvas 2D | Performance, píxeles | | Pseudo-3D estilizado | Zdog | Flat design 3D, ~2kb | | Creative coding/arte | p5.js | Ecosistema, comunidad | | Formas 3D reales | Three.js | WebGL completo | ## SVG Programático ### Formas Básicas en React ```tsx 'use client' // Todas las formas SVG son elementos JSX nativos export function GeometricShapes() { return ( {/* Rectángulo */} {/* Cuadrado con bordes redondeados */} {/* Círculo */} {/* Elipse */} {/* Línea */} {/* Polilínea (líneas conectadas) */} {/* Polígono (cerrado) - Triángulo */} {/* Polígono - Hexágono */} {/* Path - Forma custom */} {/* Path - Curva Bézier */} ) } ``` ### Generador de Polígonos ```tsx 'use client' function generatePolygonPoints( sides: number, radius: number, centerX: number, centerY: number ): string { const points: string[] = [] for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2 const x = centerX + radius * Math.cos(angle) const y = centerY + radius * Math.sin(angle) points.push(`${x},${y}`) } return points.join(' ') } export function RegularPolygon({ sides, radius, cx, cy, fill, }: { sides: number radius: number cx: number cy: number fill: string }) { const points = generatePolygonPoints(sides, radius, cx, cy) return } // Uso {/* Triángulo */} {/* Pentágono */} {/* Hexágono */} {/* Octágono */} ``` ### SVG Animado con GSAP ```tsx 'use client' import { useRef } from 'react' import { gsap, useGSAP } from '@/lib/gsap' export function AnimatedShapes() { const svgRef = useRef(null) useGSAP(() => { // Rotar polígono gsap.to('.rotating-shape', { rotation: 360, transformOrigin: 'center center', duration: 4, ease: 'none', repeat: -1, }) // Pulsar círculo gsap.to('.pulsing-circle', { scale: 1.2, duration: 0.8, ease: 'power2.inOut', yoyo: true, repeat: -1, }) // Morph path gsap.to('.morphing-rect', { attr: { rx: 40 }, duration: 1, ease: 'power2.inOut', yoyo: true, repeat: -1, }) }, { scope: svgRef }) return ( ) } ``` ### SVG Animado con Motion ```tsx 'use client' import { motion } from 'motion/react' export function MotionShapes() { return ( ) } ``` ## Canvas 2D ### Setup Básico en React ```tsx 'use client' import { useRef, useEffect } from 'react' export function CanvasShapes() { const canvasRef = useRef(null) useEffect(() => { const canvas = canvasRef.current! const ctx = canvas.getContext('2d')! // Configurar tamaño canvas.width = 400 canvas.height = 400 // Limpiar ctx.fillStyle = '#1a1a2e' ctx.fillRect(0, 0, canvas.width, canvas.height) // Rectángulo ctx.fillStyle = '#ff6b6b' ctx.fillRect(10, 10, 80, 80) // Rectángulo con borde ctx.strokeStyle = '#4ecdc4' ctx.lineWidth = 3 ctx.strokeRect(110, 10, 80, 80) // Círculo ctx.beginPath() ctx.arc(250, 50, 40, 0, Math.PI * 2) ctx.fillStyle = '#45b7d1' ctx.fill() // Triángulo ctx.beginPath() ctx.moveTo(350, 10) ctx.lineTo(390, 90) ctx.lineTo(310, 90) ctx.closePath() ctx.fillStyle = '#96ceb4' ctx.fill() // Polígono regular (hexágono) drawPolygon(ctx, 50, 150, 40, 6, '#a29bfe') }, []) return } function drawPolygon( ctx: CanvasRenderingContext2D, x: number, y: number, radius: number, sides: number, color: string ) { ctx.beginPath() for (let i = 0; i < sides; i++) { const angle = (i * 2 * Math.PI) / sides - Math.PI / 2 const px = x + radius * Math.cos(angle) const py = y + radius * Math.sin(angle) if (i === 0) ctx.moveTo(px, py) else ctx.lineTo(px, py) } ctx.closePath() ctx.fillStyle = color ctx.fill() } ``` ### Canvas Animado con requestAnimationFrame ```tsx 'use client' import { useRef, useEffect } from 'react' interface Shape { x: number y: number size: number rotation: number rotationSpeed: number color: string sides: number } export function AnimatedCanvasShapes() { const canvasRef = useRef(null) useEffect(() => { const canvas = canvasRef.current! const ctx = canvas.getContext('2d')! canvas.width = 600 canvas.height = 400 // Crear formas const shapes: Shape[] = Array.from({ length: 10 }, () => ({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: 20 + Math.random() * 40, rotation: 0, rotationSpeed: (Math.random() - 0.5) * 0.05, color: `hsl(${Math.random() * 360}, 70%, 60%)`, sides: 3 + Math.floor(Math.random() * 5), })) function drawPolygon(shape: Shape) { ctx.save() ctx.translate(shape.x, shape.y) ctx.rotate(shape.rotation) ctx.beginPath() for (let i = 0; i < shape.sides; i++) { const angle = (i * 2 * Math.PI) / shape.sides - Math.PI / 2 const px = shape.size * Math.cos(angle) const py = shape.size * Math.sin(angle) if (i === 0) ctx.moveTo(px, py) else ctx.lineTo(px, py) } ctx.closePath() ctx.fillStyle = shape.color ctx.fill() ctx.restore() } let animationId: number function animate() { ctx.fillStyle = 'rgba(26, 26, 46, 0.1)' ctx.fillRect(0, 0, canvas.width, canvas.height) shapes.forEach((shape) => { shape.rotation += shape.rotationSpeed drawPolygon(shape) }) animationId = requestAnimationFrame(animate) } animate() return () => cancelAnimationFrame(animationId) }, []) return } ``` ## CSS Grid + GSAP ### Grid de Bloques Animados (Tetris-style) ```tsx 'use client' import { useRef } from 'react' import { gsap, useGSAP, stagger } from '@/lib/gsap' export function AnimatedGrid() { const gridRef = useRef(null) useGSAP(() => { gsap.from('.grid-block', { scale: 0, rotation: 180, opacity: 0, duration: 0.6, stagger: { amount: 1, grid: [4, 4], from: 'center', }, ease: 'back.out(1.7)', }) }, { scope: gridRef }) const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#a29bfe'] return (
{Array.from({ length: 16 }).map((_, i) => (
))}
) } ``` ### Grid con Hover Interactivo ```tsx 'use client' import { useRef } from 'react' import { gsap, useGSAP } from '@/lib/gsap' export function InteractiveGrid() { const gridRef = useRef(null) const { contextSafe } = useGSAP({ scope: gridRef }) const handleMouseEnter = contextSafe((e: React.MouseEvent) => { gsap.to(e.currentTarget, { scale: 1.1, rotation: 5, duration: 0.3, ease: 'power2.out', }) }) const handleMouseLeave = contextSafe((e: React.MouseEvent) => { gsap.to(e.currentTarget, { scale: 1, rotation: 0, duration: 0.5, ease: 'elastic.out(1, 0.3)', }) }) return (
{Array.from({ length: 25 }).map((_, i) => (
))}
) } ``` ## Zdog (Pseudo-3D) ### Instalación ```bash npm install zdog # O para React npm install react-zdog ``` ### Formas Básicas con Zdog ```tsx 'use client' import { useRef, useEffect } from 'react' import Zdog from 'zdog' export function ZdogShapes() { const canvasRef = useRef(null) useEffect(() => { const illo = new Zdog.Illustration({ element: canvasRef.current!, zoom: 2, dragRotate: true, }) // Cubo (usando Rect para cada cara) const cube = new Zdog.Box({ addTo: illo, width: 60, height: 60, depth: 60, stroke: false, color: '#ff6b6b', leftFace: '#4ecdc4', rightFace: '#45b7d1', topFace: '#96ceb4', bottomFace: '#ffeaa7', }) // Cilindro new Zdog.Cylinder({ addTo: illo, diameter: 40, length: 60, stroke: false, color: '#a29bfe', backface: '#74b9ff', translate: { x: 100 }, }) // Cono new Zdog.Cone({ addTo: illo, diameter: 50, length: 70, stroke: false, color: '#fd79a8', backface: '#e84393', translate: { x: -100 }, }) // Hemisferio new Zdog.Hemisphere({ addTo: illo, diameter: 50, stroke: false, color: '#00cec9', backface: '#81ecec', translate: { y: 80 }, }) // Animación function animate() { illo.rotate.y += 0.01 illo.rotate.x += 0.005 illo.updateRenderGraph() requestAnimationFrame(animate) } animate() }, []) return } ``` ### Zdog Custom Shapes ```tsx // Forma L (estilo Tetris) new Zdog.Shape({ addTo: illo, path: [ { x: 0, y: 0 }, { x: 30, y: 0 }, { x: 30, y: 30 }, { x: 60, y: 30 }, { x: 60, y: 60 }, { x: 0, y: 60 }, ], closed: true, stroke: 20, color: '#ff6b6b', }) // Forma T new Zdog.Shape({ addTo: illo, path: [ { 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 }, ], closed: true, stroke: 20, color: '#4ecdc4', translate: { x: 100 }, }) ``` ## p5.js Creative Coding ### Instalación ```bash npm install p5 @types/p5 npm install @p5-wrapper/react ``` ### Setup Básico con React ```tsx 'use client' import { useRef, useEffect } from 'react' import p5 from 'p5' export function P5Sketch() { const containerRef = useRef(null) useEffect(() => { const sketch = (p: p5) => { p.setup = () => { p.createCanvas(400, 400) p.background(26, 26, 46) } p.draw = () => { p.background(26, 26, 46, 10) // Formas geométricas p.noStroke() // Triángulo rotante p.push() p.translate(100, 100) p.rotate(p.frameCount * 0.02) p.fill(255, 107, 107) p.triangle(0, -40, -35, 20, 35, 20) p.pop() // Hexágono p.push() p.translate(200, 200) p.rotate(-p.frameCount * 0.01) p.fill(78, 205, 196) p.beginShape() for (let i = 0; i < 6; i++) { const angle = p.TWO_PI / 6 * i - p.HALF_PI const x = p.cos(angle) * 50 const y = p.sin(angle) * 50 p.vertex(x, y) } p.endShape(p.CLOSE) p.pop() // Estrella p.push() p.translate(300, 300) p.rotate(p.frameCount * 0.015) p.fill(150, 206, 180) drawStar(p, 0, 0, 20, 50, 5) p.pop() } } function drawStar(p: p5, x: number, y: number, r1: number, r2: number, points: number) { const angle = p.TWO_PI / points const halfAngle = angle / 2 p.beginShape() for (let a = -p.HALF_PI; a < p.TWO_PI - p.HALF_PI; a += angle) { let sx = x + p.cos(a) * r2 let sy = y + p.sin(a) * r2 p.vertex(sx, sy) sx = x + p.cos(a + halfAngle) * r1 sy = y + p.sin(a + halfAngle) * r1 p.vertex(sx, sy) } p.endShape(p.CLOSE) } const p5Instance = new p5(sketch, containerRef.current!) return () => p5Instance.remove() }, []) return
} ``` ### p5.js con @p5-wrapper/react ```tsx 'use client' import dynamic from 'next/dynamic' import type { P5CanvasInstance } from '@p5-wrapper/react' // Importar dinámicamente para evitar SSR issues const ReactP5Wrapper = dynamic( () => import('@p5-wrapper/react').then((mod) => mod.ReactP5Wrapper), { ssr: false } ) function sketch(p5: P5CanvasInstance) { p5.setup = () => { p5.createCanvas(400, 400) } p5.draw = () => { p5.background(26, 26, 46) p5.fill(255, 107, 107) p5.noStroke() // Polígonos que siguen al mouse p5.push() p5.translate(p5.mouseX, p5.mouseY) p5.rotate(p5.frameCount * 0.02) regularPolygon(p5, 0, 0, 40, 6) p5.pop() } } function regularPolygon(p5: P5CanvasInstance, x: number, y: number, r: number, sides: number) { p5.beginShape() for (let i = 0; i < sides; i++) { const angle = p5.TWO_PI / sides * i - p5.HALF_PI p5.vertex(x + p5.cos(angle) * r, y + p5.sin(angle) * r) } p5.endShape(p5.CLOSE) } export function P5WrapperDemo() { return } ``` ## Tetris-Style Patterns ### Piezas de Tetris como Componentes ```tsx 'use client' import { motion } from 'motion/react' type TetrisPiece = 'I' | 'O' | 'T' | 'S' | 'Z' | 'J' | 'L' const PIECES: Record = { I: { shape: [[1, 1, 1, 1]], color: '#00f5ff' }, O: { shape: [[1, 1], [1, 1]], color: '#ffeb3b' }, T: { shape: [[0, 1, 0], [1, 1, 1]], color: '#9c27b0' }, S: { shape: [[0, 1, 1], [1, 1, 0]], color: '#4caf50' }, Z: { shape: [[1, 1, 0], [0, 1, 1]], color: '#f44336' }, J: { shape: [[1, 0, 0], [1, 1, 1]], color: '#2196f3' }, L: { shape: [[0, 0, 1], [1, 1, 1]], color: '#ff9800' }, } export function TetrisPiece({ type, size = 30, rotation = 0, }: { type: TetrisPiece size?: number rotation?: number }) { const piece = PIECES[type] return ( {piece.shape.flat().map((cell, i) => ( ))} ) } // Uso export function TetrisDemo() { return (
) } ``` ### Tetris Falling Animation ```tsx 'use client' import { useRef, useState, useEffect } from 'react' import { motion, AnimatePresence } from 'motion/react' const COLORS = ['#00f5ff', '#ffeb3b', '#9c27b0', '#4caf50', '#f44336', '#2196f3', '#ff9800'] interface FallingBlock { id: number x: number color: string } export function TetrisFalling() { const [blocks, setBlocks] = useState([]) const idRef = useRef(0) useEffect(() => { const interval = setInterval(() => { setBlocks((prev) => [ ...prev.slice(-20), // Mantener últimos 20 { id: idRef.current++, x: Math.random() * 80 + 10, // 10-90% color: COLORS[Math.floor(Math.random() * COLORS.length)], }, ]) }, 500) return () => clearInterval(interval) }, []) return (
{blocks.map((block) => ( ))}
) } ``` ### Grid Board con GSAP ```tsx 'use client' import { useRef, useState } from 'react' import { gsap, useGSAP } from '@/lib/gsap' const GRID_SIZE = 10 const CELL_SIZE = 30 export function TetrisBoard() { const boardRef = useRef(null) const [grid, setGrid] = useState( Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill('')) ) const { contextSafe } = useGSAP({ scope: boardRef }) const handleCellClick = contextSafe((row: number, col: number) => { const colors = ['#00f5ff', '#ffeb3b', '#9c27b0', '#4caf50', '#f44336'] const newColor = colors[Math.floor(Math.random() * colors.length)] // Animar el click gsap.fromTo( `[data-cell="${row}-${col}"]`, { scale: 0, rotation: 180 }, { scale: 1, rotation: 0, duration: 0.5, ease: 'back.out(1.7)' } ) setGrid((prev) => { const newGrid = [...prev] newGrid[row] = [...newGrid[row]] newGrid[row][col] = newGrid[row][col] ? '' : newColor return newGrid }) }) return (
{grid.map((row, rowIndex) => row.map((cell, colIndex) => (
handleCellClick(rowIndex, colIndex)} /> )) )}
) } ``` ## Recursos - [p5.js](https://p5js.org/) - Creative coding - [Zdog](https://zzz.dog/) - Pseudo-3D engine - [@p5-wrapper/react](https://github.com/P5-wrapper/react) - React wrapper - [SVG Tutorial MDN](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial) - [Canvas API MDN](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)