// redoal.com's hero: "sine swings" - the drifting layered sine paths // from lysbue's logo (the site this content descends from), grown from // a clipped 400px circle into a full-width band. No input: the form's // gesture canvas below is the one place a visitor draws. Content-owned: // portal knows only site.yaml's `hero: {kind: module, module: hero.js}` // and the contract - `mount(container) -> handle` with `stop()`; it // serves this file same-origin at /site/hero.js, starts it at HTML // parse time, adopts it on hydration, and stops it on navigation. // // Pure SVG + CSS: the strokes take their color from portal's own // custom properties (--accent, --ink-dim), so both themes come for // free and there's no palette copy to keep in sync. Motion is a CSS // keyframe on each layer's transform, disabled under // prefers-reduced-motion. const STYLE_ID = 'redoal-hero-style'; // One period is 400 units wide; the path is written two periods long // so translating by exactly one period loops seamlessly. const SWING = 'M 0 200 Q 100 100 200 200 Q 300 300 400 200 ' + 'Q 500 100 600 200 Q 700 300 800 200 ' + 'Q 900 100 1000 200 Q 1100 300 1200 200'; // (y offset, stroke width, opacity, seconds per period, direction) const LAYERS = [ [0, 6.5, 0.55, 9, 1], [7, 3, 0.45, 11, 1], [9, 1, 0.5, 13, 1], [15, 0.5, 0.6, 17, 1], [-60, 1.5, 0.18, 23, -1], [70, 1, 0.14, 29, -1], ]; const CSS = ` .redoal-hero { position: absolute; inset: 0; overflow: hidden; pointer-events: none; } .redoal-hero svg { position: absolute; inset: 0; width: 100%; height: 100%; } .redoal-hero .swing { fill: none; stroke: var(--accent); stroke-linecap: round; animation: redoal-swing var(--dur) linear infinite; animation-direction: var(--dir); } .redoal-hero .swing.far { stroke: var(--ink-dim); } @keyframes redoal-swing { from { transform: translateX(0); } to { transform: translateX(-400px); } } @media (prefers-reduced-motion: reduce) { .redoal-hero .swing { animation: none; } } `; function ensureStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement('style'); style.id = STYLE_ID; style.textContent = CSS; document.head.appendChild(style); } const NS = 'http://www.w3.org/2000/svg'; class SineSwings { constructor(container) { ensureStyle(); this.root = document.createElement('div'); this.root.className = 'redoal-hero'; this.root.setAttribute('aria-hidden', 'true'); // viewBox 400 wide per period; preserveAspectRatio "slice" so // the band always fills the box and the loop is seamless at // any width - the SVG is 1200 wide but only translates by 400. const svg = document.createElementNS(NS, 'svg'); svg.setAttribute('viewBox', '0 0 800 400'); svg.setAttribute('preserveAspectRatio', 'xMidYMid slice'); // The logo's clipped circle, centered - the near layers live // inside it; the far layers drift across the whole band. const defs = document.createElementNS(NS, 'defs'); const clip = document.createElementNS(NS, 'clipPath'); clip.id = 'redoal-hero-clip'; const circle = document.createElementNS(NS, 'circle'); circle.setAttribute('cx', '400'); circle.setAttribute('cy', '200'); circle.setAttribute('r', '150'); clip.appendChild(circle); defs.appendChild(clip); svg.appendChild(defs); const far = document.createElementNS(NS, 'g'); const near = document.createElementNS(NS, 'g'); near.setAttribute('clip-path', 'url(#redoal-hero-clip)'); // The logo's tilt. near.setAttribute('transform', 'rotate(-13 400 200)'); for (const [dy, width, opacity, dur, dir] of LAYERS) { const path = document.createElementNS(NS, 'path'); path.setAttribute('d', SWING); path.setAttribute('stroke-width', String(width)); path.setAttribute('opacity', String(opacity)); path.setAttribute('transform', `translate(-200 ${dy})`); const isFar = Math.abs(dy) > 30; path.setAttribute('class', isFar ? 'swing far' : 'swing'); path.style.setProperty('--dur', `${dur}s`); path.style.setProperty('--dir', dir < 0 ? 'reverse' : 'normal'); (isFar ? far : near).appendChild(path); } svg.append(far, near); this.root.appendChild(svg); container.appendChild(this.root); } stop() { this.root.remove(); } } export function mount(container) { return new SineSwings(container); }