feat(design):grok 图标动效复刻

This commit is contained in:
Edom
2026-08-14 02:24:27 +08:00
commit 3c28de92b7
23 changed files with 4124 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Grok Bot 角色复刻(个人学习)
素材归 xAI / 相应权利人所有,仅供个人学习,请勿商用或再分发。
登录页那个会眨眼的角色不是 GIF / Lottie,是 React 组件 `$_t` + 登录包装 `sd()`。几何和弹簧都在 Grok Bot.app v0.18.0 的主包里。
## 打开
浏览器打开 [`replica/index.html`](./replica/index.html)。
登录页原参数:`sizePx: 64`,`color: "black"`,`shape: "blob"`。
`pjn(n) = n%2===0 ? idle : cSe[(n-1)/2]`,间隔 1200ms。
playground 有意从 curious 起播;点「登录轮换」才按 `pjn(0)=idle` 重来。大预览默认开指针跟随,源码默认关。
## 目录
| 路径 | 内容 |
|---|---|
| `replica/index.html` | L1 舞台 |
| `replica/src/` | 拆开的引擎:character / pose / tricks / eyes / fx / math / tables |
| `replica/geometry-data.js` | `u3` 25 眼、`Jo` 18 身形、`snt` 11 色 |
| `replica/grok-blob-idle.svg` | 静态 blob + idle `u3[0]` |
| `extracted/index-UbX-y3il.js` | 主包事实源(约 5.5MB) |
| `extracted/*-raw.js` | 从主包切出的 geometry / state / spring / onboarding 片段 |
| `extract-asar.js` | 从本机 `.app` 再解 asar 的脚本 |
| `assets/icon.iconset/`、`assets/app-icon-256.png` | Dock / 应用内静态商标(不是角色引擎) |
事实源只认主包,不认截图。本机路径:
`/Applications/Grok Bot.app/Contents/Resources/app.asar`
→ `dist/renderer/assets/index-UbX-y3il.js`
重新抽出:
```bash
node extract-asar.js "/Applications/Grok Bot.app/Contents/Resources/app.asar" /tmp/grok-asar
```
只要主包 JS,不要把整包 asar 再拷进仓库。
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env node
// 解析 Electron asar 归档:列出所有文件并提取图标/logo/svg/png 相关资源
// 用法: node extract-asar.js <app.asar 路径> <输出目录>
const fs = require('fs');
const path = require('path');
const asarPath = process.argv[2];
const outDir = process.argv[3] || 'extracted';
if (!asarPath) { console.error('usage: node extract-asar.js <app.asar> [outdir]'); process.exit(1); }
const fd = fs.openSync(asarPath, 'r');
// 实际布局: [4B: 4][4B: pickleSize][4B: strLen+5][4B: strLen][JSON @16][1B: \0][数据...]
const head = Buffer.alloc(16);
fs.readSync(fd, head, 0, 16, 0);
const pickleSize = head.readUInt32LE(4);
const strLen = head.readUInt32LE(12);
console.log('pickleSize:', pickleSize, 'strLen:', strLen);
const jsonBuf = Buffer.alloc(strLen);
fs.readSync(fd, jsonBuf, 0, strLen, 16);
const header = JSON.parse(jsonBuf.toString('utf8'));
const dataStart = 16 + strLen + 1;
console.log('dataStart =', dataStart, '\n');
// 遍历文件树
const interesting = [];
const allFiles = [];
function walk(node, prefix) {
for (const [name, info] of Object.entries(node.files || {})) {
const p = prefix ? `${prefix}/${name}` : name;
if (info.files) { walk(info, p); continue; }
allFiles.push({ p, size: info.size, offset: info.offset, unpacked: !!info.unpacked });
const lower = p.toLowerCase();
if (/(icon|logo|grok|swirl|sparkle|favicon|mark\b|brand|symbol)/.test(lower) && /\.(svg|png|webp|jpg|jpeg|icns|ico)$/.test(lower)) {
interesting.push({ p, size: info.size, offset: info.offset, unpacked: !!info.unpacked });
}
}
}
walk(header, '');
console.log(`总文件数: ${allFiles.length}`);
console.log(`图标/logo 相关候选: ${interesting.length}\n`);
for (const f of interesting) {
console.log(` ${String(f.size).padStart(9)} ${f.p}${f.unpacked ? ' [unpacked]' : ''}`);
}
// 提取
fs.mkdirSync(outDir, { recursive: true });
let n = 0;
for (const f of interesting) {
const out = path.join(outDir, f.p);
fs.mkdirSync(path.dirname(out), { recursive: true });
if (f.unpacked) {
const src = path.join(path.dirname(asarPath) + '.unpacked', f.p);
if (fs.existsSync(src)) { fs.copyFileSync(src, out); n++; continue; }
}
if (f.offset === undefined) continue;
const buf = Buffer.alloc(f.size);
fs.readSync(fd, buf, 0, f.size, dataStart + parseInt(f.offset));
fs.writeFileSync(out, buf);
n++;
}
console.log(`\n已提取 ${n} 个文件到 ${outDir}/`);
fs.closeSync(fd);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
cSe=["curious","happy","playful","excited","listening","proud","laughing","shy"],djn=1200,mjn=30,fjn=cSe.length*2*mjn,hjn=T2({name:"onboarding-landing-mood",intervalMs:djn});function pjn(n){return n%2===0?"idle":cSe[(n-1)/2%cSe.length]??"idle"}const VD={root:n=>[rjn,{"--x-transform":`translateY(${n}px)`!=null?`translateY(${n}px)`:void 0}],cta:{kZKoxP:"sand-1vqgdyp",kE3dHu:"sand-d3ty66",kpe85a:"sand-8e5d8q",kOIVth:"sand-1nejdyq",kGuDYH:"sand-osj86m",kLWn49:"sand-ggjnk3",kMwMTN:"sand-5a26a2",kWkgg
+1
View File
@@ -0,0 +1 @@
const tc=n=>({x:n,v:0,t:n}),xl=(n,e,t,s)=>{n.v+=(-2*t*e*n.v-e*e*(n.x-n.t))*s,n.x+=n.v*s,(!Number.isFinite(n.x)||!Number.isFinite(n.v))&&(n.x=n.t,n.v=0)},T_t=1/120,S_t=n=>Math.max(1,Math.ceil(n/T_t)),$t=(n,e)=>n+Math.random()*(e-n),zn=(n,e,t)=>Math.min(t,Math.max(e,n)),K2=n=>n<.5?4*n*n*n:1-Math.pow(-2*n+2,3)/2,Rc=n=>1-Math.pow(1-n,3),y1e=n=>1+2.70158*Math.pow(n-1,3)+1.70158*Math.pow(n-1,2),Dke=n=>n*n*(3-2*n),x_t=(n,e)=>1-Math.exp(Math.log(1-n)*60*e),k1e=["#f9705c","#5b95f0","#3fbe86","#f5b13f","#9a72ee","#35c3bd"],kie="http://www.w3.org/2000/svg",sae={width:1,smallBoost:1,length:1,lanes:1,hueDrift:1},GBe=5,N_t=.09;
+1
View File
@@ -0,0 +1 @@
const g_t=[{label:"Lifecycle",states:["sleeping","waking","idle","listening","thinking","searching","working"]},{label:"Reactions",states:["excited","surprised","suspicious","angry","drowsy","happy","curious","confused","bored","proud","shy","sad","laughing","scared","playful","celebrate"]},{label:"Agent morphs",states:["orbit","radar","progress"]},{label:"Product lifecycle",states:["spawning","humming","loading","dictating","writing","sending","receiving","uploading","notifying","alerting","dragging","bouncing","powering-down"]}];g_t.flatMap(n=>n.states);const y_t=new Set(["progress","spawning"]),Fhe={progress:2500,spawning:2e3},k_t=1500,g1e={sleeping:[13,22,4],waking:[13],idle:[0,8],listening:[10,1,19],thinking:[8,16,14,17,5],searching:[15,9,3,20,12,18],working:[7,16,11,10],excited:[2,17,21,3,11],surprised:[3,21],suspicious:[14,5,23],angry:[7,16],drowsy:[4,22,13],happy:[2,11,17,19],curious:[3,21,0,15],confused:[14,5,8],bored:[4,22,0],proud:[15,8,2],shy:[0,24,13],sad:[4,13,22],laughing:[2,11,17],scared:[3,21],playful:[2,17,11,8],celebrate:[2,8,17],orbit:[0,8],radar:[0,8],progress:[0,8],spawning:[3,0],humming:[0,8],loading:[0,8],dictating:[10,1,19],sending:[0,8],receiving:[19,0,8],uploading:[15,9,8],writing:[15,9],notifying:[3,21,0],alerting:[3,21],bouncing:[2,17],dragging:[3,15,0],"powering-down":[13,22]},VBe={sleeping:[6e3,1e4],waking:[800,800],idle:[9e3,16e3],listening:[2800,5e3],thinking:[2e3,3600],searching:[1e3,1800],working:[1800,3200],excited:[1100,2e3],surprised:[2500,4e3],suspicious:[2600,4500],angry:[2200,3800],drowsy:[4e3,8e3],happy:[2500,4500],curious:[1800,3200],confused:[2200,3800],bored:[3500,6e3],proud:[3500,6e3],shy:[3e3,5500],sad:[4e3,7e3],laughing:[1200,2400],scared:[900,1800],playful:[1500,3e3],celebrate:[1400,2600],orbit:[4e3,8e3],radar:[4e3,8e3],progress:[4e3,8e3],spawning:[1200,1200],humming:[5e3,9e3],loading:[6e3,1e4],dictating:[4e3,8e3],sending:[4e3,8e3],receiving:[4e3,8e3],uploading:[4e3,8e3],writing:[4e3,8e3],notifying:[1500,2600],alerting:[2e3,3600],bouncing:[3e3,6e3],dragging:[1600,3e3],"powering-down":[6e3,9e3]},w_t={sleeping:null,waking:null,idle:[6e3,14e3],listening:[3e3,7e3],thinking:[3500,7e3],searching:[1600,4e3],working:[2800,5500],excited:[2e3,4e3],surprised:[1800,3500],suspicious:[4500,8e3],angry:[3500,7e3],drowsy:null,happy:[2500,5e3],curious:[2500,5500],confused:[2800,5500],bored:[4e3,8e3],proud:[3500,7e3],shy:[3e3,6e3],sad:[4e3,8e3],laughing:[2500,5e3],scared:[1200,3e3],playful:[2e3,4500],celebrate:[2200,4500],orbit:null,radar:null,progress:null,spawning:null,humming:[4e3,8e3],loading:null,dictating:null,sending:null,receiving:null,uploading:null,writing:null,notifying:[2e3,4e3],alerting:null,bouncing:null,dragging:[2200,4500],"powering-down":null},v_t=new Set(["happy","excited","proud"]),b_t=new Set(["playful"]),
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 -15 259 259" width="256" height="256">
<title>Grok Bot blob · idle eyes (source path HJt + u3[0])</title>
<path d="M228.541 114.228C228.541 130.133 225.184 145.994 218.738 160.534C212.674 174.217 203.904 186.669 193.065 196.988C155.933 232.34 99.497 238.596 55.5255 212.24C45.097 205.99 35.6851 198.072 27.7451 188.866C19.1926 178.953 12.3686 167.569 7.65781 155.351C2.60712 142.264 0 128.257 0 114.228C0 98.3219 3.35751 82.4611 9.80315 67.9215C15.8672 54.2382 24.6377 41.7862 35.4767 31.4668C72.6081 -3.88483 129.044 -10.1413 173.016 16.2153C183.444 22.4653 192.856 30.3829 200.796 39.5896C209.349 49.5018 216.173 60.8859 220.883 73.1037C225.934 86.1906 228.541 100.198 228.541 114.228Z" fill="#000"/>
<path d="M130.36 45.98L132.71 46.19L134.98 46.81L137.11 47.83L138.97 49.28L140.47 51.09L141.68 53.12L142.73 55.23L143.76 57.36L144.78 59.49L145.79 61.62L146.79 63.76L147.76 65.91L148.71 68.07L149.63 70.25L150.52 72.43L151.37 74.63L151.99 76.91L152.10 79.26L151.64 81.57L150.59 83.68L149.04 85.45L147.10 86.78L144.90 87.62L142.56 87.93L140.22 87.71L137.98 86.99L135.93 85.82L134.17 84.24L132.78 82.34L131.69 80.25L130.77 78.08L129.87 75.89L128.94 73.72L128.00 71.56L127.03 69.40L126.05 67.26L125.05 65.12L124.03 62.99L122.93 60.90L121.87 58.79L121.03 56.59L120.72 54.26L121.10 51.93L122.15 49.83L123.75 48.10L125.76 46.89L128.01 46.19Z" fill="#fff"/>
<path d="M176.61 37.08L178.72 37.59L180.70 38.48L182.52 39.65L184.20 41.03L185.71 42.59L187.03 44.31L188.20 46.14L189.26 48.03L190.27 49.96L191.26 51.89L192.23 53.84L193.16 55.80L194.05 57.78L194.92 59.77L195.74 61.78L196.53 63.80L197.27 65.84L197.97 67.90L198.47 70.01L198.63 72.18L198.40 74.33L197.58 76.33L195.95 77.72L193.83 78.08L191.71 77.65L189.76 76.69L188.03 75.38L186.53 73.82L185.28 72.05L184.25 70.13L183.40 68.14L182.63 66.11L181.87 64.07L181.07 62.05L180.25 60.04L179.39 58.05L178.49 56.07L177.57 54.10L176.61 52.15L175.62 50.22L174.59 48.31L173.53 46.41L172.54 44.48L171.86 42.42L171.76 40.26L172.62 38.30L174.45 37.19Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+363
View File
@@ -0,0 +1,363 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Grok Bot 角色 · 源码复刻</title>
<style>
:root {
--paper: #ece7dc;
--ink: #161513;
--mute: #6d675d;
--line: #d4cfc3;
--disk: #f3efe6;
--bg: #1a1916;
--panel: #22211d;
--accent: #e8d7a4;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--paper); }
body {
font: 14px/1.5 "SF Pro Text", "PingFang SC", "Hiragino Sans GB", sans-serif;
letter-spacing: 0.01em;
}
header {
padding: 28px 32px 8px;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
}
header h1 {
margin: 0;
font-size: 15px;
font-weight: 560;
letter-spacing: 0.08em;
text-transform: uppercase;
}
header p { margin: 0; color: var(--mute); font-size: 12px; }
main {
display: grid;
grid-template-columns: minmax(280px, 420px) 1fr;
gap: 28px;
padding: 12px 32px 48px;
align-items: start;
}
.stage {
position: sticky;
top: 20px;
background: var(--panel);
border: 1px solid #2e2c27;
border-radius: 20px;
padding: 28px 24px 20px;
}
.disk {
width: min(100%, 280px);
aspect-ratio: 1;
margin: 0 auto;
border-radius: 50%;
background: var(--disk);
display: grid;
place-items: center;
box-shadow: inset 0 -18px 40px rgba(40, 30, 10, 0.08), 0 24px 60px rgba(0,0,0,.35);
}
.disk svg { width: 68%; height: 68%; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; }
.stage-disks { display: flex; align-items: flex-end; justify-content: center; gap: 28px; }
.disk-login {
width: 96px;
height: 96px;
border-radius: 50%;
background: var(--disk);
display: grid;
place-items: center;
box-shadow: inset 0 -8px 18px rgba(40, 30, 10, 0.08), 0 12px 28px rgba(0,0,0,.3);
}
.disk-login svg { width: 64px; height: 64px; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; }
.disk-login span {
display: block;
margin-top: 8px;
font-size: 10px;
color: #8d877b;
text-align: center;
}
.readout {
margin-top: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
font-variant-numeric: tabular-nums;
color: #b7b1a4;
font-size: 12px;
}
.readout b { color: var(--paper); font-weight: 560; }
.modes { display: flex; gap: 8px; margin-top: 16px; }
button {
appearance: none;
border: 1px solid #3a372f;
background: #2a2823;
color: var(--paper);
border-radius: 999px;
padding: 6px 12px;
font: inherit;
cursor: pointer;
}
button[aria-pressed="true"] { background: var(--accent); color: #1a1916; border-color: var(--accent); }
button:hover { border-color: #6a6458; }
h2 {
margin: 0 0 10px;
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--mute);
font-weight: 560;
}
section + section { margin-top: 22px; }
.states { display: flex; flex-wrap: wrap; gap: 6px; }
.states button { font-size: 12px; padding: 5px 10px; }
.shapes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
gap: 8px;
}
.shape {
background: #2a2823;
border: 1px solid #3a372f;
border-radius: 10px;
padding: 8px 6px 6px;
cursor: pointer;
color: inherit;
text-align: center;
}
.shape[aria-pressed="true"] { border-color: var(--accent); background: #353228; }
.shape svg { width: 48px; height: 48px; display: block; margin: 0 auto 4px; --fg: #efe8d8; --bg: #2a2823; }
.shape span { display: block; font-size: 10px; color: #b7b1a4; }
.swatches { display: flex; flex-wrap: wrap; gap: 8px; }
.swatch {
width: 28px; height: 28px; border-radius: 50%;
border: 2px solid #111; cursor: pointer; padding: 0;
}
.swatch[aria-pressed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
.icons { display: flex; gap: 16px; align-items: center; }
.icons figure { margin: 0; text-align: center; }
.icons img {
width: 72px; height: 72px; border-radius: 16px;
background: #111; display: block;
}
.icons figcaption { margin-top: 6px; font-size: 11px; color: var(--mute); }
.note {
color: #8d877b;
font-size: 12px;
max-width: 62ch;
}
.note code { color: #d8d0c0; font-size: 11px; }
@media (max-width: 860px) {
main { grid-template-columns: 1fr; }
.stage { position: static; }
}
</style>
</head>
<body>
<header>
<h1>Grok Bot Character</h1>
<p>从 Grok Bot.app v0.18.0 的 app.asar 抽出 · 个人学习用</p>
</header>
<main>
<aside class="stage">
<div class="stage-disks">
<div class="disk" id="disk">
<svg id="bot" role="img" aria-label="Grok Bot character"></svg>
</div>
<div>
<div class="disk-login">
<svg id="bot64" role="img" aria-label="Login size 64px"></svg>
</div>
<span style="display:block;margin-top:8px;font-size:10px;color:#8d877b;text-align:center">登录 sizePx: 64</span>
</div>
</div>
<div class="modes">
<button id="mode-onboard" aria-pressed="true">登录轮换</button>
<button id="mode-hold" aria-pressed="false">锁定状态</button>
</div>
<div class="modes">
<button id="opt-follow" aria-pressed="true">跟随指针</button>
<button id="opt-emphasis" aria-pressed="false">emphasis</button>
<button id="act-spin">转一圈</button>
<button id="act-bounce">跳一下</button>
<button id="act-burst">粒子</button>
</div>
<div class="readout">
<div>状态 <b id="r-state">idle</b></div>
<div>眼睛 <b id="r-eye">0 → 0</b></div>
<div>旋转 <b id="r-spin">0.00°</b></div>
<div>挤压 <b id="r-squash">1.000</b></div>
<div>overlay <b id="r-ov">—</b></div>
</div>
</aside>
<div>
<section>
<h2>情绪状态 · 源码 39 态</h2>
<div id="states"></div>
</section>
<section>
<h2>身形 · Jo 几何生成器</h2>
<div class="shapes" id="shapes"></div>
</section>
<section>
<h2>墨色 · snt 调色板</h2>
<div class="swatches" id="swatches"></div>
</section>
<section>
<h2>Dock 静态商标(位图,不是角色引擎)</h2>
<div class="icons">
<figure>
<img src="../assets/icon.iconset/[email protected]" alt="Dock icon 256" />
<figcaption>icon.icns 256</figcaption>
</figure>
<figure>
<img src="../assets/app-icon-256.png" alt="In-app icon" />
<figcaption>asar app-icon</figcaption>
</figure>
</div>
</section>
<section>
<h2>这不是截图描摹</h2>
<p class="note">
引擎按源码 <code>$_t</code> + 登录包装 <code>sd()</code> 拆在
<code>src/</code>:math / tables / pose / tricks / eyes / fx / character。
右侧 64px 带 <code>pose.scale ≈ 1.04</code>。thinking / orbit / writing / loading
等会切 overlay;celebrate 会 <code>spinWild</code>;换身形走弹簧并轮换一记特技。
首屏仍从 curious 起播,点「登录轮换」则按 <code>pjn</code> 从 idle 重来。
大预览默认开指针跟随;源码 <code>sd().isFollowingPointer</code> 默认关。
</p>
</section>
</div>
</main>
<script src="geometry-data.js"></script>
<script src="src/math.js"></script>
<script src="src/tables.js"></script>
<script src="src/pose.js"></script>
<script src="src/tricks.js"></script>
<script src="src/fx.js"></script>
<script src="src/eyes.js"></script>
<script src="src/character.js"></script>
<script>
const readout = {
state: document.getElementById("r-state"),
eye: document.getElementById("r-eye"),
spin: document.getElementById("r-spin"),
squash: document.getElementById("r-squash"),
ov: document.getElementById("r-ov"),
};
const stateRoot = document.getElementById("states");
function paintReadout(s) {
readout.state.textContent = s.state;
readout.eye.textContent = `${s.eyeFrom} → ${s.eyeTo}`;
readout.spin.textContent = `${s.spin.toFixed(2)}°`;
readout.squash.textContent = s.squash.toFixed(3);
if (readout.ov) readout.ov.textContent = s.overlay || "—";
}
function markState(name) {
stateRoot.querySelectorAll("button").forEach((b) => {
b.setAttribute("aria-pressed", b.dataset.state === name ? "true" : "false");
});
}
function syncModes(mode) {
document.getElementById("mode-onboard").setAttribute("aria-pressed", mode === "onboarding");
document.getElementById("mode-hold").setAttribute("aria-pressed", mode === "hold");
}
GROK_META.groups.forEach((group) => {
const h = document.createElement("h2");
h.textContent = group.label;
h.style.marginTop = "8px";
stateRoot.appendChild(h);
const row = document.createElement("div");
row.className = "states";
group.states.forEach((name) => {
const b = document.createElement("button");
b.textContent = name;
b.dataset.state = name;
b.addEventListener("click", () => {
bots.forEach((el) => {
el.setMode("hold");
el.setState(name, { resetEyes: false });
});
syncModes("hold");
});
row.appendChild(b);
});
stateRoot.appendChild(row);
});
const shared = { mode: "onboarding", shape: "blob", color: "black", scheme: "light", loginWrap: true };
const bot = new GrokCharacter(document.getElementById("bot"), {
...shared,
followPointer: true,
onChange(snap) { paintReadout(snap); markState(snap.state); },
});
const bot64 = new GrokCharacter(document.getElementById("bot64"), { ...shared, sizePx: 64 });
const bots = [bot, bot64];
window.__bots = bots;
// skip the first idle beat so the face is obvious on load
bots.forEach((el) => {
el.moodN = 1;
el.setState("curious", { resetEyes: true });
});
setInterval(() => paintReadout(bot.snapshot()), 80);
const shapeRoot = document.getElementById("shapes");
Object.entries(GROK_GEO.shapes).forEach(([id, shape]) => {
const b = document.createElement("button");
b.className = "shape";
b.dataset.shape = id;
b.innerHTML = `<svg viewBox="-15 -15 259 259"><path d="${shape.path}" style="fill:var(--fg)"></path></svg><span>${shape.label}</span>`;
b.addEventListener("click", () => {
bots.forEach((el) => el.setShape(id));
shapeRoot.querySelectorAll("button").forEach((el) => {
el.setAttribute("aria-pressed", el === b ? "true" : "false");
});
});
if (id === "blob") b.setAttribute("aria-pressed", "true");
shapeRoot.appendChild(b);
});
const swatchRoot = document.getElementById("swatches");
Object.entries(GROK_GEO.palette).forEach(([id, pal]) => {
const b = document.createElement("button");
b.className = "swatch";
b.title = id;
b.style.background = pal.light;
b.addEventListener("click", () => {
bots.forEach((el) => el.setColor(id, "light"));
swatchRoot.querySelectorAll("button").forEach((el) => {
el.setAttribute("aria-pressed", el === b ? "true" : "false");
});
});
if (id === "black") b.setAttribute("aria-pressed", "true");
swatchRoot.appendChild(b);
});
document.getElementById("mode-onboard").onclick = () => {
bots.forEach((el) => el.setMode("onboarding"));
syncModes("onboarding");
};
document.getElementById("mode-hold").onclick = () => {
bots.forEach((el) => el.setMode("hold"));
syncModes("hold");
};
document.getElementById("opt-follow").onclick = () => {
const on = document.getElementById("opt-follow").getAttribute("aria-pressed") !== "true";
document.getElementById("opt-follow").setAttribute("aria-pressed", on ? "true" : "false");
bots.forEach((el) => el.setFollowPointer(on));
};
document.getElementById("opt-emphasis").onclick = () => {
const on = document.getElementById("opt-emphasis").getAttribute("aria-pressed") !== "true";
document.getElementById("opt-emphasis").setAttribute("aria-pressed", on ? "true" : "false");
bots.forEach((el) => el.setEmphasis(on));
};
document.getElementById("act-spin").onclick = () => bots.forEach((el) => el.spinOnce(1));
document.getElementById("act-bounce").onclick = () => bots.forEach((el) => el.bounceOnce());
document.getElementById("act-burst").onclick = () => bots.forEach((el) => el.burstOnce());
</script>
</body>
</html>
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env node
// Headless snapshot of replica/index.html against $_t mn() invariants.
import { spawn } from "node:child_process";
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { pathToFileURL, fileURLToPath } from "node:url";
const CHROME = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const PAGE = pathToFileURL(join(dirname(fileURLToPath(import.meta.url)), "index.html")).href;
const OUT = process.env.SNAP_DIR || "/tmp/grok-replica-snap";
class Cdp {
constructor(ws) {
this.ws = ws;
this.id = 0;
this.pending = new Map();
this.ws.addEventListener("message", (ev) => {
const msg = JSON.parse(ev.data);
const p = this.pending.get(msg.id);
if (!p) return;
this.pending.delete(msg.id);
msg.error ? p.reject(new Error(JSON.stringify(msg.error))) : p.resolve(msg.result);
});
}
send(method, params = {}) {
const id = ++this.id;
this.ws.send(JSON.stringify({ id, method, params }));
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
});
}
async eval(expr) {
const r = await this.send("Runtime.evaluate", {
expression: expr,
returnByValue: true,
awaitPromise: true,
});
if (r.exceptionDetails) {
throw new Error(r.exceptionDetails.text || "evaluate failed");
}
return r.result.value;
}
}
function waitPort(child) {
return new Promise((resolve, reject) => {
let buf = "";
const on = (chunk) => {
buf += chunk.toString();
const m = buf.match(/DevTools listening on (ws:\/\/[^\s]+)/);
if (m) resolve(m[1]);
};
child.stderr.on("data", on);
child.stdout.on("data", on);
child.on("exit", (code) => reject(new Error(`chrome exited ${code}: ${buf.slice(-400)}`)));
setTimeout(() => reject(new Error(`chrome debug timeout: ${buf.slice(-400)}`)), 15000);
});
}
async function sleep(ms) {
await new Promise((r) => setTimeout(r, ms));
}
async function shot(cdp, name) {
const r = await cdp.send("Page.captureScreenshot", { format: "png" });
await writeFile(join(OUT, `${name}.png`), Buffer.from(r.data, "base64"));
}
const SNAP_JS = `(() => {
const bot = window.__bots && window.__bots[0];
const svg = document.getElementById("bot");
const body = bot?.body || svg?.querySelector("g > path");
const eyes = bot?.eyeEls || svg?.querySelectorAll("g g path");
const d = body?.getAttribute("d") || "";
const eye = (el) => el ? ({
d: (el.getAttribute("d") || "").slice(0, 48),
t: el.getAttribute("transform") || "",
display: el.style.display || "",
}) : null;
return {
state: bot?.state,
mode: bot?.mode,
shape: bot?.shapeName,
eyeFrom: bot?.eyeFrom,
eyeTo: bot?.eyeTo,
morph: bot?.eyeMorph?.x,
blink: bot?.blink?.x,
overlayX: bot?.overlay?.x,
ovKind: bot?.ovKind,
spin: bot?.spin?.x,
bodyN: d.length,
bodyHead: d.slice(0, 72),
eyes: [eye(eyes?.[0]), eye(eyes?.[1])],
};
})()`;
async function snap(cdp, label) {
const data = await cdp.eval(SNAP_JS);
data.label = label;
return data;
}
async function click(cdp, sel) {
await cdp.eval(`document.querySelector(${JSON.stringify(sel)}).click()`);
}
async function main() {
await mkdir(OUT, { recursive: true });
const profile = await mkdtemp(join(tmpdir(), "grok-snap-"));
const chrome = spawn(CHROME, [
"--headless=new",
"--disable-gpu",
"--no-first-run",
"--hide-scrollbars",
"--window-size=1100,900",
`--user-data-dir=${profile}`,
"--remote-debugging-port=0",
"--allow-file-access-from-files",
PAGE,
], { stdio: ["ignore", "pipe", "pipe"] });
const browserWs = await waitPort(chrome);
const port = new URL(browserWs).port;
let pageWs = null;
for (let i = 0; i < 40; i++) {
const tabs = await fetch(`http://127.0.0.1:${port}/json/list`).then((r) => r.json());
const page = tabs.find((t) => t.type === "page" && t.webSocketDebuggerUrl);
if (page) {
pageWs = page.webSocketDebuggerUrl;
break;
}
await sleep(100);
}
if (!pageWs) throw new Error("no page target");
const ws = new WebSocket(pageWs);
await new Promise((res, rej) => {
ws.addEventListener("open", res);
ws.addEventListener("error", rej);
});
const cdp = new Cdp(ws);
await cdp.send("Page.enable");
await cdp.send("Runtime.enable");
await cdp.send("Page.bringToFront");
for (let i = 0; i < 50; i++) {
const ready = await cdp.eval("!!(window.__bots && window.__bots[0] && window.__bots[0].body)");
if (ready) break;
await sleep(100);
}
await click(cdp, "#mode-hold");
await click(cdp, "#opt-follow"); // default on → off for stable transforms
await sleep(80);
const report = [];
const states = [
"playful", "laughing", "sleeping", "idle", "celebrate",
"thinking", "writing", "orbit", "humming", "loading",
];
for (const name of states) {
await click(cdp, `button[data-state="${name}"]`);
await sleep(name === "sleeping" ? 400 : 700);
const s = await snap(cdp, name);
report.push(s);
await shot(cdp, name);
}
// sleeping timeline: start from curious (eye 3) then sleep
await click(cdp, 'button[data-state="curious"]');
await sleep(200);
await click(cdp, 'button[data-state="sleeping"]');
for (const ms of [300, 1300, 2200]) {
await sleep(ms === 300 ? 300 : ms - (ms === 1300 ? 300 : 1300));
report.push(await snap(cdp, `sleeping@${ms}ms`));
}
await shot(cdp, "sleeping-late");
// turnAt: bean / tablet / cloud + one spin, sample eyes
for (const shape of ["bean", "tablet", "cloud"]) {
await click(cdp, 'button[data-state="idle"]');
await click(cdp, `button[data-shape="${shape}"]`);
await sleep(80);
await click(cdp, "#act-spin");
const samples = [];
for (let i = 0; i < 12; i++) {
await sleep(90);
samples.push(await snap(cdp, `${shape}-spin-${i}`));
}
report.push({
label: `${shape}-spin`,
shape,
hidden: samples.filter((s) => s.eyes?.some((e) => e.display === "none")).length,
shown: samples.filter((s) => s.eyes?.every((e) => e.display !== "none")).length,
transforms: [...new Set(samples.map((s) => s.eyes?.[0]?.t).filter(Boolean))].length,
ovKind: samples[0]?.ovKind,
samples: samples.map((s) => ({
t: s.eyes?.[0]?.t?.slice(0, 40),
d0: s.eyes?.[1]?.display,
d1: s.eyes?.[0]?.display,
bodyHead: s.bodyHead,
})),
});
await shot(cdp, `${shape}-spin`);
}
await writeFile(join(OUT, "report.json"), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
chrome.kill("SIGTERM");
ws.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+857
View File
@@ -0,0 +1,857 @@
/* L2 — GrokCharacter. Orchestrates pose, eyes, tricks, overlays. Source $_t + sd(). */
(function (g) {
const M = g.GROK_MATH;
const T = g.GROK_TABLES;
const { applyPose, nextGaze } = g.GROK_POSE;
const TR = g.GROK_TRICKS;
const EY = g.GROK_EYES;
const FX = g.GROK_FX;
const {
spring, stepSpring, springSteps, clamp, rand, sign, K2, Dke, lerpPoly, lerpFace, relRot, mapPointer, Rn,
} = M;
const {
GROUPS, EYE_PLAYLIST, EYE_HOLD_MS, BLINK_MS,
ONBOARDING, ONBOARDING_MS, onboardMood,
SPRINGS, FACE_TUNE, POSE, POSE_HOME, UNIFORM_EYES,
V_T, B_T, WINK_STATES, poseScale, shapeEyeScale, overlayViewZoom,
VIEW, VIEW_HALF, VIEW_MID, inkFg, inkCss, EYE_BG,
} = T;
class GrokCharacter {
constructor(svg, opts = {}) {
this.svg = svg;
this.shapeName = opts.shape || "blob";
this.colorId = opts.color || "black";
this.scheme = opts.scheme || "light";
this.mode = opts.mode || "onboarding";
this.state = opts.state || "idle";
this.onChange = opts.onChange || (() => {});
this.loginWrap = opts.loginWrap !== false;
this.eyeTopology = opts.eyeTopology ?? this.loginWrap;
this.faceTune = opts.faceTune ?? (this.loginWrap ? FACE_TUNE : null);
this.pose = { ...(this.loginWrap ? POSE : { turn: 0, tilt: 0, roll: 0, scale: 1 }), ...opts.pose };
this.poseHome = opts.poseHome || (this.loginWrap ? POSE_HOME : { turn: 0, tilt: 0, roll: 0 });
this.uniformEyes = opts.uniformEyes ?? (this.loginWrap ? UNIFORM_EYES : false);
this.eyeScaleProp = opts.eyeScale ?? (this.loginWrap ? shapeEyeScale(this.shapeName) : 1);
this.emphasis = !!opts.emphasis;
this.followPointer = !!opts.followPointer;
this.gazeTarget = opts.gazeTarget || null;
this.paused = !!opts.paused;
this.reduceMotion = opts.reduceMotion ?? (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches);
this.badgeColor = opts.badgeColor || "var(--gb-badge, #1d9bf0)";
this.sizePx = opts.sizePx || null;
this.eyeColor = opts.eyeColor || null;
this.inkFlat = opts.inkFlat || null;
this.spin = spring(0);
this.tx = spring(0);
this.ty = spring(0);
this.squash = spring(1);
this.blink = spring(1);
this.eyeScale = spring(1);
this.gazeX = spring(0);
this.gazeY = spring(0);
this.eyeMorph = spring(1);
this.overlay = spring(0);
this.overlayMix = spring(1);
this.notify = spring(0);
this.humDots = spring(0);
this.shapeSpring = spring(1);
this.overlayTurn = spring(0);
this.emphasisBlend = 0;
this.eyeFrom = 0;
this.eyeTo = 0;
this.eyeStiffness = 7;
this.eyeIdx = 0;
this._fromPolys = null;
this.t0 = performance.now();
this.stateAt = this.t0;
this.last = this.t0;
this.moodN = 0;
this.eyeUntil = this.t0 + rand(...EYE_HOLD_MS.idle);
this.blinkUntil = this.t0 + rand(1500, 7000);
this.gazeUntil = this.t0 + 800;
this.blinkQueue = [];
this.winkAt = -1e9;
this.winkEye = 0;
this.winkUntil = this.t0 + rand(3000, 8000);
this.spinTurn = null;
this.trick = null;
this.hopAt = -1;
this.trickAt = this.t0 + rand(2500, 5000);
this.trickCycle = Math.floor(rand(0, 5));
this.wildWide = false;
this.ovSpin = 0;
this.ovTurnAcc = 0;
this.ovOn = false;
this.ovTurnDir = 1;
this.pointer = { x: 0, y: 0, tx: 0, ty: 0 };
this.pointerRaw = null;
this.rectCache = null;
this.rectAt = -1e9;
this.prevShape = this.shapeName;
this.prevFace = null;
this.prevRing = null;
this.prevTilt = null;
this.prevBelt = null;
this.ctx = this._freshCtx(this.t0);
this.ovKind = null;
this.ovPrev = null;
this.ovTarget = null;
this.ovRest = false;
this.ovRestAt = 0;
this.pxW = 190;
this.pxAt = 0;
this.partScale = 1;
this.celebrateAt = -1;
this.extras = { turn: null, Kr: 0, yi: 0, ki: 0, Yr: 0, Zr: 0, wi: 0, hop: 0 };
this._build();
this.setColor(this.colorId, this.scheme);
this._applyPoseScale();
this.setState(this.state, { resetEyes: true });
this._bindPointer();
this._paint(this.t0);
this._raf = requestAnimationFrame((t) => this._tick(t));
}
destroy() {
cancelAnimationFrame(this._raf);
this._unbindPointer();
this.particles?.clear();
}
_freshCtx(now) {
return {
nodUntil: now + 1800,
nodEnd: 0,
angryShakeUntil: 0,
impulseAt: now + rand(500, 1200),
tyKick: 0,
spinKick: 0,
forceSleepEye: false,
wakeEye: null,
wakeBlink: false,
wakingBlinked: false,
slumpAt: 0,
stAt: now + rand(6000, 10000),
wantPn: null,
wantBlink: false,
dragCycle: -1,
notifyPop: false,
wantBurst: null,
wakingBurst: false,
};
}
setMode(mode) {
this.mode = mode;
if (mode === "onboarding") {
this.moodN = 0;
this.stateAt = performance.now();
this.setState("idle", { resetEyes: true });
}
}
setPaused(v) {
this.paused = !!v;
}
setEmphasis(v) {
this.emphasis = !!v;
}
setFollowPointer(v) {
this.followPointer = !!v;
if (!v) {
this.pointerRaw = null;
this.gazeTarget = null;
}
}
setGazeTarget(pt) {
this.gazeTarget = pt;
}
setShape(name) {
if (!g.GROK_GEO.shapes[name] || name === this.shapeName) return;
const R = g.GROK_GEO.Re;
const k = K2(clamp(this.shapeSpring.x, 0, 1));
const rest = FX.shapeMetrics(g.GROK_GEO.shapes[this.shapeName], R);
if (k >= 1 || !this.prevFace || !this.prevRing) {
this.prevFace = rest.face;
this.prevRing = rest.ring;
this.prevTilt = rest.tilt;
this.prevBelt = rest.belt;
} else {
this.prevFace = lerpFace(this.prevFace, rest.face, k);
this.prevRing = FX.lerpRing(this.prevRing, rest.ring, k);
this.prevTilt += (rest.tilt - this.prevTilt) * k;
this.prevBelt += (rest.belt - this.prevBelt) * k;
}
this.prevShape = this.shapeName;
this.shapeName = name;
this.shapeSpring.x = 0;
this.shapeSpring.v = 0;
this.shapeSpring.t = 1;
if (this.loginWrap) this.eyeScaleProp = shapeEyeScale(name);
this._applyPoseScale();
this._cycleShapeTrick();
}
setColor(id, scheme) {
this.colorId = id;
if (scheme) this.scheme = scheme;
if (this.inkFlat) {
this.svg.style.setProperty("--fg", this.inkFlat);
} else if (this.loginWrap) {
this.svg.style.setProperty("--fg", inkFg(id));
} else {
const pal = g.GROK_GEO.palette[id] || g.GROK_GEO.palette.black;
this.svg.style.setProperty("--fg", this.scheme === "dark" ? pal.dark : pal.light);
}
this.svg.style.setProperty("--ink", inkCss(id));
this.svg.style.setProperty("--bg", this.eyeColor || EYE_BG);
}
setInk(flat) {
this.inkFlat = flat || null;
this.setColor(this.colorId);
}
setEyeColor(color) {
this.eyeColor = color || null;
this.svg.style.setProperty("--bg", this.eyeColor || EYE_BG);
}
setState(name, { resetEyes = false } = {}) {
if (!EYE_PLAYLIST[name]) return;
this.state = name;
this.stateAt = performance.now();
const list = EYE_PLAYLIST[name];
this.eyeIdx = 0;
if (resetEyes) {
this.eyeFrom = list[0];
this.eyeTo = list[0];
this._fromPolys = null;
this.eyeMorph.x = 1;
this.eyeMorph.t = 1;
this.eyeMorph.v = 0;
} else if (name !== "sleeping" && name !== "waking") {
this._morphEyes(list[0], name === "excited" ? 10 : 8);
}
this.eyeUntil = this.stateAt + rand(...EYE_HOLD_MS[name]);
const blink = BLINK_MS[name];
this.blinkUntil = blink ? this.stateAt + rand(1500, 7000) : Infinity;
this.gazeUntil = this.stateAt + rand(500, 1400);
this.winkUntil = this.stateAt + rand(3000, 8000);
this.ctx = this._freshCtx(this.stateAt);
this.ctx.stAt = this.stateAt + (
name === "excited" ? rand(400, 1100)
: name === "searching" ? rand(800, 1600)
: name === "working" ? rand(1200, 2400)
: rand(6000, 10000)
);
this.celebrateAt = name === "celebrate" ? this.stateAt + 140 : -1;
this.trick = null;
this.spinTurn = null;
this.hopAt = -1;
this.wildWide = false;
if (name !== "writing") this.fx?.resetInk();
if (name !== "waking" && name !== "sleeping" && name !== "drowsy") {
EY.queueBlink(this.blinkQueue, this.stateAt);
}
try {
this.onChange(this.snapshot());
} catch (_) { /* host UI may not be ready */ }
}
snapshot() {
return {
state: this.state,
mode: this.mode,
shape: this.shapeName,
color: this.colorId,
scheme: this.scheme,
eyeFrom: this.eyeFrom,
eyeTo: this.eyeTo,
spin: this.spin.x,
tx: this.tx.x,
ty: this.ty.x,
squash: this.squash.x,
blink: this.blink.x,
overlay: this.ovKind,
};
}
spinOnce(turns = 1) {
this._pn(turns);
}
bounceOnce() {
this._hop(performance.now());
}
burstOnce() {
if (!this.reduceMotion) this.particles.burst(22, 1.1, 0.3);
}
_applyPoseScale() {
const sc = this.loginWrap ? poseScale(this.shapeName) : (this.pose.scale || 1);
this.pose.scale = sc;
if (this.sizePx) {
this.svg.style.width = `${this.sizePx}px`;
this.svg.style.height = `${this.sizePx}px`;
}
if (Math.abs(sc - 1) > 0.001) {
this.svg.style.transform = `scale(${sc})`;
this.svg.style.transformOrigin = "50% 50%";
} else {
this.svg.style.transform = "";
}
}
_bindPointer() {
this._onMove = (e) => {
if (!this.followPointer) return;
this.pointerRaw = { x: e.clientX, y: e.clientY };
};
this._onLeave = () => {
if (this.followPointer) this.pointerRaw = null;
};
window.addEventListener("pointermove", this._onMove, { passive: true });
document.documentElement.addEventListener("pointerleave", this._onLeave);
}
_unbindPointer() {
window.removeEventListener("pointermove", this._onMove);
document.documentElement.removeEventListener("pointerleave", this._onLeave);
}
_build() {
const geo = g.GROK_GEO;
const vb = geo.viewBox;
this.svg.setAttribute("viewBox", `${vb.minX} ${vb.minY} ${vb.width} ${vb.height}`);
this.svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
this.svg.style.overflow = "visible";
this.svg.innerHTML = "";
const ns = "http://www.w3.org/2000/svg";
const defs = document.createElementNS(ns, "defs");
const clip = document.createElementNS(ns, "clipPath");
const clipId = `grok-clip-${Math.random().toString(36).slice(2, 8)}`;
clip.setAttribute("id", clipId);
this.clipPath = document.createElementNS(ns, "path");
clip.appendChild(this.clipPath);
defs.appendChild(clip);
this.svg.appendChild(defs);
this.group = document.createElementNS(ns, "g");
this.body = document.createElementNS(ns, "path");
this.body.setAttribute("fill", "var(--fg, #000)");
const eyesG = document.createElementNS(ns, "g");
eyesG.setAttribute("clip-path", `url(#${clipId})`);
this.eyeEls = [0, 1].map(() => {
const p = document.createElementNS(ns, "path");
p.setAttribute("fill", "var(--bg, #f3efe6)");
eyesG.appendChild(p);
return p;
});
this.badge = document.createElementNS(ns, "circle");
this.badge.setAttribute("style", "display:none");
this.group.appendChild(this.body);
this.group.appendChild(eyesG);
this.group.appendChild(this.badge);
this.fx = new FX.OverlayLayer();
const R = geo.Re;
this.fx.circlePath = FX.circlePathOf(R);
this.fx.pencilPath = FX.capsule(30, 88, R);
this.fx.bangPath = FX.taper(30, 17, 96, R);
this.fx.attach(this.svg, this.group);
this.particles = FX.createParticles({
back: this.fx.back,
front: this.fx.front,
idPrefix: this.fx.uid,
getRadius: () => {
const sh = geo.shapes[this.shapeName];
const k = K2(clamp(this.shapeSpring.x, 0, 1));
const to = sh?.beltRadius || FX.beltRadius(sh.path, R);
let je = k < 0.999 && this.prevBelt != null
? this.prevBelt + (to - this.prevBelt) * k
: to;
if (this.state === "loading") je += (52 - je) * clamp(this.overlay.x, 0, 1);
return je;
},
});
this.body.setAttribute("d", geo.shapes[this.shapeName].path);
this.clipPath.setAttribute("d", geo.shapes[this.shapeName].path);
}
_morphEyes(index, stiffness = 7) {
if (index === this.eyeTo && this.eyeMorph.t === 1) return;
const t = clamp(this.eyeMorph.x, 0, 1);
this.eyeFrom = this.eyeTo;
this._fromPolys = this._currentPolys(t);
this.eyeTo = index;
this.eyeMorph.x = 0;
this.eyeMorph.v = 0;
this.eyeMorph.t = 1;
this.eyeStiffness = stiffness;
}
_currentPolys(t) {
const eyes = g.GROK_GEO.eyes;
const from = this._fromPolys || eyes[this.eyeFrom];
const to = eyes[this.eyeTo];
return [lerpPoly(from[0], to[0], t), lerpPoly(from[1], to[1], t)];
}
_pn(turns = 1, dir = sign()) {
if (this.reduceMotion || this.paused || this.spinTurn) return;
this.spinTurn = TR.makeSpinTurn(turns, dir);
}
_hop(now) {
if (this.hopAt < 0) this.hopAt = now;
}
_cycleShapeTrick() {
if (this.reduceMotion || this.paused) return;
this.trickCycle = (this.trickCycle + 1) % 5;
this.wildWide = false;
if (this.trickCycle === 0) this._pn(1);
else if (this.trickCycle === 1) {
this.wildWide = true;
this._pn(2);
} else if (this.trickCycle === 2) this.trick = TR.startTrick("spinBounce", this.reduceMotion);
else if (this.trickCycle === 3) this.trick = TR.startTrick("spinDizzy", this.reduceMotion);
else {
this._pn(1);
this.particles.burst(16, 0.95, 0.3);
}
}
_stepOverlay(now) {
const want = FX.MAP[this.state] || null;
if (want !== this.ovTarget) {
this.ovTarget = want;
this.fx.overlayAt = now;
this.ovRest = false;
this.ovRestAt = 0;
}
let on = want != null;
if (want && FX.CYCLE.has(this.state)) {
if (!this.ovRest && now - this.fx.overlayAt > (FX.CYCLE_ON[this.state] || 2500)) {
this.ovRest = true;
this.ovRestAt = now;
} else if (this.ovRest && now - this.ovRestAt > FX.CYCLE_OFF) {
this.ovRest = false;
this.fx.overlayAt = now;
}
on = !this.ovRest;
}
this.overlay.t = on ? 1 : 0;
if (on !== this.ovOn) {
if (!this.reduceMotion) {
if (on) this.ovTurnDir = sign();
this.ovTurnAcc += Math.PI * this.ovTurnDir;
this.overlayTurn.t = this.ovTurnAcc;
}
this.ovOn = on;
}
if (want && want !== this.ovKind) {
if (this.ovKind && this.overlay.x > 0.02) {
this.ovPrev = this.ovKind;
this.overlayMix.x = 0;
this.overlayMix.v = 0;
this.overlayMix.t = 1;
} else {
this.ovPrev = null;
this.overlayMix.x = 1;
this.overlayMix.v = 0;
this.overlayMix.t = 1;
}
this.ovKind = want;
this.fx.overlayAt = now;
if (want !== "pencil") this.fx.resetInk();
}
if (!want && this.overlay.x < 0.004) {
this.ovKind = null;
this.ovPrev = null;
}
if (this.overlayMix.x > 0.996) this.ovPrev = null;
}
_updatePointer(now) {
const src = this.gazeTarget || (this.followPointer ? this.pointerRaw : null);
if (src && this.svg.getBoundingClientRect) {
if (now - this.rectAt > 200) {
this.rectCache = this.svg.getBoundingClientRect();
this.rectAt = now;
}
const rect = this.rectCache;
if (rect && rect.width > 0) {
const mapped = this.gazeTarget ? src : mapPointer(rect, src);
this.pointer.tx = clamp((mapped.x - (rect.left + rect.width / 2)) / rect.width, -0.6, 0.6) * 22;
this.pointer.ty = clamp((mapped.y - (rect.top + rect.height / 2)) / rect.height, -0.6, 0.6) * 14;
}
} else {
this.pointer.tx = 0;
this.pointer.ty = 0;
}
const z = Rn(0.16);
this.pointer.x += (this.pointer.tx - this.pointer.x) * z;
this.pointer.y += (this.pointer.ty - this.pointer.y) * z;
}
_tick(now) {
const dt = Math.min((now - this.last) / 1000, 0.1);
this.last = now;
if (this.paused) {
this._raf = requestAnimationFrame((t) => this._tick(t));
return;
}
if (this.mode === "onboarding" && now - this.stateAt >= ONBOARDING_MS) {
this.moodN += 1;
this.setState(onboardMood(this.moodN));
}
const mt = (now - this.t0) / 1000;
const dtState = (now - this.stateAt) / 1000;
const pose = applyPose(this.state, mt, dtState, now, this.ctx, {
eyeTo: this.eyeTo,
eyeMorphX: this.eyeMorph.x,
blinkX: this.blink.x,
});
this.spin.t = pose.spin;
this.tx.t = pose.tx;
this.ty.t = pose.ty;
this.squash.t = pose.squash;
this.eyeScale.t = pose.eyeBoost;
if (this.ctx.tyKick) {
this.ty.v += this.ctx.tyKick;
this.ctx.tyKick = 0;
}
if (this.ctx.spinKick) {
this.spin.v += this.ctx.spinKick;
this.ctx.spinKick = 0;
}
if (this.ctx.forceSleepEye) {
this.ctx.forceSleepEye = false;
this._morphEyes(13, 11);
}
if (this.ctx.wakeEye) {
this._morphEyes(this.ctx.wakeEye[0], this.ctx.wakeEye[1]);
this.ctx.wakeEye = null;
}
if (this.ctx.wakeBlink && !this.ctx.wakingBlinked && this.blinkQueue.length === 0) {
EY.queueBlink(this.blinkQueue, now);
this.ctx.wakingBlinked = true;
}
this.ctx.wakeBlink = false;
if (this.ctx.wantBlink) {
EY.queueBlink(this.blinkQueue, now);
this.ctx.wantBlink = false;
}
if (this.ctx.wantPn) {
this._pn(...this.ctx.wantPn);
this.ctx.wantPn = null;
}
if (this.ctx.wantBurst) {
this.particles.burst(this.ctx.wantBurst[0], this.ctx.wantBurst[1]);
this.ctx.wakingBurst = true;
this.ctx.wantBurst = null;
}
this._stepOverlay(now);
if (this.celebrateAt > 0 && now >= this.celebrateAt && !this.trick && !this.spinTurn) {
this.trick = TR.startTrick("spinWild", this.reduceMotion);
this.celebrateAt = now + 6200;
}
if (now >= this.trickAt) {
if ((V_T.has(this.state) || B_T.has(this.state)) && !this.spinTurn && this.hopAt < 0 && !this.trick) {
const z = Math.random();
if (V_T.has(this.state)) {
if (z < 0.55) this._pn(1);
else this.trick = TR.startTrick("spinBounce", this.reduceMotion);
} else if (z < 0.34) this.trick = TR.startTrick("spinBounce", this.reduceMotion);
else if (z < 0.62) this._hop(now);
else if (z < 0.86) this.trick = TR.startTrick("spinDizzy", this.reduceMotion);
else this._pn(1);
}
this.trickAt = now + rand(9000, 18000);
}
const tf = TR.evalTrick(this.trick, now);
if (tf.wantHop) this._hop(now);
if (tf.done) this.trick = null;
let hop = TR.hopY(this.hopAt, now);
if (hop == null) {
this.hopAt = -1;
hop = 0;
}
let turn = tf.turn;
if (this.spinTurn) {
turn = (turn ?? 0) + this.spinTurn.x;
if (TR.spinTurnSettled(this.spinTurn)) this.spinTurn = null;
}
this.extras = { ...tf, turn, hop };
if (this.extras.eyeBoost != null) this.eyeScale.t = this.extras.eyeBoost;
if (this.state !== "waking" && this.state !== "sleeping" && now >= this.eyeUntil) {
const list = EYE_PLAYLIST[this.state];
this.eyeIdx = (this.eyeIdx + 1 + Math.floor(rand(0, list.length - 1))) % list.length;
const stiff = this.state === "searching" || this.state === "excited" ? 10 : 6;
this._morphEyes(list[this.eyeIdx], stiff);
this.eyeUntil = now + rand(...EYE_HOLD_MS[this.state]);
}
const blinkCadence = BLINK_MS[this.state];
if (blinkCadence && now >= this.blinkUntil) {
EY.queueBlink(this.blinkQueue, now);
this.blinkUntil = now + rand(...blinkCadence);
}
const blinkKey = EY.consumeBlink(this.blinkQueue, now);
this.blink.t = blinkKey ?? (this.blinkQueue.length ? this.blink.t : (this.extras.lidMul ?? pose.lid));
if (now >= this.gazeUntil) {
const gz = nextGaze(this.state);
this.gazeX.t = gz.x;
this.gazeY.t = gz.y;
this.gazeUntil = now + rand(...gz.hold);
}
if (WINK_STATES.has(this.state) && now >= this.winkUntil) {
this.winkAt = now;
this.winkEye = Math.random() < 0.5 ? 0 : 1;
this.winkUntil = now + rand(4500, 10000);
}
this.emphasisBlend += ((this.emphasis ? 1 : 0) - this.emphasisBlend) * Rn(0.12);
if (this.emphasis) {
this.eyeScale.t = Math.max(this.eyeScale.t, 1.32);
this.blink.t = Math.max(this.blink.t, 1.18);
}
const humming = this.state === "humming";
const loading = this.state === "loading";
if ((humming || loading) && !this.reduceMotion) {
const Zt = dtState;
const dn = loading ? 3 : 1.6;
const on = Zt < 0.5 ? 7 * K2(Zt / 0.5) : Zt < 1.3 ? 7 + (dn - 7) * K2((Zt - 0.5) / 0.8) : dn + 0.3 * Math.sin(Zt * 0.5);
this.ovSpin += on * dt;
}
if (this.reduceMotion) {
this._morphEyes(EYE_PLAYLIST[this.state][0]);
this.spin.t = 0; this.tx.t = 0; this.ty.t = 0;
this.squash.t = 1; this.blink.t = 1; this.eyeScale.t = 1;
}
const nSteps = springSteps(dt);
const step = dt / nSteps;
for (let i = 0; i < nSteps; i++) {
stepSpring(this.eyeMorph, this.eyeStiffness, 1, step);
if (this.spinTurn) stepSpring(this.spinTurn, ...SPRINGS.spinTurn, step);
stepSpring(this.spin, ...SPRINGS.spin, step);
stepSpring(this.tx, ...SPRINGS.x, step);
stepSpring(this.ty, ...SPRINGS.y, step);
stepSpring(this.squash, ...SPRINGS.squash, step);
stepSpring(this.blink, ...SPRINGS.blink, step);
stepSpring(this.eyeScale, ...SPRINGS.eyeScale, step);
stepSpring(this.notify, ...SPRINGS.notify, step);
stepSpring(this.humDots, ...SPRINGS.humDots, step);
stepSpring(this.gazeX, ...SPRINGS.gazeX, step);
stepSpring(this.gazeY, ...SPRINGS.gazeY, step);
stepSpring(this.overlay, ...SPRINGS.overlay, step);
stepSpring(this.overlayMix, ...SPRINGS.overlayMix, step);
stepSpring(this.shapeSpring, ...SPRINGS.shape, step);
stepSpring(this.overlayTurn, ...SPRINGS.overlayTurn, step);
}
if (this.reduceMotion) {
this.overlayMix.x = 1;
this.overlayTurn.x = this.overlayTurn.t;
this.overlay.x = this.overlay.t;
}
this.notify.t = this.state === "notifying" ? 1 : 0;
this.humDots.t = this.state === "humming" ? 1 : 0;
let spinAngle = 0;
if (this.spinTurn) spinAngle = this.spinTurn.x;
else if (this.extras.turn != null) spinAngle = this.extras.turn;
else if (humming || loading) spinAngle = this.ovSpin;
if (now - this.pxAt > 500 && this.svg.getBoundingClientRect) {
const w = this.svg.getBoundingClientRect().width;
if (w > 0) {
this.pxW = w;
this.partScale = clamp(Math.pow(340 / w, 0.7), 1, 2.6);
}
this.pxAt = now;
}
this.particles.update(now, dt, {
spinAngle,
sizeScale: this.partScale,
wideStyle: this.trick?.kind === "spinWild" || this.wildWide || humming,
sustainBelts: humming || loading,
});
this._updatePointer(now);
this._paint(now);
this._raf = requestAnimationFrame((t) => this._tick(t));
}
_paint(now) {
const geo = g.GROK_GEO;
const R = geo.Re;
const shape = geo.shapes[this.shapeName];
const morphK = K2(clamp(this.shapeSpring.x, 0, 1));
const morphing = morphK < 0.999 && this.prevFace;
const face = morphing ? lerpFace(this.prevFace, shape.face, morphK) : shape.face;
const fromTilt = this.prevTilt ?? (geo.shapes[this.prevShape]?.tiltScale || 1);
const tilt = morphing
? fromTilt + ((shape.tiltScale || 1) - fromTilt) * morphK
: (shape.tiltScale || 1);
const yl = clamp(this.overlay.x, 0, 1);
const mix = clamp(this.overlayMix.x, 0, 1);
this.fx._reduce = this.reduceMotion;
const ov = this.fx.extras(now, this.stateAt, this.ovKind, this.ovPrev, yl, mix);
const bodyW = 1 - yl;
const ex = this.extras;
const tx = this.tx.x * bodyW + ex.yi * bodyW + ov.yre * yl;
const ty = (this.ty.x + ex.hop) * bodyW + ex.ki * bodyW - ov.rX.lift * ov.Lee + ov.aX * yl;
const rot = (this.spin.x * bodyW + ex.Kr * bodyW) * tilt + (ex.Yr || 0) * bodyW + ov.wl * yl;
const sx = bodyW + ov.wre * yl;
const sy = this.squash.x * bodyW + ov.wre * yl;
this.group.setAttribute(
"transform",
`translate(${(R + tx).toFixed(2)} ${(R + ty).toFixed(2)}) rotate(${rot.toFixed(2)}) scale(${sx.toFixed(4)} ${sy.toFixed(4)}) translate(${-R} ${-R})`
);
this.group.style.opacity = ((1 - (1 - ov.rX.tone) * ov.Lee) * (1 - ov.fade)).toFixed(3);
const Jc = clamp(yl / FX.P_BLEND, 0, 1);
const pencil = this.ovKind === "pencil" || this.ovPrev === "pencil";
const tear = geo.shapes.teardrop?.path;
const spinAmt = ex.turn;
const spinning = spinAmt != null;
const restRing = morphing
? FX.lerpRing(this.prevRing, FX.shapeRing(shape.path, R), morphK)
: FX.shapeRing(shape.path, R);
let liveRing = restRing;
let turned = false;
const turnAt = !morphing && spinning ? FX.turnAtOf(this.shapeName, shape.path, R) : null;
if (turnAt) {
liveRing = turnAt(spinAmt);
turned = true;
}
let faceTop = shape.top;
let faceBottom = shape.bottom;
if (morphing || turned) {
faceTop = Infinity;
faceBottom = -Infinity;
for (const p of liveRing) {
if (p[1] < faceTop) faceTop = p[1];
if (p[1] > faceBottom) faceBottom = p[1];
}
}
let bodyD;
if (Jc >= 1) {
bodyD = pencil ? FX.closedSpline(FX.overlayRing(this.ovKind, R, tear)) : this.fx.circlePath;
} else if (Jc <= 0 && !morphing && !turned) {
bodyD = shape.path;
} else {
const to = FX.overlayRing(this.ovKind || this.ovPrev, R, tear);
bodyD = FX.closedSpline(Jc <= 0 ? liveRing : FX.lerpRing(liveRing, to, K2(Jc)));
}
this.body.setAttribute("d", bodyD);
this.clipPath.setAttribute("d", bodyD);
this.fx.paint(now, this.stateAt, this.ovKind, this.ovPrev, yl, mix, R, this.reduceMotion);
const shrink = 1 - Dke(clamp((this.pxW - 44) / 90, 0, 1));
const pScale = this.pose.scale || 1;
const zCur = overlayViewZoom(this.ovKind, pScale);
const zPrev = overlayViewZoom(this.ovPrev, pScale);
const zoom = 1 + (zCur * mix + zPrev * (1 - mix) - 1) * yl * shrink;
const half = VIEW_HALF / zoom;
this.svg.setAttribute("viewBox", `${(VIEW_MID - half).toFixed(2)} ${(VIEW_MID - half).toFixed(2)} ${(half * 2).toFixed(2)} ${(half * 2).toFixed(2)}`);
const morphT = clamp(this.eyeMorph.x, 0, 1);
const polys = this._currentPolys(morphT);
const cr = this.eyeTopology ? relRot(this.pose, this.poseHome) : null;
const overlayLive = yl > 0.001 || Math.abs(this.overlayTurn.t - this.overlayTurn.x) > 0.01;
let cyl = overlayLive ? this.overlayTurn.x : null;
if (ex.turn != null) cyl = (cyl ?? 0) + ex.turn;
const ringHint = morphing || turned ? liveRing : null;
const hasPtr = !!(this.gazeTarget || (this.followPointer && this.pointerRaw));
EY.paintEyes({
now,
polys,
morphT,
shape,
face,
faceTune: this.faceTune,
uniformEyes: this.uniformEyes,
eyeScaleProp: this.eyeScaleProp,
blinkX: this.blink.x,
eyeBoostX: this.eyeScale.x,
gazeX: this.gazeX.x,
gazeY: this.gazeY.x,
winkAt: this.winkAt,
winkEye: this.winkEye,
turn: cyl,
cr,
pointer: hasPtr ? this.pointer : null,
notifyX: this.notify.x,
overlayX: this.overlay.x,
eyeEls: this.eyeEls,
badgeEl: this.badge,
badgeColor: this.badgeColor,
Re: R,
G9e: geo.G9e,
VJt: geo.VJt,
extras: ex,
ringHint,
badgeRing: restRing,
top: faceTop,
bottom: faceBottom,
emphasisBlend: this.emphasisBlend,
});
const hum = clamp(this.humDots.x, 0, 1);
if (hum > 0.01) {
for (let i = 0; i < 2; i++) {
const el = this.fx.parts[3 + i];
if (!el) continue;
const Gn = this.ovSpin * 0.85 + i * Math.PI;
const Ti = shape.radius * 1.3;
const Ui = Math.cos(Gn);
const Si = 0.55 + 0.45 * clamp((Ui + 1) / 2, 0, 1);
el.style.display = "";
el.setAttribute("cx", (R + Ti * Math.sin(Gn)).toFixed(1));
el.setAttribute("cy", (R - Ti * 0.38 * Math.cos(Gn) - 8).toFixed(1));
el.setAttribute("r", (7.5 * Si * hum).toFixed(2));
el.setAttribute("opacity", ((0.3 + 0.7 * Si) * hum).toFixed(3));
}
}
}
}
g.GrokCharacter = GrokCharacter;
g.GROK_META = {
groups: GROUPS,
onboarding: ONBOARDING,
onboardingMs: ONBOARDING_MS,
eyePlaylist: EYE_PLAYLIST,
springs: SPRINGS,
faceTune: FACE_TUNE,
pose: POSE,
poseHome: POSE_HOME,
overlays: FX.MAP,
};
})(window);
+218
View File
@@ -0,0 +1,218 @@
/* L3 — eye morph, wink, blink queue, placement. Source en / ks / mn / P2. */
(function (g) {
const {
clamp, lerpPoly, centroid, polyPath, Dke, spanAt, spanPoly, Rn,
} = g.GROK_MATH;
function queueBlink(q, now) {
q.push(
{ at: now, v: 0.05 },
{ at: now + 70, v: 0.05 },
{ at: now + 150, v: 1.08 },
{ at: now + 300, v: 1 }
);
if (Math.random() < 0.14) {
q.push({ at: now + 370, v: 0.05 }, { at: now + 480, v: 1 });
}
}
function consumeBlink(q, now) {
let key = null;
while (q.length && now >= q[0].at) key = q.shift().v;
return key;
}
function winkLid(base, now, winkAt, winkEye, i) {
let lid = Math.max(base, 0.04);
if (i === winkEye && now < winkAt + 320) {
const xr = (now - winkAt) / 320;
const Fr = xr < 0.42 ? 1 - xr / 0.42 : (xr - 0.42) / 0.58;
lid = Math.max(lid * clamp(Fr, 0, 1), 0.04);
}
return lid;
}
function paintEyes(opt) {
const {
now, polys, morphT, shape, face, faceTune, uniformEyes, eyeScaleProp,
blinkX, gazeX, gazeY, winkAt, winkEye, turn, cr, pointer, notifyX,
overlayX, eyeEls, badgeEl, badgeColor, Re, G9e, VJt, extras, ringHint,
} = opt;
const pulse = 1 + 0.07 * Math.sin(morphT * Math.PI);
const $i = {
x: face.x,
y: face.y,
sx: face.sx * (faceTune?.gap ?? 1),
sy: face.sy * (faceTune?.height ?? 1),
eye: face.eye * (faceTune?.size ?? 1),
leftDX: face.leftDX ?? 0,
};
const sX = uniformEyes ? $i.leftDX : 0;
const cents = [centroid(polys[0]), centroid(polys[1])];
let a1 = 0, o1 = 0;
for (const p of polys[0]) a1 = Math.max(a1, Math.abs(p[0] - cents[0][0]));
for (const p of polys[1]) o1 = Math.max(o1, Math.abs(p[0] - cents[1][0]));
const l1 = Math.abs(cents[1][0] - (cents[0][0] + sX)) * $i.sx;
const pre = uniformEyes ? 0 : VJt;
const _ee = a1 + o1 > 0.5 ? clamp((l1 - pre) / (a1 + o1), 0.35, 4) : 4;
const Uee = (uniformEyes ? 1 : $i.eye) * clamp(eyeScaleProp, 0.25, 4);
const oX = Math.min(clamp(opt.eyeBoostX, 0.2, 2) * Uee, _ee / pulse);
const Hee = Math.min(oX * clamp(faceTune?.eyeWidth ?? 1, 0.2, 3), _ee / pulse);
const u1 = oX * clamp(faceTune?.eyeHeight ?? 1, 0.2, 3);
const liveSpan = ringHint
? (y) => spanPoly(ringHint, y, Re)
: spanAt(shape.path, Re);
const top = opt.top ?? shape.top;
const bottom = opt.bottom ?? shape.bottom;
const Vn = opt.emphasisBlend || 0;
const midX = (cents[0][0] + cents[1][0]) / 2;
const midY = (cents[0][1] + cents[1][1]) / 2;
const pullX = (Re - midX) * 0.42 * Vn;
const pullY = (Re - midY) * 0.42 * Vn;
const gazeW = pointer ? 0.2 : 1;
const badgeRing = opt.badgeRing || ringHint;
const Yl = badgeRing
? badgeRing[Math.round(badgeRing.length * 7 / 8) % badgeRing.length]
: [Re, shape.top];
for (let i = 0; i < 2; i++) {
const poly = polys[i];
const [Gn, Ti] = cents[i];
eyeEls[i].setAttribute("d", polyPath(poly));
const lid = winkLid(blinkX, now, winkAt, winkEye, i);
const Ea = Gn + (i === 0 ? sX : 0);
let Ca = Re + $i.x;
let Wo = (Ea - Re) * $i.sx;
let _c = 1, vre = 1, km = 1, Ree = 0, Fee = 0, zee = 1, bre = true, Tre = 1;
let Sre = clamp(Re + $i.y + (Ti - Re) * $i.sy, top + 2, bottom - 2);
const use3d = !!cr;
if (use3d) {
const xr = (Ea - Re) / Re;
const Fr = (Re - Ti) / Re;
const Ia = Math.sqrt(Math.max(0, 1 - xr * xr - Fr * Fr)) || 0.02;
const li = cr[0] * xr + cr[1] * Fr + cr[2] * Ia;
const bl = cr[3] * xr + cr[4] * Fr + cr[5] * Ia;
const Io = cr[6] * xr + cr[7] * Fr + cr[8] * Ia;
Wo = li * Re * $i.sx;
Sre = clamp(Re + $i.y - bl * Re * $i.sy, top + 2, bottom - 2);
let uo = -Fr * xr, Tl = 1 - Fr * Fr, Zi = -Fr * Ia;
const Yo = Math.hypot(uo, Tl, Zi);
if (Yo < 1e-6) {
uo = 0; Tl = 0; Zi = 1;
} else {
uo /= Yo; Tl /= Yo; Zi /= Yo;
}
const md = Fr * Zi - Ia * Tl, Oc = Ia * uo - xr * Zi, yu = xr * Tl - Fr * uo;
const vm = cr[0] * uo + cr[1] * Tl + cr[2] * Zi;
const Hme = cr[3] * uo + cr[4] * Tl + cr[5] * Zi;
const Nre = cr[0] * md + cr[1] * Oc + cr[2] * yu;
const Ere = cr[3] * md + cr[4] * Oc + cr[5] * yu;
const Cre = md, Ha = -Oc, ci = uo, Ys = -Tl;
const ku = Cre * Ys - ci * Ha || 1e-6;
const Ql = Ys / ku, Gee = -ci / ku, bm = -Ha / ku, Ire = Cre / ku;
km = Nre * Ql + vm * bm;
Fee = Nre * Gee + vm * Ire;
Ree = -Ere * Ql + -Hme * bm;
zee = -Ere * Gee + -Hme * Ire;
_c = Math.max(Math.hypot(km, Ree), 0.02);
vre = Math.max(Math.hypot(Fee, zee), 0.02);
bre = Io > 0.02;
Tre = Dke(clamp(Io / 0.5, 0, 1));
}
if (turn != null) {
const [spL, spR] = liveSpan(Sre);
const rad = Math.max((spR - spL) / 2, 12);
Ca = (spL + spR) / 2;
const li0 = Math.asin(clamp(Wo / rad, -1, 1));
const bl0 = li0 + turn;
const Io0 = Math.cos(bl0);
const uo0 = Math.max(Math.cos(li0), 0.02);
bre = Io0 > 0.02;
_c = Math.max(Io0, 0.02) / uo0;
Wo = rad * Math.sin(bl0);
Tre = Dke(clamp(Io0 / 0.5, 0, 1));
}
let Kj = Math.sin(now * 42e-5 + i) * 1.4 + Math.sin(now * 0.001 + i * 2) * 0.5;
let Ko = Math.sin(now * 58e-5 + i) * 0.9;
if (pointer) {
Kj += pointer.x * (1 - 0.6 * Vn) + pullX;
Ko += pointer.y * (1 - 0.6 * Vn) + pullY;
} else {
Kj += pullX;
Ko += pullY;
}
Kj += gazeX * gazeW + (extras.Zr || 0);
Ko += gazeY * gazeW + (extras.wi || 0);
const $ee = clamp(notifyX, 0, 1);
Kj -= 10 * $ee;
Ko += 7 * $ee;
const Vee = clamp(_c * Hee * pulse, 0.02, 2.4);
const _2 = clamp(vre * lid * u1 * pulse, 0.02, 2.4);
eyeEls[i].style.display = bre && overlayX < 0.5 ? "" : "none";
const useTurnOr3d = turn != null || use3d;
const Ume = G9e * _2 + 2;
const vl = clamp(
useTurnOr3d ? Sre + Ko * $i.sy : Re + $i.y + (Ti + Ko - Re) * $i.sy,
top + Ume,
bottom - Ume
);
let O2 = -Infinity, Xl = Infinity;
for (let p = 0; p < poly.length; p += 2) {
const Frp = (poly[p][0] - Gn) * Vee;
const [Ia2, li2] = liveSpan(vl + (poly[p][1] - Ti) * _2);
if (Ia2 - Frp > O2) O2 = Ia2 - Frp;
if (li2 - Frp < Xl) Xl = li2 - Frp;
}
const xre = Ca + Wo + Kj * $i.sx;
const lX = O2 <= Xl ? clamp(xre, O2, Xl) : (O2 + Xl) / 2;
let dd = lX + (xre - lX) * (1 - Tre);
let Yj = vl;
if (notifyX > 0.01) {
const xr = 20 * clamp(notifyX, 0, 1.4);
const Fr = dd - Yl[0], Ia = Yj - Yl[1];
const li = Math.hypot(Fr, Ia) || 1;
const bl = Fr / li, Io = Ia / li;
const uo = (i === 0 ? a1 : o1) * Vee;
const Tl = Math.hypot(uo * bl, G9e * _2 * Io);
const Zi = xr + Tl + 5;
if (li < Zi) {
dd += bl * (Zi - li);
Yj += Io * (Zi - li);
}
}
if (use3d) {
const FrM = clamp((turn != null ? _c : 1) * Hee * pulse, 0.02, 2.4);
const IaM = clamp(lid * u1 * pulse, 0.02, 2.4);
const liM = km * FrM, blM = Ree * FrM, IoM = Fee * IaM, uoM = zee * IaM;
eyeEls[i].setAttribute(
"transform",
`translate(${dd.toFixed(2)} ${Yj.toFixed(2)}) matrix(${liM.toFixed(4)} ${blM.toFixed(4)} ${IoM.toFixed(4)} ${uoM.toFixed(4)} 0 0) translate(${(-Gn).toFixed(2)} ${(-Ti).toFixed(2)})`
);
} else {
eyeEls[i].setAttribute(
"transform",
`translate(${dd.toFixed(2)} ${Yj.toFixed(2)}) scale(${Vee.toFixed(4)} ${_2.toFixed(4)}) translate(${(-Gn).toFixed(2)} ${(-Ti).toFixed(2)})`
);
}
}
if (badgeEl) {
const amt = clamp(notifyX, 0, 1.4);
if (amt <= 0.01) badgeEl.style.display = "none";
else {
badgeEl.style.display = "";
badgeEl.style.fill = badgeColor || "var(--gb-badge, #1d9bf0)";
badgeEl.setAttribute("cx", Yl[0].toFixed(1));
badgeEl.setAttribute("cy", Yl[1].toFixed(1));
badgeEl.setAttribute("r", (20 * amt).toFixed(2));
}
}
}
g.GROK_EYES = { queueBlink, consumeBlink, paintEyes, lerpPoly };
})(window);
+908
View File
@@ -0,0 +1,908 @@
/* L3 — overlay glyphs + particle ribbons. Symbols: E_t I_t za/$a/Ms/oi/_r/ua/No/Eo/Ho/hl/cd/Bt. */
(function (global) {
const NS = "http://www.w3.org/2000/svg";
const Re0 = 114.2705;
const STAR_COLOR = "#f4c34e";
const PALETTE = ["#f9705c", "#5b95f0", "#3fbe86", "#f5b13f", "#9a72ee", "#35c3bd"];
const STAR = (() => {
const n = [];
for (let e = 0; e < 10; e++) {
const t = -Math.PI / 2 + (e * Math.PI) / 5;
const s = e % 2 === 0 ? 1 : 0.42;
n.push(`${(Math.cos(t) * s).toFixed(3)} ${(Math.sin(t) * s).toFixed(3)}`);
}
return "M" + n.join("L") + "Z";
})();
const MAP = {
thinking: "dots",
orbit: "orbit",
radar: "radar",
progress: "progress",
spawning: "gather",
dictating: "wave",
sending: "send",
receiving: "receive",
uploading: "dock",
bouncing: "ball",
loading: "whirl",
"powering-down": "standby",
writing: "pencil",
alerting: "bang",
};
const CYCLE = new Set(["progress", "spawning"]);
const CYCLE_ON = { progress: 2500, spawning: 2000 };
const CYCLE_OFF = 1500;
const SCALE = {
dots: 1.5, orbit: 1.14, radar: 1.14, progress: 1.32, gather: 1.15,
wave: 1.42, send: 1.12, receive: 1.12, dock: 1.3, ball: 1.22,
whirl: 1.45, pencil: 1.18, bang: 1.28, standby: 1.75,
};
const RADIUS = {
dots: 22, orbit: 19, radar: 19, progress: 19, gather: 19, wave: 16,
send: 20, receive: 20, dock: 20, ball: 18, whirl: 15, pencil: 17,
bang: 13, standby: 13,
};
const P_BLEND = 0.62;
const DOT_R = 22;
const DOT_GAP = 62;
const POP0 = 0.84;
const POP1 = 0.22;
const SEND_MS = 1500;
const RECV_MS = 1700;
const PENCIL_MS = 2500;
const clamp = (n, a, b) => Math.min(b, Math.max(a, n));
const rand = (a, b) => a + Math.random() * (b - a);
const Rc = (n) => 1 - Math.pow(1 - n, 3);
const y1e = (n) => 1 + 2.70158 * Math.pow(n - 1, 3) + 1.70158 * Math.pow(n - 1, 2);
const K2 = (n) => (n < 0.5 ? 4 * n * n * n : 1 - Math.pow(-2 * n + 2, 3) / 2);
function el(tag, attrs) {
const n = document.createElementNS(NS, tag);
if (attrs) for (const k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function closedSpline(pts) {
const e = pts.length;
let t = `M${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`;
for (let s = 0; s < e; s++) {
const r = pts[(s - 1 + e) % e], i = pts[s], o = pts[(s + 1) % e], l = pts[(s + 2) % e];
t += `C${(i[0] + (o[0] - r[0]) / 6).toFixed(2)} ${(i[1] + (o[1] - r[1]) / 6).toFixed(2)} ${(o[0] - (l[0] - i[0]) / 6).toFixed(2)} ${(o[1] - (l[1] - i[1]) / 6).toFixed(2)} ${o[0].toFixed(2)} ${o[1].toFixed(2)}`;
}
return t + "Z";
}
function circleRing(R, n = 96) {
return Array.from({ length: n }, (_, e) => {
const t = (e / n) * Math.PI * 2;
return [R + Math.cos(t) * R, R + Math.sin(t) * R];
});
}
function flattenPath(d, step = 4) {
const t = d.match(/[MLCQZmlcqz]|-?\d*\.?\d+(?:e[-+]?\d+)?/g) ?? [];
const s = [];
let r = 0, i = "", o = 0, l = 0, c = 0, u = 0;
const rd = () => parseFloat(t[r++]);
const m = (f, h) => {
const y = Math.max(2, Math.ceil(h / step));
for (let k = 1; k <= y; k++) s.push(f(k / y));
};
while (r < t.length) {
if (/[a-z]/i.test(t[r])) i = t[r++].toUpperCase();
if (i === "Z") {
if (Math.hypot(c - o, u - l) > 0.01) m((f) => [o + (c - o) * f, l + (u - l) * f], Math.hypot(c - o, u - l));
o = c; l = u; continue;
}
if (r >= t.length) break;
if (i === "M") { o = rd(); l = rd(); c = o; u = l; s.push([o, l]); i = "L"; }
else if (i === "L") { const f = rd(), h = rd(); m((y) => [o + (f - o) * y, l + (h - l) * y], Math.hypot(f - o, h - l)); o = f; l = h; }
else if (i === "C") {
const f = rd(), h = rd(), y = rd(), k = rd(), v = rd(), b = rd(), x = o, N = l;
m((E) => {
const A = 1 - E;
return [A * A * A * x + 3 * A * A * E * f + 3 * A * E * E * y + E * E * E * v, A * A * A * N + 3 * A * A * E * h + 3 * A * E * E * k + E * E * E * b];
}, Math.hypot(f - o, h - l) + Math.hypot(y - f, k - h) + Math.hypot(v - y, b - k));
o = v; l = b;
} else r++;
}
return s;
}
function polarRing(pts, R, n = 96) {
return Array.from({ length: n }, (_, t) => {
const s = (t / n) * Math.PI * 2, r = Math.cos(s), i = Math.sin(s);
let o = 0;
for (let l = 0; l < pts.length; l++) {
const c = pts[l], u = pts[(l + 1) % pts.length];
const d = c[0] - R, m = c[1] - R, f = u[0] - R, h = u[1] - R;
const y = (f - d) * i - (h - m) * r;
if (Math.abs(y) < 1e-9) continue;
const k = (d * i - m * r) / -y;
if (k < 0 || k > 1) continue;
const v = (d + (f - d) * k) * r + (m + (h - m) * k) * i;
if (v > o) o = v;
}
return [R + r * o, R + i * o];
});
}
function rotateRing(n, e, R) {
const t = n.length, s = (e / t) * Math.PI * 2, r = Math.cos(s), i = Math.sin(s);
return Array.from({ length: t }, (_, l) => {
const [c, u] = n[((l - e) % t + t) % t];
const d = c - R, m = u - R;
return [R + d * r - m * i, R + d * i + m * r];
});
}
function lerpRing(a, b, t) {
return a.map((p, i) => [p[0] + (b[i][0] - p[0]) * t, p[1] + (b[i][1] - p[1]) * t]);
}
function spanHalf(ring, y, R) {
let left = -Infinity, right = Infinity;
for (let i = 0; i < ring.length; i++) {
const a = ring[i], b = ring[(i + 1) % ring.length];
if (a[1] <= y === b[1] <= y) continue;
const x = a[0] + (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]);
if (x <= R) {
if (x > left) left = x;
} else if (x < right) right = x;
}
return [Number.isFinite(left) ? left : R, Number.isFinite(right) ? right : R];
}
function capsule(n, e, R) {
const t = n / 2, s = R - e / 2 + t, r = R + e / 2 - t;
return `M${R - t} ${s}A${t} ${t} 0 0 1 ${R + t} ${s}L${R + t} ${r}A${t} ${t} 0 0 1 ${R - t} ${r}Z`;
}
function taper(n, e, t, R) {
const s = n / 2, r = e / 2, i = R - t / 2, o = R + t / 2;
return `M${R - s} ${i + s}A${s} ${s} 0 0 1 ${R + s} ${i + s}L${R + r} ${o - r}A${r} ${r} 0 0 1 ${R - r} ${o - r}Z`;
}
const ringCache = new Map();
function shapeRing(path, R) {
let r = ringCache.get(path);
if (!r) {
r = polarRing(flattenPath(path), R);
ringCache.set(path, r);
}
return r;
}
function pencilPose(now, stateAt) {
const Pt = now - stateAt;
const mt = (((Pt / PENCIL_MS) % 1) + 1) % 1;
if (mt < 0.68) {
const Mt = mt / 0.68;
const Lt = Mt * Mt * (3 - 2 * Mt);
const yn = clamp(Mt / 0.08, 0, 1) * clamp((1 - Mt) / 0.08, 0, 1);
return { x: -54 + 118 * Lt, y: 26, wig: Math.sin(Mt * 24) * 3.2 * yn, rot: 17 + Math.sin(Pt * 6e-4) * 1, lift: false };
}
const Dt = K2((mt - 0.68) / 0.32);
return { x: 64 - 118 * Dt, y: 26 - 20 * Math.sin(Dt * Math.PI), wig: 0, rot: 17 - 2 * Math.sin(Dt * Math.PI) + Math.sin(Pt * 6e-4) * 1, lift: true };
}
function smoothLine(pts) {
const Pt = pts.length;
let mt = `M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
if (Pt === 2) return mt + `L${pts[1][0].toFixed(1)} ${pts[1][1].toFixed(1)}`;
for (let Dt = 0; Dt < Pt - 1; Dt++) {
const Mt = pts[Math.max(Dt - 1, 0)], Lt = pts[Dt], yn = pts[Dt + 1], an = pts[Math.min(Dt + 2, Pt - 1)];
mt += `C${(Lt[0] + (yn[0] - Mt[0]) / 6).toFixed(1)} ${(Lt[1] + (yn[1] - Mt[1]) / 6).toFixed(1)} ${(yn[0] - (an[0] - Lt[0]) / 6).toFixed(1)} ${(yn[1] - (an[1] - Lt[1]) / 6).toFixed(1)} ${yn[0].toFixed(1)} ${yn[1].toFixed(1)}`;
}
return mt;
}
function wave(ze) {
return 0.42 + 0.29 * Math.sin(ze * 0.0021) * Math.sin(ze * 0.0034) + 0.29 * Math.sin(ze * 0.0013 + 1.7);
}
function createParticles({ back, front, idPrefix, getRadius }) {
const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
const scale = () => getRadius() / Re0;
let spin = 0, sizeScale = 1, wide = false, sustain = false, last = -1;
let parts = [];
const burst = (W = 20, H = 1, G = 0) => {
if (reduce || !back || parts.length > 120) return;
const R = global.GROK_GEO.Re;
for (let Y = 0; Y < W; Y++) {
const U = (Y / W) * Math.PI * 2 + rand(-0.35, 0.35);
const ee = rand(96, 116) * scale();
const te = rand(170, 360) * H;
const ne = -Math.sin(U), j = Math.cos(U), Z = G * te * 0.2;
const X = Math.random() < 0.18;
parts.push({
x: R + Math.cos(U) * ee, y: R + Math.sin(U) * ee,
vx: Math.cos(U) * te + ne * Z, vy: Math.sin(U) * te + j * Z - rand(20, 75),
life: 0, max: rand(0.45, 0.85), r: X ? rand(4, 7) : rand(3.5, 8),
rot: rand(0, 360), vr: rand(-260, 260), color: X ? STAR_COLOR : PALETTE[(Math.random() * PALETTE.length) | 0],
round: !X && Math.random() < 0.3, star: X, orbit: null, el: null,
});
}
};
let planes = [], hue0 = 0, beltN = 4, spawnQ = [];
let prevSpin = 0, spinVel = 0, seeding = false, cooling = false, trailId = 0;
const THRESH = 0.9, HARD = 5;
const makePlanes = (W = 1) => {
const H = rand(-0.85, 0.85);
planes = [];
for (let G = 0; G < W; G++) planes.push({ tilt: rand(0.16, 0.5), roll: H + (G * Math.PI) / W + rand(-0.12, 0.12) });
beltN = W > 1 ? W * 3 : Math.round(rand(3, 5));
hue0 = rand(0, 360);
};
const spawnBelt = (lam, dir, i) => {
if (parts.length > 110) return;
if (!planes.length) makePlanes();
const Y = planes[i % planes.length];
const R = global.GROK_GEO.Re;
parts.push({
x: R, y: R, vx: 0, vy: 0, ret: 0, life: 0, max: 9,
r: (beltN <= 3 ? rand(8, 10.5) : beltN === 4 ? rand(6.6, 8.6) : rand(5.6, 7.4)),
rot: rand(0, 360), vr: rand(-240, 240), color: PALETTE[(Math.random() * PALETTE.length) | 0],
hue: hue0 + (i * 360) / Math.max(beltN, 1) + rand(-14, 14),
hueSpan: rand(45, 95) * (Math.random() < 0.5 ? 1 : -1),
hueVel: rand(18, 42) * (Math.random() < 0.5 ? 1 : -1),
orbit: {
lam, lamVel: dir * rand(0.5, 1.1), tilt: Y.tilt + rand(-0.04, 0.04), roll: Y.roll + rand(-0.05, 0.05),
rad: scale() * 116 + ((i / planes.length) | 0) * (38 / Math.max(Math.ceil(beltN / planes.length) - 1, 1)) + rand(-1.5, 1.5),
radVel: rand(0, 2.5), follow: rand(0.74, 0.94), carry: 0, arc: rand(2.2, 3.4),
},
hist: [], el: null, trailEl: null, trailFrontEl: null, gradEl: null, stops: null,
});
};
const project = (W, H) => {
const G = W.rad * Math.sin(H), Y = -W.rad * Math.cos(H) * Math.sin(W.tilt);
const U = Math.cos(W.roll), ee = Math.sin(W.roll);
const R = global.GROK_GEO.Re;
return { x: R + G * U - Y * ee, y: R + G * ee + Y * U };
};
const depth = (W, H) => Math.cos(H) * Math.cos(W.tilt);
const q = (W) => Math.round(W * 10) / 10;
const ribbon = (W, H) => {
const G = W.length;
let Y = 0;
for (let le = 1; le < G; le++) Y += Math.hypot(W[le].x - W[le - 1].x, W[le].y - W[le - 1].y);
const U = Math.min(H, Y * 0.34), ee = [], te = [];
for (let le = 0; le < G; le++) {
const Q = W[le > 0 ? le - 1 : 0], ae = W[le < G - 1 ? le + 1 : G - 1];
let ce = ae.x - Q.x, xe = ae.y - Q.y;
const Se = Math.hypot(ce, xe) || 1;
ce /= Se; xe /= Se;
const fe = (U * (0.5 + 0.5 * (le / (G - 1)))) / 2;
ee.push(-xe * fe); te.push(ce * fe);
}
const arc = (le) => `A${q(Math.max(Math.hypot(ee[le], te[le]), 0.2))} ${q(Math.max(Math.hypot(ee[le], te[le]), 0.2))} 0 0 0 `;
const band = (le, Q) => {
let ae = "";
for (let ce = le; ce <= Q; ce++) ae += `${ce === le ? "M" : "L"}${q(W[ce].x + ee[ce])} ${q(W[ce].y + te[ce])}`;
ae += Q === G - 1 ? arc(Q) : "L";
for (let ce = Q; ce >= le; ce--) ae += `${ce === Q ? "" : "L"}${q(W[ce].x - ee[ce])} ${q(W[ce].y - te[ce])}`;
if (le === 0) ae += `${arc(0)}${q(W[0].x + ee[0])} ${q(W[0].y + te[0])}`;
return ae + "Z";
};
if (Y < 2) return { front: "", back: "" };
let Z = "", X = "", se = 0;
while (se < G) {
const le = W[se].z >= 0;
let Q = se;
while (Q + 1 < G && W[Q + 1].z >= 0 === le) Q++;
const ae = Math.max(se - 1, 0), ce = Math.min(Q + 1, G - 1);
if (ce > ae) { const xe = band(ae, ce); le ? (Z += xe) : (X += xe); }
se = Q + 1;
}
return { front: Z, back: X };
};
const tickVel = (dt) => {
let H = spin - prevSpin;
if (!isFinite(H) || Math.abs(H) > 1.2) H = 0;
prevSpin = spin;
const was = Math.abs(spinVel) >= THRESH;
spinVel = dt > 0 ? H / dt : 0;
const nowFast = Math.abs(spinVel) >= THRESH;
if (!was && nowFast) { makePlanes(wide ? 3 : 1); seeding = false; cooling = false; }
if (was && !nowFast) { spawnQ.length = 0; cooling = false; }
};
const seedBelts = (now) => {
if (reduce || !back) return;
const H = Math.abs(spinVel);
const live = parts.some((U) => U.orbit != null && U.ret < 1);
if (sustain && seeding && spawnQ.length === 0 && H >= THRESH && !live) { seeding = false; cooling = true; }
if (!seeding && (H >= HARD || (sustain && cooling && H >= THRESH))) {
seeding = true; cooling = false; spawnQ = [];
for (let U = 0; U < beltN; U++) spawnQ.push({ at: now + U * rand(55, 105), i: U });
}
while (spawnQ.length && now >= spawnQ[0].at) {
const U = spawnQ.shift();
spawnBelt(spin - rand(0, 0.18), Math.sign(spinVel) || 1, U.i);
}
};
const step = (dt, realDt) => {
if (!back || !parts.length) return;
const spinning = Math.abs(spinVel) >= THRESH;
const keep = [];
const R = global.GROK_GEO.Re;
for (const j of parts) {
j.life += j.life > 0 ? realDt : dt;
const Z = clamp(j.life / j.max, 0, 1);
if (j.orbit) {
const ce = !spinning || Z > 0.55;
j.ret = clamp(j.ret + (ce ? realDt / 0.5 : -realDt / 0.35), 0, 1);
if (j.ret >= 1) { j.trailEl?.remove(); j.trailFrontEl?.remove(); j.gradEl?.remove(); continue; }
} else if (j.life >= j.max) { j.el?.remove(); continue; }
const X = j.orbit ? Math.min(1, j.life / 0.26) : Z < 0.1 ? Z / 0.1 : Math.pow(1 - (Z - 0.1) / 0.9, 1.7);
if (j.orbit) {
const ce = j.orbit;
if (spinning) {
ce.carry = spinVel * ce.follow;
ce.lam += spinVel * dt * ce.follow + ce.lamVel * dt;
ce.rad += ce.radVel * dt;
} else {
ce.lam += (ce.carry + ce.lamVel) * dt;
ce.carry *= Math.exp(-2.6 * dt);
ce.lamVel *= Math.exp(-2.6 * dt);
ce.rad += ce.radVel * dt;
}
const xe = project(ce, ce.lam);
j.x = xe.x; j.y = xe.y;
const Se = depth(ce, ce.lam);
const fe = 0.72 + 0.28 * clamp(Se, 0, 1);
const ke = Math.min(j.life / 0.34, 1);
const be = ke * ke * (3 - 2 * ke);
const Ne = Math.max(j.r * fe * 1.7 * sizeScale * be * (1 - 0.72 * j.ret * j.ret), 0.5);
if (!j.trailEl) {
const de = el("path", { "data-trail": "", stroke: "none" });
const Te = el("linearGradient", { id: `${idPrefix}t${trailId++}`, gradientUnits: "userSpaceOnUse" });
j.stops = [];
for (let Ie = 0; Ie < 5; Ie++) {
const qe = el("stop", { offset: (Ie / 4).toFixed(3) });
Te.appendChild(qe); j.stops.push(qe);
}
back.appendChild(Te); j.gradEl = Te;
de.setAttribute("fill", `url(#${Te.id})`);
back.appendChild(de); j.trailEl = de;
const Ce = el("path", { "data-trail": "", stroke: "none", fill: de.getAttribute("fill") });
front?.appendChild(Ce); j.trailFrontEl = Ce;
}
const Ae = j.hist;
const oe = Ae.length ? Ae[Ae.length - 1].l : ce.lam;
const ve = ce.lam - oe;
const ge = Math.min(Math.ceil(Math.abs(ve) / 0.09), 24);
for (let de = 1; de <= ge; de++) {
const Te = oe + (ve * de) / ge, Je = project(ce, Te);
Ae.push({ x: Je.x, y: Je.y, l: Te, z: depth(ce, Te) });
}
if (!Ae.length) Ae.push({ x: j.x, y: j.y, l: ce.lam, z: Se });
const ye = ce.arc * (1 - j.ret * j.ret * (3 - 2 * j.ret));
while (Ae.length > 2 && Math.abs(ce.lam - Ae[0].l) > ye) Ae.shift();
const ue = Math.abs(ce.lam - Ae[0].l) - ye;
if (Ae.length >= 2 && ue > 0) {
const de = Ae[0].l + Math.sign(ce.lam - Ae[0].l) * ue, Te = project(ce, de);
Ae[0] = { x: Te.x, y: Te.y, l: de, z: depth(ce, de) };
}
if (Ae.length > 48) Ae.splice(0, Ae.length - 48);
if (Ae.length >= 2) {
const { front: de, back: Te } = ribbon(Ae, Ne);
const Je = X.toFixed(3);
j.trailEl.setAttribute("d", Te); j.trailEl.setAttribute("opacity", Je);
j.trailFrontEl?.setAttribute("d", de); j.trailFrontEl?.setAttribute("opacity", Je);
if (j.stops) {
const qe = (j.hue ?? 0) + (j.hueVel ?? 0) * j.life;
for (let we = 0; we < j.stops.length; we++) {
const Pe = we / (j.stops.length - 1), je = qe + Pe * (j.hueSpan ?? 120);
j.stops[we].setAttribute("stop-color", `hsl(${(((je % 360) + 360) % 360).toFixed(0)} 56% ${(56 + 11 * Pe).toFixed(0)}%)`);
}
}
const Ce = Ae[0], Ie = Ae[Ae.length - 1];
j.gradEl?.setAttribute("x1", Ce.x.toFixed(1));
j.gradEl?.setAttribute("y1", Ce.y.toFixed(1));
j.gradEl?.setAttribute("x2", Ie.x.toFixed(1));
j.gradEl?.setAttribute("y2", Ie.y.toFixed(1));
} else {
j.trailEl.setAttribute("opacity", "0");
j.trailFrontEl?.setAttribute("opacity", "0");
}
keep.push(j);
continue;
}
j.x += j.vx * dt; j.y += j.vy * dt;
const se = Math.pow(0.94, dt * 60);
j.vx *= se; j.vy = j.vy * se + 40 * dt;
const le = j.life / j.max;
const Q = le < 0.1 ? le / 0.1 : Math.pow(1 - (le - 0.1) / 0.9, 1.7);
const ae = Math.max(j.r * (1 - le * 0.4), 0.5);
if (!j.el) {
const ce = el(j.star ? "path" : j.round ? "circle" : "rect");
if (j.star) ce.setAttribute("d", STAR);
ce.setAttribute("fill", j.color);
back.appendChild(ce); j.el = ce;
}
j.el.setAttribute("opacity", Q.toFixed(3));
if (j.star) {
j.rot += j.vr * dt;
j.el.setAttribute("transform", `translate(${j.x.toFixed(1)} ${j.y.toFixed(1)}) rotate(${j.rot.toFixed(1)}) scale(${ae.toFixed(2)})`);
} else if (j.round) {
j.el.setAttribute("cx", j.x.toFixed(1)); j.el.setAttribute("cy", j.y.toFixed(1)); j.el.setAttribute("r", ae.toFixed(2));
} else {
const ce = Math.hypot(j.vx, j.vy);
const xe = Math.max(ae * 2, Math.min(ce * 0.05, 30)), Se = ae * 1.5;
const fe = (Math.atan2(j.vy, j.vx) * 180) / Math.PI;
j.el.setAttribute("width", xe.toFixed(1)); j.el.setAttribute("height", Se.toFixed(1));
j.el.setAttribute("rx", (Se / 2).toFixed(2));
j.el.setAttribute("x", (j.x - xe / 2).toFixed(1)); j.el.setAttribute("y", (j.y - Se / 2).toFixed(1));
j.el.setAttribute("transform", `rotate(${fe.toFixed(1)} ${j.x.toFixed(1)} ${j.y.toFixed(1)})`);
}
keep.push(j);
}
parts = keep;
};
return {
burst,
clear() {
for (const W of parts) { W.el?.remove(); W.trailEl?.remove(); W.trailFrontEl?.remove(); W.gradEl?.remove(); }
parts = []; spawnQ = []; seeding = false; cooling = false;
},
update(now, dt, G) {
const Y = last < 0 ? dt : Math.max((now - last) / 1000, 0);
last = now;
sizeScale = G.sizeScale; spin = G.spinAngle; wide = G.wideStyle; sustain = G.sustainBelts === true;
tickVel(dt); seedBelts(now); step(dt, Y);
},
hasLife: () => parts.length > 0 || spawnQ.length > 0,
};
}
class OverlayLayer {
constructor() {
this.uid = `fx-${Math.random().toString(36).slice(2, 8)}`;
this.back = el("g", { "aria-hidden": "true" });
this.front = el("g", { "aria-hidden": "true" });
this.dots = [0, 1].map(() => el("path", { style: "fill:var(--fg);display:none" }));
this.rings = [0, 1, 2, 3, 4, 5, 6].map(() =>
el("circle", { cx: "0", cy: "0", r: "0", fill: "none", style: "display:none;stroke:var(--fg)" })
);
this.parts = [0, 1, 2, 3, 4, 5, 6].map(() =>
el("circle", { cx: "0", cy: "0", r: "0", style: "fill:var(--fg);display:none" })
);
this.glyphs = [0, 1, 2].map(() => el("path", { style: "display:none" }));
this.ink = [];
this.recvDir = -0.7;
this.recvTick = -1;
this.overlayAt = 0;
this.circlePath = "";
this.pencilPath = "";
this.bangPath = "";
}
attach(svg, bodyGroup) {
svg.appendChild(this.back);
this.dots.forEach((n) => svg.appendChild(n));
this.rings.forEach((n) => svg.appendChild(n));
this.parts.forEach((n) => svg.appendChild(n));
this.glyphs.forEach((n) => svg.appendChild(n));
svg.appendChild(bodyGroup);
svg.appendChild(this.front);
}
hideAll() {
for (const n of this.dots) n.style.display = "none";
for (const n of this.rings) n.style.display = "none";
for (const n of this.parts) n.style.display = "none";
for (const n of this.glyphs) n.style.display = "none";
}
amount(name, cur, prev, yl, mix) {
if (name === cur) return yl * mix;
if (name === prev) return yl * (1 - mix);
return 0;
}
dotsPulse(now, slot, yl, reduce = false) {
const Dt = ((((now - this.overlayAt) / 1400 + 0.119) % 1) + 1) % 1;
let Mt = Math.abs(Dt - slot / 3);
Mt = Math.min(Mt, 1 - Mt);
const Lt = reduce ? 1 : Math.exp(-(Mt * Mt) / (2 * 0.15 * 0.15));
const yn = reduce ? 0 : 1;
return { lift: Lt * 9 * yl * yn, pop: 1 + yn * (POP0 + POP1 * Lt - 1), tone: 1 - yn * 0.5 * (1 - Lt) };
}
paint(now, stateAt, cur, prev, yl, mix, R, reduce = false) {
this.hideAll();
this._reduce = reduce;
const extra = this.extras(now, stateAt, cur, prev, yl, mix);
const kl = (name) => this.amount(name, cur, prev, yl, mix);
const run = (name, fn) => { const a = kl(name); if (a > 0.004) fn(a); };
run("dots", (a) => this.paintDots(a, now, R));
run("orbit", (a) => this.paintOrbit(a, now, R));
run("radar", (a) => this.paintRadar(a, now, R, extra.A2));
run("progress", (a) => this.paintProgress(a, now, R));
run("gather", (a) => this.paintGather(a, now, R));
run("wave", (a) => this.paintWave(a, now, R));
run("send", (a) => this.paintSend(a, now, stateAt, R));
run("receive", (a) => this.paintRecv(a, now, stateAt, R));
run("dock", (a) => this.paintDock(a, now, stateAt, R));
run("pencil", (a) => this.paintPencil(a, now, stateAt, R));
run("bang", (a) => this.paintBang(a, now, stateAt, R));
run("standby", (a) => this.paintStandby(a, now, R));
}
paintDots(ze, now, R) {
const mt = [R - DOT_GAP, R + DOT_GAP];
for (let Dt = 0; Dt < 2; Dt++) {
const Mt = this.dots[Dt];
const Lt = clamp((ze - Dt * 0.12) / (1 - Dt * 0.12), 0, 1);
if (Lt <= 0.004) { Mt.style.display = "none"; continue; }
const yn = Rc(Lt), an = y1e(Lt), Et = this.dotsPulse(now, Dt === 0 ? 0 : 2, ze, this._reduce);
const En = (DOT_R * yn * Et.pop) / R * 1.02;
Mt.style.display = "";
Mt.setAttribute("d", this.circlePath);
Mt.setAttribute("transform", `translate(${(R + (mt[Dt] - R) * an).toFixed(1)} ${(R - Et.lift).toFixed(1)}) scale(${En.toFixed(4)}) translate(${-R} ${-R})`);
Mt.setAttribute("opacity", (yn * Et.tone).toFixed(3));
}
}
paintOrbit(ze, now, R) {
const mt = Rc(ze), Mt = 52 * y1e(ze), Lt = 12, yn = now * 0.0017;
for (let an = 0; an < 5; an++) {
const Et = this.parts[an];
const En = yn + (an * Math.PI * 2) / 5, Zt = Math.cos(En), dn = 0.5 + 0.5 * clamp(Zt, 0, 1);
Et.style.display = "";
Et.setAttribute("cx", (R + Mt * Math.sin(En)).toFixed(1));
Et.setAttribute("cy", (R - Mt * 0.42 * Math.cos(En)).toFixed(1));
Et.setAttribute("r", Math.max(Lt * dn * mt, 0.3).toFixed(2));
Et.setAttribute("opacity", (clamp((Zt + 0.4) / 0.6, 0.18, 1) * mt).toFixed(3));
}
}
paintRadar(ze, now, R, bodyR) {
const Dt = Rc(ze);
for (let yn = 0; yn < 3; yn++) {
const an = this.rings[yn];
const Et = (now / 1300 + yn / 3) % 1;
an.style.display = "";
an.removeAttribute("stroke-dasharray");
an.removeAttribute("transform");
an.setAttribute("cx", `${R}`); an.setAttribute("cy", `${R}`);
an.setAttribute("r", (bodyR + (104 - bodyR) * Et).toFixed(1));
an.setAttribute("stroke-width", (3.4 * (1 - Et * 0.55)).toFixed(2));
an.setAttribute("opacity", (Dt * (1 - Et) * 0.9).toFixed(3));
}
}
paintProgress(ze, now, R) {
const mt = Rc(ze), Dt = y1e(ze), Mt = 62;
const Lt = clamp((now - this.overlayAt) / CYCLE_ON.progress, 0, 1);
const yn = clamp(Lt / 0.85, 0, 1);
const an = this.rings[3];
an.style.display = "";
an.setAttribute("cx", `${R}`); an.setAttribute("cy", `${R}`);
an.setAttribute("r", (Mt * Dt).toFixed(1));
an.setAttribute("stroke-width", "5");
an.removeAttribute("stroke-dasharray");
an.removeAttribute("transform");
an.setAttribute("opacity", (mt * 0.16).toFixed(3));
const Et = this.rings[4];
const En = Mt * Dt, Zt = 2 * Math.PI * En;
Et.style.display = "";
Et.setAttribute("cx", `${R}`); Et.setAttribute("cy", `${R}`);
Et.setAttribute("r", En.toFixed(1));
Et.setAttribute("stroke-width", "5");
Et.setAttribute("stroke-dasharray", `${Zt.toFixed(1)}`);
Et.setAttribute("stroke-dashoffset", (Zt * (1 - yn)).toFixed(1));
Et.setAttribute("transform", `rotate(-90 ${R} ${R})`);
Et.setAttribute("opacity", mt.toFixed(3));
}
paintGather(ze, now, R) {
const mt = Rc(ze), Dt = CYCLE_ON.spawning;
for (let Mt = 0; Mt < 5; Mt++) {
const Lt = this.parts[Mt];
const yn = clamp(((now - this.overlayAt) / Dt - Mt * 0.09) / 0.62, 0, 1);
if (yn >= 1) { Lt.style.display = "none"; continue; }
const an = 1 - Math.pow(1 - yn, 3), Et = Mt * 2.4 + yn * 2.2, En = 96 * (1 - an);
Lt.style.display = "";
Lt.setAttribute("cx", (R + En * Math.cos(Et)).toFixed(1));
Lt.setAttribute("cy", (R + En * Math.sin(Et) * 0.8).toFixed(1));
Lt.setAttribute("r", (9 * (0.5 + 0.5 * an) * mt).toFixed(2));
Lt.setAttribute("opacity", (mt * clamp(yn * 5, 0, 1) * (1 - an * 0.25)).toFixed(3));
}
}
paintWave(ze, now, R) {
const mt = [-2, -1, 1, 2], Dt = 44;
for (let Mt = 0; Mt < 4; Mt++) {
const Lt = Mt < 2 ? this.dots[Mt] : this.parts[3 + Mt];
const yn = mt[Mt];
const an = clamp((ze - Math.abs(yn) * 0.1) / (1 - Math.abs(yn) * 0.1), 0, 1);
if (an <= 0.004) { Lt.style.display = "none"; continue; }
const Et = y1e(an), En = wave(now) * (0.55 + 0.45 * Math.sin(now * 0.012 - Math.abs(yn) * 1.05));
const Zt = (7 + 9 * clamp(En, 0.08, 1)) * Rc(an), dn = 6 * clamp(En, 0, 1) * an;
Lt.style.display = "";
if (Mt < 2) {
const on = (Zt / R) * 1.02;
Lt.setAttribute("d", this.circlePath);
Lt.setAttribute("transform", `translate(${(R + yn * Dt * Et).toFixed(1)} ${(R - dn).toFixed(1)}) scale(${on.toFixed(4)}) translate(${-R} ${-R})`);
Lt.setAttribute("opacity", an.toFixed(3));
} else {
Lt.setAttribute("cx", (R + yn * Dt * Et).toFixed(1));
Lt.setAttribute("cy", (R - dn).toFixed(1));
Lt.setAttribute("r", Zt.toFixed(2));
Lt.setAttribute("opacity", an.toFixed(3));
}
}
}
paintSend(ze, now, stateAt, R) {
const mt = Rc(ze), Dt = ((((now - stateAt) / SEND_MS) % 1) + 1) % 1;
const Mt = clamp((Dt - 0.18) / 0.55, 0, 1), Lt = Mt * Mt * (0.4 + 0.6 * Mt);
const yn = 0.74, an = -0.62, Et = 108 * Lt, En = this.parts[5];
const on = Mt > 0 && Mt < 1;
En.style.display = on ? "" : "none";
if (on) {
En.setAttribute("cx", (R + yn * Et).toFixed(1));
En.setAttribute("cy", (R + an * Et).toFixed(1));
En.setAttribute("r", (10 * (1 - Lt * 0.55) * mt).toFixed(2));
En.setAttribute("opacity", (mt * (1 - Lt * Lt)).toFixed(3));
}
const Zt = this.parts[6];
const on2 = clamp((Dt - 0.26) / 0.55, 0, 1), bn = on2 * on2 * (0.4 + 0.6 * on2);
const Cn = Mt > 0 && on2 > 0 && on2 < 1;
Zt.style.display = Cn ? "" : "none";
if (Cn) {
const bi = 108 * bn;
Zt.setAttribute("cx", (R + yn * bi).toFixed(1));
Zt.setAttribute("cy", (R + an * bi).toFixed(1));
Zt.setAttribute("r", (5 * (1 - bn * 0.6) * mt).toFixed(2));
Zt.setAttribute("opacity", (mt * 0.3 * (1 - bn)).toFixed(3));
}
const dn = this.rings[5];
const on3 = clamp((Dt - 0.18) / 0.3, 0, 1), bn3 = on3 > 0 && on3 < 1;
dn.style.display = bn3 ? "" : "none";
if (bn3) {
dn.removeAttribute("stroke-dasharray"); dn.removeAttribute("transform");
dn.setAttribute("cx", `${R}`); dn.setAttribute("cy", `${R}`);
dn.setAttribute("r", (20 + 34 * Rc(on3)).toFixed(1));
dn.setAttribute("stroke-width", (2.8 * (1 - on3)).toFixed(2));
dn.setAttribute("opacity", (mt * (1 - on3) * 0.8).toFixed(3));
}
}
paintRecv(ze, now, stateAt, R) {
const mt = Rc(ze), Dt = now - stateAt, Mt = Math.floor(Dt / RECV_MS);
if (Mt !== this.recvTick) { this.recvTick = Mt; this.recvDir = rand(-Math.PI * 1.25, Math.PI * 0.25); }
const Lt = (((Dt / RECV_MS) % 1) + 1) % 1, yn = clamp(Lt / 0.6, 0, 1), an = 1 - Math.pow(1 - yn, 3);
const Et = Math.cos(this.recvDir), En = Math.sin(this.recvDir), Zt = 108 * (1 - an), dn = this.parts[5];
const bn = yn < 1;
dn.style.display = bn ? "" : "none";
if (bn) {
const Cn = 18 * Math.sin(yn * Math.PI) * (1 - an * 0.7);
dn.setAttribute("cx", (R + Et * Zt + -En * Cn).toFixed(1));
dn.setAttribute("cy", (R + En * Zt + Et * Cn).toFixed(1));
dn.setAttribute("r", (3.5 + 6.5 * an).toFixed(2));
dn.setAttribute("opacity", (mt * clamp(yn * 3.5, 0, 1) * (0.3 + 0.7 * an)).toFixed(3));
}
const on = this.rings[6];
const bn2 = clamp((Lt - 0.58) / 0.32, 0, 1), Cn = bn2 > 0 && bn2 < 1;
on.style.display = Cn ? "" : "none";
if (Cn) {
on.removeAttribute("stroke-dasharray"); on.removeAttribute("transform");
on.setAttribute("cx", `${R}`); on.setAttribute("cy", `${R}`);
on.setAttribute("r", (20 + 26 * Rc(bn2)).toFixed(1));
on.setAttribute("stroke-width", (2.8 * (1 - bn2)).toFixed(2));
on.setAttribute("opacity", (mt * (1 - bn2) * 0.8).toFixed(3));
}
}
paintDock(ze, now, stateAt, R) {
const mt = Rc(ze), Dt = (now - stateAt) / 1000, Mt = 42, Lt = 1.1;
for (let yn = 0; yn < 2; yn++) {
const an = this.parts[5 + yn];
const Et = clamp((Dt - (0.2 + yn * 1.3)) / 0.9, 0, 1);
if (Et <= 0) { an.style.display = "none"; continue; }
const En = 1 - Math.pow(1 - Et, 3);
const Zt = now * 0.001 * Lt + yn * Math.PI;
const dn = R + Mt * Math.sin(Zt), on = R + Mt * 0.5 * Math.cos(Zt) + Math.sin(now * 0.003 + yn) * 2;
const bn = R - 120 + yn * 30, Cn = R + 95;
an.style.display = "";
an.setAttribute("cx", (bn + (dn - bn) * En).toFixed(1));
an.setAttribute("cy", (Cn + (on - Cn) * En).toFixed(1));
an.setAttribute("r", ((7 + 3 * En) * mt).toFixed(2));
an.setAttribute("opacity", (mt * clamp(Et * 4, 0, 1)).toFixed(3));
}
}
paintPencil(ze, now, stateAt, R) {
const mt = pencilPose(now, stateAt), Dt = this.glyphs[0];
const Lt = ((mt.rot - 90) * Math.PI) / 180, yn = 68, an = Math.cos(Lt) * yn, Et = Math.sin(Lt) * yn;
Dt.style.display = "";
Dt.setAttribute("d", this.pencilPath);
Dt.style.fill = "var(--fg)";
Dt.setAttribute("transform", `translate(${(R + (mt.x + an) * ze).toFixed(1)} ${(R + (mt.y + mt.wig * 0.15 + Et) * ze).toFixed(1)}) rotate(${(mt.rot * ze).toFixed(1)}) scale(${Rc(ze).toFixed(3)}) translate(${-R} ${-R})`);
Dt.setAttribute("opacity", clamp(ze * 1.6 - 0.3, 0, 1).toFixed(3));
if (ze > 0.6 && !mt.lift) {
const x = R + mt.x, y = R + mt.y + mt.wig + 19, last = this.ink[this.ink.length - 1];
if (!last || Math.hypot(x - last[0], y - last[1]) > 2.4) {
this.ink.push([x, y]);
if (this.ink.length > 64) this.ink.shift();
} else { last[0] = x; last[1] = y; }
} else if (this.ink.length) this.ink.splice(0, 2);
const line = this.glyphs[1];
if (this.ink.length < 2) line.style.display = "none";
else {
line.style.display = "";
line.style.fill = "none";
line.style.stroke = "var(--fg)";
line.setAttribute("stroke-width", "6");
line.setAttribute("stroke-linecap", "round");
line.setAttribute("stroke-linejoin", "round");
line.setAttribute("d", smoothLine(this.ink));
line.setAttribute("opacity", clamp(ze * 1.2, 0, 1).toFixed(3));
}
}
paintBang(ze, now, stateAt, R) {
const mt = this.glyphs[2];
const Dt = (now - stateAt) / 1000, Mt = Rc(clamp(ze * 1.1, 0, 1));
const Lt = Math.exp(-((Dt % 2.2) * 5.5)), yn = Math.sin(Dt * 42) * 2.2 * Lt;
mt.style.display = "";
mt.setAttribute("d", this.bangPath);
mt.style.fill = "var(--fg)";
mt.setAttribute("transform", `translate(0 ${(-26 - (1 - Mt) * 70).toFixed(1)}) rotate(${yn.toFixed(2)} ${R} ${(R - 74).toFixed(1)}) translate(${R} ${R}) scale(${clamp(ze * 1.2, 0, 1).toFixed(3)}) translate(${-R} ${-R})`);
mt.setAttribute("opacity", clamp(ze * 1.5 - 0.2, 0, 1).toFixed(3));
}
paintStandby(ze, now, R) {
const mt = Rc(ze), Dt = this.parts[4];
const Lt = 0.5 + 0.5 * Math.sin(now * 0.0016);
Dt.style.display = "";
Dt.setAttribute("cx", `${R}`); Dt.setAttribute("cy", `${R}`);
Dt.setAttribute("r", (26 + 7 * Lt).toFixed(1));
Dt.setAttribute("opacity", (mt * (0.06 + 0.1 * Lt)).toFixed(3));
const Mt = this.rings[2];
const show = ze < 0.995;
Mt.style.display = show ? "" : "none";
if (show) {
Mt.removeAttribute("stroke-dasharray"); Mt.removeAttribute("transform");
Mt.setAttribute("cx", `${R}`); Mt.setAttribute("cy", `${R}`);
Mt.setAttribute("r", (104 - 88 * mt).toFixed(1));
Mt.setAttribute("stroke-width", "2.4");
Mt.setAttribute("opacity", ((1 - mt) * 0.5).toFixed(3));
}
}
extras(now, stateAt, cur, prev, yl, mix) {
const kl = (name) => this.amount(name, cur, prev, yl, mix);
const Lee = kl("dots");
const rX = this.dotsPulse(now, 1, yl, this._reduce);
let iX = 1;
if (cur === "dots" || prev === "dots") iX = 1 + (rX.pop - 1) * (Lee / Math.max(yl, 0.001));
const Fme = kl("receive");
if (Fme > 0.004) {
const _t = ((((now - stateAt) / RECV_MS) % 1) + 1) % 1, gn = clamp((_t - 0.58) / 0.34, 0, 1);
iX *= 1 + 0.11 * Math.sin(gn * Math.PI) * Fme;
}
const zme = kl("send");
if (zme > 0.004) {
const _t = ((((now - stateAt) / SEND_MS) % 1) + 1) % 1;
const gn = _t < 0.18 ? -0.06 * Math.sin((_t / 0.18) * Math.PI) : 0;
const Gn = _t >= 0.18 && _t < 0.42 ? 0.05 * Math.sin(((_t - 0.18) / 0.24) * Math.PI) : 0;
iX *= 1 + (gn + Gn) * zme;
}
const qee = kl("bang");
if (qee > 0.004) iX *= 1 + 0.04 * Math.exp(-(((now - stateAt) / 1000) % 2.2) * 5.5) * qee;
let yre = 0, aX = 0, wl = 0;
const c1 = kl("pencil");
if (c1 > 0.004) {
const _t = pencilPose(now, stateAt);
yre += _t.x * c1; aX += (_t.y + _t.wig * 0.5) * c1; wl += _t.rot * c1;
}
if (qee > 0.004) aX += 58 * qee;
const jee = kl("whirl");
if (jee > 0.004) {
const _t = now / 1000;
yre += (Math.sin(_t * 0.9) * 2 + Math.sin(_t * 1.7) * 0.8) * jee;
aX += (Math.sin(_t * 1.3) * 2.4 + Math.sin(_t * 0.6) * 1.2) * jee;
}
const kre = kl("ball");
if (kre > 0.004) {
const _t = (now - stateAt) / 1000, gn = 0.62, Gn = 52, Ti = (8 * Gn) / (gn * gn), Ui = 40;
const Si = Math.sqrt((2 * Ui) / Ti);
let Ea;
if (_t < Si) Ea = Ui - 0.5 * Ti * _t * _t;
else {
const Ca = (((_t - Si) / gn) % 1 + 1) % 1;
Ea = 4 * Gn * Ca * (1 - Ca);
}
aX += (40 - Ea) * kre;
}
const mixR = (tbl) => {
if (!cur) return 19;
return (tbl[cur] || 19) * mix + (prev ? tbl[prev] || tbl[cur] : tbl[cur] || 19) * (1 - mix);
};
const A2 = mixR(RADIUS);
const wre = (A2 / (global.GROK_GEO?.Re || Re0)) * iX;
const standby = kl("standby");
const fade = standby > 0 ? (0.28 + 0.2 * Math.sin(now * 0.0016)) * standby : 0;
const zoomCur = cur ? Math.max(SCALE[cur], 1) : 1;
const zoomPrev = prev ? Math.max(SCALE[prev], 1) : zoomCur;
const zoom = 1 + (zoomCur * mix + zoomPrev * (1 - mix) - 1) * yl;
return { Lee, rX, yre, aX, wl, wre, fade, zoom, A2 };
}
resetInk() { this.ink = []; }
}
const turnAtCache = new Map();
function turnAtOf(name, path, R) {
if (turnAtCache.has(name)) return turnAtCache.get(name);
const solid = global.GROK_GEO.solids?.[name];
if (!solid || !global.GROK_MATH?.makeTurnAt) {
turnAtCache.set(name, null);
return null;
}
const fn = global.GROK_MATH.makeTurnAt(solid, shapeRing(path, R), R);
turnAtCache.set(name, fn);
return fn;
}
const beltCache = new Map();
function beltRadius(path, R) {
let m = beltCache.get(path);
if (m != null) return m;
const ring = shapeRing(path, R);
let top = Infinity, bot = -Infinity;
for (const p of ring) {
if (p[1] < top) top = p[1];
if (p[1] > bot) bot = p[1];
}
m = 0;
for (let y = top; y <= bot; y += 2) {
const [L, Right] = spanHalf(ring, y, R);
const half = (Right - L) / 2;
if (half > m) m = half;
}
beltCache.set(path, m);
return m;
}
function shapeMetrics(shape, R) {
return {
face: shape.face,
ring: shapeRing(shape.path, R),
tilt: shape.tiltScale || 1,
belt: shape.beltRadius || beltRadius(shape.path, R),
};
}
global.GROK_FX = {
MAP, CYCLE, CYCLE_ON, CYCLE_OFF, SCALE, P_BLEND,
createParticles,
OverlayLayer,
closedSpline,
circleRing,
shapeRing,
lerpRing,
rotateRing,
capsule,
taper,
turnAtOf,
beltRadius,
shapeMetrics,
circlePathOf(R) { return closedSpline(circleRing(R)); },
overlayRing(kind, R, teardropPath) {
if (kind === "pencil" && teardropPath) {
const ring = polarRing(flattenPath(teardropPath), R);
return rotateRing(ring, ring.length / 2, R);
}
return circleRing(R);
},
};
})(window);
+247
View File
@@ -0,0 +1,247 @@
/* L4 — springs, easing, path flatten, span, 3D. From index-UbX-y3il.js xl / $Be / l_t / gm. */
(function (g) {
const spring = (x) => ({ x, v: 0, t: x });
const stepSpring = (s, freq, damp, dt) => {
s.v += (-2 * damp * freq * s.v - freq * freq * (s.x - s.t)) * dt;
s.x += s.v * dt;
if (!Number.isFinite(s.x) || !Number.isFinite(s.v)) {
s.x = s.t;
s.v = 0;
}
};
const DT = 1 / 120;
const springSteps = (dt) => Math.max(1, Math.ceil(dt / DT));
const clamp = (n, a, b) => Math.min(b, Math.max(a, n));
const lerp = (a, b, t) => a + (b - a) * t;
const rand = (a, b) => a + Math.random() * (b - a);
const sign = () => (Math.random() < 0.5 ? -1 : 1);
const K2 = (n) => (n < 0.5 ? 4 * n * n * n : 1 - Math.pow(-2 * n + 2, 3) / 2);
const Rc = (n) => 1 - Math.pow(1 - n, 3);
const y1e = (n) => 1 + 2.70158 * Math.pow(n - 1, 3) + 1.70158 * Math.pow(n - 1, 2);
const Dke = (n) => n * n * (3 - 2 * n);
const x_t = (n, e) => 1 - Math.exp(Math.log(1 - n) * 60 * e);
const Rn = (n, bs = 1 / 60) => x_t(n, bs);
const polyPath = (pts) =>
"M" + pts.map((p) => `${p[0].toFixed(2)} ${p[1].toFixed(2)}`).join("L") + "Z";
const centroid = (pts) => {
let x = 0, y = 0;
for (const p of pts) {
x += p[0];
y += p[1];
}
return [x / pts.length, y / pts.length];
};
const lerpPoly = (a, b, t) =>
a.map((p, i) => [p[0] + (b[i][0] - p[0]) * t, p[1] + (b[i][1] - p[1]) * t]);
const lerpFace = (n, e, t) => ({
x: n.x + (e.x - n.x) * t,
y: n.y + (e.y - n.y) * t,
sx: n.sx + (e.sx - n.sx) * t,
sy: n.sy + (e.sy - n.sy) * t,
eye: n.eye + (e.eye - n.eye) * t,
leftDX: (n.leftDX ?? 0) + ((e.leftDX ?? 0) - (n.leftDX ?? 0)) * t,
});
function flattenPath(d, e = 4) {
const t = d.match(/[MLCQZmlcqz]|-?\d*\.?\d+(?:e[-+]?\d+)?/g) ?? [];
const s = [];
let r = 0, i = "", o = 0, l = 0, c = 0, u = 0;
const rd = () => parseFloat(t[r++]);
const m = (f, h) => {
const y = Math.max(2, Math.ceil(h / e));
for (let k = 1; k <= y; k++) s.push(f(k / y));
};
while (r < t.length) {
if (/[a-z]/i.test(t[r])) i = t[r++].toUpperCase();
if (i === "Z") {
if (Math.hypot(c - o, u - l) > 0.01) m((f) => [o + (c - o) * f, l + (u - l) * f], Math.hypot(c - o, u - l));
o = c;
l = u;
continue;
}
if (r >= t.length) break;
if (i === "M") {
o = rd();
l = rd();
c = o;
u = l;
s.push([o, l]);
i = "L";
} else if (i === "L") {
const f = rd(), h = rd();
m((y) => [o + (f - o) * y, l + (h - l) * y], Math.hypot(f - o, h - l));
o = f;
l = h;
} else if (i === "Q") {
const f = rd(), h = rd(), y = rd(), k = rd(), v = o, b = l;
m((x) => {
const N = 1 - x;
return [N * N * v + 2 * N * x * f + x * x * y, N * N * b + 2 * N * x * h + x * x * k];
}, Math.hypot(f - o, h - l) + Math.hypot(y - f, k - h));
o = y;
l = k;
} else if (i === "C") {
const f = rd(), h = rd(), y = rd(), k = rd(), v = rd(), b = rd(), x = o, N = l;
m((E) => {
const A = 1 - E;
return [
A * A * A * x + 3 * A * A * E * f + 3 * A * E * E * y + E * E * E * v,
A * A * A * N + 3 * A * A * E * h + 3 * A * E * E * k + E * E * E * b,
];
}, Math.hypot(f - o, h - l) + Math.hypot(y - f, k - h) + Math.hypot(v - y, b - k));
o = v;
l = b;
} else r++;
}
return s;
}
function buildSpan(pts, Re, e = 160) {
let t = Infinity, s = -Infinity;
for (const m of pts) {
if (m[1] < t) t = m[1];
if (m[1] > s) s = m[1];
}
const r = s - t;
const i = (m) => t + r * (m + 0.5) / e;
const o = new Float64Array(e);
const l = new Float64Array(e);
for (let m = 0; m < e; m++) {
const f = i(m);
let h = -Infinity, y = Infinity;
for (let b = 0; b < pts.length; b++) {
const x = pts[b], N = pts[(b + 1) % pts.length];
if (x[1] <= f === N[1] <= f) continue;
const E = x[0] + (N[0] - x[0]) * (f - x[1]) / (N[1] - x[1]);
if (E <= Re) {
if (E > h) h = E;
} else if (E < y) y = E;
}
o[m] = Number.isFinite(h) ? h : Re;
l[m] = Number.isFinite(y) ? y : Re;
}
return (h) => {
const y = clamp((h - t) / r * e - 0.5, 0, e - 1);
const k = Math.floor(y);
const v = y - k;
const b = Math.min(k + 1, e - 1);
return [o[k] + (o[b] - o[k]) * v, l[k] + (l[b] - l[k]) * v];
};
}
const spanCache = new Map();
function spanAt(path, Re) {
const key = path;
let fn = spanCache.get(key);
if (!fn) {
fn = buildSpan(flattenPath(path), Re);
spanCache.set(key, fn);
}
return fn;
}
// Source z_t — live span of a polyline at Y (used while shape-morphing)
function spanPoly(n, e, Re) {
let t = -Infinity, s = Infinity;
for (let r = 0; r < n.length; r++) {
const i = n[r], o = n[(r + 1) % n.length];
if (i[1] <= e === o[1] <= e) continue;
const l = i[0] + (o[0] - i[0]) * (e - i[1]) / (o[1] - i[1]);
if (l <= Re) {
if (l > t) t = l;
} else if (l < s) s = l;
}
return [Number.isFinite(t) ? t : Re, Number.isFinite(s) ? s : Re];
}
function rot3(turn, tilt, roll) {
const d = Math.PI / 180;
const Ui = Math.cos(turn * d), Si = Math.sin(turn * d);
const Ea = Math.cos(tilt * d), Ca = Math.sin(tilt * d);
const Wo = Math.cos(roll * d), _c = Math.sin(roll * d);
return [
Wo * Ui - _c * Ca * Si, -_c * Ea, Wo * Si + _c * Ca * Ui,
_c * Ui + Wo * Ca * Si, Wo * Ea, _c * Si - Wo * Ca * Ui,
-Ea * Si, Ca, Ea * Ui,
];
}
function relRot(pose, home) {
const gn = rot3(pose.turn, pose.tilt, pose.roll);
const Gn = rot3(home.turn, home.tilt, home.roll);
return [
gn[0] * Gn[0] + gn[1] * Gn[1] + gn[2] * Gn[2],
gn[0] * Gn[3] + gn[1] * Gn[4] + gn[2] * Gn[5],
gn[0] * Gn[6] + gn[1] * Gn[7] + gn[2] * Gn[8],
gn[3] * Gn[0] + gn[4] * Gn[1] + gn[5] * Gn[2],
gn[3] * Gn[3] + gn[4] * Gn[4] + gn[5] * Gn[5],
gn[3] * Gn[6] + gn[4] * Gn[7] + gn[5] * Gn[8],
gn[6] * Gn[0] + gn[7] * Gn[1] + gn[8] * Gn[2],
gn[6] * Gn[3] + gn[7] * Gn[4] + gn[8] * Gn[5],
gn[6] * Gn[6] + gn[7] * Gn[7] + gn[8] * Gn[8],
];
}
// Source HBe — silhouette radii of a solid-of-revolution at yaw `angle`.
function solidRadii(solid, angle, n = 96) {
const c = Math.cos(angle), s = Math.sin(angle);
const r = solid.map(([x, y, z, rad]) => [x * c + z * s, y, rad]);
const raw = Array.from({ length: n }, (_, idx) => {
const u = (idx / n) * Math.PI * 2;
const d = Math.cos(u), m = Math.sin(u);
let f = 0;
for (const [h, y, k] of r) {
const v = d * h + m * y;
const b = v * v - (h * h + y * y) + k * k;
if (b <= 0) continue;
const x = v + Math.sqrt(b);
if (x > f) f = x;
}
return f;
});
const o = raw.length;
return raw.map((l, i) => (
raw[(i - 2 + o) % o] + 4 * raw[(i - 1 + o) % o] + 6 * raw[i]
+ 4 * raw[(i + 1) % o] + raw[(i + 2) % o]
) / 16);
}
// Source Po().turnAt — scale the polar ring by HBe(yaw)/HBe(0).
function makeTurnAt(solid, ring, Re) {
const rest = solidRadii(solid, 0);
return (yaw) => {
let v = solidRadii(solid, yaw).map((x, i) => clamp((x + 12) / (rest[i] + 12), 0.32, 1.5));
const n = v.length;
for (let p = 0; p < 3; p++) {
const prev = v;
v = prev.map((E, A) => (
prev[(A - 2 + n) % n] + 4 * prev[(A - 1 + n) % n] + 6 * prev[A]
+ 4 * prev[(A + 1) % n] + prev[(A + 2) % n]
) / 16);
}
return ring.map(([x, y], i) => [Re + (x - Re) * v[i], Re + (y - Re) * v[i]]);
};
}
// Source _Fe — map a pointer onto an ellipse around the mark
function mapPointer(rect, pt, JFe = 0.6, iin = 22, ain = 14, oin = 2) {
const t = rect.left + rect.width / 2;
const s = rect.top + rect.height / 2;
const r = pt.x - t, i = pt.y - s;
const o = Math.min(1, Math.sqrt(Math.hypot(r, i) / (rect.width * oin)));
const l = Math.atan2(i, r);
return {
x: t + JFe * (ain / iin) * o * Math.cos(l) * rect.width,
y: s + JFe * o * Math.sin(l) * rect.height,
};
}
g.GROK_MATH = {
spring, stepSpring, DT, springSteps,
clamp, lerp, rand, sign, K2, Rc, y1e, Dke, x_t, Rn,
polyPath, centroid, lerpPoly, lerpFace,
flattenPath, buildSpan, spanAt, spanPoly,
rot3, relRot, mapPointer, solidRadii, makeTurnAt,
};
})(window);
+409
View File
@@ -0,0 +1,409 @@
/* L3 — applyPose switch(Pt) + nextGaze. Source $_t Gr(). */
(function (g) {
const { clamp, rand, sign, K2 } = g.GROK_MATH;
const { EYE_PLAYLIST } = g.GROK_TABLES;
function applyPose(state, mt, dtState, now, ctx, extra = {}) {
const pt = 0;
let lid = 1;
let eyeBoost = 1;
let spin = pt, tx = 0, ty = 0, squash = 1;
switch (state) {
case "sleeping": {
const En = Math.min(dtState / 2, 1);
const Zt = Math.sin(clamp(dtState / 0.5, 0, 1) * Math.PI);
spin = pt + 4 * En + Math.sin(mt * 0.25) * 2;
tx = -2 * En;
ty = 8 * En + Math.sin(mt * 0.55) * 3 - Zt * 5;
squash = 1 + Math.sin(mt * 0.55) * 0.016 + Zt * 0.05;
if (EYE_PLAYLIST.sleeping.includes(extra.eyeTo)) {
lid = extra.eyeMorphX > 0.85 ? 1 : 0.08;
} else if (dtState < 1.2) {
const dn = Math.min(1, dtState / 1);
lid = Math.max(0.08, 1 - dn * (1 + 0.15 * Math.sin(dtState * 6.5)));
} else {
lid = 0.08;
if (extra.blinkX < 0.18) ctx.forceSleepEye = true;
}
break;
}
case "waking": {
if (dtState < 0.5) {
lid = 0.07;
ty = 6;
ctx.wakeEye = [3, 12];
} else if (dtState < 1.2) {
lid = 1;
eyeBoost = 1.12;
ty = -5;
tx = 0;
spin = pt;
squash = 1.04;
if (!ctx.wakingBurst) ctx.wantBurst = [rand(9, 13), 0.8];
} else if (dtState < 2.2) {
ty = 0;
squash = 1;
ctx.wakeEye = [0, 7];
if (dtState < 1.4) ctx.wakeBlink = true;
} else {
const Et = Math.min((dtState - 2.2) / 0.8, 1);
ctx.wakeEye = [0, 7];
spin = pt + Math.sin(Et * Math.PI * 3) * 6 * (1 - Et);
ty = Math.sin(mt * 0.9) * 2;
}
break;
}
case "idle":
spin = pt + Math.sin(mt * 0.5) * 1.5 + Math.sin(mt * 0.17) * 0.6;
tx = Math.sin(mt * 0.27) * 1;
ty = Math.sin(mt * 0.85) * 1.2;
squash = 1 + Math.sin(mt * 0.85) * 0.007;
break;
case "listening":
spin = pt + 8 + Math.sin(mt * 0.5) * 1.5;
tx = 2;
ty = -2 + Math.sin(mt * 0.8) * 0.8;
squash = 1.015;
if (now >= ctx.nodUntil) {
ctx.nodUntil = now + rand(1800, 3200);
ctx.nodEnd = now + 380;
}
if (now < ctx.nodEnd) {
const Et = 1 - (ctx.nodEnd - now) / 380;
ty += Math.sin(Et * Math.PI) * 4.5;
spin += Math.sin(Et * Math.PI) * 2;
}
break;
case "thinking":
spin = pt - 9 + Math.sin(mt * 0.35) * 5;
tx = Math.sin(mt * 0.3) * 5;
ty = Math.sin(mt * 0.6) * 2.5;
squash = 1;
break;
case "searching": {
const Et = Math.sin(mt * 1.3);
spin = pt + Et * 13;
tx = Et * 7;
ty = Math.sin(mt * 1.7) * 3;
squash = 1;
if (now >= ctx.stAt) {
ctx.wantPn = [1, sign()];
ctx.stAt = now + rand(4000, 7000);
}
break;
}
case "working": {
const Et = Math.sin(mt * Math.PI * 2 * 1.6);
spin = pt + 4 + Et * 2.5;
tx = 3;
ty = 1.5 + Math.max(0, Et) * 3;
squash = 1 - Math.max(0, Et) * 0.02;
if (now >= ctx.stAt) {
ctx.wantPn = [1, 1];
ctx.stAt = now + rand(6000, 9000);
}
break;
}
case "excited": {
const Et = (mt * 2.2) % 1;
const En = Math.sin(Et * Math.PI);
ty = -En * 10 + 2;
squash = Et < 0.1 ? 0.92 : Et < 0.3 ? 1.05 : 1;
tx = Math.sin(mt * 1.1) * 4;
eyeBoost = 1.06;
spin = pt + Math.sin(mt * Math.PI * 2 * 1.1) * 7;
if (now >= ctx.stAt) {
ctx.wantPn = [1, sign()];
ctx.stAt = now + rand(2800, 5000);
}
break;
}
case "surprised": {
const Et = Math.min(dtState / 1.2, 1);
tx = -4 * (1 - Et);
ty = -8 * (1 - Et);
squash = dtState < 0.2 ? 1.08 : 1;
eyeBoost = 1.15 - Et * 0.08;
spin = pt + Math.sin(mt * 11) * 1.5 * (1 - Et);
break;
}
case "suspicious":
spin = pt - 6 + Math.sin(mt * 0.3) * 3;
tx = Math.sin(mt * 0.25) * -4;
ty = 1 + Math.sin(mt * 0.45) * 1.2;
squash = 1;
lid = 0.85;
if (now >= ctx.impulseAt) {
ctx.spinKick = 30;
ctx.impulseAt = now + rand(4000, 7000);
}
break;
case "angry":
if (now >= ctx.impulseAt) {
ctx.angryShakeUntil = now + 420;
ctx.tyKick = 70;
ctx.impulseAt = now + rand(1800, 3200);
}
spin = pt + (now < ctx.angryShakeUntil ? Math.sin(now * 0.05) * 4.5 : 0);
tx = 0;
ty = 3.5;
squash = 0.975;
break;
case "drowsy": {
spin = pt + Math.sin(mt * 0.32) * 2.5;
tx = Math.sin(mt * 0.2) * 1.5;
ty = 6 + Math.sin(mt * 0.36) * 2.2;
squash = 1 + Math.sin(mt * 0.36) * 0.022;
lid = 0.34 + Math.sin(mt * 0.8) * 0.07;
if (now >= ctx.nodUntil && !ctx.slumpAt) ctx.slumpAt = now;
if (ctx.slumpAt) {
const En = (now - ctx.slumpAt) / 1000;
const Zt = 1.7, dn = 0.3, on = 1.5;
if (En < Zt) {
const bn = En / Zt, Cn = bn * bn;
const bi = Math.sin(bn * Math.PI * 2.5) * 2.2 * (1 - bn);
ty = 6 + Cn * 19 + bi;
spin = pt + Cn * 10;
lid = 0.34 - Cn * (0.34 - 0.04);
squash = 1 - Cn * 0.045;
} else if (En < Zt + dn) {
const bn = (En - Zt) / dn, Cn = Math.sin(bn * Math.PI);
ty = 25 - Cn * 7;
spin = pt + 10 - Cn * 4;
lid = 0.04 + Cn * 0.42;
} else if (En < Zt + dn + on) {
const bn = (En - Zt - dn) / on;
const Cn = 1 - Math.pow(1 - bn, 2.2);
ty = 25 - 19 * Cn;
spin = pt + 10 * (1 - Cn);
lid = 0.46 + (0.34 - 0.46) * Cn;
if (bn > 0.32 && bn < 0.46) lid = 0.05;
} else {
ctx.slumpAt = 0;
ctx.nodUntil = now + rand(1500, 3500);
}
}
break;
}
case "happy": {
const Et = Math.sin(mt * 2.4);
spin = pt + Math.sin(mt * 1.2) * 3;
tx = Math.sin(mt * 1.1) * 2.5;
ty = -Math.abs(Et) * 3;
squash = 1 + Et * 0.02;
eyeBoost = 1.05;
break;
}
case "curious":
spin = pt + 10 + Math.sin(mt * 0.7) * 6;
tx = Math.sin(mt * 0.6) * 5;
ty = -2 + Math.sin(mt * 0.9) * 1.5;
squash = 1.01;
eyeBoost = 1.08;
if (now >= ctx.nodUntil) {
ctx.nodUntil = now + rand(1600, 2800);
ctx.nodEnd = now + 440;
}
if (now < ctx.nodEnd) {
const Et = 1 - (ctx.nodEnd - now) / 440;
tx += Math.sin(Et * Math.PI) * 8;
spin += Math.sin(Et * Math.PI) * 5;
}
break;
case "confused": {
const Et = Math.sin(mt * 0.8);
spin = pt + Et * 12;
tx = Et * 3;
ty = Math.sin(mt * 0.5) * 2;
squash = 1;
lid = 0.9;
if (now >= ctx.impulseAt) {
ctx.spinKick = 22;
ctx.impulseAt = now + rand(2600, 4200);
}
break;
}
case "bored":
spin = pt - 3 + Math.sin(mt * 0.25) * 4;
tx = Math.sin(mt * 0.2) * 4;
ty = 5 + Math.sin(mt * 0.35) * 1.5;
squash = 0.99;
lid = 0.6;
eyeBoost = 0.98;
if (now >= ctx.impulseAt) {
ctx.nodEnd = now + 600;
ctx.impulseAt = now + rand(4000, 7000);
}
if (now < ctx.nodEnd) {
const Et = 1 - (ctx.nodEnd - now) / 600;
squash = 1 + Math.sin(Et * Math.PI) * 0.05;
ty += Math.sin(Et * Math.PI) * 3;
}
break;
case "proud":
spin = pt + Math.sin(mt * 0.4) * 2.5;
tx = Math.sin(mt * 0.35) * 2;
ty = -4 + Math.sin(mt * 0.6);
squash = 1.03;
eyeBoost = 1.02;
lid = 0.9;
break;
case "shy":
spin = pt - 8 + Math.sin(mt * 0.5) * 3;
tx = -3 + Math.sin(mt * 0.4) * 2;
ty = 3;
squash = 0.98;
eyeBoost = 0.95;
lid = 0.85;
break;
case "sad":
spin = pt + 3 + Math.sin(mt * 0.3) * 2;
tx = Math.sin(mt * 0.25) * 1.5;
ty = 7 + Math.sin(mt * 0.4);
squash = 0.97;
lid = 0.7;
eyeBoost = 0.97;
break;
case "laughing": {
const Et = Math.sin(mt * Math.PI * 2 * 3.2);
spin = pt + Et * 4;
tx = Math.sin(mt * 2) * 2;
ty = -Math.abs(Et) * 5;
squash = 1 + Et * 0.03;
lid = 0.7;
break;
}
case "scared":
spin = pt + Math.sin(now * 0.04) * 2;
tx = -2 + Math.sin(now * 0.05) * 1.5;
ty = 2 + Math.sin(mt * 1.5);
squash = 0.97;
eyeBoost = 1.12;
lid = 1.05;
break;
case "playful":
spin = pt + Math.sin(mt * 1.4) * 8;
tx = Math.sin(mt * 1.1) * 4;
ty = -Math.abs(Math.sin(mt * 2.2)) * 3;
squash = 1 + Math.sin(mt * 2.2) * 0.015;
eyeBoost = 1.06;
if (now >= ctx.stAt) {
ctx.wantPn = [1, sign()];
ctx.stAt = now + rand(3500, 6000);
}
break;
case "celebrate":
spin = pt;
tx = 0;
ty = -Math.abs(Math.sin(mt * 1.6)) * 2.5;
squash = 1;
eyeBoost = 1.1;
lid = 1.1;
break;
case "dragging": {
const En = (dtState % 3.4) / 3.4;
if (En < 0.12) {
tx = -16;
ty = -22;
spin = pt - 5;
} else if (En < 0.62) {
const dn = (En - 0.12) / 0.5;
tx = -16 + 32 * K2(dn);
ty = -22 + Math.sin(mt * 1.4) * 2;
spin = pt + Math.sin(mt * 2.6) * 6;
eyeBoost = 1.06;
} else {
const cycle = Math.floor(dtState / 3.4);
if (cycle !== ctx.dragCycle) {
ctx.dragCycle = cycle;
ctx.tyKick = 90;
}
tx = 16;
ty = 0;
spin = pt;
}
squash = 1;
break;
}
case "humming":
spin = pt + Math.sin(mt * 0.4) * 2;
tx = Math.sin(mt * 0.3) * 1.5;
ty = Math.sin(mt * 0.7) * 1.5;
squash = 1;
break;
case "notifying":
if (!ctx.notifyPop && dtState > 0.12) {
ctx.notifyPop = true;
ctx.tyKick = -26;
ctx.wantBlink = true;
}
eyeBoost = 1 + 0.05 * Math.exp(-dtState * 3);
spin = pt + 3;
tx = 2;
ty = -1;
squash = 1;
break;
default:
spin = pt;
tx = 0;
ty = 0;
squash = 1;
break;
}
return { spin, tx, ty, squash, lid, eyeBoost };
}
function nextGaze(state) {
switch (state) {
case "idle":
return { x: 0, y: 0, hold: [2500, 5500] };
case "listening":
return { x: rand(-0.3, 0.3) * 15, y: rand(-0.25, 0.25) * 9, hold: [2200, 4200] };
case "thinking":
return { x: sign() * rand(0.5, 1) * 15, y: -rand(0.4, 1) * 9, hold: [1500, 2800] };
case "searching":
return { x: sign() * rand(0.7, 1) * 15, y: rand(-1, 1) * 9, hold: [550, 1150] };
case "working":
return { x: rand(-0.4, 0.4) * 15, y: rand(0.4, 1) * 9, hold: [1200, 2400] };
case "excited":
return { x: rand(-1, 1) * 15, y: rand(-1, 0.3) * 9, hold: [700, 1400] };
case "surprised":
return { x: 0, y: 0, hold: [1600, 2600] };
case "suspicious":
return { x: sign() * 15, y: 0.3 * 9, hold: [2200, 4200] };
case "angry":
return { x: rand(-0.2, 0.2) * 15, y: 0.2 * 9, hold: [1800, 3200] };
case "drowsy":
return { x: rand(-0.4, 0.4) * 15, y: rand(0.4, 1) * 9, hold: [2500, 4500] };
case "happy":
return { x: rand(-0.7, 0.7) * 15, y: -rand(0, 0.6) * 9, hold: [1800, 3400] };
case "curious":
return { x: sign() * rand(0.6, 1) * 15, y: rand(-1, 1) * 9, hold: [950, 1900] };
case "confused":
return { x: sign() * rand(0.5, 1) * 15, y: rand(-0.6, 1) * 9, hold: [1100, 2300] };
case "bored":
return { x: sign() * rand(0.7, 1) * 15, y: rand(0.4, 0.9) * 9, hold: [3000, 6000] };
case "proud":
return { x: rand(-0.3, 0.3) * 15, y: -rand(0.3, 0.7) * 9, hold: [2600, 4600] };
case "shy":
return { x: sign() * rand(0.6, 1) * 15, y: rand(0.5, 1) * 9, hold: [2000, 4000] };
case "sad":
return { x: rand(-0.3, 0.3) * 15, y: rand(0.6, 1) * 9, hold: [2800, 5000] };
case "laughing":
return { x: rand(-0.5, 0.5) * 15, y: -rand(0.2, 0.6) * 9, hold: [800, 1700] };
case "scared":
return { x: sign() * rand(0.7, 1) * 15, y: rand(-0.6, 0.6) * 9, hold: [450, 1050] };
case "playful":
return { x: sign() * rand(0.5, 1) * 15, y: -rand(0, 0.6) * 9, hold: [900, 1800] };
case "notifying": {
const look = Math.random() < 0.72;
return { x: (look ? 0.45 : 0.1) * 15, y: -(look ? 0.3 : 0.05) * 9, hold: [1200, 2400] };
}
default:
return { x: rand(-0.4, 0.4) * 15, y: rand(-0.3, 0.3) * 9, hold: [2500, 5000] };
}
}
g.GROK_POSE = { applyPose, nextGaze };
})(window);
+133
View File
@@ -0,0 +1,133 @@
/* L4 — playlists, blink, login wrap, springs. Symbols: g1e VBe w_t cSe rnt int ant ont snt. */
(function (g) {
const GROUPS = [
{ label: "Lifecycle", states: ["sleeping", "waking", "idle", "listening", "thinking", "searching", "working"] },
{ label: "Reactions", states: ["excited", "surprised", "suspicious", "angry", "drowsy", "happy", "curious", "confused", "bored", "proud", "shy", "sad", "laughing", "scared", "playful", "celebrate"] },
{ label: "Agent morphs", states: ["orbit", "radar", "progress"] },
{ label: "Product lifecycle", states: ["spawning", "humming", "loading", "dictating", "writing", "sending", "receiving", "uploading", "notifying", "alerting", "dragging", "bouncing", "powering-down"] },
];
const EYE_PLAYLIST = {
sleeping: [13, 22, 4], waking: [13], idle: [0, 8], listening: [10, 1, 19],
thinking: [8, 16, 14, 17, 5], searching: [15, 9, 3, 20, 12, 18],
working: [7, 16, 11, 10], excited: [2, 17, 21, 3, 11], surprised: [3, 21],
suspicious: [14, 5, 23], angry: [7, 16], drowsy: [4, 22, 13],
happy: [2, 11, 17, 19], curious: [3, 21, 0, 15], confused: [14, 5, 8],
bored: [4, 22, 0], proud: [15, 8, 2], shy: [0, 24, 13], sad: [4, 13, 22],
laughing: [2, 11, 17], scared: [3, 21], playful: [2, 17, 11, 8],
celebrate: [2, 8, 17], orbit: [0, 8], radar: [0, 8], progress: [0, 8],
spawning: [3, 0], humming: [0, 8], loading: [0, 8], dictating: [10, 1, 19],
sending: [0, 8], receiving: [19, 0, 8], uploading: [15, 9, 8], writing: [15, 9],
notifying: [3, 21, 0], alerting: [3, 21], bouncing: [2, 17],
dragging: [3, 15, 0], "powering-down": [13, 22],
};
const EYE_HOLD_MS = {
sleeping: [6000, 10000], waking: [800, 800], idle: [9000, 16000],
listening: [2800, 5000], thinking: [2000, 3600], searching: [1000, 1800],
working: [1800, 3200], excited: [1100, 2000], surprised: [2500, 4000],
suspicious: [2600, 4500], angry: [2200, 3800], drowsy: [4000, 8000],
happy: [2500, 4500], curious: [1800, 3200], confused: [2200, 3800],
bored: [3500, 6000], proud: [3500, 6000], shy: [3000, 5500],
sad: [4000, 7000], laughing: [1200, 2400], scared: [900, 1800],
playful: [1500, 3000], celebrate: [1400, 2600], orbit: [4000, 8000],
radar: [4000, 8000], progress: [4000, 8000], spawning: [1200, 1200],
humming: [5000, 9000], loading: [6000, 10000], dictating: [4000, 8000],
sending: [4000, 8000], receiving: [4000, 8000], uploading: [4000, 8000],
writing: [4000, 8000], notifying: [1500, 2600], alerting: [2000, 3600],
bouncing: [3000, 6000], dragging: [1600, 3000], "powering-down": [6000, 9000],
};
const BLINK_MS = {
sleeping: null, waking: null, idle: [6000, 14000], listening: [3000, 7000],
thinking: [3500, 7000], searching: [1600, 4000], working: [2800, 5500],
excited: [2000, 4000], surprised: [1800, 3500], suspicious: [4500, 8000],
angry: [3500, 7000], drowsy: null, happy: [2500, 5000], curious: [2500, 5500],
confused: [2800, 5500], bored: [4000, 8000], proud: [3500, 7000],
shy: [3000, 6000], sad: [4000, 8000], laughing: [2500, 5000],
scared: [1200, 3000], playful: [2000, 4500], celebrate: [2200, 4500],
orbit: null, radar: null, progress: null, spawning: null, humming: [4000, 8000],
loading: null, dictating: null, sending: null, receiving: null, uploading: null,
writing: null, notifying: [2000, 4000], alerting: null, bouncing: null,
dragging: [2200, 4500], "powering-down": null,
};
const ONBOARDING = ["curious", "happy", "playful", "excited", "listening", "proud", "laughing", "shy"];
const ONBOARDING_MS = 1200;
const onboardMood = (n) => (n % 2 === 0 ? "idle" : ONBOARDING[((n - 1) / 2) % ONBOARDING.length] ?? "idle");
const SPRINGS = {
spin: [5, 0.9],
x: [3.5, 1],
y: [4, 1],
squash: [10, 0.8],
blink: [26, 1],
eyeScale: [9, 0.85],
gazeX: [13, 1],
gazeY: [13, 1],
notify: [9, 0.55],
humDots: [6, 1],
overlay: [14, 1],
overlayMix: [11, 1],
shape: [10, 1],
overlayTurn: [14, 1],
spinTurn: [6.2, 1],
};
const FACE_TUNE = { size: 0.86, gap: 1.18, height: 1, eyeWidth: 0.96, eyeHeight: 0.92 };
const POSE = { turn: 17, tilt: -14, roll: 29, scale: 1 };
const POSE_HOME = { turn: 33, tilt: -19, roll: 38 };
const UNIFORM_EYES = true;
const V_T = new Set(["happy", "excited", "proud"]);
const B_T = new Set(["playful"]);
const WINK_STATES = new Set(["idle", "happy", "excited", "curious", "playful"]);
const SHAPE_ZOOM = { blob: 0.92, pebble: 0.96, squircle: 0.84, tablet: 1, wedge: 0.94, hex: 0.94, cloud: 1, teardrop: 1 };
const VIEW_SCALE = 259 / 229;
const shapeZoom = (name) => SHAPE_ZOOM[name] ?? 1;
const poseScale = (name) => shapeZoom(name) * VIEW_SCALE;
const shapeEyeScale = (name) => SHAPE_ZOOM.blob / shapeZoom(name);
const VIEW = { minX: -15, minY: -15, width: 259, height: 259 };
const VIEW_HALF = VIEW.width / 2;
const VIEW_MID = VIEW.minX + VIEW_HALF;
const OVERLAY_ZOOM = {
dots: 1.5, orbit: 1.14, radar: 1.14, progress: 1.32, gather: 1.15,
wave: 1.42, send: 1.12, receive: 1.12, dock: 1.3, ball: 1.22,
whirl: 1.45, pencil: 1.18, bang: 1.28, standby: 1.75,
};
const overlayViewZoom = (kind, scale) => (kind == null ? 1 : Math.max(OVERLAY_ZOOM[kind] / Math.max(scale, 1), 1));
// Source G_t / MNe / Y_t — login ink. Live $_t uses --fg = ink.flat ?? MNe(color).
const INK = {
black: { lightFrom: "#585858", lightTo: "#000000", darkFrom: "#FFFFFF", darkTo: "#C2C2C2" },
brown: { lightFrom: "#AE8968", lightTo: "#855C36", darkFrom: "#A27952", darkTo: "#604227" },
red: { lightFrom: "#FF5667", lightTo: "#E02135", darkFrom: "#FF3E51", darkTo: "#A21826" },
orange: { lightFrom: "#FF8838", lightTo: "#E05B00", darkFrom: "#FF781C", darkTo: "#C24E00" },
yellow: { lightFrom: "#FFAF38", lightTo: "#E08600", darkFrom: "#FFA31C", darkTo: "#C27400" },
green: { lightFrom: "#1CCF82", lightTo: "#009957", darkFrom: "#00C972", darkTo: "#008048" },
cyan: { lightFrom: "#58D3C5", lightTo: "#00A592", darkFrom: "#1CC3B0", darkTo: "#007769" },
blue: { lightFrom: "#459FFE", lightTo: "#0E74E0", darkFrom: "#2A92FE", darkTo: "#0C64C1" },
violet: { lightFrom: "#B792FE", lightTo: "#804EE0", darkFrom: "#9159FE", darkTo: "#5C39A1" },
magenta: { lightFrom: "#FF77BE", lightTo: "#E02A88", darkFrom: "#FF47A6", darkTo: "#A21E62" },
gray: { lightFrom: "#A6A6A6", lightTo: "#696969", darkFrom: "#B7B7B7", darkTo: "#777777" },
};
const INK_ANGLE = 135;
const inkFg = (id) => {
const pal = g.GROK_GEO.palette[id] || g.GROK_GEO.palette.black;
return `light-dark(${pal.light}, ${pal.dark})`;
};
const inkCss = (id) => {
const e = INK[id] || INK.black;
return `linear-gradient(${INK_ANGLE + 90}deg, light-dark(${e.lightFrom}, ${e.darkFrom}), light-dark(${e.lightTo}, ${e.darkTo}))`;
};
const EYE_BG = "var(--sand-bg-base, var(--disk, #f3efe6))";
g.GROK_TABLES = {
GROUPS, EYE_PLAYLIST, EYE_HOLD_MS, BLINK_MS,
ONBOARDING, ONBOARDING_MS, onboardMood,
SPRINGS, FACE_TUNE, POSE, POSE_HOME, UNIFORM_EYES,
V_T, B_T, WINK_STATES,
SHAPE_ZOOM, VIEW_SCALE, shapeZoom, poseScale, shapeEyeScale,
VIEW, VIEW_HALF, VIEW_MID, OVERLAY_ZOOM, overlayViewZoom,
INK, INK_ANGLE, inkFg, inkCss, EYE_BG,
};
})(window);
+115
View File
@@ -0,0 +1,115 @@
/* L3 — hop / pn / spinBounce / spinDizzy / spinWild. Source hr, ai, wie, ys. */
(function (g) {
const { spring, K2, rand, sign } = g.GROK_MATH;
const HOP_SEGS = [
{ h: 48, d: 0.5 },
{ h: 28, d: 0.382 },
{ h: 14, d: 0.27 },
{ h: 6, d: 0.177 },
];
const HOP_DUR = HOP_SEGS.reduce((s, x) => s + x.d, 0);
function hopY(hopAt, now) {
if (hopAt < 0) return 0;
const Et = (now - hopAt) / 1000;
if (Et >= HOP_DUR) return null;
let En = 0;
for (const seg of HOP_SEGS) {
if (Et < En + seg.d) {
const bn = (Et - En) / seg.d;
return -4 * seg.h * bn * (1 - bn);
}
En += seg.d;
}
return 0;
}
function startTrick(kind, reduce) {
if (reduce) return null;
const dir = sign();
const turns = kind === "spinDizzy" ? Math.round(rand(3, 4)) : kind === "spinWild" ? 9 : 1;
return { kind, t0: performance.now(), dir, turns };
}
function evalTrick(trick, now) {
const empty = { turn: null, Kr: 0, yi: 0, ki: 0, Yr: 0, Zr: 0, wi: 0, lidMul: null, eyeBoost: null, hop: 0, done: !trick, wantHop: false };
if (!trick) return empty;
const Et = (now - trick.t0) / 1000;
const { kind, dir, turns } = trick;
let turn = null, Kr = 0, yi = 0, ki = 0, Yr = 0, Zr = 0, wi = 0, lidMul = null, eyeBoost = null;
let done = false, wantHop = false;
if (kind === "spinDizzy") {
const on = 0.55 + turns * 0.16, bn = 1.5;
if (Et < on) {
const Cn = Et / on;
turn = turns * Math.PI * 2 * dir * (Cn * Cn);
} else if (Et < on + bn) {
const Cn = Et - on;
const bi = Math.pow(1 - Cn / bn, 1.3);
Kr = Math.sin(Cn * 10) * 17 * dir * bi;
yi = Math.cos(Cn * 10) * 10 * dir * bi;
ki = Math.sin(Cn * 20) * 3 * bi;
lidMul = 0.46 + 0.14 * Math.sin(Cn * 21);
eyeBoost = 1.03;
} else done = true;
} else if (kind === "spinWild") {
const ud = 2.3 - 0.3;
const Pc = 0.5;
const Go = Math.PI * 2;
const $i = 0.24 + 2.3 + 1.25;
const gm = (turns * Go + Pc) / (0.3 / 2 + ud + 1.25 / 4);
if (Et < $i + 1.7) {
let cr;
if (Et < 0.24) cr = -Pc * (1 - Math.cos((Et / 0.24) * Math.PI)) / 2;
else if (Et < 0.24 + 0.3) {
const Ua = Et - 0.24;
cr = -Pc + gm * Ua * Ua / (2 * 0.3);
} else if (Et < 0.24 + 2.3) cr = -Pc + gm * (0.3 / 2 + (Et - 0.24 - 0.3));
else if (Et < $i) {
const Ua = (Et - 0.24 - 2.3) / 1.25;
cr = -Pc + gm * (0.3 / 2 + ud) + gm * 1.25 * (1 - Math.pow(1 - Ua, 4)) / 4;
} else cr = turns * Go;
turn = cr * dir;
let pl = 0;
if (Et > 0.24 + 2.3) {
const Ua = Math.min((Et - 0.24 - 2.3) / 1.25, 1);
pl = Ua < 0.4 ? 0 : Math.pow((Ua - 0.4) / 0.6, 2);
if (Et >= $i) pl = Math.pow(1 - (Et - $i) / 1.7, 1.6);
}
const Yl = Math.max(Et - 0.24 - 2.3, 0);
Yr = cr / (turns * Go) * 3 * 360 * dir;
Kr = Math.sin(Yl * 9.2) * 11 * dir * pl;
yi = (Math.cos(Yl * 9.2) - 1) * 6 * dir * pl;
ki = Math.sin(Yl * 18.4) * 2.6 * pl;
Zr = Math.sin(Yl * 11.5) * 13 * dir * pl;
wi = (Math.cos(Yl * 9) - 1) * 3.5 * pl;
lidMul = 1.14 - 0.44 * pl + 0.1 * Math.sin(Yl * 16) * pl;
eyeBoost = 1.12 - 0.09 * pl;
} else done = true;
} else if (kind === "spinBounce") {
if (Et < 0.7) turn = turns * Math.PI * 2 * dir * K2(Et / 0.7);
else {
wantHop = true;
done = true;
}
}
return { turn, Kr, yi, ki, Yr, Zr, wi, lidMul, eyeBoost, hop: 0, done, wantHop };
}
function makeSpinTurn(turns = 1, dir = sign()) {
const s = spring(0);
s.t = turns * Math.PI * 2 * dir;
return s;
}
function spinTurnSettled(s) {
return Math.abs(s.t - s.x) < 0.004 && Math.abs(s.v) < 0.015;
}
g.GROK_TRICKS = {
HOP_SEGS, HOP_DUR, hopY, startTrick, evalTrick, makeSpinTurn, spinTurnSettled,
};
})(window);