1<!doctype html> 2<html lang="en"> 3<head> 4 <meta charset="utf-8"> 5 <title>SAKIZLI AI â Applied-AI Learning & Research</title> 6 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"> 7 <!-- Preload the 4 fonts Lighthouse flagged on the critical rendering path 8 (only discovered otherwise once the CSS parses, adding a round trip). --> 9 <link rel="preload" href="/fonts/plexmono-regular.woff2" as="font" type="font/woff2" crossorigin> 10 <link rel="preload" href="/fonts/plexmono-500.woff2" as="font" type="font/woff2" crossorigin> 11 <link rel="preload" href="/fonts/fraunces-300.woff2" as="font" type="font/woff2" crossorigin> 12 <link rel="preload" href="/fonts/fraunces-300italic.woff2" as="font" type="font/woff2" crossorigin> 13 <!-- set theme before first paint (light is default) â prevents a dark flash --> 14
14<script>(function(){try{document.documentElement.classList.toggle('light',localStorage.getItem('skzl_theme')!=='dark');}catch(e){document.documentElement.classList.add('light');}})();</script>
14 15 <meta name="description" content="Learn applied AI, hands-on â a trilingual journal, courses and research notes on prompt & context engineering, plus an interactive neural-network studio."> 16 17 <!-- Open Graph / social sharing (crawlers read these, not the visible page) --> 18 <link rel="canonical" href="https://sakizli.ai/"> 19 <meta property="og:type" content="website"> 20 <meta property="og:site_name" content="SAKIZLI AI"> 21 <meta property="og:title" content="SAKIZLI AI â Applied-AI Learning & Research"> 22 <meta property="og:description" content="Learn applied AI, hands-on â a trilingual journal, courses and research notes on prompt & context engineering, plus an interactive neural-network studio."> 23 <meta property="og:url" content="https://sakizli.ai/"> 24 <meta property="og:image" content="https://sakizli.ai/og-image.png"> 25 <meta property="og:image:width" content="1200"> 26 <meta property="og:image:height" content="630"> 27 <meta property="og:image:alt" content="SAKIZLI AI"> 28 <meta property="og:locale" content="en_US"> 29 <meta name="twitter:card" content="summary_large_image"> 30 <meta name="twitter:title" content="SAKIZLI AI â Applied-AI Learning & Research"> 31 <meta name="twitter:description" content="Learn applied AI â a trilingual journal, courses & research on prompt and context engineering."> 32 <meta name="twitter:image" content="https://sakizli.ai/og-image.png"> 33 <style> 34 /* self-hosted fonts (no Google CDN â DSGVO) */ 35 @font-face { font-family:"Fraunces"; font-style:normal; font-weight:300; font-display:swap; src:url("/fonts/fraunces-300.woff2") format("woff2"); } 36 @font-face { font-family:"Fraunces"; font-style:italic; font-weight:300; font-display:swap; src:url("/fonts/fraunces-300italic.woff2") format("woff2"); } 37 @font-face { font-family:"Fraunces"; font-style:normal; font-weight:400; font-display:swap; src:url("/fonts/fraunces-regular.woff2") format("woff2"); } 38 @font-face { font-family:"Fraunces"; font-style:italic; font-weight:400; font-display:swap; src:url("/fonts/fraunces-italic.woff2") format("woff2"); } 39 @font-face { font-family:"IBM Plex Mono"; font-style:normal; font-weight:400; font-display:swap; src:url("/fonts/plexmono-regular.woff2") format("woff2"); } 40 @font-face { font-family:"IBM Plex Mono"; font-style:normal; font-weight:500; font-display:swap; src:url("/fonts/plexmono-500.woff2") format("woff2"); } 41 42 :root { 43 --ink: #e8ecf4; 44 --dim: #8b94a8; 45 --blue: #4f7cff; 46 --bg: #06080f; 47 } 48 html, body { margin:0; padding:0; height:100%; background:var(--bg); color:var(--ink); overflow:hidden; } 49 body { font-family:"IBM Plex Mono", monospace; } 50 a { color:var(--blue); text-decoration:none; } 51 a:hover { color:#8fadff; } 52 53 /* ââ mystic background: two slow-drifting blue glows ââ */ 54 .glow { position:fixed; inset:0; z-index:0; pointer-events:none; } 55 .glow::before, .glow::after { 56 content:""; position:absolute; width:110vmax; height:110vmax; border-radius:50%; 57 filter:blur(60px); opacity:0.5; 58 } 59 .glow::before { 60 background:radial-gradient(circle at center, rgba(38,64,140,0.22) 0%, rgba(20,32,72,0.10) 32%, transparent 62%); 61 left:-30vmax; top:-40vmax; 62 } 63 .glow::after { 64 background:radial-gradient(circle at center, rgba(52,86,190,0.14) 0%, rgba(16,26,60,0.07) 36%, transparent 64%); 65 right:-38vmax; bottom:-44vmax; 66 } 67 @media (prefers-reduced-motion: no-preference) { 68 .glow::before { animation:driftA 26s cubic-bezier(0.16,1,0.3,1) infinite alternate, breathe 9s ease-in-out infinite; } 69 .glow::after { animation:driftB 34s cubic-bezier(0.16,1,0.3,1) infinite alternate, breathe 12s ease-in-out infinite -4s; } 70 } 71 @keyframes driftA { from { transform:translate(0,0); } to { transform:translate(9vmax,6vmax); } } 72 @keyframes driftB { from { transform:translate(0,0); } to { transform:translate(-8vmax,-5vmax); } } 73 @keyframes breathe { 0%,100% { opacity:0.38; } 50% { opacity:0.6; } } 74 75 #stage { position:fixed; inset:0; z-index:1; cursor:grab; } 76 #stage:active { cursor:grabbing; }
77 #stage canvas { display:block; } 78 79 /* ââ chrome ââ */ 80 .top { 81 position:fixed; top:0; left:0; right:0; z-index:3; 82 display:flex; justify-content:space-between; align-items:center; 83 padding:26px 38px; pointer-events:none; 84 } 85 .wordmark { pointer-events:auto; font-family:"Fraunces", Times, serif; font-weight:300; font-size:21px; letter-spacing:0.14em; color:var(--ink); } 86 .wordmark .ai { color:var(--blue); font-style:italic; } 87 .top .status { font-size:10px; letter-spacing:0.26em; text-transform:uppercase; color:var(--dim); display:flex; align-items:center; gap:10px; } 88 .top .status .dot { width:5px; height:5px; border-radius:50%; background:var(--blue); box-shadow:0 0 8px var(--blue); } 89 @media (prefers-reduced-motion: no-preference) { .top .status .dot { animation:breathe 4s ease-in-out infinite; } } 90 91 /* ââ conversation surface: diffuse, network stays visible behind ââ */ 92 .io { 93 position:fixed; left:50%; bottom:44px; transform:translateX(-50%); 94 z-index:3; width:min(680px, calc(100vw - 48px)); 95 display:flex; flex-direction:column; gap:16px; 96 } 97 .answer { 98 max-height:118px; overflow-y:auto; 99 font-size:13.5px; line-height:1.75; color:var(--ink); 100 background:rgba(6,10,20,0.22);
100 backdrop-filter:blur(7px); -webkit-backdrop-filter:blur(7px); 101 border:1px solid rgba(79,124,255,0.12); border-radius:2px; 102 padding:20px 24px; white-space:pre-wrap; word-break:break-word; 103 text-shadow:0 1px 8px rgba(4,6,12,0.9); 104 opacity:0; transform:translateY(8px); transition:opacity 700ms cubic-bezier(0.16,1,0.3,1), transform 700ms cubic-bezier(0.16,1,0.3,1); 105 display:none; scrollbar-width:thin; scrollbar-color:rgba(79,124,255,0.3) transparent; 106 } 107 .answer.on { display:block; opacity:1; transform:none; } 108 .answer .q { color:var(--dim); font-size:11px; letter-spacing:0.18em; text-transform:uppercase; display:block; margin-bottom:10px; } 109 110 form.bar { 111 display:flex; gap:0; align-items:stretch; 112 background:rgba(6,10,20,0.26); backdrop-filter:blur(7px); -webkit-backdrop-filter:blur(7px); 113 border:1px solid rgba(79,124,255,0.18); border-radius:2px; 114 transition:border-color 300ms ease, box-shadow 300ms ease, background 300ms ease; 115 } 116 form.bar:focus-within { border-color:rgba(79,124,255,0.5); box-shadow:0 0 32px rgba(79,124,255,0.1); background:rgba(6,10,20,0.34); } 117 form.bar input { 118 flex:1; background:none; border:none; outline:none; color:var(--ink); 119 font-family:"IBM Plex Mono", monospace; font-size:14px; padding:18px 20px; 120 text-shadow:0 1px 6px rgba(4,6,12,0.9); 121 } 122 form.bar input::placeholder { color:#6a7390; } 123 form.bar button { 124 font-family:"IBM Plex Mono", monospace; font-size:11px; letter-spacing:0.2em; text-transform:uppercase; 125 background:none; border:none; border-left:1px solid rgba(79,124,255,0.18); 126 color:var(--blue); padding:0 26px; cursor:pointer; transition:background 200ms ease, color 200ms ease; 127 } 128 form.bar button:hover:enabled { background:rgba(79,124,255,0.12); color:#a9c0ff; } 129 form.bar button:disabled { color:#3a4258; cursor:default; } 130 .hint { text-align:center; font-size:10px; letter-spacing:0.22em; text-transform:uppercase; color:#4a5268; } 131 .hint b { color:var(--dim); font-weight:400; } 132 .hint.ai-note { letter-spacing:0.14em; line-height:1.7; margin-top:2px; } 133 .hint.ai-note a { color:var(--dim); text-decoration:underline; text-underline-offset:3px; } 134 .hint.ai-note a:hover { color:var(--blue); } 135 136 /* controls hint â fixed at top, directly under the menu */ 137 .controls-hint { 138 position:fixed; top:76px; left:0; right:0; z-index:3; text-align:center; 139 pointer-events:none; 140 } 141 @media (max-width: 760px) { .controls-hint { top:112px; } } 142 143 /* inline "hide" button, sits next to Send inside the chat bar */ 144 form.bar button.bar-hide { 145 border-left:1px solid rgba(79,124,255,0.18); 146 padding:0 18px; color:var(--dim); 147 } 148 form.bar button.bar-hide:hover:enabled { background:rgba(79,124,255,0.08); color:var(--blue); } 149 150 ::selection { background:rgba(79,124,255,0.35); } 151 152 /* ââ light mode ââ */ 153 html.light { --ink:#1a2334; --dim:#5a6478; --bg:#eef1f7; } 154 html.light .glow::before { background:radial-gradient(circle at center, rgba(79,124,255,0.14) 0%, rgba(79,124,255,0.06) 32%, transparent 62%); } 155 html.light .glow::after { background:radial-gradient(circle at center, rgba(52,86,190,0.10) 0%, rgba(52,86,190,0.04) 36%, transparent 64%); } 156 html.light .answer, html.light form.bar { background:rgba(255,255,255,0.5); border-color:rgba(36,83,230,0.2); } 157 html.light .answer, html.light form.bar input { text-shadow:none; } 158 html.light form.bar input::placeholder { color:#8a93a8; } 159 html.light form.bar button:disabled { color:#aab2c4; } 160 html.light .hint { color:#7a8398; } 161 162 .mode { 163 pointer-events:auto; font-family:"IBM Plex Mono", monospace; font-size:10px; letter-spacing:0.2em; 164 text-transform:uppercase; background:none; border:1px solid rgba(139,148,168,0.35); 165 color:var(--dim); padding:7px 14px; cursor:pointer; border-radius:2px; 166 transition:color 200ms ease, border-color 200ms ease; 167 } 168 .mode:hover { color:var(--blue); border-color:var(--blue); } 169 .top .right { display:flex; gap:18px; align-items:center; pointer-events:auto; } 170 171 /* ââ nav menu ââ */ 172 nav.menu { 173 pointer-events:auto; display:flex; gap:22px; align-items:center; 174 font-size:10px; letter-spacing:0.22em; text-transform:uppercase; 175 } 176 nav.menu a { color:var(--dim); transition:color 200ms ease; text-shadow:0 1px 6px rgba(4,6,12,0.9); } 177 nav.menu a:hover { color:var(--blue); } 178 html.light nav.menu a { text-shadow:none; } 179 180 /* ââ chat hide/minimize ââ */ 181 .io { transition:opacity 400ms cubic-bezier(0.16,1,0.3,1), transform 400ms cubic-bezier(0.16,1,0.3,1); } 182 body.chat-hidden .io { opacity:0; transform:translateX(-50%) translateY(24px); pointer-events:none; } 183 .hint button.hint-link { 184 background:none; border:none; cursor:pointer; padding:0; 185 font-family:"IBM Plex Mono", monospace; font-size:10px; letter-spacing:0.22em; 186 text-transform:uppercase; color:var(--dim); text-decoration:underline; 187 text-underline-offset:3px; transition:color 200ms ease; 188 } 189 .hint button.hint-link:hover { color:var(--blue); } 190 .chat-toggle { 191 display:none; 192 position:fixed; left:50%; transform:translateX(-50%); bottom:44px; z-index:4; pointer-events:auto; 193 font-family:"IBM Plex Mono", monospace; font-size:10px; letter-spacing:0.2em; 194 text-transform:uppercase; background:rgba(6,10,20,0.26);
195 backdrop-filter:blur(7px); -webkit-backdrop-filter:blur(7px); 196 border:1px solid rgba(79,124,255,0.18); color:var(--dim); 197 padding:9px 16px; cursor:pointer; border-radius:2px; 198 transition:color 200ms ease, border-color 200ms ease; 199 } 200 .chat-toggle:hover { color:var(--blue); border-color:rgba(79,124,255,0.5); } 201 html.light .chat-toggle { background:rgba(255,255,255,0.5); border-color:rgba(36,83,230,0.2); } 202 body.chat-hidden .chat-toggle { display:block; } 203 204 /* ââ legal corner links ââ */ 205 .legal { 206 position:fixed; bottom:16px; z-index:3; pointer-events:auto; 207 font-size:9px; letter-spacing:0.22em; text-transform:uppercase; color:#4a5268; 208 } 209 .legal a { color:inherit; transition:color 200ms ease; } 210 .legal a:hover { color:var(--dim); } 211 .legal.l { left:38px; } 212 .legal.r { right:38px; } 213 214 @media (max-width: 760px) { 215 /* 16px input font â prevents iOS Safari's automatic zoom on focus */ 216 form.bar input { font-size:16px; } 217 .top { flex-wrap:wrap; gap:12px; padding:18px 20px; } 218 nav.menu { order:3; width:100%; justify-content:center; flex-wrap:wrap; gap:12px 18px; } 219 .top .status { display:none; } 220 .legal.l { left:20px; } 221 .legal.r { right:20px; } 222 } 223 /* ââ language switch ââ */ 224 .lang { pointer-events:auto; display:inline-flex; border:1px solid rgba(139,148,168,0.35); border-radius:2px; overflow:hidden; } 225 .lang button { font-family:"IBM Plex Mono", monospace; font-size:10px; letter-spacing:0.16em; text-transform:uppercase; background:none; border:none; color:var(--dim); padding:6px 9px; cursor:pointer; transition:color 200ms ease, background 200ms ease; } 226 .lang button + button { border-left:1px solid rgba(139,148,168,0.35); } 227 .lang button.active { background:var(--blue); color:#fff; } 228 .lang button:not(.active):hover { color:var(--blue); } 229 </style> 230 <!-- Structured data: homepage is the canonical place for WebSite (sitelinks 231 search box) + Organization. --> 232
232<script type="application/ld+json"> 233 { 234 "@context": "https://schema.org", 235 "@graph": [ 236 { 237 "@type": "Organization", 238 "name": "SAKIZLI AI", 239 "url": "https://sakizli.ai", 240 "logo": "https://sakizli.ai/android-chrome-512x512.png", 241 "founder": { "@type": "Person", "name": "Furkan Sakızlı" }, 242 "sameAs": [ 243 "https://github.com/SKZL-AI", 244 "https://www.youtube.com/@SakizliAI", 245 "https://de.linkedin.com/in/sakizli", 246 "https://www.instagram.com/sakizli_ai/", 247 "https://x.com/SAKIZLI_AI" 248 ] 249 }, 250 { 251 "@type": "WebSite", 252 "name": "SAKIZLI AI", 253 "url": "https://sakizli.ai", 254 "description": "Applied-AI learning, courses & research by Furkan Sakızlı", 255 "inLanguage": ["en", "de", "tr"], 256 "potentialAction": { 257 "@type": "SearchAction", 258 "target": { "@type": "EntryPoint", "urlTemplate": "https://sakizli.ai/search?q={search_term_string}" }, 259 "query-input": "required name=search_term_string" 260 } 261 } 262 ] 263 } 264 </script>
264 265</head> 266<body> 267 <div class="glow"></div> 268 <div id="stage"></div> 269 270 <header class="top"> 271 <div class="wordmark">SAKIZLI <span class="ai">AI</span></div> 272 <nav class="menu"> 273 <a href="/journal" data-i18n="nav_journal">Dossier</a> 274 <a href="/research" data-i18n="nav_research">Research</a> 275 <a href="/courses" data-i18n="nav_courses">Courses</a> 276 <a href="/enter" data-i18n="nav_immersive">Immersive</a> 277 <a href="/dashboard" data-i18n="nav_dashboard" id="nav-dashboard-link" style="display:none">Dashboard</a> 278 <a href="/account" data-i18n="nav_login" id="nav-login-link">Sign in</a> 279 </nav> 280 <div class="right"> 281 <div class="status"><span class="dot"></span><span id="status-label" data-i18n="status">standby · network resting</span></div> 282 <div class="lang" id="lang-switch" role="group" aria-label="Language"> 283 <button type="button" data-lang="en">EN</button><button type="button" data-lang="de">DE</button><button type="button" data-lang="tr">TR</button> 284 </div> 285 <button class="mode" id="mode-toggle" type="button">Light</button> 286 </div> 287 </header> 288 289 <!-- controls hint â top, under the menu --> 290 <div class="controls-hint hint" data-i18n="hint">Drag to rotate · scroll to zoom</div> 291 292 <div class="io"> 293 <div class="answer" id="answer"><span class="q" id="answer-q"></span><span id="answer-text"></span></div> 294 <form class="bar" id="ask"> 295 <input id="prompt" type="text" autocomplete="off" spellcheck="false" placeholder="Ask the network anythingâ¦" data-i18n-ph="ask" aria-label="Prompt"> 296 <button type="submit" id="send" data-i18n="send">Send</button> 297 <button type="button" id="chat-hide" class="bar-hide" title="Hide chat" data-i18n="hide">Hide</button> 298 </form> 299 <div class="hint ai-note">You are chatting with an <b>AI language model</b> (LLM) â answers may be inaccurate. 300 Messages are sent to AI providers (Groq · USA, Mistral · EU, Google · USA) to generate a reply; please do not 301 enter personal data. <a href="/privacy">Privacy Policy</a></div> 302 </div> 303 304 <button class="chat-toggle" id="chat-toggle" type="button" aria-label="Show chat">Chat</button> 305 306 <div class="legal l"><a href="/imprint" data-i18n="imprint">Imprint</a></div> 307 <div class="legal r"><a href="/privacy" data-i18n="privacy">Privacy Policy</a></div> 308 309
309<script> 310 // Site-wide language switch (shares the "skzl-locale" key with the React app). 311 (function () { 312 var T = { 313 en: { nav_journal:"Dossier", nav_research:"Research", nav_courses:"Courses", nav_immersive:"Immersive", nav_dashboard:"Dashboard", nav_login:"Sign in", status:"standby · network resting", hint:"Drag to rotate · scroll to zoom", ask:"Ask the network anythingâ¦", send:"Send", hide:"Hide", imprint:"Imprint", privacy:"Privacy Policy" }, 314 de: { nav_journal:"Dossier", nav_research:"Forschung", nav_courses:"Kurse", nav_immersive:"Immersiv", nav_dashboard:"Dashboard", nav_login:"Anmelden", status:"Standby · Netzwerk ruht", hint:"Zum Drehen ziehen · zum Zoomen scrollen", ask:"Frag das Netzwerkâ¦", send:"Senden", hide:"Ausblenden", imprint:"Impressum", privacy:"Datenschutz" }, 315 tr: { nav_journal:"Dossier", nav_research:"AraÅtırma", nav_courses:"Kurslar", nav_immersive:"Immersive", nav_dashboard:"Panel", nav_login:"GiriÅ yap", status:"Beklemede · aÄ dinleniyor", hint:"Döndürmek için sürükle · yakınlaÅtırmak için kaydır", ask:"AÄa istediÄini sorâ¦", send:"Gönder", hide:"Gizle", imprint:"Künye", privacy:"Gizlilik" }, 316 }; 317 function read() { 318 try { 319 var m = document.cookie.match(/(?:^|; )skzl-locale=([^;]+)/); 320 var v = localStorage.getItem("skzl-locale") || (m && m[1]); 321 return (v === "de" || v === "tr" || v === "en") ? v : "en"; 322 } catch (e) { return "en"; } 323 } 324 function apply(loc) { 325 var dict = T[loc] || T.en; 326 document.querySelectorAll("[data-i18n]").forEach(function (el) { 327 var k = el.getAttribute("data-i18n"); if (dict[k]) el.textContent = dict[k]; 328 }); 329 document.querySelectorAll("[data-i18n-ph]").forEach(function (el) { 330 var k = el.getAttribute("data-i18n-ph"); if (dict[k]) el.setAttribute("placeholder", dict[k]); 331 }); 332 // Intentionally not setting <html lang> â keeps CSS uppercase casing 333 // Latin (SAKIZLI / LIGHT with a normal I, not the Turkish İ). 334 document.querySelectorAll("#lang-switch button").forEach(function (b) { 335 b.classList.toggle("active", b.getAttribute("data-lang") === loc); 336 }); 337 } 338 function set(loc) { 339 try { 340 localStorage.setItem("skzl-locale", loc); 341 document.cookie = "skzl-locale=" + loc + ";path=/;max-age=31536000;samesite=lax"; 342 } catch (e) {} 343 apply(loc); 344 } 345 document.querySelectorAll("#lang-switch button").forEach(function (b) { 346 b.addEventListener("click", function () { set(b.getAttribute("data-lang")); }); 347 }); 348 apply(read()); 349 })(); 350 </script>
350 351 352
352<script type="importmap"> 353 { 354 "imports": { 355 "three": "https://unpkg.com/[email protected]/build/three.module.min.js", 356 "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/" 357 } 358 } 359 </script>
359 360
360<script type="module"> 361 import * as THREE from 'three'; 362 import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; 363 364 const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; 365 366 /* ââ chat hide/minimize: enjoy the network without the chat surface ââ 367 Wired immediately (cheap, independent of the 3D hero below), so the 368 chat is fully usable the instant the page paints. */ 369 function setChatHidden(hidden) { 370 document.body.classList.toggle('chat-hidden', hidden); 371 try { localStorage.setItem('skzl_chat_hidden', hidden ? '1' : '0'); } catch (e) {} 372 } 373 document.getElementById('chat-hide').addEventListener('click', () => setChatHidden(true)); 374 document.getElementById('chat-toggle').addEventListener('click', () => setChatHidden(false)); 375 setChatHidden((() => { try { return localStorage.getItem('skzl_chat_hidden') === '1'; } catch (e) { return false; } })()); 376 377 /* ââ chat wiring: storm intensity + duration follow the answer. 378 window.NEURAL is only defined once the deferred 3D scene below has 379 loaded â optional-chained so chatting works immediately regardless. ââ */ 380 const form = document.getElementById('ask'); 381 const input = document.getElementById('prompt'); 382 const send = document.getElementById('send'); 383 const answer = document.getElementById('answer'); 384 const answerQ = document.getElementById('answer-q'); 385 const answerText = document.getElementById('answer-text'); 386 387 form.addEventListener('submit', async (e) => { 388 e.preventDefault(); 389 const q = input.value.trim(); 390 if (!q || send.disabled) return; 391 send.disabled = true; 392 send.textContent = '···'; 393 window.NEURAL?.think(); 394 answerQ.textContent = q; 395 answerText.textContent = ''; 396 answer.classList.add('on');
397 try { 398 const res = await fetch('/api/chat', { 399 method: 'POST', 400 headers: { 'Content-Type': 'application/json' }, 401 body: JSON.stringify({ messages: [{ role: 'user', content: q }] }) 402 }); 403 if (!res.ok) throw new Error('chat api ' + res.status); 404 const text = (await res.json()).message; 405 if (!text) throw new Error('empty answer'); 406 window.NEURAL?.talk(text.length); 407 if (reduced) { answerText.textContent = text; } 408 else { 409 let i = 0; 410 await new Promise(res => { 411 (function tick() { 412 answerText.textContent = text.slice(0, i += 3); 413 answer.scrollTop = answer.scrollHeight; 414 if (i < text.length) setTimeout(tick, 16); else res(); 415 })(); 416 }); 417 } 418 } catch (err) { 419 answerText.textContent = 'The network is unreachable right now â please try again.'; 420 } finally { 421 window.NEURAL?.idle(); 422 send.disabled = false; 423 send.textContent = 'Send'; 424 input.value = ''; 425 input.focus(); 426 } 427 }); 428 429 /* ââ heavy 3D hero: deferred until the browser is idle so building 430 ~4,500 instanced nodes + ~4,200 edges and starting the render loop 431 doesn't compete with the page's initial interactive paint (this was 432 the actual driver of a very high Total Blocking Time in Lighthouse 433 audits â the chat above is already fully usable before this runs). ââ */ 434 function initScene() { 435 const stage = document.getElementById('stage'); 436 const renderer = new THREE.WebGLRenderer({ antialias:true, alpha:true }); 437 renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); 438 renderer.setSize(innerWidth, innerHeight); 439 stage.appendChild(renderer.domElement); 440 441 const scene = new THREE.Scene(); 442 const camera = new THREE.PerspectiveCamera(38, innerWidth/innerHeight, 0.1, 300); 443 camera.position.set(5.6, 8.2, 43); 444 445 const controls = new OrbitControls(camera, renderer.domElement); 446 controls.enableDamping = true; 447 controls.dampingFactor = 0.06; 448 controls.enablePan = true; 449 controls.screenSpacePanning = true; 450 controls.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN }; 451 controls.minDistance = 4; 452 controls.maxDistance = 60; 453 controls.rotateSpeed = 0.55; 454 /* target sits below the net's center so the network renders ~12% higher 455 on screen â keeps the answer panel from covering the visualization */ 456 controls.target.set(0, 2.6, 0); 457 458 /* â responsive framing: on narrow screens the horizontal FOV shrinks, so the 459 camera must sit further back to keep the whole net (input â digits) visible. 460 Also lifts maxDistance so two-finger pinch-out can always reveal the full net. â */ 461 const NET_W = 34, NET_H = 25; /* network bounds incl. digit column, around offset target */ 462 function fitDistance() { 463 const vFov = camera.fov * Math.PI / 180; 464 const hFov = 2 * Math.atan(Math.tan(vFov / 2) * camera.aspect); 465 return Math.max((NET_H / 2) / Math.tan(vFov / 2), (NET_W / 2) / Math.tan(hFov / 2)) * 1.06; 466 } 467 function frameNetwork(force) { 468 const d = fitDistance(); 469 controls.maxDistance = Math.max(60, d * 1.35); 470 if (force && camera.position.distanceTo(controls.target) < d) { 471 const dir = camera.position.clone().sub(controls.target).normalize(); 472 camera.position.copy(controls.target.clone().addScaledVector(dir, d)); 473 } 474 } 475 frameNetwork(true); 476 477 /* lights â for the slightly reflective black nodes */ 478 scene.add(new THREE.AmbientLight(0xffffff, 0.55)); 479 scene.add(new THREE.HemisphereLight(0xcfd8ee, 0x2a2f3c, 0.85)); 480 const keyLight = new THREE.DirectionalLight(0xffffff, 1.15); 481 keyLight.position.set(6, 10, 8); 482 scene.add(keyLight); 483 const rimLight = new THREE.DirectionalLight(0x9db4e8, 0.45); 484 rimLight.position.set(-8, -4, -6); 485 scene.add(rimLight); 486 487 /* âââ MLP after the reference images â 3à density âââ 488 input pixel screen (16Ã16) â 3 block-shaped hidden layers (dark cubes) 489 â output digits 0â9. Deterministic (seed fixed). */ 490 let seed = 20260712; 491 const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647; 492 493 const group = new THREE.Group(); 494 group.position.y = 6.2; // sit high, clear of the chat area (default view) 495 scene.add(group); 496 497 const layers = []; 498 const nodePos = []; 499 const nodeLayer = []; 500 function addNode(v, l) { nodePos.push(v); nodeLayer.push(l); (layers[l] ||= []).push(nodePos.length - 1); return nodePos.length - 1; } 501 502 /* L0 · input screen: 16Ã16 grid of flat pixels */ 503 const GRID = 16, PIX = 0.62; 504 for (let r = 0; r < GRID; r++) for (let c = 0; c < GRID; c++) 505 addNode(new THREE.Vector3(-13.2, ((GRID-1)/2 - r) * PIX, (c - (GRID-1)/2) * PIX), 0); 506 507 /* L1âL3 · hidden blocks: 3à more, much smaller cubes */ 508 const blockDefs = [ 509 { n: 1370, x: -6.4, w: 2.6, h: 9.2, d: 7.6 }, 510 { n: 1490, x: 0.4, w: 2.8, h: 9.6, d: 8.0 }, 511 { n: 1370, x: 7.2, w: 2.6, h: 9.2, d: 7.6 } 512 ];
513 blockDefs.forEach((b, bi) => { 514 for (let i = 0; i < b.n; i++) 515 addNode(new THREE.Vector3( 516 b.x + (rnd() - 0.5) * b.w, 517 (rnd() - 0.5) * b.h, 518 (rnd() - 0.5) * b.d), bi + 1); 519 }); 520 521 /* L4 · output: 10 nodes in a receding column (digits drawn next to them) */ 522 const OUT_X = 13.6; 523 for (let i = 0; i < 10; i++) 524 addNode(new THREE.Vector3(OUT_X + i * 0.34, 3.4 - i * 0.76, -i * 0.30), 4); 525 526 const N = nodePos.length; 527 528 /* ââ nodes as instanced cubes: small dark blocks that flare white ââ */ 529 const cubeGeo = new THREE.BoxGeometry(1, 1, 1); 530 const cubeMat = new THREE.MeshStandardMaterial({ roughness:0.16, metalness:0.55, transparent:true, opacity:0.98 }); 531 const cubes = new THREE.InstancedMesh(cubeGeo, cubeMat, N); 532 cubes.instanceMatrix.setUsage(THREE.DynamicDrawUsage); 533 const iColor = new Float32Array(N * 3); 534 cubes.instanceColor = new THREE.InstancedBufferAttribute(iColor, 3); 535 const m4 = new THREE.Matrix4(), q0 = new THREE.Quaternion(), sV = new THREE.Vector3(); 536 const baseScale = new Float32Array(N); 537 for (let i = 0; i < N; i++) { 538 const l = nodeLayer[i]; 539 let s; 540 if (l === 0) { sV.set(0.08, 0.44, 0.44); s = 1; } // flat pixels 541 else if (l === 4) { sV.set(0.22, 0.22, 0.22); s = 1; } // output nodes 542 else { s = 0.038 + rnd() * 0.042; sV.set(s, s, s); } // tiny hidden cubes 543 baseScale[i] = sV.x; 544 m4.compose(nodePos[i], q0, sV.clone()); 545 cubes.setMatrixAt(i, m4); 546 } 547 group.add(cubes); 548 549 /* ââ per-node weights: random â0.99â¦+0.99, readable only up close ââ */ 550 const nodeVal = new Float32Array(N); 551 for (let i = 0; i < N; i++) nodeVal[i] = Math.round((rnd() * 1.98 - 0.99) * 100) / 100; 552 const valTexCache = new Map(); 553 function valTexture(v) { 554 const s = (v < 0 ? 'â' : '') + Math.abs(v).toFixed(2); 555 if (valTexCache.has(s)) return valTexCache.get(s); 556 const cv = document.createElement('canvas'); cv.width = 128; cv.height = 64; 557 const g = cv.getContext('2d'); 558 g.font = '500 30px "IBM Plex Mono", monospace'; 559 g.textAlign = 'center'; g.textBaseline = 'middle'; 560 g.fillStyle = '#fff'; 561 g.fillText(s, 64, 34); 562 const tx = new THREE.CanvasTexture(cv); 563 valTexCache.set(s, tx); 564 return tx; 565 } 566 const VPOOL = 56; 567 const valSprites = []; 568 for (let k = 0; k < VPOOL; k++) { 569 const m = new THREE.SpriteMaterial({ transparent:true, opacity:0, depthWrite:false, depthTest:false }); 570 const sp = new THREE.Sprite(m); 571 sp.scale.set(0.30, 0.15, 1); 572 sp.visible = false; 573 group.add(sp); 574 valSprites.push(sp); 575 } 576 let valTimer = 0; 577 const wv = new THREE.Vector3(); 578 function updateValues(dt) { 579 valTimer += dt; 580 if (valTimer < 0.2) return; 581 valTimer = 0; 582 const cand = []; 583 for (let l = 1; l <= 3; l++) for (const i of layers[l]) { 584 wv.copy(nodePos[i]).applyMatrix4(group.matrixWorld); 585 const d = wv.distanceTo(camera.position); 586 if (d < 7.5) cand.push([d, i]); 587 } 588 cand.sort((a, b) => a[0] - b[0]); 589 /* greedy screen-space thinning so numbers don't overlap */ 590 const placed = []; 591 const pv = new THREE.Vector3(); 592 let used = 0; 593 for (let c = 0; c < cand.length && used < VPOOL; c++) { 594 const d = cand[c][0], i = cand[c][1]; 595 pv.copy(nodePos[i]).applyMatrix4(group.matrixWorld).project(camera); 596 if (pv.z > 1) continue; 597 let clash = false; 598 for (const q of placed) { 599 if (Math.abs(pv.x - q.x) < 0.075 && Math.abs(pv.y - q.y) < 0.05) { clash = true; break; } 600 } 601 if (clash) continue; 602 placed.push({ x: pv.x, y: pv.y }); 603 const sp = valSprites[used++]; 604 sp.visible = true; 605 sp.position.copy(nodePos[i]); 606 sp.material.map = valTexture(nodeVal[i]); 607 sp.material.opacity = Math.min(1, (7.5 - d) / 2.2) * 0.95; 608 sp.material.color.set(LIGHT ? 0x202634 : 0xd7deec); 609 } 610 for (let k = used; k < VPOOL; k++) valSprites[k].visible = false;
611 } 612 613 /* ââ edges: dense spider-web fan-in, ALWAYS visible light gray; 614 vertex colors so single edges can glow when a pulse rides them ââ */ 615 const edges = []; // {a,b} 616 const linePos = []; 617 const adjF = Array.from({ length:N }, () => []); // forward: {n, e} 618 const adjA = Array.from({ length:N }, () => []); // any dir: {n, e} 619 const linkCounts = [1040, 1360, 1360, 480]; 620 for (let l = 0; l < 4; l++) { 621 const A = layers[l], B = layers[l+1]; 622 for (let k = 0; k < linkCounts[l]; k++) { 623 const a = A[Math.floor(rnd()*A.length)], b = B[Math.floor(rnd()*B.length)]; 624 const e = edges.length; 625 edges.push({ a, b }); 626 adjF[a].push({ n:b, e }); 627 adjA[a].push({ n:b, e }); adjA[b].push({ n:a, e }); 628 const va = nodePos[a], vb = nodePos[b]; 629 linePos.push(va.x, va.y, va.z, vb.x, vb.y, vb.z); 630 } 631 } 632 const E = edges.length; 633 const edgeGlow = new Float32Array(E); 634 const lCol = new Float32Array(E * 6); // 2 verts à rgb 635 const EDGE_BASE = 0.045; // near-black threads 636 for (let i = 0; i < E * 2; i++) { lCol[i*3] = EDGE_BASE; lCol[i*3+1] = EDGE_BASE; lCol[i*3+2] = EDGE_BASE * 1.08; } 637 const lGeo = new THREE.BufferGeometry(); 638 lGeo.setAttribute('position', new THREE.Float32BufferAttribute(linePos, 3)); 639 lGeo.setAttribute('color', new THREE.BufferAttribute(lCol, 3)); 640 const lMat = new THREE.LineBasicMaterial({ vertexColors:true, transparent:true, opacity:0.045, blending:THREE.NormalBlending, depthWrite:false }); 641 group.add(new THREE.LineSegments(lGeo, lMat)); 642 643 /* glow overlay: small dynamic buffer holding ONLY currently glowing edges */ 644 const MAXG = 4000; 645 const gPos = new Float32Array(MAXG * 6); 646 const gCol = new Float32Array(MAXG * 6); 647 const gGeo = new THREE.BufferGeometry(); 648 gGeo.setAttribute('position', new THREE.BufferAttribute(gPos, 3)); 649 gGeo.setAttribute('color', new THREE.BufferAttribute(gCol, 3)); 650 gGeo.setDrawRange(0, 0); 651 const gMat = new THREE.LineBasicMaterial({ vertexColors:true, transparent:true, opacity:0.5, blending:THREE.NormalBlending, depthWrite:false }); 652 group.add(new THREE.LineSegments(gGeo, gMat)); 653 654 /* ââ output digits 0â9 as canvas sprites next to their nodes ââ */ 655 const digitMats = []; 656 for (let d = 0; d < 10; d++) { 657 const cv = document.createElement('canvas'); cv.width = cv.height = 128; 658 const g = cv.getContext('2d'); 659 g.font = '300 92px "Fraunces", Times, serif'; 660 g.textAlign = 'center'; g.textBaseline = 'middle'; 661 g.fillStyle = '#dfe7f5'; 662 g.fillText(String(d), 64, 70); 663 const tex = new THREE.CanvasTexture(cv); 664 const mat = new THREE.SpriteMaterial({ map:tex, transparent:true, opacity:0.5, depthWrite:false }); 665 const sp = new THREE.Sprite(mat); 666 const np = nodePos[layers[4][d]]; 667 sp.position.set(np.x + 0.62, np.y + 0.30, np.z); 668 sp.scale.setScalar(0.85); 669 group.add(sp); 670 digitMats.push(mat); 671 } 672 673 /* ââ digit bitmaps for the input screen (16Ã16 samples of 0â9) ââ */ 674 const digitPix = []; 675 { 676 const cv = document.createElement('canvas'); cv.width = cv.height = GRID; 677 const g = cv.getContext('2d'); 678 for (let d = 0; d < 10; d++) { 679 g.clearRect(0, 0, GRID, GRID); 680 g.font = '600 15px "IBM Plex Mono", monospace'; 681 g.textAlign = 'center'; g.textBaseline = 'middle'; 682 g.fillStyle = '#fff'; 683 g.fillText(String(d), GRID/2, GRID/2 + 1); 684 const data = g.getImageData(0, 0, GRID, GRID).data; 685 const bm = new Float32Array(GRID * GRID); 686 for (let p = 0; p < GRID * GRID; p++) bm[p] = data[p*4 + 3] / 255; 687 digitPix.push(bm); 688 } 689 } 690 let screenDigit = -1; // -1 = blank (idle) 691 692 /* ââ activations & travelling pulses ââ */ 693 const act = new Float32Array(N); 694 const MAXP = 3200; 695 const sPos = new Float32Array(MAXP * 3); 696 const sGeo = new THREE.BufferGeometry(); 697 sGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3)); 698 const sMat = new THREE.PointsMaterial({ color:0x6f96ff, size:0.15, transparent:true, opacity:0.9, blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true }); 699 group.add(new THREE.Points(sGeo, sMat)); 700 const pulses = []; // {a,b,e,t,speed,forward,depth} 701 702 function spawn(i, { forward = true, depth = 1, fanout = 4 } = {}) { 703 const pool = forward ? adjF[i] : adjA[i]; 704 if (!pool.length) return; 705 for (let k = 0; k < fanout; k++) { 706 if (pulses.length >= MAXP) return; 707 const pick = pool[Math.floor(Math.random() * pool.length)]; 708 pulses.push({ a:i, b:pick.n, e:pick.e, t:-Math.random()*0.12, speed:2.8 + Math.random()*3.0, forward, depth }); 709 } 710 } 711 function fire(i, opts) { act[i] = 1; spawn(i, opts); } 712 713 /* forward wave from the lit screen pixels down to the digits */ 714 function wave(strength = 1) { 715 const src = layers[0]; 716 const bm = screenDigit >= 0 ? digitPix[screenDigit] : null; 717 const count = Math.round(16 * strength); 718 let tries = 0; 719 for (let k = 0; k < count && tries < 220; ) { 720 const p = Math.floor(Math.random() * src.length); tries++; 721 if (bm && bm[p] < 0.45) continue; // fire from lit pixels only 722 fire(src[p], { forward:true, depth:3, fanout:4 }); 723 k++; 724 } 725 } 726 727 /* ââ modes ââ */ 728 let storm = 0; 729 const statusLabel = document.getElementById('status-label'); 730 window.NEURAL = { 731 think() { storm = 10; statusLabel.textContent = 'processing · forward pass'; }, 732 talk(chars) { storm = Math.min(46, Math.max(18, chars / 22)); statusLabel.textContent = 'answering · network firing'; }, 733 idle() { storm = 0; screenDigit = -1; statusLabel.textContent = 'standby · network resting'; }, 734 fire, wave, mode: () => storm > 0 ? 'storm' : 'standby' 735 }; 736 737 /* hover: gentle â a small local blip only */ 738 const ray = new THREE.Raycaster(); 739 const ndc = new THREE.Vector2(2, 2); 740 let dragging = false, lastHoverFire = 0; 741 renderer.domElement.addEventListener('pointerdown', () => dragging = true); 742 addEventListener('pointerup', () => dragging = false); 743 renderer.domElement.addEventListener('pointermove', e => { 744 ndc.x = (e.clientX / innerWidth) * 2 - 1;
745 ndc.y = -(e.clientY / innerHeight) * 2 + 1; 746 }); 747 748 /* ââ theme ââ */ 749 let LIGHT = false; 750 const modeBtn = document.getElementById('mode-toggle'); 751 function applyTheme(light) { 752 LIGHT = light; 753 document.documentElement.classList.toggle('light', light); 754 modeBtn.textContent = light ? 'Dark' : 'Light'; 755 lMat.transparent = true; 756 lMat.opacity = light ? 0.14 : 0.045; 757 const eb = light ? 0.42 : EDGE_BASE; 758 for (let i = 0; i < E * 2; i++) { lCol[i*3] = eb; lCol[i*3+1] = eb; lCol[i*3+2] = eb * 1.08; } 759 lGeo.attributes.color.needsUpdate = true; 760 lMat.needsUpdate = true; 761 sMat.blending = light ? THREE.NormalBlending : THREE.AdditiveBlending; 762 sMat.color.set(light ? 0x1a5cff : 0xe6ecf8); 763 sMat.needsUpdate = true; 764 try { localStorage.setItem('skzl_theme', light ? 'light' : 'dark'); } catch (e) {} 765 } 766 modeBtn.addEventListener('click', () => applyTheme(!LIGHT)); 767 768 const clock = new THREE.Clock(); 769 let stormAcc = 0, digitTimer = 0, standbyAcc = 0; 770 const digitGlow = new Float32Array(10); 771 const white = new THREE.Color(1, 1, 1), flare = new THREE.Color(0xe6ecf8); 772 const inkL = new THREE.Color(0x243154), flareL = new THREE.Color(0x1a5cff); 773 774 function frame() { 775 requestAnimationFrame(frame); 776 const dt = Math.min(clock.getDelta(), 0.05); 777 const t = clock.elapsedTime; 778 controls.update(); 779 780 if (!reduced) { 781 if (!dragging && storm === 0) group.rotation.y += dt * 0.035; 782 updateValues(dt); 783 784 /* screen: cycle digits fast while processing, blank at idle */ 785 if (storm > 0) { 786 digitTimer += dt; 787 if (digitTimer > 0.14) { screenDigit = (screenDigit + 1 + Math.floor(Math.random()*3)) % 10; digitTimer = 0; } 788 } 789 790 /* hover: small, quiet blip (the real show is the prompt storm) */ 791 if (!dragging && t - lastHoverFire > 0.22 && ndc.x <= 1) { 792 ray.setFromCamera(ndc, camera); 793 const hit = ray.intersectObject(cubes, false)[0]; 794 if (hit && hit.instanceId != null && nodeLayer[hit.instanceId] >= 1 && nodeLayer[hit.instanceId] <= 3) { 795 fire(hit.instanceId, { forward:false, depth:2, fanout:3 }); 796 lastHoverFire = t; 797 } 798 } 799 800 /* chat storm: heavy continuous waves; idle: silence */ 801 if (storm > 0) { 802 stormAcc += dt * storm; 803 while (stormAcc >= 1) { wave(1.8); stormAcc--; } 804 } else if (LIGHT) { 805 /* light mode: subtle standby blips so the blue is discoverable */ 806 standbyAcc += dt; 807 if (standbyAcc > 2.0 + Math.random() * 1.5) { 808 const l = 1 + Math.floor(Math.random() * 3); 809 fire(layers[l][Math.floor(Math.random() * layers[l].length)], { forward:false, depth:1, fanout:2 }); 810 standbyAcc = 0; 811 } 812 } 813 814 /* advance pulses; light their edges as they travel */ 815 for (let i = pulses.length - 1; i >= 0; i--) { 816 const p = pulses[i]; 817 p.t += dt * p.speed; 818 if (p.t > 0) edgeGlow[p.e] = Math.max(edgeGlow[p.e], Math.min(1, p.t < 0.5 ? p.t * 2.4 : (1 - p.t) * 2.4 + 0.3)); 819 if (p.t >= 1) { 820 act[p.b] = 1; 821 if (nodeLayer[p.b] === 4) digitGlow[layers[4].indexOf(p.b)] = 1; 822 if (p.depth > 0 && Math.random() < 0.8) 823 spawn(p.b, { forward:p.forward, depth:p.depth - 1, fanout:p.forward ? 4 : 2 }); 824 pulses.splice(i, 1); 825 } 826 } 827 sMat.opacity = pulses.length ? 0.95 : 0; 828 for (let i = 0; i < MAXP; i++) { 829 if (i < pulses.length) { 830 const p = pulses[i], a = nodePos[p.a], b = nodePos[p.b], k = Math.max(p.t, 0); 831 sPos[i*3] = a.x + (b.x - a.x) * k; 832 sPos[i*3+1] = a.y + (b.y - a.y) * k; 833 sPos[i*3+2] = a.z + (b.z - a.z) * k; 834 } else { sPos[i*3] = 0; sPos[i*3+1] = 9999; sPos[i*3+2] = 0; } 835 } 836 sGeo.attributes.position.needsUpdate = true; 837 838 /* edge glow: base threads stay static; overlay holds only glowing edges */ 839 let gc = 0; 840 for (let e = 0; e < E; e++) { 841 const g = edgeGlow[e]; 842 if (g <= 0.02) continue;
843 edgeGlow[e] = g * Math.exp(-dt * 4.2); 844 if (gc >= MAXG) continue; 845 const o = gc * 6, p = e * 6; 846 gPos[o] = linePos[p]; gPos[o+1] = linePos[p+1]; gPos[o+2] = linePos[p+2]; 847 gPos[o+3] = linePos[p+3]; gPos[o+4] = linePos[p+4]; gPos[o+5] = linePos[p+5]; 848 let r, gg, b; 849 if (!LIGHT) { 850 /* fade toward dark bg as glow decays; peak = bright white */ 851 r = 0.08 + g * 0.92; gg = 0.10 + g * 0.90; b = 0.14 + g * 0.86; 852 } else { 853 /* fade toward light bg; peak = strong blue */ 854 r = 0.93 - g * 0.83; gg = 0.95 - g * 0.59; b = 0.98; 855 } 856 gCol[o] = r; gCol[o+1] = gg; gCol[o+2] = b; 857 gCol[o+3] = r; gCol[o+4] = gg; gCol[o+5] = b; 858 gc++; 859 } 860 gGeo.setDrawRange(0, gc * 2); 861 gGeo.attributes.position.needsUpdate = true; 862 gGeo.attributes.color.needsUpdate = true; 863 864 /* cube colors + flare pop */ 865 const breathe = 0.85 + Math.sin(t * 0.55) * 0.15; 866 for (let i = 0; i < N; i++) { 867 act[i] *= Math.exp(-dt * 3.4); 868 const a = act[i]; 869 const l = nodeLayer[i]; 870 const bm0 = l === 0 ? (screenDigit >= 0 ? digitPix[screenDigit][i] : 0) : 0; 871 if (!LIGHT) { 872 /* black, slightly reflective nodes; warm amber current on firing */ 873 let base; 874 if (l === 0) base = 0.42; // light gray screen pixels 875 else if (l === 4) base = 0.34; 876 else base = 0.50 * breathe; // light gray cubes 877 iColor[i*3] = base + bm0 * 0.82 + a * 0.60; 878 iColor[i*3+1] = base + bm0 * 0.84 + a * 0.62; 879 iColor[i*3+2] = base + bm0 * 0.88 + a * 0.68; 880 } else { 881 /* dark gray / black nodes on light ground; firing turns blue */ 882 let base; 883 if (l === 0) base = 0.32 - bm0 * 0.26; 884 else if (l === 4) base = 0.20; 885 else base = 0.24; 886 iColor[i*3] = base - a * 0.14; 887 iColor[i*3+1] = base + a * 0.18; 888 iColor[i*3+2] = base + a * 0.88; 889 } 890 if (a > 0.01 && l >= 1 && l <= 3) { 891 const s = baseScale[i] * (1 + a * 0.45); 892 sV.set(s, s, s); 893 m4.compose(nodePos[i], q0, sV); 894 cubes.setMatrixAt(i, m4); 895 cubes.instanceMatrix.needsUpdate = true; 896 } 897 } 898 cubes.instanceColor.needsUpdate = true; 899 900 /* digits glow when their node is hit */ 901 for (let d = 0; d < 10; d++) { 902 digitGlow[d] *= Math.exp(-dt * 1.8); 903 digitMats[d].opacity = (LIGHT ? 0.75 : 0.45) + digitGlow[d] * (LIGHT ? 0.25 : 0.55); 904 if (!LIGHT) digitMats[d].color.copy(white).lerp(flare, digitGlow[d] * 0.8); 905 else digitMats[d].color.copy(inkL).lerp(flareL, digitGlow[d]); 906 } 907 } else { 908 for (let i = 0; i < N; i++) { const v = nodeLayer[i] === 0 ? 0.16 : 0.1; iColor[i*3] = v; iColor[i*3+1] = v * 1.05; iColor[i*3+2] = v * 1.25; } 909 cubes.instanceColor.needsUpdate = true; 910 sMat.opacity = 0; 911 } 912 renderer.render(scene, camera); 913 } 914 frame(); 915 applyTheme((() => { try { return localStorage.getItem('skzl_theme') !== 'dark'; } catch (e) { return true; } })()); 916 917 /* re-sync size + framing once layout is final (init runs before the stage 918 reaches its final viewport size, which otherwise leaves the net off-screen) */ 919 function syncViewport() { 920 camera.aspect = innerWidth / innerHeight; 921 camera.updateProjectionMatrix(); 922 renderer.setSize(innerWidth, innerHeight); 923 frameNetwork(true); 924 } 925 requestAnimationFrame(syncViewport); 926 addEventListener('load', syncViewport); 927 928 addEventListener('resize', () => { 929 camera.aspect = innerWidth/innerHeight; 930 camera.updateProjectionMatrix(); 931 renderer.setSize(innerWidth, innerHeight); 932 /* re-frame on narrow/portrait viewports (e.g. rotation); never yank the 933 camera away from a user who deliberately zoomed in on desktop */ 934 frameNetwork(camera.aspect < 1); 935 }); 936 } // end initScene 937 938 if ('requestIdleCallback' in window) requestIdleCallback(initScene, { timeout: 2000 }); 939 else setTimeout(initScene, 200); 940 </script>
940 941
941<script src="/consent-banner.js" defer></script>
941 942
942<script> 943 /* Toggle Dashboard vs. Sign-in in the nav based on whether a Supabase 944 session exists (Weg B â consistent with the React header, which shows 945 Dashboard only when signed in and a sign-in link otherwise). */ 946 (function () { 947 try { 948 var dash = document.getElementById("nav-dashboard-link"); 949 var login = document.getElementById("nav-login-link"); 950 if (!dash || !login) return; 951 var signedIn = false; 952 for (var i = 0; i < localStorage.length; i++) { 953 var k = localStorage.key(i); 954 if (k && k.indexOf("sb-") === 0 && k.indexOf("-auth-token") > -1) { 955 var v = localStorage.getItem(k) || ""; 956 if (v.indexOf("access_token") > -1) { signedIn = true; break; } 957 } 958 } 959 dash.style.display = signedIn ? "" : "none"; 960 login.style.display = signedIn ? "none" : ""; 961 } catch (e) {} 962 })(); 963 </script>
963 964</body> 965</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.