// weave.jsx — Sol Madresia vivo // // Una estrella de 8 puntas escalonada, interpretación original de la // gramática visual Wayuu (simetría octaval, bordes stepped, anillos // concéntricos). No hay lattice exterior. No hay telaraña. Solo el Sol. // // Dos fases: // (1) TEJIDO (0–8 s): las puntadas aparecen en espiral desde el centro // hacia afuera, puntada a puntada, como si se tejiera en vivo. // (2) VIVO (8 s+): el Sol sigue pensando. Capas de vida superpuestas: // — respiración global lenta // — onda radial de pensamiento (heartbeat cada ~4 s) que viaja // desde el centro hacia afuera iluminando puntadas a su paso // — escaneo angular rotando en 8-fold — cells en el eje actual // brillan más, simula atención barriendo el tejido // — disparos aleatorios de celdas individuales (neuronas firing) // — shimmer per-cell para textura viva // // El Sol es la red neuronal. Cada puntada es una neurona. No se detiene. function WovenScene() { const time = useTime(); const canvasRef = React.useRef(null); const geom = React.useMemo(() => buildGeometry(), []); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const dpr = Math.min(window.devicePixelRatio || 1, 1.25); canvas.width = 1600 * dpr; canvas.height = 1000 * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); }, []); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); draw(ctx, time, geom); }, [time, geom]); return ( ); } // ── Geometría del Sol ────────────────────────────────────────────────────── function buildGeometry() { // El Sol flota un poco arriba del centro vertical para dejar espacio // a la frase y la atribución Wayuu abajo. const CX = 800, CY = 420; const cellSize = 11; const gridRadius = 26; const cells = []; for (let gy = -gridRadius; gy <= gridRadius; gy++) { for (let gx = -gridRadius; gx <= gridRadius; gx++) { const v = solPattern(gx, gy); if (!v) continue; const r = Math.sqrt(gx * gx + gy * gy); const theta = Math.atan2(gy, gx); cells.push({ gx, gy, x: CX + gx * cellSize, y: CY + gy * cellSize, r, theta, rPx: r * cellSize, // radio en píxeles (para ondas radiales) weight: v, // Cada celda tiene un offset de fase para shimmer individual seed: Math.sin(gx * 12.9898 + gy * 78.233) * 43758.5453, }); } } // Orden de tejido — espiral desde el centro. r domina, theta agrega giro. cells.sort((a, b) => { const keyA = a.r + a.theta * 0.05; const keyB = b.r + b.theta * 0.05; return keyA - keyB; }); cells.forEach((c, i) => { c.order = i / Math.max(1, cells.length - 1); }); // Radio máximo del patrón (en píxeles) — para normalizar ondas radiales const maxRPx = Math.max(...cells.map(c => c.rPx)); // Polvo ambiental mínimo const RNG = mulberry32(17); const particles = []; for (let i = 0; i < 32; i++) { particles.push({ x: RNG() * 1600, y: RNG() * 1000, r: 0.4 + RNG() * 0.9, speed: 0.03 + RNG() * 0.12, phase: RNG() * Math.PI * 2, dir: RNG() * Math.PI * 2, }); } return { CX, CY, cells, cellSize, maxRPx, particles }; } // ── Patrón del Sol ────────────────────────────────────────────────────────── // Composición original: simetría octaval, rayos cardinales + diagonales, // anillos concéntricos escalonados. No reproduce ningún motivo específico. function solPattern(gx, gy) { const adx = Math.abs(gx); const ady = Math.abs(gy); const r = Math.sqrt(gx * gx + gy * gy); const diff = Math.abs(adx - ady); // Núcleo 3×3 if (adx <= 1 && ady <= 1) return 1; // Rayos cardinales: gruesos al centro, se adelgazan if (r <= 9) { const t = Math.max(0, 2 - Math.floor(r / 3)); if (ady <= t && adx >= 2) return 1; if (adx <= t && ady >= 2) return 1; } // Rayos diagonales (intermedios) if (r <= 7 && r >= 2) { const t = Math.max(0, 1 - Math.floor(r / 4)); if (diff <= t) return 1; } // Primer anillo — triángulos escalonados entre rayos cardinales if (r >= 11 && r <= 14) { const theta = Math.atan2(gy, gx); const sector = ((theta / (Math.PI * 2)) * 8 + 8) % 1; const fromEdge = Math.min(sector, 1 - sector); const localR = r - 11; if (localR < fromEdge * 6) return 1; } // Anillo punteado intermedio a r ≈ 17 if (r >= 16.5 && r <= 17.5) { const theta = Math.atan2(gy, gx); const pos = ((theta / (Math.PI * 2)) * 32 + 32) % 1; if (pos < 0.5) return 0.55; } // Anillo exterior — rombos en 8 ejes, r ≈ 21–24 if (r >= 20 && r <= 24) { const theta = Math.atan2(gy, gx); const angle8 = ((theta / (Math.PI * 2)) * 8 + 8) % 1; const angleFromAxis = Math.min(angle8, 1 - angle8); if (angleFromAxis < 0.06) { const localR = Math.abs(r - 22); if (localR < 1.8 - angleFromAxis * 15) return 1; } } return 0; } // ── Render ────────────────────────────────────────────────────────────────── function draw(ctx, time, geom) { const { CX, CY, cells, cellSize, maxRPx, particles } = geom; const W = 1600, H = 1000; ctx.save(); // Transparente — dejamos que la corriente de código del fondo se vea a través. ctx.clearRect(0, 0, W, H); // ── FASE 1: TEJIDO (0–8s) ── // reveal pasa de 0 a 1 durante los primeros 8 segundos const revealDuration = 8; const reveal = smoothstep(0, revealDuration, time); const woven = reveal >= 0.999; const holdTime = Math.max(0, time - revealDuration); // tiempo en fase viva // ── Parámetros de vida continua (activos en fase 2) ── const breath = 0.5 + 0.5 * Math.sin(holdTime * 0.55); const breathSigned = Math.sin(holdTime * 0.55); const pulseScale = 1 + (woven ? breathSigned * 0.012 : 0); // Heartbeat: pulso radial que viaja desde el centro hacia afuera. // Un pulso nuevo cada ~4 s. Duración del viaje: ~2.4 s. const heartPeriod = 4.0; const heartDuration = 2.4; const heartElapsed = (holdTime % heartPeriod); const heartActive = heartElapsed < heartDuration; const heartProgress = heartActive ? (heartElapsed / heartDuration) : 1; const heartRadius = heartProgress * (maxRPx + 30); const heartWidth = 32; // Escaneo angular rotando en 8-fold const scanAngle = holdTime * 0.28; // Firing aleatorio de celdas — usa hash de tiempo+seed para determinismo suave // (cada celda tiene ~1% chance de firing en una ventana de 1.5s) const fireWindow = 1.5; const fireSlot = Math.floor(holdTime / fireWindow); const fireLocal = (holdTime % fireWindow) / fireWindow; // 0..1 // Polvo ambiental for (const p of particles) { const drift = time * p.speed; const x = (p.x + Math.cos(p.dir + drift * 0.3) * 3 + drift * 2) % W; const y = (p.y + Math.sin(p.dir + drift * 0.2) * 3) % H; const a = 0.025 + 0.05 * (0.5 + 0.5 * Math.sin(time * 0.4 + p.phase)); ctx.fillStyle = `rgba(210,205,190,${a.toFixed(3)})`; ctx.beginPath(); ctx.arc(x, y, p.r, 0, Math.PI * 2); ctx.fill(); } // ── Tejido (Sol) ── ctx.save(); ctx.translate(CX, CY); ctx.scale(pulseScale, pulseScale); ctx.translate(-CX, -CY); const cellHalf = cellSize / 2; const inset = 0.8; for (const c of cells) { const revealHere = smoothstep(c.order - 0.015, c.order + 0.03, reveal); if (revealHere <= 0.001) continue; // Alpha base — sólido y estable let a = c.weight * 0.86 * revealHere; if (woven) { // ── Capas de vida, todas aditivas y acotadas ── let boost = 0; // 1. Respiración suave (amplitud ±0.04) boost += 0.04 * breathSigned; // 2. Shimmer por celda (amplitud ±0.03) boost += 0.03 * Math.sin(holdTime * 0.9 + c.seed * 0.0001); // 3. Heartbeat radial — intensificado para que se note if (heartActive) { const distFromWave = Math.abs(c.rPx - heartRadius); if (distFromWave < heartWidth) { const waveT = 1 - distFromWave / heartWidth; boost += 0.45 * waveT * waveT; } } // 4. Escaneo angular 8-fold — intensificado const eightFoldDiff = Math.abs( ((c.theta - scanAngle + Math.PI * 20) % (Math.PI / 4)) - Math.PI / 8 ); const scanBoost = Math.max(0, 1 - eightFoldDiff * 5); boost += 0.26 * scanBoost * Math.min(1, c.r / 6); // 5. Firings aleatorios — más visibles const fireHash = Math.sin(c.seed * 0.0001 + fireSlot * 13.37); const fireRand = ((fireHash * 43758.5453) % 1 + 1) % 1; if (fireRand < 0.022) { const fireShape = Math.max(0, 1 - fireLocal * 1.2); boost += 0.62 * fireShape * fireShape; } a = Math.min(0.98, a + boost); } if (a <= 0.01) continue; ctx.fillStyle = `rgba(236,230,214,${a.toFixed(3)})`; ctx.fillRect(c.x - cellHalf + inset, c.y - cellHalf + inset, cellSize - inset * 2, cellSize - inset * 2); } // ── PULSOS desde las 8 puntas exteriores hacia el centro ── // Cada pulso nace en uno de los 8 rombos exteriores (r ≈ 22 celdas) y // viaja acelerando hacia el centro. Cuando llega, el corazón del Sol // flash-ea. Representa las voces colombianas alimentando a la Madre. let arrivalFlash = 0; if (woven) { const outerRadius = 22 * cellSize; // ≈ 242 px const pulseInterval = 0.55; const pulseDuration = 1.8; // Miramos todos los pulsos que podrían estar en vuelo ahora mismo. // Como pulseDuration > pulseInterval, habrá ~3 pulsos simultáneos. const currentSlot = Math.floor(holdTime / pulseInterval); for (let k = -4; k <= 0; k++) { const slot = currentSlot + k; if (slot < 0) continue; const startTime = slot * pulseInterval; const elapsed = holdTime - startTime; if (elapsed < 0 || elapsed > pulseDuration) continue; const progress = elapsed / pulseDuration; // Trayectoria: accelerating ease const eased = progress * progress; const r = outerRadius * (1 - eased); // Ángulo — ciclamos por los 8 ejes const angleIdx = ((slot * 3) + 1) % 8; // stride 3 para alternar visualmente const angle = (angleIdx / 8) * Math.PI * 2; const x = CX + Math.cos(angle) * r; const y = CY + Math.sin(angle) * r; // Halo del pulso const halo = ctx.createRadialGradient(x, y, 0, x, y, 16); halo.addColorStop(0, 'rgba(250,240,215,0.55)'); halo.addColorStop(0.5, 'rgba(230,220,195,0.18)'); halo.addColorStop(1, 'rgba(230,220,195,0)'); ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(x, y, 16, 0, Math.PI * 2); ctx.fill(); // Núcleo del pulso const coreA = 0.95 * (1 - Math.max(0, progress - 0.95) * 20); ctx.fillStyle = `rgba(252,242,218,${coreA.toFixed(3)})`; ctx.beginPath(); ctx.arc(x, y, 2.6, 0, Math.PI * 2); ctx.fill(); // Estela corta for (let t = 1; t <= 5; t++) { const prevT = progress - t * 0.025; if (prevT < 0) break; const prevEased = prevT * prevT; const prevR = outerRadius * (1 - prevEased); const px = CX + Math.cos(angle) * prevR; const py = CY + Math.sin(angle) * prevR; const tailA = 0.48 - t * 0.08; if (tailA <= 0) break; ctx.fillStyle = `rgba(232,220,195,${tailA.toFixed(3)})`; ctx.beginPath(); ctx.arc(px, py, 1.5 - t * 0.18, 0, Math.PI * 2); ctx.fill(); } // Si está llegando, acumular flash if (progress > 0.9) { arrivalFlash = Math.max(arrivalFlash, (progress - 0.9) / 0.1); } } } // Punto luminoso en el corazón del Sol — boost cuando llega un pulso if (reveal > 0.05) { const beatBoost = heartActive ? Math.max(0, 1 - heartProgress * 3) : 0; const centerA = (0.42 + 0.18 * breath + 0.22 * beatBoost + 0.45 * arrivalFlash) * reveal; const g = ctx.createRadialGradient(CX, CY, 0, CX, CY, 38); g.addColorStop(0, `rgba(255,248,225,${(centerA * 0.98).toFixed(3)})`); g.addColorStop(0.45, `rgba(238,228,205,${(centerA * 0.32).toFixed(3)})`); g.addColorStop(1, 'rgba(255,248,225,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(CX, CY, 38, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); // Viñeta muy suave — apenas insinúa foco sin tapar la corriente de código const vg = ctx.createRadialGradient(CX, CY, 520, CX, CY, 960); vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, 'rgba(0,0,0,0.32)'); ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H); ctx.restore(); } // ── Utilidades ────────────────────────────────────────────────────────────── function smoothstep(a, b, x) { const t = Math.max(0, Math.min(1, (x - a) / (b - a))); return t * t * (3 - 2 * t); } function mulberry32(seed) { let a = seed >>> 0; return function () { a = (a + 0x6D2B79F5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } Object.assign(window, { WovenScene });