| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> |
| <meta name="color-scheme" content="dark" /> |
| <title>Liquid AI · LFM2.5 230M</title> |
| <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚡️</text></svg>"> |
| <meta name="description" content="LFM2.5-230M (Q4_0 GGUF) running fully in your browser on WebGPU. Every kernel written and optimized by Fable 5 + Opus 4.8." /> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| <link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" /> |
| |
| |
| <link href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css" rel="stylesheet" /> |
| <style> |
| |
| |
| |
| :root{ |
| --bg:#050608; |
| --ink:#f4f5f7; |
| --muted:#9aa0ab; |
| --muted-2:#6b7280; |
| --warm:#ff7a30; |
| --cool:#7aa0ff; |
| --hair:rgba(255,255,255,0.14); |
| --hair-soft:rgba(255,255,255,0.08); |
| --font-display:"Hanken Grotesk", system-ui, sans-serif; |
| --font-mono:"JetBrains Mono", ui-monospace, monospace; |
| |
| |
| --t1:var(--ink); |
| --t2:var(--muted); |
| --t3:var(--muted-2); |
| --t4:rgba(255,255,255,0.20); |
| --line:rgba(255,255,255,0.10); |
| --line-soft:rgba(255,255,255,0.07); |
| --panel:rgba(255,255,255,0.03); |
| --panel-hover:rgba(255,255,255,0.06); |
| --ok:#ff8a4d; |
| --warn:#ffcd6b; |
| --danger:#ff7a6b; |
| --user-bubble:rgba(255,255,255,0.06); |
| --display:var(--font-display); |
| --body:var(--font-display); |
| --mono:var(--font-mono); |
| --maxw:820px; |
| } |
| |
| *{ box-sizing:border-box; margin:0; padding:0; } |
| html,body{ height:100%; } |
| body{ |
| background: |
| radial-gradient(120% 80% at 50% 118%, rgba(255,110,40,0.10), rgba(255,110,40,0) 55%), |
| radial-gradient(90% 70% at 88% -10%, rgba(80,120,230,0.10), rgba(80,120,230,0) 55%), |
| radial-gradient(140% 120% at 50% 50%, #0a0c11 0%, #050608 55%, #030305 100%); |
| color:var(--ink); |
| font-family:var(--font-display); |
| font-size:16px; |
| line-height:1.6; |
| overflow:hidden; |
| -webkit-font-smoothing:antialiased; |
| text-rendering:optimizeLegibility; |
| } |
| |
| button, textarea, input{ font:inherit; color:inherit; } |
| button{ appearance:none; background:none; border:0; cursor:pointer; } |
| a{ color:inherit; text-decoration:none; } |
| ::selection{ background:rgba(255,255,255,0.18); } |
| |
| |
| |
| |
| |
| #bg-amb{ |
| position:fixed; inset:-20%; z-index:-1; pointer-events:none; |
| background: |
| radial-gradient(38% 38% at 30% 72%, rgba(255,120,50,0.11), rgba(255,120,50,0) 70%), |
| radial-gradient(34% 34% at 72% 28%, rgba(90,130,255,0.10), rgba(90,130,255,0) 70%); |
| filter:blur(10px); |
| animation:ambDrift 28s ease-in-out infinite alternate; |
| will-change:transform; |
| } |
| @keyframes ambDrift{ |
| 0%{ transform:translate3d(-2%,-1%,0) scale(1.02); } |
| 50%{ transform:translate3d(2%,3%,0) scale(1.1); } |
| 100%{ transform:translate3d(-1%,1%,0) scale(1.04); } |
| } |
| #scene-wrap{ |
| position:fixed; inset:0; z-index:0; |
| transition:opacity 1.1s ease, filter 1.1s ease; |
| } |
| #scene-wrap.dim{ opacity:0.18; filter:blur(6px); } |
| #scene{ display:block; width:100%; height:100%; } |
| |
| .vignette{ |
| position:fixed; inset:0; z-index:2; pointer-events:none; |
| background:radial-gradient(120% 100% at 50% 45%, rgba(0,0,0,0) 40%, rgba(0,0,0,0.55) 100%); |
| } |
| .grain{ |
| position:fixed; inset:-50%; z-index:3; pointer-events:none; opacity:0.05; |
| background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); |
| animation:grain 7s steps(6) infinite; |
| will-change:transform; |
| } |
| @keyframes grain{ |
| 0%{transform:translate(0,0)} 20%{transform:translate(-4%,2%)} |
| 40%{transform:translate(2%,-3%)} 60%{transform:translate(-2%,3%)} |
| 80%{transform:translate(3%,1%)} 100%{transform:translate(0,0)} |
| } |
| |
| |
| |
| |
| body.chatting #scene-wrap, |
| body.chatting #bg-amb, |
| body.chatting .grain, |
| body.chatting .vignette{ display:none; } |
| |
| |
| |
| .ui{ position:fixed; inset:0; z-index:5; pointer-events:none; } |
| .ui a, .ui button{ pointer-events:auto; } |
| |
| |
| .hero{ opacity:0; transition:opacity .9s ease; } |
| body.ready .hero{ opacity:1; } |
| .hero.fade-out{ opacity:0 !important; transition:opacity .7s ease; pointer-events:none; } |
| |
| |
| |
| |
| .nav{ |
| position:absolute; top:0; left:0; right:0; |
| display:flex; align-items:center; justify-content:space-between; |
| padding:30px clamp(20px,4vw,56px); |
| } |
| .brand{ display:flex; align-items:center; gap:10px; text-decoration:none; color:var(--ink); cursor:pointer; margin-left:clamp(0px,2vw,36px); } |
| .brand-glyph{ display:inline-flex; width:22px; height:22px; color:var(--ink); } |
| .brand-glyph svg{ width:100%; height:100%; display:block; } |
| .brand-name{ font-weight:700; letter-spacing:0.14em; font-size:1.02rem; } |
| .brand-tag{ font-family:var(--font-mono); font-size:0.62rem; letter-spacing:0.14em; color:var(--muted-2); margin-top:2px; } |
| |
| .nav-right{ display:flex; align-items:center; gap:12px; } |
| |
| .pill{ |
| font-family:var(--font-mono); font-size:0.74rem; letter-spacing:0.1em; |
| text-decoration:none; color:var(--ink); cursor:pointer; |
| padding:11px 18px; border-radius:999px; |
| background:rgba(255,255,255,0.04); border:1px solid var(--hair); |
| transition:background .25s ease, border-color .25s ease, color .25s ease; |
| display:inline-flex; align-items:center; gap:8px; |
| } |
| .pill:hover{ background:var(--ink); color:#08090c; border-color:var(--ink); } |
| |
| |
| |
| |
| .hero-copy{ |
| position:absolute; left:clamp(20px,4vw,56px); top:50%; transform:translateY(-50%); |
| max-width:min(80ch,72vw); |
| } |
| .headline{ |
| font-weight:600; line-height:0.98; letter-spacing:-0.03em; |
| font-size:clamp(1.8rem,4vw,3.5rem); |
| } |
| .hl-line{ display:block; } |
| .hl-line.accent{ color:#fff; } |
| .word{ |
| display:inline-block; |
| opacity:0; filter:blur(16px); transform:translateY(0.32em); |
| animation:wordIn 1.15s cubic-bezier(0.2,0.7,0.15,1) var(--d,0s) forwards; |
| } |
| .accent .word{ text-shadow:0 0 28px rgba(255,122,48,0.32); } |
| @keyframes wordIn{ to{ opacity:1; filter:blur(0); transform:none; } } |
| |
| .subtext{ |
| margin-top:26px; max-width:48ch; |
| color:var(--muted); font-size:clamp(0.95rem,1.1vw,1.06rem); |
| line-height:1.55; font-weight:400; |
| opacity:0; animation:fadeUp 1s ease 0.6s forwards; |
| } |
| .subtext b{ color:var(--ink); font-weight:600; } |
| @keyframes fadeUp{ from{opacity:0;transform:translateY(12px)} to{opacity:1;transform:none} } |
| |
| .cta{ |
| display:inline-flex; align-items:center; gap:14px; |
| margin-top:30px; text-decoration:none; |
| font-family:var(--font-mono); font-size:0.82rem; letter-spacing:0.16em; |
| color:var(--ink); padding:0 0 8px 0; cursor:pointer; |
| background:transparent; appearance:none; -webkit-appearance:none; |
| border:0; border-bottom:1px solid var(--hair); |
| opacity:0; animation:fadeUp 1s ease 0.82s forwards; |
| transition:border-color .3s ease, gap .3s ease, opacity .3s ease; |
| } |
| .cta:hover:not(:disabled){ border-color:var(--ink); gap:20px; } |
| .cta:disabled{ opacity:0.4; cursor:not-allowed; border-color:transparent; } |
| .cta-arrow{ transition:transform .3s ease; } |
| .cta:hover:not(:disabled) .cta-arrow{ transform:translateX(4px); } |
| |
| |
| |
| |
| .hint{ |
| position:absolute; left:0; right:0; bottom:30px; |
| display:flex; flex-direction:column; align-items:center; |
| text-align:center; font-family:var(--font-mono); |
| font-size:0.72rem; letter-spacing:0.14em; color:var(--muted); |
| line-height:1.9; opacity:0; animation:fadeUp 1s ease 1.1s forwards; |
| } |
| .hint-row{ display:inline-flex; align-items:center; justify-content:center; gap:8px; } |
| .hint-ic{ width:14px; height:14px; } |
| .hint-ic.spark{ color:var(--warm); } |
| |
| |
| |
| |
| #flash{ |
| position:fixed; inset:0; z-index:8; pointer-events:none; opacity:0; |
| background:radial-gradient(54% 48% at 50% 48%, rgba(255,200,150,0.82), rgba(255,150,82,0.4) 46%, rgba(255,120,50,0) 80%); |
| } |
| |
| |
| |
| |
| .page2{ |
| position:fixed; inset:0; z-index:10; pointer-events:none; |
| display:grid; place-items:center; |
| opacity:0; transform:translateY(8px); |
| transition:opacity .6s ease, transform .6s ease; |
| } |
| .page2.show{ opacity:1; transform:none; pointer-events:auto; } |
| .boot{ width:min(460px,86vw); text-align:center; } |
| .boot-logo{ display:inline-flex; width:46px; height:46px; color:var(--ink); margin-bottom:24px; |
| filter:drop-shadow(0 0 18px rgba(255,122,48,0.55)); animation:logoPulse 3.4s ease-in-out infinite; } |
| .boot-logo svg{ width:100%; height:100%; } |
| @keyframes logoPulse{ 0%,100%{filter:drop-shadow(0 0 14px rgba(255,122,48,0.4))} 50%{filter:drop-shadow(0 0 26px rgba(255,122,48,0.7))} } |
| .boot-title{ font-family:var(--font-mono); letter-spacing:0.2em; font-size:0.78rem; color:var(--ink); margin-bottom:22px; } |
| .boot-bar-wrap{ height:2px; background:var(--hair-soft); border-radius:2px; overflow:hidden; } |
| .boot-bar{ height:100%; width:0%; background:linear-gradient(90deg, var(--warm), #ffd9a8); box-shadow:0 0 12px rgba(255,122,48,0.7); } |
| .boot-meta{ display:flex; justify-content:space-between; gap:14px; margin-top:12px; font-family:var(--font-mono); font-size:0.68rem; letter-spacing:0.04em; color:var(--muted); } |
| .boot-meta #boot-step{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-variant-numeric:tabular-nums; text-align:left; } |
| .boot-meta #boot-pct{ flex-shrink:0; font-variant-numeric:tabular-nums; } |
| .gpu-chip{ display:inline-block; margin-top:22px; font-family:var(--font-mono); font-size:0.66rem; letter-spacing:0.1em; |
| padding:7px 14px; border-radius:999px; border:1px solid var(--hair); color:var(--muted); } |
| .gpu-chip.ok{ color:#bfe6ff; border-color:rgba(122,160,255,0.5); } |
| .gpu-chip.bad{ color:#ffb38a; border-color:rgba(255,122,48,0.45); } |
| |
| |
| .boot-error{ display:none; margin-top:24px; text-align:left; } |
| .boot-error.show{ display:block; } |
| .boot-error-msg{ |
| margin:0 0 14px; white-space:pre-wrap; word-break:break-word; |
| font-family:var(--font-mono); font-size:0.72rem; line-height:1.55; color:var(--muted); |
| background:rgba(0,0,0,0.3); border:1px solid rgba(255,122,107,0.3); border-radius:10px; padding:12px 14px; |
| } |
| .boot-retry{ animation:none; opacity:1; } |
| |
| |
| |
| |
| #chat{ |
| position:fixed; inset:0; z-index:15; display:flex; flex-direction:column; |
| opacity:0; pointer-events:none; transform:translateY(10px); |
| transition:opacity .6s ease, transform .6s ease; |
| |
| background: |
| radial-gradient(120% 80% at 50% 118%, rgba(255,110,40,0.10), rgba(255,110,40,0) 55%), |
| radial-gradient(90% 70% at 88% -10%, rgba(80,120,230,0.10), rgba(80,120,230,0) 55%), |
| radial-gradient(140% 120% at 50% 50%, #0a0c11 0%, #050608 55%, #030305 100%); |
| } |
| #chat.show{ opacity:1; pointer-events:auto; transform:none; } |
| |
| .chat-head{ |
| position:relative; flex:0 0 auto; |
| border-bottom:1px solid var(--line-soft); |
| background:rgba(5,6,8,0.72); |
| backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); |
| } |
| .chat-head-inner{ |
| margin:0 auto; max-width:var(--maxw); padding:16px 28px; |
| display:flex; align-items:center; gap:16px; |
| } |
| .to-top{ display:inline-flex; align-items:center; gap:9px; flex-shrink:0; color:var(--ink); transition:opacity .2s ease; } |
| .to-top:hover{ opacity:0.7; } |
| .to-top .brand-glyph{ width:18px; height:18px; } |
| .to-top .brand-name{ font-size:0.78rem; } |
| |
| .status{ flex:1; min-width:0; display:flex; align-items:center; gap:9px; color:var(--t2); font-size:13px; } |
| .status-dot{ width:7px; height:7px; border-radius:50%; background:var(--t4); flex-shrink:0; transition:background .3s ease, box-shadow .3s ease; } |
| .status.loading .status-dot{ background:var(--warn); box-shadow:0 0 10px rgba(255,205,107,0.5); } |
| .status.ready .status-dot{ background:var(--ok); box-shadow:0 0 10px rgba(255,138,77,0.55); } |
| .status.busy .status-dot{ background:var(--ok); box-shadow:0 0 10px rgba(255,138,77,0.55); animation:statusPulse 1.1s ease-in-out infinite; } |
| .status.error .status-dot{ background:var(--danger); box-shadow:0 0 10px rgba(255,122,107,0.5); } |
| .status-text{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-variant-numeric:tabular-nums; } |
| .status-text strong{ color:var(--t1); font-weight:600; } |
| |
| .chat-head-actions{ display:flex; align-items:center; gap:8px; flex-shrink:0; } |
| .head-btn{ |
| display:inline-flex; align-items:center; gap:7px; padding:8px 15px; |
| font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; |
| color:var(--t2); border:1px solid var(--line); border-radius:100px; |
| transition:border-color .2s ease, color .2s ease, opacity .2s ease; |
| } |
| .head-btn:hover:not(:disabled){ border-color:rgba(255,255,255,0.35); color:var(--t1); } |
| .head-btn:disabled{ opacity:0.35; cursor:not-allowed; } |
| .head-btn[hidden]{ display:none; } |
| |
| |
| |
| .thread-scroll{ flex:1 1 auto; min-height:0; overflow-y:auto; } |
| .thread{ |
| margin:0 auto; max-width:var(--maxw); min-height:100%; |
| padding:40px 28px 24px; display:flex; flex-direction:column; gap:32px; |
| } |
| |
| .welcome{ margin:auto 0; text-align:center; padding:4vh 0; animation:rise 0.7s cubic-bezier(0.2,0.7,0.2,1) both; } |
| .welcome h2{ font-family:var(--display); font-size:clamp(30px,6vw,46px); font-weight:600; line-height:1.05; color:#fff; margin-bottom:14px; letter-spacing:-0.02em; } |
| .welcome h2 .thin{ color:var(--t3); } |
| .welcome p{ color:var(--t2); max-width:46ch; margin:0 auto 28px; font-weight:400; } |
| .seeds{ display:flex; flex-wrap:wrap; gap:10px; justify-content:center; } |
| .seed{ |
| padding:9px 16px; font-size:13.5px; color:var(--t2); |
| background:var(--panel); border:1px solid var(--line); border-radius:100px; |
| transition:border-color .2s ease, color .2s ease, background .2s ease, transform .15s ease, opacity .2s ease; |
| } |
| .seed:hover:not(:disabled){ border-color:rgba(255,255,255,0.3); color:var(--t1); background:var(--panel-hover); transform:translateY(-1px); } |
| .seed:disabled{ opacity:0.4; cursor:not-allowed; } |
| |
| .msg{ display:flex; flex-direction:column; animation:rise 0.4s cubic-bezier(0.2,0.7,0.2,1) both; } |
| .role{ display:flex; align-items:center; gap:8px; font-family:var(--mono); font-size:10.5px; letter-spacing:0.16em; text-transform:uppercase; margin-bottom:10px; } |
| .role::before{ content:""; width:5px; height:5px; border-radius:50%; } |
| .msg.user{ align-items:flex-end; } |
| .msg.user .role{ color:var(--t3); } |
| .msg.user .role::before{ background:var(--t3); } |
| .msg.assistant .role{ color:var(--ok); } |
| .msg.assistant .role::before{ background:var(--ok); } |
| |
| .bubble{ overflow-wrap:anywhere; } |
| .bubble.user{ |
| background:var(--user-bubble); border:1px solid var(--line); |
| border-radius:16px 16px 4px 16px; padding:12px 17px; line-height:1.55; |
| max-width:min(82%,600px); white-space:pre-wrap; color:var(--t1); |
| } |
| .bubble.assistant{ font-size:16px; line-height:1.72; color:var(--t1); max-width:100%; } |
| .bubble.assistant > :first-child{ margin-top:0; } |
| .bubble.assistant > :last-child{ margin-bottom:0; } |
| .bubble.assistant p{ margin:0 0 0.9em; } |
| .bubble.assistant strong{ color:#fff; font-weight:700; } |
| .bubble.assistant em{ font-style:italic; } |
| .bubble.assistant a{ color:#8ab4ff; text-decoration:underline; text-underline-offset:2px; } |
| .bubble.assistant a:hover{ color:#aecbff; } |
| .bubble.assistant h1, .bubble.assistant h2, .bubble.assistant h3, |
| .bubble.assistant h4, .bubble.assistant h5, .bubble.assistant h6{ |
| color:#fff; font-weight:600; line-height:1.25; margin:1.3em 0 0.6em; |
| } |
| .bubble.assistant h1{ font-size:1.5em; } |
| .bubble.assistant h2{ font-size:1.3em; } |
| .bubble.assistant h3{ font-size:1.13em; } |
| .bubble.assistant h4, .bubble.assistant h5, .bubble.assistant h6{ font-size:1em; } |
| .bubble.assistant ul, .bubble.assistant ol{ margin:0 0 0.9em; padding-left:1.5em; } |
| .bubble.assistant li{ margin:0.25em 0; } |
| .bubble.assistant li::marker{ color:var(--t3); } |
| .bubble.assistant blockquote{ margin:0 0 0.9em; padding:2px 0 2px 16px; border-left:2px solid var(--line); color:var(--t2); } |
| .bubble.assistant hr{ border:0; border-top:1px solid var(--line); margin:1.3em 0; } |
| .bubble.assistant code{ font-family:var(--mono); font-size:0.85em; background:var(--panel); border:1px solid var(--line); border-radius:5px; padding:1px 5px; } |
| .bubble.assistant pre{ margin:0 0 0.9em; padding:14px 16px; background:#0a0a0c; border:1px solid var(--line); border-radius:12px; overflow-x:auto; } |
| .bubble.assistant pre code{ background:none; border:0; padding:0; font-size:0.82em; line-height:1.6; } |
| .bubble.assistant table{ border-collapse:collapse; margin:0 0 0.9em; font-size:0.92em; display:block; overflow-x:auto; } |
| .bubble.assistant th, .bubble.assistant td{ border:1px solid var(--line); padding:6px 11px; text-align:left; } |
| .bubble.assistant th{ background:var(--panel); color:var(--t1); font-weight:600; } |
| .bubble.assistant pre::-webkit-scrollbar, .bubble.assistant table::-webkit-scrollbar{ height:8px; } |
| .bubble.assistant pre::-webkit-scrollbar-thumb, .bubble.assistant table::-webkit-scrollbar-thumb{ background:var(--line); border-radius:8px; } |
| |
| .bubble.assistant .katex{ font-size:1.05em; } |
| .bubble.assistant .katex-display{ margin:0.6em 0; overflow-x:auto; overflow-y:hidden; padding:2px 0; } |
| |
| .meta{ font-family:var(--mono); font-size:10.5px; letter-spacing:0.04em; color:var(--t3); margin-top:12px; } |
| |
| .thinking{ display:inline-flex; gap:5px; padding:4px 0; } |
| .thinking span{ width:7px; height:7px; border-radius:50%; background:var(--ok); opacity:0.5; animation:bob 1.3s ease-in-out infinite; } |
| .thinking span:nth-child(2){ animation-delay:0.18s; } |
| .thinking span:nth-child(3){ animation-delay:0.36s; } |
| |
| .caret{ display:inline-block; width:2px; height:1.05em; margin-left:2px; vertical-align:-0.16em; background:var(--ok); animation:blink 1s steps(2) infinite; } |
| |
| .composer-wrap{ flex:0 0 auto; background:linear-gradient(rgba(5,6,8,0), var(--bg) 30%); padding-top:8px; } |
| .composer{ margin:0 auto; max-width:var(--maxw); padding:0 28px 22px; } |
| .field{ |
| display:grid; grid-template-columns:1fr 44px; align-items:flex-end; gap:8px; |
| background:var(--panel); border:1px solid var(--line); border-radius:18px; padding:8px 8px 8px 18px; |
| transition:border-color .2s ease, background .2s ease; |
| } |
| .field:focus-within{ border-color:rgba(255,255,255,0.28); background:rgba(255,255,255,0.05); } |
| textarea{ background:transparent; border:0; outline:none; resize:none; width:100%; min-height:42px; max-height:180px; padding:8px 0; color:var(--t1); } |
| textarea::placeholder{ color:var(--t3); } |
| |
| .icon-button{ display:grid; place-items:center; width:42px; height:42px; border-radius:13px; transition:background .2s ease, opacity .2s ease, transform .1s ease; } |
| .icon-button svg{ width:19px; height:19px; } |
| .icon-button:active:not(:disabled){ transform:scale(0.94); } |
| .icon-button:disabled{ opacity:0.3; cursor:not-allowed; } |
| .send-button{ background:#fff; color:#000; } |
| .send-button:hover:not(:disabled){ opacity:0.86; } |
| .stop-button{ background:rgba(255,122,107,0.14); color:var(--danger); border:1px solid rgba(255,122,107,0.3); display:none; } |
| .stop-button:hover:not(:disabled){ background:rgba(255,122,107,0.22); } |
| |
| .composer-meta{ |
| display:flex; align-items:center; justify-content:center; gap:12px; |
| margin:10px 4px 0; min-height:22px; |
| font-family:var(--mono); font-size:10.5px; letter-spacing:0.04em; text-transform:uppercase; color:var(--t3); |
| } |
| |
| .thread-scroll::-webkit-scrollbar{ width:10px; } |
| .thread-scroll::-webkit-scrollbar-thumb{ background:var(--line); border:3px solid var(--bg); border-radius:10px; } |
| textarea:focus, textarea:focus-visible{ outline:none; } |
| |
| |
| |
| |
| .kx{ position:fixed; inset:0; z-index:50; display:flex; align-items:center; justify-content:center; padding:28px; } |
| .kx[hidden]{ display:none; } |
| .kx-backdrop{ position:absolute; inset:0; background:rgba(0,0,0,0.62); backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px); animation:kxFade 0.25s ease; } |
| .kx-panel{ |
| position:relative; display:flex; flex-direction:column; |
| width:min(1080px,100%); height:min(86vh,920px); |
| background:#0a0a0c; border:1px solid var(--line); border-radius:18px; overflow:hidden; |
| box-shadow:0 40px 120px -30px rgba(0,0,0,0.9); animation:kxRise 0.3s cubic-bezier(0.2,0.7,0.2,1); |
| } |
| .kx-head{ display:flex; align-items:flex-start; justify-content:space-between; gap:16px; padding:20px 22px; border-bottom:1px solid var(--line-soft); } |
| .kx-title h3{ font-family:var(--display); font-weight:600; font-size:24px; color:#fff; line-height:1; letter-spacing:-0.01em; } |
| .kx-sub{ display:block; margin-top:7px; font-size:12.5px; color:var(--t3); } |
| .kx-close{ display:grid; place-items:center; width:34px; height:34px; flex-shrink:0; border-radius:9px; color:var(--t2); border:1px solid var(--line); transition:color .2s ease, border-color .2s ease, background .2s ease; } |
| .kx-close:hover{ color:var(--t1); border-color:rgba(255,255,255,0.35); background:var(--panel); } |
| .kx-close svg{ width:15px; height:15px; } |
| .kx-body{ flex:1; display:grid; grid-template-columns:236px 1fr; min-height:0; } |
| .kx-side{ position:relative; min-width:0; min-height:0; border-right:1px solid var(--line-soft); } |
| .kx-list{ height:100%; overflow-y:auto; padding:10px; display:flex; flex-direction:column; gap:2px; } |
| .kx-side::after{ content:""; position:absolute; left:0; right:1px; bottom:0; height:54px; background:linear-gradient(transparent, #0a0a0c 88%); pointer-events:none; opacity:1; transition:opacity .25s ease; } |
| .kx-side.at-end::after{ opacity:0; } |
| .kx-item{ flex-shrink:0; text-align:left; padding:9px 12px; border-radius:8px; font-family:var(--mono); font-size:12px; line-height:1.5; color:var(--t2); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; transition:background .15s ease, color .15s ease; } |
| .kx-item:hover{ background:var(--panel); color:var(--t1); } |
| .kx-item.active{ background:var(--panel-hover); color:#fff; } |
| .kx-view{ display:flex; flex-direction:column; min-width:0; min-height:0; overflow:hidden; } |
| .kx-view-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:12px 18px; border-bottom:1px solid var(--line-soft); } |
| .kx-name{ font-family:var(--mono); font-size:13px; color:var(--t1); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } |
| .kx-view-actions{ display:flex; align-items:center; gap:14px; flex-shrink:0; } |
| .kx-lines{ font-family:var(--mono); font-size:11px; color:var(--t4); } |
| .kx-copy{ font-family:var(--mono); font-size:11px; text-transform:uppercase; letter-spacing:0.08em; color:var(--t2); border:1px solid var(--line); border-radius:100px; padding:5px 13px; transition:color .2s ease, border-color .2s ease; } |
| .kx-copy:hover{ color:var(--t1); border-color:rgba(255,255,255,0.35); } |
| .kx-code{ flex:1; min-height:0; min-width:0; overflow:auto; margin:0; padding:18px 20px; font-family:var(--mono); font-size:12.5px; line-height:1.65; color:var(--t2); tab-size:2; } |
| .kx-code code{ white-space:pre; } |
| |
| .k-cm{ color:#5d6b6f; font-style:italic; } |
| .k-kw{ color:#c792ea; } |
| .k-ty{ color:#6fb3ff; } |
| .k-at{ color:#ffb074; } |
| .k-nu{ color:#7ee787; } |
| |
| .kx-source{ flex:1; min-height:0; display:flex; flex-direction:column; } |
| .kx-source[hidden]{ display:none; } |
| .kx-intro{ flex:1; min-height:0; overflow-y:auto; display:flex; align-items:center; justify-content:center; padding:34px 30px; } |
| .kx-intro[hidden]{ display:none; } |
| .kx-intro-inner{ max-width:540px; text-align:center; animation:kxIntroRise 0.6s cubic-bezier(0.2,0.7,0.2,1) both; } |
| |
| .kx-spark{ position:relative; width:60px; height:60px; margin:0 auto 24px; } |
| .kx-spark-ring{ position:absolute; inset:0; border-radius:50%; background:conic-gradient(from 0deg, #ff7a6b, #ffcd6b, #64ffa0, #6fb3ff, #c792ea, #ff7a6b); animation:kxSpin 6s linear infinite; } |
| .kx-spark-ring::after{ content:""; position:absolute; inset:-7px; border-radius:50%; background:inherit; filter:blur(13px); opacity:0.5; } |
| .kx-spark-core{ position:absolute; inset:2px; border-radius:50%; background:#0a0a0c; } |
| .kx-spark-icon{ position:absolute; inset:0; margin:auto; width:32px; height:32px; color:#fff; } |
| |
| .kx-intro-title{ font-family:var(--display); font-weight:600; font-size:clamp(26px,3vw,32px); line-height:1.12; color:#fff; margin-bottom:16px; letter-spacing:-0.01em; } |
| .kx-intro-lead{ font-size:14.5px; font-weight:400; line-height:1.7; color:var(--t2); margin:0 auto 26px; max-width:48ch; } |
| .kx-points{ list-style:none; display:flex; flex-direction:column; gap:13px; text-align:left; margin-bottom:28px; } |
| .kx-points li{ position:relative; padding-left:22px; font-size:13.5px; line-height:1.62; color:var(--t2); } |
| .kx-points li::before{ content:""; position:absolute; left:2px; top:0.6em; width:6px; height:6px; border-radius:50%; } |
| .kx-points li:nth-child(1)::before{ background:#ff7a6b; box-shadow:0 0 8px rgba(255,122,107,0.7); } |
| .kx-points li:nth-child(2)::before{ background:#6fb3ff; box-shadow:0 0 8px rgba(111,179,255,0.7); } |
| .kx-points li:nth-child(3)::before{ background:#64ffa0; box-shadow:0 0 8px rgba(100,255,160,0.7); } |
| .kx-points b{ color:var(--t1); font-weight:600; } |
| .kx-intro-hint{ font-family:var(--mono); font-size:11px; letter-spacing:0.08em; text-transform:uppercase; color:var(--t3); } |
| |
| .kx-code::-webkit-scrollbar, .kx-list::-webkit-scrollbar{ width:10px; height:10px; } |
| .kx-code::-webkit-scrollbar-thumb, .kx-list::-webkit-scrollbar-thumb{ background:var(--line); border:3px solid #0a0a0c; border-radius:10px; } |
| body.kx-locked{ overflow:hidden; } |
| @keyframes kxFade{ from{ opacity:0; } to{ opacity:1; } } |
| @keyframes kxRise{ from{ opacity:0; transform:translateY(12px) scale(0.99); } to{ opacity:1; transform:none; } } |
| @keyframes kxSpin{ to{ transform:rotate(360deg); } } |
| @keyframes kxIntroRise{ from{ opacity:0; transform:translateY(12px); } to{ opacity:1; transform:none; } } |
| |
| |
| |
| |
| @keyframes statusPulse{ 0%,100%{ opacity:1; } 50%{ opacity:0.3; } } |
| @keyframes rise{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } } |
| @keyframes blink{ 0%,49%{ opacity:1; } 50%,100%{ opacity:0; } } |
| @keyframes bob{ 0%,60%,100%{ opacity:0.5; transform:translateY(0); } 30%{ opacity:1; transform:translateY(-5px); } } |
| |
| |
| |
| |
| @media (max-width:760px){ |
| .hero-copy{ max-width:88vw; top:clamp(120px,20vh,180px); transform:none; } |
| .subtext{ max-width:34ch; } |
| .brand-tag{ display:none; } |
| .pill#modelcard-btn{ display:none; } |
| .hint{ bottom:22px; } |
| .chat-head-inner{ padding:13px 18px; gap:12px; } |
| .to-top .brand-name{ display:none; } |
| .thread{ padding:28px 18px 20px; } |
| .composer{ padding:0 18px 18px; } |
| .bubble.user{ max-width:88%; } |
| .head-btn{ padding:8px 12px; } |
| } |
| @media (max-width:420px){ .nav{ padding:22px; } } |
| @media (max-width:760px){ |
| .kx{ padding:0; } |
| .kx-panel{ width:100%; height:100%; border-radius:0; border:0; } |
| .kx-body{ grid-template-columns:1fr; grid-template-rows:auto 1fr; } |
| .kx-side{ border-right:0; border-bottom:1px solid var(--line-soft); } |
| .kx-side::after{ display:none; } |
| .kx-list{ flex-direction:row; height:auto; overflow-x:auto; overflow-y:hidden; } |
| .kx-item{ flex-shrink:0; } |
| } |
| |
| :focus-visible{ outline:2px solid var(--cool); outline-offset:3px; border-radius:2px; } |
| |
| @media (prefers-reduced-motion: reduce){ |
| *, *::before, *::after{ animation:none !important; transition:none !important; scroll-behavior:auto !important; } |
| .grain, #bg-amb{ animation:none; } |
| .hero, .subtext, .cta, .hint{ opacity:1 !important; } |
| .word{ opacity:1 !important; filter:none !important; transform:none !important; } |
| .caret{ opacity:1; } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="bg-amb"></div> |
| <div id="scene-wrap"><canvas id="scene"></canvas></div> |
| <div class="vignette"></div> |
| <div class="grain"></div> |
|
|
| |
| <main class="hero ui"> |
| <header class="nav"> |
| <a class="brand" aria-label="Liquid LFM2.5 home"> |
| <span class="brand-glyph"> |
| <svg viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd"><path d="M12.028 8.546l-.008.005 3.03 5.25a3.94 3.94 0 01.643 2.162c0 .754-.212 1.46-.58 2.062l6.173-1.991L11.63 0 9.304 3.872l2.724 4.674zM6.837 24l4.85-4.053h-.013c-2.219 0-4.017-1.784-4.017-3.984 0-.794.235-1.534.64-2.156l2.865-4.976-2.381-4.087L2 16.034 6.83 24h.007zM13.737 19.382h-.001L8.222 24h8.182l4.148-6.769-6.815 2.151z"></path></svg> |
| </span> |
| <span class="brand-name">LIQUID</span> |
| <span class="brand-tag">/ LFM2.5</span> |
| </a> |
| <nav class="nav-right"> |
| <button class="pill" id="modelcard-btn" type="button">MODEL CARD</button> |
| </nav> |
| </header> |
|
|
| <section class="hero-copy"> |
| <h1 class="headline"> |
| <span class="hl-line"><span class="word" style="--d:.05s">Agentic</span> <span class="word" style="--d:.13s">Kernel</span> <span class="word" style="--d:.21s">Optimization</span></span> |
| <span class="hl-line accent"><span class="word" style="--d:.34s">at</span> <span class="word" style="--d:.40s">the</span> <span class="word" style="--d:.46s">scale</span> <span class="word" style="--d:.52s">of</span> <span class="word" style="--d:.58s">the</span> <span class="word" style="--d:.64s">web</span></span> |
| </h1> |
| <p class="subtext"><b>LFM2.5 230M</b> from Liquid AI running entirely locally in the browser on WebGPU, with every kernel optimized by Fable 5 and Opus 4.8. Private by design: no data ever leaves your machine.</p> |
| <button class="cta" id="cta" type="button">LOAD MODEL <span class="cta-arrow">→</span></button> |
| </section> |
|
|
| <div class="hint" aria-hidden="true"> |
| <div class="hint-row" id="hint-text">HOLD TO LOAD MODEL</div> |
| </div> |
|
|
| </main> |
|
|
| <div id="flash"></div> |
|
|
| |
| <section id="page2" class="page2"> |
| <div class="boot"> |
| <div class="boot-logo"> |
| <svg viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd"><path d="M12.028 8.546l-.008.005 3.03 5.25a3.94 3.94 0 01.643 2.162c0 .754-.212 1.46-.58 2.062l6.173-1.991L11.63 0 9.304 3.872l2.724 4.674zM6.837 24l4.85-4.053h-.013c-2.219 0-4.017-1.784-4.017-3.984 0-.794.235-1.534.64-2.156l2.865-4.976-2.381-4.087L2 16.034 6.83 24h.007zM13.737 19.382h-.001L8.222 24h8.182l4.148-6.769-6.815 2.151z"></path></svg> |
| </div> |
| <div class="boot-title">INITIALIZING LFM2.5</div> |
| <div class="boot-bar-wrap"><div id="boot-bar" class="boot-bar"></div></div> |
| <div class="boot-meta"><span id="boot-step">Fetching LFM2.5 weights</span><span id="boot-pct">0%</span></div> |
| <div id="gpu-chip" class="gpu-chip">Checking WebGPU…</div> |
| <div id="boot-error" class="boot-error"> |
| <p class="boot-error-msg"></p> |
| <button id="boot-retry" class="cta boot-retry" type="button">TRY AGAIN <span class="cta-arrow">→</span></button> |
| </div> |
| </div> |
| </section> |
|
|
| |
| <section id="chat"> |
| <header class="chat-head"> |
| <div class="chat-head-inner"> |
| <button class="to-top" id="toHero" type="button" title="Back to intro"> |
| <span class="brand-glyph"> |
| <svg viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd"><path d="M12.028 8.546l-.008.005 3.03 5.25a3.94 3.94 0 01.643 2.162c0 .754-.212 1.46-.58 2.062l6.173-1.991L11.63 0 9.304 3.872l2.724 4.674zM6.837 24l4.85-4.053h-.013c-2.219 0-4.017-1.784-4.017-3.984 0-.794.235-1.534.64-2.156l2.865-4.976-2.381-4.087L2 16.034 6.83 24h.007zM13.737 19.382h-.001L8.222 24h8.182l4.148-6.769-6.815 2.151z"></path></svg> |
| </span> |
| <span class="brand-name">LFM2.5</span> |
| </button> |
| <div class="status ready" id="status"> |
| <span class="status-dot"></span> |
| <span class="status-text" id="statusText">Ready · on-device</span> |
| </div> |
| <div class="chat-head-actions"> |
| <button class="head-btn" id="kernelsBtnChat" type="button">View Kernels</button> |
| <button class="head-btn" id="clearBtn" type="button" disabled>Clear</button> |
| </div> |
| </div> |
| </header> |
|
|
| <div class="thread-scroll" id="threadScroll"> |
| <div class="thread" id="thread"> |
| <div class="welcome" id="welcome"> |
| <h2>What's on your <span class="thin">mind today?</span></h2> |
| <p>LFM2.5 runs entirely on your device.</p> |
| <div class="seeds"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <footer class="composer-wrap"> |
| <div class="composer"> |
| <div class="field"> |
| <textarea id="input" rows="1" placeholder="Ask anything…" disabled></textarea> |
| <button class="icon-button send-button" id="sendBtn" type="button" disabled aria-label="Send message"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg> |
| </button> |
| <button class="icon-button stop-button" id="stopBtn" type="button" aria-label="Stop generation"> |
| <svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1.5"/></svg> |
| </button> |
| </div> |
| <div class="composer-meta"> |
| <span id="hint">Runs fully on-device — nothing leaves your machine</span> |
| <span id="liveStat"></span> |
| </div> |
| </div> |
| </footer> |
| </section> |
|
|
| |
| <div class="kx" id="kernelsOverlay" hidden> |
| <div class="kx-backdrop" data-close></div> |
| <div class="kx-panel" role="dialog" aria-modal="true" aria-label="Rendered kernels"> |
| <div class="kx-head"> |
| <div class="kx-title"> |
| <h3>Kernels</h3> |
| <span class="kx-sub" id="kxSub"></span> |
| </div> |
| <button class="kx-close" id="kxClose" type="button" aria-label="Close" data-close> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg> |
| </button> |
| </div> |
| <div class="kx-body"> |
| <div class="kx-side"><div class="kx-list" id="kxList"></div></div> |
| <div class="kx-view"> |
| <div class="kx-intro" id="kxIntro"> |
| <div class="kx-intro-inner"> |
| <div class="kx-spark" aria-hidden="true"> |
| <span class="kx-spark-ring"></span> |
| <span class="kx-spark-core"></span> |
| <svg class="kx-spark-icon" viewBox="0 0 20 20" fill="currentColor"><path d="M11.7858 1.66699C11.9437 1.66699 12.0957 1.72951 12.2074 1.84115C12.3189 1.95276 12.3815 2.10483 12.3815 2.2627V3.45247H15.3568C15.6725 3.45247 15.9767 3.57757 16.1999 3.80078C16.4231 4.02403 16.5482 4.32813 16.5482 4.64388V7.61914H17.738C17.8957 7.61914 18.0479 7.68181 18.1595 7.79329C18.2712 7.90491 18.3337 8.05698 18.3337 8.21484C18.3336 8.37247 18.2709 8.52323 18.1595 8.63477C18.0478 8.7464 17.8958 8.81055 17.738 8.81055H16.5482V11.1901H17.738C17.8958 11.1901 18.0478 11.2543 18.1595 11.3659C18.2709 11.4774 18.3336 11.6282 18.3337 11.7858C18.3337 11.9437 18.2712 12.0957 18.1595 12.2074C18.0479 12.3188 17.8957 12.3815 17.738 12.3815H16.5482V15.3568C16.5482 15.6725 16.4231 15.9767 16.1999 16.1999C15.9767 16.4231 15.6725 16.5482 15.3568 16.5482H12.3815V17.738C12.3815 17.8959 12.3188 18.0479 12.2074 18.1595C12.0957 18.271 11.9437 18.3337 11.7858 18.3337C11.6282 18.3336 11.4774 18.2707 11.3659 18.1595C11.2543 18.0478 11.1901 17.896 11.1901 17.738V16.5482H8.81055V17.738C8.81055 17.896 8.7464 18.0478 8.63477 18.1595C8.52323 18.2707 8.37247 18.3336 8.21484 18.3337C8.05698 18.3337 7.90491 18.271 7.79329 18.1595C7.68181 18.0479 7.61914 17.8958 7.61914 17.738V16.5482H4.64388C4.32813 16.5482 4.02403 16.4231 3.80078 16.1999C3.57756 15.9767 3.45247 15.6725 3.45247 15.3568V12.3815H2.2627C2.10485 12.3815 1.95276 12.3189 1.84115 12.2074C1.72951 12.0957 1.66699 11.9437 1.66699 11.7858C1.66708 11.6283 1.72987 11.4774 1.84115 11.3659C1.95276 11.2543 2.10483 11.1901 2.2627 11.1901H3.45247V8.81055H2.2627C2.10483 8.81055 1.95276 8.7464 1.84115 8.63477C1.72987 8.52324 1.66704 8.37243 1.66699 8.21484C1.66699 8.05698 1.72951 7.90491 1.84115 7.79329C1.95275 7.68175 2.1049 7.61914 2.2627 7.61914H3.45247V4.64388C3.45247 4.32813 3.57751 4.02403 3.80078 3.80078C4.02403 3.57753 4.32813 3.45247 4.64388 3.45247H7.61914V2.2627C7.61914 2.10488 7.68175 1.95275 7.79329 1.84115C7.90491 1.72951 8.05698 1.66699 8.21484 1.66699C8.37243 1.66704 8.52324 1.72987 8.63477 1.84115C8.7464 1.95276 8.81055 2.10481 8.81055 2.2627V3.45247H11.1901V2.2627C11.1901 2.10481 11.2543 1.95276 11.3659 1.84115C11.4774 1.72987 11.6283 1.66708 11.7858 1.66699ZM6.14616 5.31445C5.6863 5.31489 5.31445 5.68782 5.31445 6.14779V13.8529C5.31445 14.3131 5.68755 14.6862 6.14779 14.6862H13.8529C14.3131 14.6862 14.6862 14.3131 14.6862 13.8529V6.14779C14.6862 5.68755 14.3131 5.31445 13.8529 5.31445H6.14616Z" fill-opacity="0.4"></path><rect x="5.31348" y="5.31445" width="9.37256" height="9.37256" rx="0.833333" fill-opacity="0.15"></rect><path d="M8.19238 7.91225V12.254C8.19238 12.585 8.55699 12.7862 8.83777 12.606L12.2491 10.4351C12.3088 10.3973 12.358 10.3451 12.3921 10.2831C12.4262 10.2212 12.4441 10.1517 12.4441 10.081C12.4441 10.0103 12.4262 9.9408 12.3921 9.87889C12.358 9.81697 12.3088 9.76468 12.2491 9.72688L8.83777 7.56022C8.77456 7.51934 8.70149 7.49627 8.62626 7.49346C8.55103 7.49064 8.47644 7.50819 8.41035 7.54423C8.34426 7.58028 8.28913 7.6335 8.25077 7.69827C8.2124 7.76304 8.19223 7.83697 8.19238 7.91225Z" fill-opacity="0.8"></path></svg> |
| </div> |
| <h2 class="kx-intro-title">What are Kernels?</h2> |
| <p class="kx-intro-lead">Kernels are the low-level GPU programs that do the model's actual math — the matrix multiplications, attention, and normalization behind every token. And how well they're optimized can dramatically speed up inference.</p> |
| <ul class="kx-points"> |
| <li><b>WebGPU & WGSL.</b> Each kernel is a WebGPU compute shader, written in WGSL — the language that runs general-purpose math on the GPU — entirely locally in your browser.</li> |
| <li><b>Agentic Kernel Optimization.</b> Every kernel was generated by AI (in this case, Fable 5 and Opus 4.8), benchmarked on an Apple M4 Max, and refined through an evolutionary, genetic-style search toward the fastest version.</li> |
| <li><b>Blazingly Fast.</b> This means we are able to run LFM2.5 230M at ~1400 tokens/sec on an M4 Max, pushing your device to its limits.</li> |
| </ul> |
| <div class="kx-intro-hint">Select a kernel to read its real source</div> |
| </div> |
| </div> |
| <div class="kx-source" id="kxSource" hidden> |
| <div class="kx-view-head"> |
| <span class="kx-name" id="kxName"></span> |
| <div class="kx-view-actions"> |
| <span class="kx-lines" id="kxLines"></span> |
| <button class="kx-copy" id="kxCopy" type="button">Copy</button> |
| </div> |
| </div> |
| <pre class="kx-code"><code id="kxCode"></code></pre> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| |
| |
| <script type="importmap"> |
| { |
| "imports": { |
| "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js", |
| "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/" |
| } |
| } |
| </script> |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| <script type="module"> |
| import { Lfm2Mobile } from "./lfm2_5.js"; |
| |
| |
| |
| |
| |
| |
| |
| let marked = null; |
| let katexLib = null; |
| const katexCache = new Map(); |
| |
| |
| |
| |
| let katexFragments = null; |
| import("https://esm.sh/marked@17") |
| .then((m) => { |
| marked = m.marked; |
| marked.use({ gfm: true, breaks: true }); |
| return import("https://esm.sh/katex@0.16") |
| .then((k) => { katexLib = k.default ?? k; marked.use(makeKatexExtension()); }) |
| .catch(() => { }); |
| }) |
| .catch(() => { marked = null; }); |
| |
| |
| |
| |
| |
| function makeKatexExtension() { |
| const inline = { |
| name: "katexInline", level: "inline", |
| start(src) { return src.match(/\\\(/)?.index; }, |
| tokenizer(src) { |
| const m = /^\\\(([\s\S]+?)\\\)/.exec(src); |
| if (m) return { type: "katexInline", raw: m[0], text: m[1] }; |
| }, |
| renderer(token) { return stashKatex(token.text, false); }, |
| }; |
| const block = { |
| name: "katexBlock", level: "inline", |
| start(src) { return src.match(/\\\[/)?.index; }, |
| tokenizer(src) { |
| const m = /^\\\[([\s\S]+?)\\\]/.exec(src); |
| if (m) return { type: "katexBlock", raw: m[0], text: m[1] }; |
| }, |
| renderer(token) { return stashKatex(token.text, true); }, |
| }; |
| return { extensions: [inline, block] }; |
| } |
| |
| function renderKatex(text, display) { |
| const key = (display ? "d:" : "i:") + text; |
| let html = katexCache.get(key); |
| if (html === undefined) { |
| try { html = katexLib.renderToString(text.trim(), { throwOnError: false, displayMode: display }); } |
| catch { html = escapeHtml(text); } |
| katexCache.set(key, html); |
| } |
| return html; |
| } |
| |
| |
| |
| |
| function stashKatex(text, display) { |
| const html = renderKatex(text, display); |
| if (!katexFragments) return html; |
| return `<!--katex:${katexFragments.push(html) - 1}-->`; |
| } |
| |
| |
| |
| |
| function trimIncompleteMath(text) { |
| let cut = -1; |
| for (const [open, close] of [["\\[", "\\]"], ["\\(", "\\)"]]) { |
| const lastOpen = text.lastIndexOf(open); |
| if (lastOpen !== -1 && text.indexOf(close, lastOpen + open.length) === -1) { |
| if (cut === -1 || lastOpen < cut) cut = lastOpen; |
| } |
| } |
| return cut === -1 ? text : text.slice(0, cut); |
| } |
| |
| const $ = (id) => document.getElementById(id); |
| |
| |
| const heroEl = document.querySelector(".hero"); |
| const ctaBtn = $("cta"); |
| const hintText = $("hint-text"); |
| const modelCardBtn = $("modelcard-btn"); |
| const page2El = $("page2"); |
| const bootBar = $("boot-bar"); |
| const bootStep = $("boot-step"); |
| const bootPct = $("boot-pct"); |
| const gpuChip = $("gpu-chip"); |
| const bootError = $("boot-error"); |
| const bootRetryBtn = $("boot-retry"); |
| |
| |
| const chatEl = $("chat"); |
| const toHeroBtn = $("toHero"); |
| const statusEl = $("status"); |
| const statusText = $("statusText"); |
| const threadScroll = $("threadScroll"); |
| const thread = $("thread"); |
| const input = $("input"); |
| const sendBtn = $("sendBtn"); |
| const stopBtn = $("stopBtn"); |
| const clearBtn = $("clearBtn"); |
| const hint = $("hint"); |
| const liveStat = $("liveStat"); |
| const kernelsBtnChat = $("kernelsBtnChat"); |
| const kernelsOverlay = $("kernelsOverlay"); |
| |
| |
| |
| const MODEL_ID = "LiquidAI/LFM2.5-230M-GGUF"; |
| const MODEL_CARD_URL = `https://huggingface.co/${MODEL_ID}`; |
| |
| |
| |
| |
| const SEED_EXAMPLES = [ |
| { |
| label: "Extract contact details to JSON", |
| prompt: `Extract the name, company, email, and phone number from this text as JSON:\n\n"Hi, I'm Priya Nair from Acme Robotics — reach me at priya.nair@acme.dev or +1 (415) 555-0173."`, |
| }, |
| { |
| label: "Solve a quadratic equation", |
| prompt: `Solve for x: 2x² + 5x - 3 = 0`, |
| }, |
| { |
| label: "Pull out the dates and amounts", |
| prompt: `Extract the relevant information and create a well-formatted table with appropriate headers:\n\n"Invoice #4471, dated April 9 2026, is due May 1. Subtotal is $3,820.00 with 7% tax and a $500 credit applied."`, |
| }, |
| ]; |
| |
| let model = null; |
| let kernels = []; |
| let kxCopySource = ""; |
| let messages = []; |
| let abortController = null; |
| let isGenerating = false; |
| let loadStarted = false; |
| let bootEntered = false; |
| let loadBlocked = false; |
| let renderScheduled = false; |
| let renderState = null; |
| let lastStreamRenderAt = 0; |
| |
| |
| let bootTarget = 0, bootShown = 0, bootRaf = 0, creepRaf = 0, creepCeil = 0; |
| |
| |
| |
| window.LFMApp = { beginBoot, canLoad: () => !loadBlocked }; |
| |
| |
| requestAnimationFrame(() => requestAnimationFrame(() => document.body.classList.add("ready"))); |
| |
| |
| renderSeeds(document.querySelector("#welcome .seeds")); |
| |
| |
| if (!navigator.gpu) { |
| blockLoad("WebGPU isn't available here. Try a recent Chrome or Edge, or enable WebGPU in your browser."); |
| } else { |
| |
| Lfm2Mobile.checkAvailability(MODEL_ID) |
| .then((res) => { if (!model && !loadStarted && res && !res.ok && res.reason) blockLoad(res.reason); }) |
| .catch(() => { }); |
| } |
| |
| function blockLoad(reason) { |
| loadBlocked = true; |
| ctaBtn.disabled = true; |
| ctaBtn.textContent = "UNAVAILABLE ON THIS DEVICE"; |
| if (hintText) hintText.textContent = reason.length > 96 ? reason.slice(0, 94) + "…" : reason; |
| } |
| |
| |
| |
| |
| ctaBtn.addEventListener("click", (e) => { |
| e.preventDefault(); |
| if (loadBlocked) return; |
| if (window.LFMScene) window.LFMScene.triggerBlast(); |
| else beginBoot(); |
| }); |
| modelCardBtn.addEventListener("click", () => window.open(MODEL_CARD_URL, "_blank", "noopener")); |
| kernelsBtnChat.addEventListener("click", openKernels); |
| toHeroBtn.addEventListener("click", backToHero); |
| bootRetryBtn.addEventListener("click", (e) => { e.preventDefault(); hideBootError(); startRealLoad(); }); |
| |
| kernelsOverlay.addEventListener("click", (e) => { if (e.target.closest("[data-close]")) closeKernels(); }); |
| $("kxList").addEventListener("scroll", updateListFade, { passive: true }); |
| $("kxCopy").addEventListener("click", copyKernel); |
| document.addEventListener("keydown", (e) => { if (e.key === "Escape" && !kernelsOverlay.hidden) closeKernels(); }); |
| |
| sendBtn.addEventListener("click", send); |
| stopBtn.addEventListener("click", () => abortController?.abort()); |
| clearBtn.addEventListener("click", clearChat); |
| input.addEventListener("input", () => { autoGrow(); refreshSend(); }); |
| input.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (!sendBtn.disabled) send(); } |
| }); |
| document.addEventListener("click", (e) => { |
| const seed = e.target.closest(".seed"); |
| if (!seed || seed.disabled || !model || isGenerating) return; |
| input.value = seed.dataset.prompt || seed.textContent; |
| send(); |
| }); |
| |
| |
| |
| |
| |
| |
| |
| function beginBoot() { |
| if (bootEntered) return; |
| bootEntered = true; |
| ctaBtn.disabled = true; |
| heroEl.classList.add("fade-out"); |
| page2El.classList.add("show"); |
| updateGpuChip(); |
| startRealLoad(); |
| } |
| |
| function updateGpuChip() { |
| const ok = !!navigator.gpu; |
| gpuChip.textContent = ok ? "WebGPU available" : "WebGPU unavailable"; |
| gpuChip.classList.toggle("ok", ok); |
| gpuChip.classList.toggle("bad", !ok); |
| } |
| |
| async function startRealLoad() { |
| |
| if (model) { |
| setBootProgressImmediate(1); |
| bootStep.textContent = "Ready · entering chat…"; |
| setTimeout(enterChat, 300); |
| return; |
| } |
| if (loadStarted) return; |
| loadStarted = true; |
| hideBootError(); |
| setBootProgressImmediate(0); |
| startCreep(0.05); |
| bootStep.textContent = "Requesting WebGPU device…"; |
| |
| try { |
| |
| |
| |
| |
| model = await Lfm2Mobile.load(MODEL_ID, { onProgress: onLoadProgress }); |
| stopCreep(); |
| setBootProgress(0.95); |
| |
| |
| |
| |
| bootStep.textContent = "Warming up kernels…"; |
| startCreep(0.98); await model.warmup(); stopCreep(); |
| |
| setBootProgress(1); |
| gpuChip.textContent = "WebGPU ready"; gpuChip.classList.add("ok"); gpuChip.classList.remove("bad"); |
| bootStep.textContent = "Ready · entering chat…"; |
| |
| setTimeout(enterChat, 450); |
| } catch (error) { |
| stopCreep(); |
| console.error(error); |
| loadStarted = false; |
| model = null; |
| showBootError(String(error?.message ?? error)); |
| } |
| } |
| |
| function labelFor(status) { |
| return { |
| init: "Requesting WebGPU device…", |
| tokenizer: "Loading tokenizer…", |
| weights: "Downloading weights…", |
| ready: "Ready.", |
| }[status] ?? status; |
| } |
| |
| function onLoadProgress(event) { |
| if (event.status !== "weights") { |
| bootStep.textContent = labelFor(event.status); |
| setPhaseProgress(event.status, event.fraction); |
| return; |
| } |
| |
| |
| |
| const fraction = finiteNumber(event.fraction) ? clamp(event.fraction, 0, 1) : null; |
| if (fraction !== null) setBootProgress(0.05 + 0.90 * fraction); |
| bootStep.textContent = formatWeightProgress(event, fraction); |
| } |
| |
| |
| |
| |
| |
| function setPhaseProgress(status, frac) { |
| const [lo, hi] = status === "weights" |
| ? [0.05, 0.90] |
| : ({ init: [0, 0.02], tokenizer: [0.02, 0.05], ready: [0.90, 0.90] }[status] ?? [0, 1]); |
| const f = finiteNumber(frac) ? clamp(frac, 0, 1) : 0; |
| setBootProgress(lo + (hi - lo) * f); |
| } |
| |
| |
| |
| function setBootProgress(value) { |
| if (!finiteNumber(value)) return; |
| bootTarget = Math.max(clamp(value, 0, 1), bootTarget); |
| if (!bootRaf) bootRaf = requestAnimationFrame(stepBootBar); |
| } |
| function stepBootBar() { |
| const gap = bootTarget - bootShown; |
| bootShown += gap < 0.0015 ? gap : gap * 0.16; |
| bootBar.style.width = `${(bootShown * 100).toFixed(2)}%`; |
| bootPct.textContent = `${Math.round(bootShown * 100)}%`; |
| bootRaf = bootShown < bootTarget - 0.0002 ? requestAnimationFrame(stepBootBar) : 0; |
| } |
| |
| function setBootProgressImmediate(value) { |
| stopCreep(); |
| if (bootRaf) { cancelAnimationFrame(bootRaf); bootRaf = 0; } |
| bootTarget = bootShown = clamp(value, 0, 1); |
| bootBar.style.width = `${(bootShown * 100).toFixed(2)}%`; |
| bootPct.textContent = `${Math.round(bootShown * 100)}%`; |
| } |
| |
| |
| |
| function startCreep(ceiling) { |
| creepCeil = ceiling; |
| if (!creepRaf) creepRaf = requestAnimationFrame(creepStep); |
| } |
| function creepStep() { |
| const gap = creepCeil - bootTarget; |
| if (gap > 0.0005) { setBootProgress(bootTarget + gap * 0.02); creepRaf = requestAnimationFrame(creepStep); } |
| else creepRaf = 0; |
| } |
| function stopCreep() { if (creepRaf) { cancelAnimationFrame(creepRaf); creepRaf = 0; } } |
| |
| |
| function formatWeightProgress(event, fraction) { |
| const kind = event.kind ?? inferProgressKind(event); |
| const pct = fraction === null ? "" : ` (${Math.round(fraction * 100)}%)`; |
| const loaded = finiteNumber(event.loaded) ? event.loaded : null; |
| const total = finiteNumber(event.total) ? event.total : null; |
| if (kind === "bytes") { |
| const verb = event.fromCache ? "Loading cached weights" : "Downloading weights"; |
| if (loaded !== null && total !== null) return `${verb}: ${formatBytes(loaded)} / ${formatBytes(total)}${pct}`; |
| if (total !== null) return `${verb}: ${formatBytes(total)} total`; |
| return `${event.message || verb}…`; |
| } |
| if (loaded !== null && total !== null) { |
| const label = event.message ? ` (${event.message})` : ""; |
| return `Preparing GPU weights: ${formatInteger(loaded)} / ${formatInteger(total)} tensors${pct}${label}`; |
| } |
| return event.message ? `Preparing GPU weights: ${event.message}` : "Preparing GPU weights…"; |
| } |
| |
| function inferProgressKind(event) { |
| if (event.kind === "bytes" || event.kind === "tensors") return event.kind; |
| if (finiteNumber(event.total) && event.total > 1_000_000) return "bytes"; |
| return "tensors"; |
| } |
| |
| function showBootError(message) { |
| gpuChip.textContent = "Load failed"; |
| gpuChip.classList.add("bad"); gpuChip.classList.remove("ok"); |
| const cantRun = /cannot run on this GPU\/browser|maxBufferSize|storage buffers per shader stage|WebGPU isn't available/i.test(message); |
| bootStep.textContent = cantRun ? "This device can't run the model" : "Couldn't load the model"; |
| bootError.querySelector(".boot-error-msg").textContent = message; |
| bootError.classList.add("show"); |
| } |
| function hideBootError() { bootError.classList.remove("show"); } |
| |
| |
| |
| |
| |
| function enterChat() { |
| if (!model || chatEl.classList.contains("show")) return; |
| window.LFMScene?.stop(); |
| page2El.classList.remove("show"); |
| heroEl.classList.add("fade-out"); |
| chatEl.classList.add("show"); |
| document.body.classList.add("chatting"); |
| input.disabled = false; |
| clearBtn.disabled = false; |
| setSeedButtonsEnabled(true); |
| setStatus("ready", "Ready · on-device"); |
| refreshSend(); |
| input.focus(); |
| } |
| |
| |
| |
| function backToHero() { |
| chatEl.classList.remove("show"); |
| document.body.classList.remove("chatting"); |
| page2El.classList.remove("show"); |
| heroEl.classList.remove("fade-out"); |
| bootEntered = false; |
| if (!loadBlocked) ctaBtn.disabled = false; |
| hideBootError(); |
| setBootProgressImmediate(0); |
| bootStep.textContent = "Fetching LFM2.5 weights"; |
| if (window.LFMScene) window.LFMScene.replay(); |
| } |
| |
| function setStatus(state, text) { |
| statusEl.className = "status" + (state ? " " + state : ""); |
| if (text !== undefined) statusText.textContent = text; |
| } |
| |
| |
| |
| |
| |
| async function send() { |
| const text = input.value.trim(); |
| if (!text || !model || isGenerating) return; |
| |
| removeWelcome(); |
| input.value = ""; |
| autoGrow(); refreshSend(); |
| |
| appendUserMessage(text); |
| messages.push({ role: "user", content: text }); |
| |
| const assistant = appendAssistantMessage(); |
| const bubble = assistant.querySelector(".bubble"); |
| bubble.innerHTML = '<span class="thinking"><span></span><span></span><span></span></span>'; |
| scrollDown(); |
| |
| setGenerating(true); |
| abortController = new AbortController(); |
| |
| let reply = ""; |
| let startedAt = 0, firstTokenAt = 0, endedAt = 0, generatedTokens = 0; |
| |
| try { |
| const stream = model.generate(messages, { maxNewTokens: 4096, signal: abortController.signal }); |
| startedAt = performance.now(); |
| for await (const { text: full } of stream) { |
| const now = performance.now(); |
| if (!firstTokenAt) firstTokenAt = now; |
| generatedTokens++; |
| reply = full; |
| scheduleAssistantRender(bubble, reply); |
| updateLiveStat({ startedAt, firstTokenAt, now, generatedTokens }); |
| } |
| } catch (error) { |
| console.error(error); |
| if (!reply) reply = `_Stopped: ${String(error?.message ?? error)}_`; |
| } finally { |
| endedAt = performance.now(); |
| renderState = null; |
| renderAssistant(bubble, reply, false); |
| appendMeta(assistant, { startedAt, firstTokenAt, endedAt, generatedTokens }); |
| scrollDown(); |
| messages.push({ role: "assistant", content: reply }); |
| setGenerating(false); |
| liveStat.textContent = ""; |
| abortController = null; |
| input.focus(); |
| } |
| } |
| |
| function setGenerating(on) { |
| isGenerating = on; |
| input.disabled = on; |
| clearBtn.disabled = on; |
| sendBtn.style.display = on ? "none" : ""; |
| stopBtn.style.display = on ? "grid" : "none"; |
| setStatus(on ? "busy" : "ready", on ? "Generating…" : "Ready · on-device"); |
| hint.textContent = on ? "Generating on-device…" : "Runs fully on-device — nothing leaves your machine"; |
| refreshSend(); |
| } |
| |
| |
| |
| |
| const LIVE_STAT_MS = 120; |
| let lastLiveStatAt = 0; |
| function updateLiveStat({ startedAt, firstTokenAt, now, generatedTokens }) { |
| if (generatedTokens <= 1) { liveStat.textContent = `TTFT ${(firstTokenAt - startedAt).toFixed(0)} ms`; lastLiveStatAt = now; return; } |
| if (now - lastLiveStatAt < LIVE_STAT_MS) return; |
| lastLiveStatAt = now; |
| const decodeSeconds = Math.max((now - firstTokenAt) / 1000, 1e-9); |
| const tps = (generatedTokens - 1) / decodeSeconds; |
| liveStat.textContent = `${tps.toFixed(0)} tok/s`; |
| } |
| |
| function clearChat() { |
| messages = []; |
| model?.reset(); |
| thread.replaceChildren(createWelcome()); |
| clearBtn.disabled = !model; |
| setSeedButtonsEnabled(Boolean(model)); |
| input.focus(); |
| } |
| |
| function appendUserMessage(text) { |
| const msg = document.createElement("div"); |
| msg.className = "msg user"; |
| msg.appendChild(roleLabel("You")); |
| const bubble = document.createElement("div"); |
| bubble.className = "bubble user"; |
| bubble.textContent = text; |
| msg.appendChild(bubble); |
| thread.appendChild(msg); |
| scrollDown(); |
| return msg; |
| } |
| |
| function appendAssistantMessage() { |
| const msg = document.createElement("div"); |
| msg.className = "msg assistant"; |
| msg.appendChild(roleLabel("LFM2.5")); |
| const bubble = document.createElement("div"); |
| bubble.className = "bubble assistant"; |
| msg.appendChild(bubble); |
| thread.appendChild(msg); |
| return msg; |
| } |
| |
| function roleLabel(text) { |
| const label = document.createElement("div"); |
| label.className = "role"; |
| label.textContent = text; |
| return label; |
| } |
| |
| function appendMeta(msg, timing) { |
| if (timing.generatedTokens <= 0) return; |
| const stats = generationStats(timing); |
| const meta = document.createElement("div"); |
| meta.className = "meta"; |
| const parts = [`${timing.generatedTokens} tok`, `TTFT ${stats.ttftMs.toFixed(0)} ms`]; |
| if (stats.decodeTokensPerSecond > 0) parts.push(`${stats.decodeTokensPerSecond.toFixed(1)} tok/s`); |
| meta.textContent = parts.join(" · "); |
| msg.appendChild(meta); |
| } |
| |
| function generationStats({ startedAt, firstTokenAt, endedAt, generatedTokens }) { |
| if (generatedTokens <= 0 || !startedAt || !firstTokenAt || !endedAt) return { ttftMs: 0, decodeTokensPerSecond: 0 }; |
| const decodeTokens = Math.max(generatedTokens - 1, 0); |
| const decodeSeconds = Math.max((endedAt - firstTokenAt) / 1000, 1e-9); |
| return { ttftMs: firstTokenAt - startedAt, decodeTokensPerSecond: decodeTokens > 0 ? decodeTokens / decodeSeconds : 0 }; |
| } |
| |
| |
| |
| |
| |
| const STREAM_RENDER_MS = 33; |
| function scheduleAssistantRender(bubble, raw) { |
| renderState = { bubble, raw }; |
| if (renderScheduled) return; |
| renderScheduled = true; |
| const tick = () => { |
| if (!renderState) { renderScheduled = false; return; } |
| if (performance.now() - lastStreamRenderAt < STREAM_RENDER_MS) { |
| requestAnimationFrame(tick); |
| return; |
| } |
| renderScheduled = false; |
| lastStreamRenderAt = performance.now(); |
| renderAssistant(renderState.bubble, renderState.raw, true); |
| scrollDown(); |
| }; |
| requestAnimationFrame(tick); |
| } |
| |
| function renderAssistant(bubble, raw, withCaret) { |
| |
| |
| |
| |
| const text = (withCaret && katexLib) ? trimIncompleteMath(raw || "") : (raw || ""); |
| if (marked) { |
| try { |
| katexFragments = []; |
| let html = sanitizeHtml(marked.parse(text)); |
| if (katexFragments.length) { |
| html = html.replace(/<!--katex:(\d+)-->/g, (_, i) => katexFragments[+i] ?? ""); |
| } |
| bubble.innerHTML = html; |
| if (withCaret) appendCaret(bubble); |
| return; |
| } catch { } |
| finally { katexFragments = null; } |
| } |
| const safe = escapeHtml(text); |
| const paragraphs = safe.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean); |
| if (paragraphs.length === 0) { bubble.textContent = ""; return; } |
| bubble.innerHTML = paragraphs.map((p) => `<p>${formatInline(p).replace(/\n/g, "<br>")}</p>`).join(""); |
| if (withCaret) appendCaret(bubble); |
| } |
| |
| function appendCaret(bubble) { |
| const caret = document.createElement("span"); |
| caret.className = "caret"; |
| (bubble.querySelector("p:last-of-type") || bubble).appendChild(caret); |
| } |
| |
| |
| function sanitizeHtml(html) { |
| const tpl = document.createElement("template"); |
| tpl.innerHTML = html; |
| tpl.content.querySelectorAll("script,style,iframe,object,embed,link,meta,form").forEach((el) => el.remove()); |
| tpl.content.querySelectorAll("*").forEach((el) => { |
| for (const attr of [...el.attributes]) { |
| const name = attr.name.toLowerCase(); |
| if (name.startsWith("on") || ((name === "href" || name === "src") && /^\s*(javascript|data):/i.test(attr.value))) { |
| el.removeAttribute(attr.name); |
| } |
| } |
| }); |
| return tpl.innerHTML; |
| } |
| |
| function formatInline(text) { |
| return text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+?)`/g, "<code>$1</code>"); |
| } |
| |
| function removeWelcome() { $("welcome")?.remove(); } |
| |
| |
| |
| function renderSeeds(container) { |
| if (!container) return; |
| container.replaceChildren(...SEED_EXAMPLES.map((s) => { |
| const b = document.createElement("button"); |
| b.className = "seed"; |
| b.type = "button"; |
| b.dataset.prompt = s.prompt; |
| b.textContent = s.label; |
| return b; |
| })); |
| } |
| |
| function createWelcome() { |
| const welcome = document.createElement("div"); |
| welcome.className = "welcome"; |
| welcome.id = "welcome"; |
| welcome.innerHTML = ` |
| <h2>What's on your <span class="thin">mind today?</span></h2> |
| <p>LFM2.5 runs entirely on your device.</p> |
| <div class="seeds"></div>`; |
| renderSeeds(welcome.querySelector(".seeds")); |
| return welcome; |
| } |
| |
| function setSeedButtonsEnabled(enabled) { |
| document.querySelectorAll(".seed").forEach((s) => { s.disabled = !enabled; }); |
| } |
| function refreshSend() { sendBtn.disabled = isGenerating || !model || input.value.trim() === ""; } |
| function autoGrow() { input.style.height = "auto"; input.style.height = `${Math.min(input.scrollHeight, 180)}px`; } |
| function scrollDown() { threadScroll.scrollTop = threadScroll.scrollHeight; } |
| |
| |
| |
| |
| |
| function openKernels() { |
| kernels = model ? (model.runtime.getRenderedShaders?.() ?? []) : []; |
| const list = $("kxList"); |
| list.replaceChildren(); |
| $("kxSub").textContent = kernels.length |
| ? `${kernels.length} WGSL compute shaders · written & optimized by Fable 5 + Opus 4.8 · running on your GPU` |
| : (model ? "No kernels compiled yet — send a message first." : "Load the model to inspect its compiled kernels."); |
| kernels.forEach((k, i) => { |
| const item = document.createElement("button"); |
| item.className = "kx-item"; |
| item.type = "button"; |
| item.textContent = k.name; |
| item.addEventListener("click", () => selectKernel(i)); |
| list.appendChild(item); |
| }); |
| [...list.children].forEach((el) => el.classList.remove("active")); |
| $("kxSource").hidden = true; |
| $("kxIntro").hidden = false; |
| kxCopySource = ""; |
| kernelsOverlay.hidden = false; |
| document.body.classList.add("kx-locked"); |
| list.scrollTop = 0; |
| requestAnimationFrame(updateListFade); |
| } |
| |
| function updateListFade() { |
| const list = $("kxList"); |
| const atEnd = list.scrollHeight <= list.clientHeight + 4 |
| || list.scrollTop >= list.scrollHeight - list.clientHeight - 4; |
| list.parentElement.classList.toggle("at-end", atEnd); |
| } |
| |
| function selectKernel(i) { |
| const k = kernels[i]; |
| if (!k) return; |
| $("kxIntro").hidden = true; |
| $("kxSource").hidden = false; |
| [...$("kxList").children].forEach((el, j) => el.classList.toggle("active", j === i)); |
| $("kxName").textContent = k.name; |
| $("kxLines").textContent = `${k.source.split("\n").length} lines`; |
| $("kxCode").innerHTML = highlightWgsl(k.source); |
| $("kxCode").parentElement.scrollTop = 0; |
| kxCopySource = k.source; |
| } |
| |
| function closeKernels() { |
| kernelsOverlay.hidden = true; |
| document.body.classList.remove("kx-locked"); |
| } |
| |
| async function copyKernel() { |
| if (!kxCopySource) return; |
| try { |
| await navigator.clipboard.writeText(kxCopySource); |
| const btn = $("kxCopy"); |
| btn.textContent = "Copied"; |
| setTimeout(() => { btn.textContent = "Copy"; }, 1200); |
| } catch { } |
| } |
| |
| const WGSL_KEYWORDS = new Set(["fn","let","var","const","const_assert","struct","if","else","for","loop","return","break","continue","switch","case","default","while","override","enable","requires","discard","alias","true","false","workgroup","storage","uniform","function","private","read","write","read_write","bitcast"]); |
| const WGSL_TYPES = new Set(["u32","i32","f32","f16","bool","vec2","vec3","vec4","mat2x2","mat3x3","mat4x4","mat2x3","mat3x2","mat2x4","mat4x2","mat3x4","mat4x3","array","atomic","ptr","sampler"]); |
| const WGSL_TOKEN = /(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(@[A-Za-z_]\w*)|([A-Za-z_]\w*)|(\d[\w.]*)|(\s+)|([\s\S])/g; |
| |
| function highlightWgsl(src) { |
| let out = ""; |
| WGSL_TOKEN.lastIndex = 0; |
| let m; |
| while ((m = WGSL_TOKEN.exec(src))) { |
| const [tok, comment, attr, ident, num, ws] = m; |
| if (comment) out += `<span class="k-cm">${escapeHtml(comment)}</span>`; |
| else if (attr) out += `<span class="k-at">${escapeHtml(attr)}</span>`; |
| else if (ident) { |
| const cls = WGSL_KEYWORDS.has(ident) ? "k-kw" : WGSL_TYPES.has(ident) ? "k-ty" : null; |
| out += cls ? `<span class="${cls}">${ident}</span>` : escapeHtml(ident); |
| } |
| else if (num) out += `<span class="k-nu">${escapeHtml(num)}</span>`; |
| else if (ws) out += ws; |
| else out += escapeHtml(tok); |
| } |
| return out; |
| } |
| |
| |
| function finiteNumber(v) { return typeof v === "number" && Number.isFinite(v); } |
| function clamp(v, min, max) { return Math.min(max, Math.max(min, v)); } |
| function formatInteger(v) { return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(v); } |
| function formatBytes(bytes) { |
| const units = ["B", "KB", "MB", "GB"]; let v = bytes, u = 0; |
| while (v >= 1024 && u < units.length - 1) { v /= 1024; u++; } |
| const digits = u === 3 ? 2 : (v >= 10 || u === 0 ? 0 : 1); |
| return `${v.toFixed(digits)} ${units[u]}`; |
| } |
| const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; |
| function escapeHtml(v) { |
| return v.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]); |
| } |
| </script> |
|
|
| |
| |
| |
| |
| |
| |
| |
| <script type="module"> |
| import * as THREE from 'three'; |
| import { SVGLoader } from 'three/addons/loaders/SVGLoader.js'; |
| import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; |
| import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; |
| import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; |
| import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'; |
| import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js'; |
| |
| |
| |
| |
| const CFG = { |
| TARGET_HEIGHT: 4.6, EXTRUDE_DEPTH: 3.8, BEVEL_THICK: 0.2, BEVEL_SIZE: 0.08, |
| BASE_EMISSIVE: 0.03, HOVER_EMISSIVE: 0.06, CHARGE_EMISSIVE: 1.0, |
| WARM_BASE: 30, ORBIT_R: 4.6, |
| BLOOM_BASE: 0.6, BLOOM_RADIUS: 0.32, BLOOM_THRESHOLD: 0.72, EXPOSURE: 1.12, |
| INTRO_DELAY: 0.25, INTRO_STAGGER: 0.14, INTRO_DUR: 1.5, |
| EXPAND_STRENGTH: 0.42, EXPAND_RANGE: 1.9, |
| FOV: 38, CAM_Z: 12, BASE_ROT: { x:-0.30, y:-0.5, z:0.08 }, |
| SHIMMY_AMP: 0.13, SHIMMY_SPEED: 0.85, |
| }; |
| |
| const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); |
| const lerp=(a,b,t)=>a+(b-a)*t; |
| const rand=(a,b)=>a+Math.random()*(b-a); |
| const easeOutExpo=t=>t>=1?1:1-Math.pow(2,-10*t); |
| |
| const EMISSIVE_BASE=new THREE.Color(0xff5a18); |
| const EMISSIVE_HOT =new THREE.Color(0xffb070); |
| |
| const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; |
| const motion = reduceMotion ? 0.3 : 1.0; |
| |
| |
| const canvas=document.getElementById('scene'); |
| const sceneWrap=document.getElementById('scene-wrap'); |
| const flashEl=document.getElementById('flash'); |
| |
| |
| let renderer,scene,camera,composer,bloom,clock; |
| let tiltGroup,logoGroup; |
| let material; |
| let warmLight; |
| const pieces=[]; |
| const solidMeshes=[]; |
| const orbs=[]; |
| const sparks=[]; |
| const particleData=[]; |
| let ambGroup, particles, shockwave; |
| let shatterMesh, shatterMat, shatterUniform; |
| let raycaster; |
| |
| |
| let state='INTRO'; |
| let stateTime=0, elapsed=0; |
| let charge=0, floatLevel=0, hoverGlow=0, camKick=0; |
| let isHolding=false, autoBlast=false, transitionStarted=false; |
| let sceneActive=true; |
| let rafId=null; |
| |
| const pointerN={x:0,y:0}; |
| |
| function setState(s){ state=s; stateTime=0; } |
| function canLoad(){ return !window.LFMApp || window.LFMApp.canLoad(); } |
| |
| |
| const LOGO_PATH_D="M12.028 8.546l-.008.005 3.03 5.25a3.94 3.94 0 01.643 2.162c0 .754-.212 1.46-.58 2.062l6.173-1.991L11.63 0 9.304 3.872l2.724 4.674zM6.837 24l4.85-4.053h-.013c-2.219 0-4.017-1.784-4.017-3.984 0-.794.235-1.534.64-2.156l2.865-4.976-2.381-4.087L2 16.034 6.83 24h.007zM13.737 19.382h-.001L8.222 24h8.182l4.148-6.769-6.815 2.151z"; |
| const LOGO_SVG=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" d="${LOGO_PATH_D}"/></svg>`; |
| |
| function buildLogo(){ |
| const data=new SVGLoader().parse(LOGO_SVG); |
| const shapes=[]; |
| for(const path of data.paths){ |
| let s; |
| if(typeof SVGLoader.createShapes==='function') s=SVGLoader.createShapes(path); |
| else s=path.toShapes(true); |
| for(const sh of s) shapes.push(sh); |
| } |
| |
| const extrude={ |
| depth:CFG.EXTRUDE_DEPTH, bevelEnabled:true, |
| bevelThickness:CFG.BEVEL_THICK, bevelSize:CFG.BEVEL_SIZE, |
| bevelSegments:3, curveSegments:32, steps:1 |
| }; |
| |
| const geoms=shapes.map(sh=>{ |
| const g=new THREE.ExtrudeGeometry(sh,extrude); |
| g.rotateX(Math.PI); |
| return g; |
| }); |
| |
| const combined=new THREE.Box3(); |
| geoms.forEach(g=>{ g.computeBoundingBox(); combined.union(g.boundingBox); }); |
| const center=new THREE.Vector3(); combined.getCenter(center); |
| const size=new THREE.Vector3(); combined.getSize(size); |
| const scale=CFG.TARGET_HEIGHT/size.y; |
| |
| const corners=[ |
| new THREE.Vector3( 15, 11, 5), |
| new THREE.Vector3(-16,-11, 5), |
| new THREE.Vector3( 16,-12, -5), |
| new THREE.Vector3(-15, 12, -4), |
| ]; |
| |
| geoms.forEach((g,i)=>{ |
| g.translate(-center.x,-center.y,-center.z); |
| g.scale(scale,scale,scale); |
| g.computeBoundingBox(); |
| |
| const mesh=new THREE.Mesh(g,material); |
| logoGroup.add(mesh); |
| solidMeshes.push(mesh); |
| mesh.layers.enable(1); |
| |
| const gc=new THREE.Vector3(); g.boundingBox.getCenter(gc); |
| const outDir=gc.clone(); |
| if(outDir.lengthSq()<1e-6) outDir.set(rand(-1,1),rand(-1,1),rand(-1,1)); |
| outDir.normalize(); |
| |
| const axis=new THREE.Vector3(rand(-1,1),rand(-1,1),rand(-1,1)).normalize(); |
| const angle=rand(Math.PI*1.5,Math.PI*3)*(Math.random()<0.5?-1:1); |
| |
| pieces.push({ |
| mesh, |
| home:new THREE.Vector3(0,0,0), |
| center:gc.clone(), |
| expandCur:new THREE.Vector2(), |
| introStart:corners[i%corners.length].clone(), |
| introAxis:axis, |
| introAngle:angle, |
| introQuat:new THREE.Quaternion().setFromAxisAngle(axis,angle), |
| outDir, |
| }); |
| |
| mesh.position.copy(pieces[pieces.length-1].introStart); |
| mesh.quaternion.copy(pieces[pieces.length-1].introQuat); |
| }); |
| |
| buildShatter(shapes, center, scale, extrude); |
| } |
| |
| |
| function buildShatter(shapes, center, scale, extrude){ |
| const RES=256; |
| const _cv=document.createElement('canvas'); _cv.width=_cv.height=RES; |
| const _ctx=_cv.getContext('2d'); |
| _ctx.fillStyle='#fff'; _ctx.scale(RES/24,RES/24); |
| _ctx.fill(new Path2D(LOGO_PATH_D),'evenodd'); |
| const _img=_ctx.getImageData(0,0,RES,RES).data; |
| const inside=(sx,sy)=>{ |
| const px=Math.floor(sx*RES/24), py=Math.floor(sy*RES/24); |
| if(px<0||py<0||px>=RES||py>=RES) return false; |
| return _img[(py*RES+px)*4+3]>128; |
| }; |
| |
| const baseGeoms=shapes.map(sh=>{ |
| const g=new THREE.ExtrudeGeometry(sh,extrude); |
| g.rotateX(Math.PI); g.translate(-center.x,-center.y,-center.z); g.scale(scale,scale,scale); |
| g.computeBoundingBox(); return g; |
| }); |
| const bb=new THREE.Box3(); baseGeoms.forEach(g=>bb.union(g.boundingBox)); |
| baseGeoms.forEach(g=>g.dispose()); |
| const zBack=bb.min.z, zFront=bb.max.z, depthZ=zFront-zBack; |
| |
| const CELL=0.24, NZ=2, cellZ=depthZ/NZ; |
| const _dir=new THREE.Vector3(); |
| const boxes=[]; |
| for(let ly=bb.min.y; ly<bb.max.y; ly+=CELL){ |
| for(let lx=bb.min.x; lx<bb.max.x; lx+=CELL){ |
| const ccx=lx+CELL*0.5, ccy=ly+CELL*0.5; |
| const sx=ccx/scale+center.x, sy=-(ccy/scale+center.y); |
| if(!inside(sx,sy)) continue; |
| for(let zi=0; zi<NZ; zi++){ |
| const ccz=zBack+cellZ*(zi+0.5); |
| const bg=new THREE.BoxGeometry(CELL*0.95, CELL*0.95, cellZ*0.9); |
| bg.translate(ccx,ccy,ccz); |
| _dir.set(ccx,ccy,ccz); |
| if(_dir.lengthSq()<1e-6) _dir.set(rand(-1,1),rand(-1,1),rand(-1,1)); |
| _dir.normalize(); |
| _dir.x+=rand(-0.2,0.2); _dir.y+=rand(-0.2,0.2); _dir.z+=rand(-0.05,0.45); |
| _dir.normalize(); |
| const ax=new THREE.Vector3(rand(-1,1),rand(-1,1),rand(-1,1)).normalize(); |
| const rnd=Math.random(); |
| const vc=bg.attributes.position.count; |
| const aC=new Float32Array(vc*3),aD=new Float32Array(vc*3),aA=new Float32Array(vc*3),aR=new Float32Array(vc); |
| for(let v=0;v<vc;v++){ |
| aC[v*3]=ccx; aC[v*3+1]=ccy; aC[v*3+2]=ccz; |
| aD[v*3]=_dir.x; aD[v*3+1]=_dir.y; aD[v*3+2]=_dir.z; |
| aA[v*3]=ax.x; aA[v*3+1]=ax.y; aA[v*3+2]=ax.z; |
| aR[v]=rnd; |
| } |
| bg.setAttribute('aCentroid',new THREE.BufferAttribute(aC,3)); |
| bg.setAttribute('aDir',new THREE.BufferAttribute(aD,3)); |
| bg.setAttribute('aAxis',new THREE.BufferAttribute(aA,3)); |
| bg.setAttribute('aRand',new THREE.BufferAttribute(aR,1)); |
| boxes.push(bg); |
| } |
| } |
| } |
| if(!boxes.length) return; |
| const sgeo=mergeGeometries(boxes,false); |
| boxes.forEach(b=>b.dispose()); |
| |
| shatterUniform={value:0}; |
| shatterMat=new THREE.MeshPhysicalMaterial({ |
| color:0x0a0c12, metalness:0.95, roughness:0.2, |
| clearcoat:1.0, clearcoatRoughness:0.2, envMapIntensity:1.6, |
| emissive:0xff6a28, emissiveIntensity:0.0, |
| side:THREE.DoubleSide |
| }); |
| shatterMat.onBeforeCompile=(shader)=>{ |
| shader.uniforms.uProgress=shatterUniform; |
| shader.vertexShader=shader.vertexShader |
| .replace('#include <common>', `#include <common> |
| attribute vec3 aCentroid; attribute vec3 aDir; attribute vec3 aAxis; attribute float aRand; |
| uniform float uProgress; |
| vec3 rotAxis(vec3 v, vec3 axis, float a){ |
| axis=normalize(axis); float c=cos(a), s=sin(a); |
| return v*c + cross(axis,v)*s + axis*dot(axis,v)*(1.0-c); |
| }`) |
| .replace('#include <beginnormal_vertex>', ` |
| float fp=clamp((uProgress-aRand*0.22)/(1.0-aRand*0.22),0.0,1.0); |
| float fe=1.0-pow(1.0-fp,2.6); |
| float fang=fe*(1.6+aRand*4.2); |
| vec3 objectNormal=rotAxis(normalize(normal),aAxis,fang); |
| #ifdef USE_TANGENT |
| vec3 objectTangent=vec3(tangent.xyz); |
| #endif`) |
| .replace('#include <begin_vertex>', ` |
| vec3 rel=position-aCentroid; |
| rel=rotAxis(rel,aAxis,fang); |
| vec3 transformed=aCentroid+rel+aDir*fe*(3.4+aRand*5.4);`); |
| }; |
| shatterMesh=new THREE.Mesh(sgeo,shatterMat); |
| shatterMesh.visible=false; |
| shatterMesh.frustumCulled=false; |
| shatterMesh.layers.enable(1); |
| logoGroup.add(shatterMesh); |
| } |
| |
| |
| function buildEnv(){ |
| const c=document.createElement('canvas'); c.width=1024; c.height=512; |
| const ctx=c.getContext('2d'); |
| const g=ctx.createLinearGradient(0,0,0,512); |
| g.addColorStop(0,'#10151f'); g.addColorStop(0.5,'#080b12'); g.addColorStop(1,'#030406'); |
| ctx.fillStyle=g; ctx.fillRect(0,0,1024,512); |
| const hb=ctx.createLinearGradient(0,255,0,360); |
| hb.addColorStop(0,'rgba(205,216,236,0)'); |
| hb.addColorStop(0.5,'rgba(214,224,242,0.32)'); |
| hb.addColorStop(1,'rgba(205,216,236,0)'); |
| ctx.fillStyle=hb; ctx.fillRect(0,255,1024,105); |
| let rg=ctx.createRadialGradient(370,300,40,370,300,340); |
| rg.addColorStop(0,'rgba(255,150,70,0.62)'); rg.addColorStop(1,'rgba(255,150,70,0)'); |
| ctx.fillStyle=rg; ctx.fillRect(0,0,1024,512); |
| let cg=ctx.createRadialGradient(775,150,40,775,150,360); |
| cg.addColorStop(0,'rgba(95,140,255,0.42)'); cg.addColorStop(1,'rgba(95,140,255,0)'); |
| ctx.fillStyle=cg; ctx.fillRect(0,0,1024,512); |
| |
| const img=ctx.getImageData(0,0,1024,512); const dd=img.data; |
| for(let i=0;i<dd.length;i+=4){ |
| const n=(Math.random()-0.5)*9; |
| dd[i]+=n; dd[i+1]+=n; dd[i+2]+=n; |
| } |
| ctx.putImageData(img,0,0); |
| |
| const tex=new THREE.CanvasTexture(c); |
| tex.mapping=THREE.EquirectangularReflectionMapping; |
| tex.colorSpace=THREE.SRGBColorSpace; |
| const pmrem=new THREE.PMREMGenerator(renderer); |
| const rt=pmrem.fromEquirectangular(tex); |
| scene.environment=rt.texture; |
| tex.dispose(); pmrem.dispose(); |
| } |
| |
| |
| function softSprite(rgb){ |
| const s=128, cv=document.createElement('canvas'); cv.width=cv.height=s; |
| const x=cv.getContext('2d'); |
| const g=x.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2); |
| g.addColorStop(0,`rgba(${rgb},0.9)`); |
| g.addColorStop(0.4,`rgba(${rgb},0.32)`); |
| g.addColorStop(1,`rgba(${rgb},0)`); |
| x.fillStyle=g; x.fillRect(0,0,s,s); |
| const t=new THREE.CanvasTexture(cv); t.colorSpace=THREE.SRGBColorSpace; return t; |
| } |
| function buildAmbience(){ |
| ambGroup=new THREE.Group(); scene.add(ambGroup); |
| const warmTex=softSprite('255,150,80'); |
| const coolTex=softSprite('120,160,255'); |
| const defs=[ |
| {tex:warmTex, n:3, sMin:7, sMax:12, op:0.10}, |
| {tex:coolTex, n:3, sMin:6, sMax:11, op:0.085}, |
| ]; |
| for(const def of defs){ |
| for(let i=0;i<def.n;i++){ |
| const mat=new THREE.SpriteMaterial({ map:def.tex, transparent:true, opacity:def.op, |
| blending:THREE.AdditiveBlending, depthWrite:false }); |
| const sp=new THREE.Sprite(mat); |
| const sc=rand(def.sMin,def.sMax); sp.scale.set(sc,sc,1); |
| const base=new THREE.Vector3(rand(-9,9),rand(-6,6),rand(-16,-7)); |
| sp.position.copy(base); |
| ambGroup.add(sp); |
| orbs.push({ sp, base, op:def.op, |
| ax:rand(1.2,3.0), ay:rand(1.0,2.4), |
| sx:rand(0.04,0.10)*(Math.random()<0.5?-1:1), |
| sy:rand(0.03,0.08)*(Math.random()<0.5?-1:1), |
| ph:rand(0,Math.PI*2), tw:rand(0.2,0.5) }); |
| } |
| } |
| } |
| |
| |
| const _pm=new THREE.Matrix4(), _pq=new THREE.Quaternion(), _pp=new THREE.Vector3(), _ps=new THREE.Vector3(); |
| function buildParticles(){ |
| const N=80; |
| const geo=new THREE.IcosahedronGeometry(1,0); |
| const mat=new THREE.MeshStandardMaterial({ |
| color:0x7184a6, metalness:0.4, roughness:0.55, |
| emissive:0x1d2c49, emissiveIntensity:0.3 |
| }); |
| particles=new THREE.InstancedMesh(geo,mat,N); |
| particles.instanceMatrix.setUsage(THREE.DynamicDrawUsage); |
| for(let i=0;i<N;i++){ |
| particleData.push({ |
| base:new THREE.Vector3(rand(-13,13),rand(-9,9),rand(-15,2)), |
| axis:new THREE.Vector3(rand(-1,1),rand(-1,1),rand(-1,1)).normalize(), |
| spd:rand(0.2,0.8)*(Math.random()<0.5?-1:1), |
| phase:rand(0,Math.PI*2), |
| scl:rand(0.05,0.13), |
| drift:rand(0.3,0.7) |
| }); |
| } |
| scene.add(particles); |
| } |
| function updateParticles(){ |
| if(!particles) return; |
| for(let i=0;i<particleData.length;i++){ |
| const d=particleData[i]; |
| _pq.setFromAxisAngle(d.axis, elapsed*d.spd*motion + d.phase); |
| _pp.set( |
| d.base.x+Math.sin(elapsed*0.2*d.drift+d.phase)*0.6*motion, |
| d.base.y+Math.cos(elapsed*0.16*d.drift+d.phase)*0.5*motion, |
| d.base.z+Math.sin(elapsed*0.13*d.drift+d.phase)*0.4*motion |
| ); |
| _ps.setScalar(d.scl); |
| _pm.compose(_pp,_pq,_ps); |
| particles.setMatrixAt(i,_pm); |
| } |
| particles.instanceMatrix.needsUpdate=true; |
| } |
| |
| |
| function buildShockwave(){ |
| const s=256, cv=document.createElement('canvas'); cv.width=cv.height=s; |
| const x=cv.getContext('2d'); |
| const g=x.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2); |
| g.addColorStop(0.00,'rgba(255,222,188,0)'); |
| g.addColorStop(0.70,'rgba(255,222,188,0)'); |
| g.addColorStop(0.84,'rgba(255,236,212,0.7)'); |
| g.addColorStop(0.93,'rgba(255,178,112,0.14)'); |
| g.addColorStop(1.00,'rgba(255,150,80,0)'); |
| x.fillStyle=g; x.fillRect(0,0,s,s); |
| const tex=new THREE.CanvasTexture(cv); tex.colorSpace=THREE.SRGBColorSpace; |
| const mat=new THREE.MeshBasicMaterial({ map:tex, transparent:true, opacity:0, |
| blending:THREE.AdditiveBlending, depthWrite:false, depthTest:false, side:THREE.DoubleSide }); |
| shockwave=new THREE.Mesh(new THREE.PlaneGeometry(2,2), mat); |
| shockwave.scale.setScalar(0.1); |
| logoGroup.add(shockwave); |
| } |
| |
| |
| function buildSparks(){ |
| const tex=softSprite('255,205,150'); |
| for(let i=0;i<26;i++){ |
| const mat=new THREE.SpriteMaterial({ map:tex, transparent:true, opacity:0, |
| blending:THREE.AdditiveBlending, depthWrite:false, depthTest:false }); |
| const sp=new THREE.Sprite(mat); sp.scale.setScalar(0.001); scene.add(sp); |
| sparks.push({ sp, vel:new THREE.Vector3(), life:0, max:1 }); |
| } |
| } |
| function fireSparks(){ |
| for(const s of sparks){ |
| const dir=new THREE.Vector3(rand(-1,1),rand(-1,1),rand(-0.5,0.7)).normalize(); |
| s.vel.copy(dir).multiplyScalar(rand(6,13)); |
| s.life=s.max=rand(0.5,0.95); |
| s.sp.position.set(rand(-0.3,0.3),rand(-0.3,0.3),rand(-0.3,0.3)); |
| s.sp.material.opacity=1; |
| } |
| } |
| function updateSparks(dt){ |
| const drag=Math.pow(0.2,dt); |
| for(const s of sparks){ |
| if(s.life>0){ |
| s.life-=dt; |
| s.sp.position.addScaledVector(s.vel,dt); |
| s.vel.multiplyScalar(drag); |
| const f=Math.max(0,s.life/s.max); |
| s.sp.material.opacity=f; |
| s.sp.scale.setScalar(0.05+0.4*f); |
| if(s.life<=0) s.sp.material.opacity=0; |
| } |
| } |
| } |
| |
| |
| function updateAmbience(){ |
| for(const o of orbs){ |
| o.sp.position.set( |
| o.base.x+Math.sin(elapsed*o.sx+o.ph)*o.ax, |
| o.base.y+Math.cos(elapsed*o.sy+o.ph)*o.ay, |
| o.base.z |
| ); |
| o.sp.material.opacity=o.op*(0.7+0.3*Math.sin(elapsed*o.tw+o.ph)); |
| } |
| if(ambGroup){ ambGroup.position.x=pointerN.x*0.2; ambGroup.position.y=pointerN.y*0.15; } |
| } |
| |
| |
| function startExplosion(){ |
| setState('EXPLODING'); |
| tiltGroup.scale.setScalar(1); |
| logoGroup.scale.setScalar(1); |
| hoverGlow=0; |
| transitionStarted=false; |
| for(const p of pieces) p.mesh.visible=false; |
| if(shatterMesh){ shatterMesh.visible=true; shatterMat.emissiveIntensity=0.0; } |
| if(shatterUniform) shatterUniform.value=0; |
| warmLight.position.set(0,0,2.5); |
| fireSparks(); |
| if(shockwave){ shockwave.position.set(0,0,0); shockwave.scale.setScalar(0.5); shockwave.material.opacity=0.7; } |
| camKick=1.0; |
| flashEl.style.transition='opacity 0.06s ease-out'; flashEl.style.opacity='0.4'; |
| requestAnimationFrame(()=>requestAnimationFrame(()=>{ |
| flashEl.style.transition='opacity 0.75s ease-out'; flashEl.style.opacity='0'; |
| })); |
| warmLight.intensity=175; warmLight.color.setHSL(0.07,0.78,0.58); |
| bloom.strength=1.4; |
| } |
| |
| |
| function beginTransition(){ |
| sceneWrap.classList.add('dim'); |
| window.LFMApp?.beginBoot(); |
| } |
| |
| |
| function sceneReplay(){ |
| setState('INTRO'); charge=0; floatLevel=0; hoverGlow=0; camKick=0; |
| isHolding=false; autoBlast=false; transitionStarted=false; |
| material.opacity=1; material.emissive.copy(EMISSIVE_BASE); material.emissiveIntensity=CFG.BASE_EMISSIVE; |
| logoGroup.scale.setScalar(1); |
| for(const p of pieces){ p.mesh.position.copy(p.introStart); p.mesh.quaternion.copy(p.introQuat); p.expandCur.set(0,0); p.mesh.visible=true; } |
| if(shatterMesh) shatterMesh.visible=false; |
| if(shatterUniform) shatterUniform.value=0; |
| if(shockwave) shockwave.material.opacity=0; |
| for(const s of sparks){ s.life=0; s.sp.material.opacity=0; } |
| warmLight.intensity=CFG.WARM_BASE; warmLight.color.setHSL(0.06,1,0.5); |
| bloom.strength=CFG.BLOOM_BASE; |
| tiltGroup.scale.setScalar(1); |
| sceneWrap.classList.remove('dim'); |
| sceneActive=true; |
| if(rafId===null){ clock.getDelta(); animate(); } |
| } |
| |
| |
| function stopScene(){ sceneActive=false; if(rafId){ cancelAnimationFrame(rafId); rafId=null; } } |
| |
| |
| function applyFloat(p){ |
| p.mesh.position.set(p.expandCur.x, p.expandCur.y, 0); |
| p.mesh.quaternion.identity(); |
| } |
| |
| |
| const _eRay=new THREE.Ray(); |
| const _ePlane=new THREE.Plane(new THREE.Vector3(0,0,1),0); |
| const _eHit=new THREE.Vector3(); |
| const _eInv=new THREE.Matrix4(); |
| function updateExpand(dt){ |
| let haveHit=false, hx=0, hy=0; |
| if(state==='IDLE'){ |
| _eInv.copy(logoGroup.matrixWorld).invert(); |
| _eRay.copy(raycaster.ray).applyMatrix4(_eInv); |
| if(_eRay.intersectPlane(_ePlane,_eHit)){ hx=_eHit.x; hy=_eHit.y; haveHit=true; } |
| } |
| const k=Math.min(1,dt*6); |
| pieces.forEach(p=>{ |
| let tx=0, ty=0; |
| if(haveHit){ |
| const ox=p.center.x-hx, oy=p.center.y-hy; |
| const d=Math.hypot(ox,oy)+0.001; |
| const push=CFG.EXPAND_STRENGTH*Math.exp(-d/CFG.EXPAND_RANGE); |
| tx=(ox/d)*push; ty=(oy/d)*push; |
| } |
| p.expandCur.x+=(tx-p.expandCur.x)*k; |
| p.expandCur.y+=(ty-p.expandCur.y)*k; |
| }); |
| } |
| |
| const _chgEuler=new THREE.Euler(); |
| function applyCharge(){ |
| const c=charge, c2=c*c; |
| pieces.forEach((p,i)=>{ |
| const rA=0.11*c2; |
| const rx=(Math.sin(elapsed*48+i)*0.6+(Math.random()*2-1)*0.4)*rA; |
| const ry=(Math.cos(elapsed*53+i)*0.6+(Math.random()*2-1)*0.4)*rA; |
| const rz=(Math.sin(elapsed*44+i)*0.6+(Math.random()*2-1)*0.4)*rA; |
| const pull=-0.12*c; |
| p.mesh.position.set( |
| p.expandCur.x + rx + p.outDir.x*pull, |
| p.expandCur.y + ry + p.outDir.y*pull, |
| rz + p.outDir.z*pull |
| ); |
| const j=0.08*c; |
| _chgEuler.set(Math.sin(elapsed*46+i)*j, Math.cos(elapsed*43+i)*j, Math.sin(elapsed*49+i)*j); |
| p.mesh.quaternion.setFromEuler(_chgEuler); |
| }); |
| material.emissiveIntensity=CFG.BASE_EMISSIVE+c2*CFG.CHARGE_EMISSIVE; |
| material.emissive.copy(EMISSIVE_BASE).lerp(EMISSIVE_HOT,c2); |
| warmLight.intensity=CFG.WARM_BASE+c*110; |
| warmLight.color.setHSL(lerp(0.06,0.12,c),lerp(1,0.6,c),lerp(0.5,0.78,c)); |
| bloom.strength=CFG.BLOOM_BASE+c*0.55; |
| tiltGroup.scale.setScalar(1-0.03*c); |
| } |
| |
| function toBase(dt,k){ |
| const emiTarget=CFG.BASE_EMISSIVE+hoverGlow*CFG.HOVER_EMISSIVE; |
| const warmTarget=CFG.WARM_BASE+hoverGlow*14; |
| const bloomTarget=CFG.BLOOM_BASE+hoverGlow*0.06; |
| warmLight.intensity=lerp(warmLight.intensity,warmTarget,dt*k); |
| warmLight.color.setHSL(0.06,1,0.5); |
| material.emissive.copy(EMISSIVE_BASE); |
| material.emissiveIntensity=lerp(material.emissiveIntensity,emiTarget,dt*k); |
| bloom.strength=lerp(bloom.strength,bloomTarget,dt*k); |
| } |
| |
| |
| const _ndc=new THREE.Vector2(); |
| function updateHover(dt){ |
| let over=false; |
| if(state==='INTRO'||state==='IDLE'){ |
| over=raycaster.intersectObjects(solidMeshes,false).length>0; |
| } |
| const target=over?1:0; |
| hoverGlow+=(target-hoverGlow)*Math.min(1,dt*8); |
| } |
| |
| |
| function update(dt){ |
| if(state!=='INTRO') floatLevel=Math.min(1,floatLevel+dt/0.7); |
| if(state==='IDLE' && (isHolding||autoBlast)) setState('CHARGING'); |
| _ndc.set(pointerN.x,pointerN.y); |
| raycaster.setFromCamera(_ndc,camera); |
| updateHover(dt); |
| updateExpand(dt); |
| |
| if(state==='INTRO'){ |
| let done=true; |
| pieces.forEach((p,i)=>{ |
| const local=clamp((stateTime-CFG.INTRO_DELAY-i*CFG.INTRO_STAGGER)/CFG.INTRO_DUR,0,1); |
| if(local<1) done=false; |
| const e=easeOutExpo(local); |
| p.mesh.position.lerpVectors(p.introStart,p.home,e); |
| p.mesh.quaternion.setFromAxisAngle(p.introAxis,p.introAngle*(1-e)); |
| }); |
| toBase(dt,4); |
| if(done) setState('IDLE'); |
| } else if(state==='IDLE'){ |
| pieces.forEach(applyFloat); |
| toBase(dt,4); |
| } else if(state==='CHARGING'){ |
| const rate=autoBlast?1/0.6:1/1.0; |
| if(isHolding||autoBlast) charge=Math.min(1,charge+dt*rate); |
| else charge=Math.max(0,charge-dt/0.35); |
| applyCharge(); |
| if(charge>=1) startExplosion(); |
| else if(charge<=0 && !isHolding && !autoBlast){ tiltGroup.scale.setScalar(1); setState('IDLE'); } |
| } else if(state==='EXPLODING'){ |
| const t=stateTime; |
| if(shatterUniform) shatterUniform.value=Math.min(1,t/1.2); |
| if(shatterMat) shatterMat.emissiveIntensity=Math.max(0,0.55*Math.exp(-t*3.2)); |
| warmLight.intensity=lerp(warmLight.intensity,CFG.WARM_BASE,dt*2.0); |
| bloom.strength=lerp(bloom.strength,CFG.BLOOM_BASE,dt*1.7); |
| if(shockwave){ |
| const st=clamp(t/0.55,0,1), e=1-(1-st)*(1-st); |
| shockwave.scale.setScalar(0.5+e*5.5); |
| shockwave.material.opacity=0.7*(1-st)*(1-st); |
| } |
| if(t>0.32 && !transitionStarted){ transitionStarted=true; beginTransition(); } |
| if(t>1.5) setState('DONE'); |
| } else if(state==='DONE'){ |
| toBase(dt,4); |
| if(sceneActive) stopScene(); |
| } |
| |
| if(state!=='EXPLODING' && state!=='DONE'){ |
| warmLight.position.set( |
| Math.cos(elapsed*0.6)*CFG.ORBIT_R, |
| Math.sin(elapsed*0.45)*2.2+1.0, |
| Math.sin(elapsed*0.6)*CFG.ORBIT_R+1.5 |
| ); |
| } |
| |
| camKick=lerp(camKick,0,dt*4); |
| const tpx=pointerN.x*0.22, tpy=pointerN.y*0.16; |
| camera.position.x+=(tpx-camera.position.x)*0.05 + (Math.random()*2-1)*camKick*0.018; |
| camera.position.y+=(tpy-camera.position.y)*0.05 + (Math.random()*2-1)*camKick*0.018; |
| camera.position.z=CFG.CAM_Z; |
| camera.lookAt(0,0,0); |
| |
| logoGroup.position.set(Math.sin(elapsed*0.35)*0.02*floatLevel, Math.sin(elapsed*0.5)*0.035*floatLevel, 0); |
| logoGroup.rotation.set(0,0,0); |
| logoGroup.scale.setScalar(1); |
| |
| const shimmy=Math.sin(elapsed*CFG.SHIMMY_SPEED)*CFG.SHIMMY_AMP*motion |
| +Math.sin(elapsed*CFG.SHIMMY_SPEED*0.53+1.3)*CFG.SHIMMY_AMP*0.4*motion; |
| tiltGroup.rotation.x=CFG.BASE_ROT.x+Math.sin(elapsed*0.34)*0.035*motion-pointerN.y*0.035*motion; |
| tiltGroup.rotation.y=CFG.BASE_ROT.y+shimmy+pointerN.x*0.045*motion; |
| tiltGroup.rotation.z=CFG.BASE_ROT.z+Math.sin(elapsed*CFG.SHIMMY_SPEED*0.7+0.6)*CFG.SHIMMY_AMP*0.28*motion; |
| |
| updateAmbience(); |
| updateParticles(); |
| updateSparks(dt); |
| } |
| |
| |
| function onResize(){ |
| const w=window.innerWidth, h=window.innerHeight; |
| const dpr=Math.min(window.devicePixelRatio,2); |
| camera.aspect=w/h; camera.updateProjectionMatrix(); |
| renderer.setPixelRatio(dpr); |
| renderer.setSize(w,h); |
| composer.setPixelRatio(dpr); |
| composer.setSize(w,h); |
| } |
| |
| |
| function animate(){ |
| if(!sceneActive){ rafId=null; return; } |
| rafId=requestAnimationFrame(animate); |
| const dt=Math.min(clock.getDelta(),0.05); |
| elapsed+=dt; stateTime+=dt; |
| update(dt); |
| composer.render(); |
| document.body.classList.add('ready'); |
| } |
| document.addEventListener('visibilitychange',()=>{ |
| if(document.hidden){ if(rafId){ cancelAnimationFrame(rafId); rafId=null; } } |
| else if(rafId===null && sceneActive){ clock.getDelta(); animate(); } |
| }); |
| |
| |
| function init(){ |
| const test=document.createElement('canvas'); |
| if(!(test.getContext('webgl2')||test.getContext('webgl'))){ |
| |
| document.body.classList.add('ready'); |
| return; |
| } |
| |
| renderer=new THREE.WebGLRenderer({ canvas, antialias:false, alpha:true, powerPreference:'high-performance' }); |
| renderer.setPixelRatio(Math.min(window.devicePixelRatio,2)); |
| renderer.setSize(window.innerWidth,window.innerHeight); |
| renderer.setClearColor(0x000000,0); |
| renderer.toneMapping=THREE.ACESFilmicToneMapping; |
| renderer.toneMappingExposure=CFG.EXPOSURE; |
| renderer.outputColorSpace=THREE.SRGBColorSpace; |
| |
| scene=new THREE.Scene(); |
| camera=new THREE.PerspectiveCamera(CFG.FOV,window.innerWidth/window.innerHeight,0.1,200); |
| camera.position.set(0,0,CFG.CAM_Z); |
| clock=new THREE.Clock(); |
| |
| tiltGroup=new THREE.Group(); |
| logoGroup=new THREE.Group(); |
| tiltGroup.add(logoGroup); |
| tiltGroup.rotation.set(CFG.BASE_ROT.x,CFG.BASE_ROT.y,CFG.BASE_ROT.z); |
| scene.add(tiltGroup); |
| |
| material=new THREE.MeshPhysicalMaterial({ |
| color:0x0a0c12, metalness:0.9, roughness:0.4, |
| clearcoat:0.9, clearcoatRoughness:0.42, envMapIntensity:1.3, |
| emissive:0xff5a18, emissiveIntensity:CFG.BASE_EMISSIVE, |
| side:THREE.FrontSide, transparent:true |
| }); |
| |
| raycaster=new THREE.Raycaster(); |
| |
| buildEnv(); |
| buildLogo(); |
| buildAmbience(); |
| buildParticles(); |
| buildShockwave(); |
| buildSparks(); |
| |
| const hemi=new THREE.HemisphereLight(0x38445e,0x070a12,0.45); scene.add(hemi); |
| const keyLight=new THREE.DirectionalLight(0x9ab4ff,1.4); keyLight.position.set(-7,8,6); scene.add(keyLight); |
| const rimLight=new THREE.DirectionalLight(0xffcaa0,1.9); rimLight.position.set(5,3,-7); scene.add(rimLight); |
| const fillLight=new THREE.DirectionalLight(0x5a78c8,0.45); fillLight.position.set(4,-4,5); scene.add(fillLight); |
| warmLight=new THREE.PointLight(0xff7a30,CFG.WARM_BASE,0,2); warmLight.layers.set(1); scene.add(warmLight); |
| |
| const dpr=Math.min(window.devicePixelRatio,2); |
| const rt=new THREE.WebGLRenderTarget(window.innerWidth,window.innerHeight,{ |
| type:THREE.HalfFloatType, samples:4 |
| }); |
| composer=new EffectComposer(renderer,rt); |
| composer.addPass(new RenderPass(scene,camera)); |
| bloom=new UnrealBloomPass(new THREE.Vector2(window.innerWidth,window.innerHeight),CFG.BLOOM_BASE,CFG.BLOOM_RADIUS,CFG.BLOOM_THRESHOLD); |
| composer.addPass(bloom); |
| composer.addPass(new OutputPass()); |
| composer.setPixelRatio(dpr); |
| composer.setSize(window.innerWidth,window.innerHeight); |
| |
| window.addEventListener('resize',onResize); |
| try{ renderer.compile(scene,camera); }catch(e){} |
| animate(); |
| |
| |
| window.LFMScene = { triggerBlast, replay: sceneReplay, stop: stopScene }; |
| } |
| |
| function triggerBlast(){ |
| if(!canLoad()) return; |
| if(state==='IDLE'||state==='INTRO'){ autoBlast=true; if(state==='IDLE') setState('CHARGING'); } |
| } |
| |
| |
| |
| canvas.addEventListener('pointerdown',()=>{ if(!canLoad()) return; if(state!=='EXPLODING'&&state!=='DONE') isHolding=true; }); |
| window.addEventListener('pointerup',()=>{ isHolding=false; }); |
| canvas.addEventListener('pointercancel',()=>{ isHolding=false; }); |
| window.addEventListener('pointermove',e=>{ |
| pointerN.x=(e.clientX/window.innerWidth)*2-1; |
| pointerN.y=-((e.clientY/window.innerHeight)*2-1); |
| }); |
| |
| window.addEventListener('keydown',e=>{ |
| if(e.code==='Space' && canLoad() && state!=='EXPLODING' && state!=='DONE' |
| && document.activeElement?.tagName!=='TEXTAREA' && document.activeElement?.tagName!=='INPUT'){ |
| e.preventDefault(); isHolding=true; |
| } |
| }); |
| window.addEventListener('keyup',e=>{ if(e.code==='Space') isHolding=false; }); |
| |
| |
| try{ init(); }catch(err){ console.error(err); document.body.classList.add('ready'); } |
| </script> |
| </body> |
| </html> |
|
|