/* sener labs — orbital system engine (vanilla) */ (function () { "use strict"; // ---- palettes ---------------------------------------------------------- const PALETTES = { cosmic: { bgCore: "#160d2e", bgEdge: "#070512", star: "#cbb8ff", neb: ["#7c3aed", "#2563eb", "#db2777"], orbs: { "value-invest": ["#5eead4", "#a3e635"], "dinner": ["#fb7185", "#fb923c"], "lab-1": ["#a78bfa", "#7c3aed"], "lab-2": ["#38bdf8", "#6366f1"], }, }, sunset: { bgCore: "#2a0f24", bgEdge: "#0a0410", star: "#ffd9a8", neb: ["#f97316", "#db2777", "#fbbf24"], orbs: { "value-invest": ["#fbbf24", "#f97316"], "dinner": ["#fb7185", "#e11d48"], "lab-1": ["#f472b6", "#a21caf"], "lab-2": ["#fdba74", "#f59e0b"], }, }, neon: { bgCore: "#05121a", bgEdge: "#02060a", star: "#aef7ff", neb: ["#06b6d4", "#22d3ee", "#a3e635"], orbs: { "value-invest": ["#22d3ee", "#34d399"], "dinner": ["#f472b6", "#22d3ee"], "lab-1": ["#a3e635", "#22d3ee"], "lab-2": ["#818cf8", "#22d3ee"], }, }, aurora: { bgCore: "#07211c", bgEdge: "#04100c", star: "#bdfce8", neb: ["#10b981", "#22d3ee", "#a78bfa"], orbs: { "value-invest": ["#34d399", "#22d3ee"], "dinner": ["#f0abfc", "#a78bfa"], "lab-1": ["#5eead4", "#34d399"], "lab-2": ["#93c5fd", "#a78bfa"], }, }, }; // ---- projects ---------------------------------------------------------- const PROJECTS = [ { id: "value-invest", name: "Value-Invest", status: "live", tag: "Evaluate stocks with AI-generated sentiment & outlook.", url: "https://invest.sener-labs.com", rxF: 0.30, ryF: 0.135, size: 96, speed: 0.085, phase: 0.4, }, { id: "dinner", name: "dinner app", status: "soon", tag: "Swipe meals like Tinder — when everyone matches, dinner's decided.", url: null, rxF: 0.435, ryF: 0.188, size: 72, speed: -0.062, phase: 2.2, }, { id: "lab-1", name: "in the lab", status: "placeholder", tag: "A new idea taking shape.", url: null, rxF: 0.225, ryF: 0.10, size: 46, speed: 0.124, phase: 4.1, }, { id: "lab-2", name: "in the lab", status: "placeholder", tag: "Something is coming.", url: null, rxF: 0.49, ryF: 0.207, size: 38, speed: 0.047, phase: 5.4, }, ]; // ---- state ------------------------------------------------------------- const state = { palette: "cosmic", speed: 1, rings: true, starDensity: 1, hovered: null, }; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; // ---- dom --------------------------------------------------------------- const sky = document.getElementById("sky"); const ctx = sky.getContext("2d"); const planetsLayer = document.getElementById("planets"); const core = document.getElementById("core"); const nebEls = [...document.querySelectorAll(".neb")]; const card = document.getElementById("card"); const veil = document.getElementById("veil"); let W = 0, H = 0, DPR = 1; let stars = []; const runtime = {}; // id -> { angle, el, orb, label } // ---- helpers ----------------------------------------------------------- function shade(hex, amt) { const n = parseInt(hex.slice(1), 16); let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255; r = Math.round(r * (1 + amt)); g = Math.round(g * (1 + amt)); b = Math.round(b * (1 + amt)); r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b)); return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } function hexA(hex, a) { const n = parseInt(hex.slice(1), 16); return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`; } // ---- build planets ----------------------------------------------------- function buildPlanets() { PROJECTS.forEach((p) => { const el = document.createElement("div"); el.className = "planet " + (p.status === "placeholder" ? "placeholder" : ""); el.dataset.id = p.id; const wrap = document.createElement("div"); wrap.className = "orb-wrap"; const isLink = p.status === "live" && p.url; const orb = document.createElement(isLink ? "a" : "div"); orb.className = "orb"; orb.style.width = p.size + "px"; orb.style.height = p.size + "px"; if (isLink) { orb.href = p.url; orb.setAttribute("aria-label", p.name); } else { orb.setAttribute("role", "img"); orb.setAttribute("aria-label", p.name); } wrap.appendChild(orb); el.appendChild(wrap); if (p.status !== "placeholder") { const label = document.createElement("span"); label.className = "plabel"; label.textContent = p.name; el.appendChild(label); runtime[p.id] = { angle: p.phase, el, orb, label }; } else { runtime[p.id] = { angle: p.phase, el, orb, label: null }; } // interactions el.addEventListener("pointerenter", () => setHover(p.id)); el.addEventListener("pointerleave", () => setHover(null)); if (!isLink) { orb.addEventListener("click", () => { el.classList.remove("wobble"); void el.offsetWidth; el.classList.add("wobble"); }); } planetsLayer.appendChild(el); }); } function setHover(id) { if (state.hovered === id) return; state.hovered = id; PROJECTS.forEach((p) => { runtime[p.id].el.classList.toggle("hover", p.id === id); }); if (id) showCard(id); else hideCard(); } function showCard(id) { const p = PROJECTS.find((x) => x.id === id); const orbCols = PALETTES[state.palette].orbs[id]; card.style.setProperty("--glow", orbCols[0]); card.className = "card show " + (p.status === "live" ? "live" : p.status); const statusText = p.status === "live" ? "live now" : p.status === "soon" ? "in the lab" : "coming soon"; let html = `${statusText}` + `
${p.name}
` + `
${p.tag}
`; if (p.status === "live") { const host = p.url.replace(/^https?:\/\//, ""); html += `${host} `; } card.innerHTML = html; } function hideCard() { card.className = "card"; } // ---- palette application ---------------------------------------------- function applyPalette(name) { state.palette = name; const pal = PALETTES[name]; const root = document.documentElement.style; root.setProperty("--bg-core", pal.bgCore); root.setProperty("--bg-edge", pal.bgEdge); root.setProperty("--star", pal.star); nebEls.forEach((el, i) => { el.style.background = `radial-gradient(circle, ${pal.neb[i]} 0%, transparent 66%)`; }); PROJECTS.forEach((p) => { if (p.status === "placeholder") return; const c = pal.orbs[p.id]; const orb = runtime[p.id].orb; orb.style.background = `radial-gradient(circle at 32% 26%, ${shade(c[0], 0.25)} 0%, ${c[0]} 32%, ${c[1]} 76%, ${shade(c[1], -0.4)} 100%)`; runtime[p.id].el.style.setProperty("--glow", hexA(c[0], 0.85)); runtime[p.id].orb.style.setProperty("--glow", hexA(c[0], 0.6)); }); } // ---- starfield --------------------------------------------------------- function buildStars() { const base = Math.round((W * H) / 5400); const count = Math.round(base * state.starDensity); stars = []; for (let i = 0; i < count; i++) { stars.push({ x: Math.random() * W, y: Math.random() * H, z: Math.random(), // depth 0..1 r: Math.random() * 1.3 + 0.25, tw: Math.random() * Math.PI * 2, // twinkle phase }); } } // ---- resize ------------------------------------------------------------ function resize() { W = window.innerWidth; H = window.innerHeight; DPR = Math.min(window.devicePixelRatio || 1, 2); sky.width = W * DPR; sky.height = H * DPR; sky.style.width = W + "px"; sky.style.height = H + "px"; ctx.setTransform(DPR, 0, 0, DPR, 0, 0); buildStars(); } // ---- pointer parallax -------------------------------------------------- let mx = 0, my = 0, emx = 0, emy = 0; window.addEventListener("pointermove", (e) => { mx = (e.clientX - W / 2) / (W / 2); my = (e.clientY - H / 2) / (H / 2); }); window.addEventListener("pointerout", (e) => { if (!e.relatedTarget) { mx = 0; my = 0; } }); // ---- main loop --------------------------------------------------------- let last = performance.now(); let intro = 0; const palStar = () => PALETTES[state.palette].star; function frame(now) { let dt = (now - last) / 1000; last = now; if (dt > 0.05) dt = 0.05; intro = Math.min(1, intro + dt / 1.3); const introE = 1 - Math.pow(1 - intro, 3); // easeOutCubic // ease pointer emx += (mx - emx) * 0.06; emy += (my - emy) * 0.06; const cx = W / 2 + emx * 12; const cy = H / 2 + emy * 9; const base = Math.min(W, 1440); const tsec = now / 1000; // ---- draw sky (stars + rings) ---- ctx.clearRect(0, 0, W, H); // stars const sc = palStar(); const scn = parseInt(sc.slice(1), 16); const sr = (scn >> 16) & 255, sg = (scn >> 8) & 255, sb = scn & 255; for (let i = 0; i < stars.length; i++) { const s = stars[i]; const px = s.x + emx * (8 + s.z * 34); const py = s.y + emy * (6 + s.z * 26); const a = (0.25 + s.z * 0.55) * (0.6 + 0.4 * Math.sin(tsec * 1.2 + s.tw)) * introE; ctx.beginPath(); ctx.arc(px, py, s.r * (0.6 + s.z), 0, Math.PI * 2); ctx.fillStyle = `rgba(${sr},${sg},${sb},${a})`; ctx.fill(); } // orbit rings if (state.rings) { ctx.lineWidth = 1; PROJECTS.forEach((p) => { const rx = p.rxF * base, ry = p.ryF * base; ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.strokeStyle = `rgba(${sr},${sg},${sb},${0.10 * introE})`; ctx.stroke(); }); } // core parallax core.style.transform = `translate(-50%, -50%) translate(${emx * -14}px, ${emy * -10}px)`; // nebula parallax + slow drift nebEls.forEach((el, i) => { const dir = i % 2 === 0 ? 1 : -1; const dx = emx * (24 + i * 12) * dir + Math.sin(tsec * 0.08 + i) * 14; const dy = emy * (18 + i * 10) * dir + Math.cos(tsec * 0.07 + i) * 12; el.style.transform = `translate(${dx}px, ${dy}px)`; }); // ---- planets ---- PROJECTS.forEach((p) => { const r = runtime[p.id]; const hovered = state.hovered === p.id; const otherHover = state.hovered && !hovered; const spd = p.speed * state.speed * (reduceMotion ? 0.25 : 1) * (hovered ? 0.12 : 1); r.angle += spd * dt; const c = Math.cos(r.angle), s = Math.sin(r.angle); const depth = (s + 1) / 2; // 0 back .. 1 front const rx = p.rxF * base, ry = p.ryF * base; const bob = Math.sin(tsec * 0.6 + p.phase) * 4; let x = cx + c * rx * introE; let y = cy + s * ry * introE + bob; // per-depth parallax (closer = more) x += emx * (6 + depth * 22); y += emy * (5 + depth * 16); const k = (0.6 + 0.4 * depth) * (hovered ? 1.14 : 1) * (otherHover ? 0.95 : 1) * (0.4 + 0.6 * introE); const z = Math.round(10 + depth * 80); const op = (0.5 + 0.5 * depth) * introE * (otherHover ? 0.7 : 1); r.el.style.transform = `translate3d(${x}px, ${y}px, 0)`; r.el.style.zIndex = z; r.el.style.opacity = op; r.orb.parentElement.style.transform = `translate(-50%, -50%) scale(${k})`; if (r.label) { r.label.style.transform = `translate(-50%, ${(p.size / 2) * k + 13}px)`; } // position hover card above the orb if (hovered) { card.style.left = x + "px"; card.style.top = (y - (p.size / 2) * k - 16) + "px"; } }); requestAnimationFrame(frame); } // ---- public api for tweaks -------------------------------------------- window.Orbit = { setPalette: applyPalette, setSpeed: (v) => { state.speed = v; }, setRings: (v) => { state.rings = !!v; }, setStarDensity: (v) => { state.starDensity = v; buildStars(); }, palettes: Object.keys(PALETTES), }; // ---- boot -------------------------------------------------------------- function boot() { resize(); buildPlanets(); applyPalette(state.palette); window.addEventListener("resize", resize); requestAnimationFrame(frame); // reveal: paint the opaque veil first, then trigger the fade, with a hard fallback void veil.offsetWidth; setTimeout(() => { veil.classList.add("gone"); setTimeout(() => { veil.style.display = "none"; }, 850); }, 60); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();