docs:发布不含几何的角色机芯学习仓

以 kivo@9337f7e 的 replica 机芯为基准,单独开源弹簧/状态机/眼睛编排。
不包含 geometry-data、官方图标和抽出的应用主包。
This commit is contained in:
Edom
2026-08-15 22:45:18 +08:00
commit 5502b2d97a
13 changed files with 3632 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
*.log
# Local-only character data. Do not commit extracted or third-party geometry.
replica/geometry-data.js
replica/grok-blob-idle.svg
# Local snap output
/tmp/
snap-out/
+26
View File
@@ -0,0 +1,26 @@
MIT License
Copyright (c) 2026 Edom
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This license covers only the original engine, playground, and documentation
in this repository. It does not grant rights to any third-party character
geometry, trademarks, icons, or application source. Those materials are not
included here.
+55
View File
@@ -0,0 +1,55 @@
# grok-icon-study
Unofficial study of a spring-driven character engine.
非官方学习项目,与 xAI / Grok 无关。本仓库只开源机芯,不含角色几何、商标或第三方应用源码。
登录页那种会眨眼、换情绪、被弹簧拉着走的角色,不是 GIF / Lottie,而是状态机 + 弹簧 + 多边形眼睛。这里把机芯拆开,方便阅读和改。
## 打开
用浏览器打开 [`replica/index.html`](./replica/index.html)。
克隆下来默认**看不到完整角色外形**。没有 `replica/geometry-data.js` 时,页面会停在说明态,不会报错崩掉。
自备几何的接口见 [`replica/geometry.schema.md`](./replica/geometry.schema.md)。把符合接口的文件放到 `replica/geometry-data.js` 即可本地预览。该路径已被 git 忽略。
不要把从商业应用抽出的几何、图标或主包提交到本仓库,也不要再分发。
## 机芯在做什么
- 39 个情绪 / 生命周期状态,每态有眼睛 playlist、眨眼间隔、注视节奏
- 弹簧驱动位移、挤压、旋转、眨眼、换形
- 眼睛是多边形 morph,不是贴图
- 部分状态会切 overlay(thinking / orbit / writing / loading 等)
- 登录包装带 `pose.scale`、指针跟随、onboarding 轮换
登录页常见参数:`sizePx: 64`,`color: "black"`,`shape: "blob"`。
`pjn(n) = n % 2 === 0 ? idle : onboarding[(n-1)/2]`,间隔 1200ms。
playground 有几何时从 `curious` 起播;点「登录轮换」才按 `pjn(0)=idle` 重来。
## 目录
| 路径 | 内容 |
|---|---|
| `replica/index.html` | playground。无几何时显示说明 |
| `replica/geometry.schema.md` | `window.GROK_GEO` 接口,不含数据 |
| `replica/src/math.js` | 弹簧、插值、多边形 |
| `replica/src/tables.js` | 状态表、playlist、弹簧参数 |
| `replica/src/pose.js` | 姿态 / 注视 |
| `replica/src/tricks.js` | 转圈、跳跃等特技 |
| `replica/src/fx.js` | overlay 与粒子 |
| `replica/src/eyes.js` | 眨眼、wink、眼睛绘制 |
| `replica/src/character.js` | `GrokCharacter` 编排 |
| `replica/snap.mjs` | 可选的 headless 快照脚本 |
本仓库故意不含:
- 角色几何(`geometry-data.js`)
- 官方图标 / iconset
- 从应用包抽出的主包或切片
- 解包脚本
## 许可
[MIT](./LICENSE) 只覆盖本仓库里的原创机芯、页面和文档。
它不授权任何第三方角色造型、商标或应用源码;那些东西也不在这个仓库里。
+56
View File
@@ -0,0 +1,56 @@
# `window.GROK_GEO` 接口
Playground 只在检测到可用的 `window.GROK_GEO` 时才会创建角色。
本仓库不附带这份数据。若要本地看完整外形,自行把 `replica/geometry-data.js` 放到同目录,并保证它在 `src/*.js` 之前执行。
不要把从商业应用抽出的几何、图标或主包提交到本仓库,也不要再分发。
## 最小可用对象
```js
window.GROK_GEO = {
Re: 100, // 身体逻辑半径,绘制原点
G9e: 20, // 眼睛纵向基准
VJt: 4, // 非 uniform 眼距预留
viewBox: { minX: 0, minY: 0, width: 200, height: 200 },
palette: {
black: { light: "#111111", dark: "#f4f4f4" },
},
eyes: [
// 下标对应 tables.js 的 playlist。每组 [左眼多边形, 右眼多边形]
// 每个多边形是 [[x, y], ...]
],
shapes: {
blob: {
label: "Blob",
path: "M ... Z", // 你自己的 SVG path d
face: { x: 0, y: 0, sx: 1, sy: 1, eye: 1 },
radius: 100,
tiltScale: 1,
top: 0,
bottom: 200,
},
},
};
```
## 字段
| 字段 | 用途 |
|---|---|
| `Re` | 身体中心 / 变换原点 |
| `G9e` `VJt` | 眼睛缩放与间距 |
| `viewBox` | 舞台视口;机芯里的 `VIEW` 与此对齐 |
| `palette[id].light/dark` | `setColor` 在非 login 墨水下的纯色 |
| `eyes[i]` | 第 `i` 号眼形。`tables.js` 的 playlist 用下标引用,默认按 25 组编排 |
| `shapes[id].path` | 身形轮廓 |
| `shapes[id].face` | 面部相对身体的偏移与缩放,可选 `leftDX` |
| `shapes[id].radius/top/bottom/tiltScale` | 眼睛夹持、点头倾斜 |
| `solids` | 可选。`bean` / `tablet` / `cloud` 等旋转时的截面带 |
默认 playground 从 `shapes.blob` 起播。`tables.js` 的 `SHAPE_ZOOM` 给部分 id 配了缩放;其它 id 仍可渲染。
`tables.js` 里的 `VIEW` 决定取景框。身形 path 的坐标系最好和它对齐,否则角色会偏出画面。
## 加载
`index.html` 会尝试加载 `geometry-data.js`。文件不存在时页面保持说明态,不会创建 `GrokCharacter`。该文件已被 `.gitignore` 忽略。
+384
View File
@@ -0,0 +1,384 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Character Engine Playground</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; }
.missing {
width: min(100%, 280px);
margin: 0 auto;
padding: 22px 18px;
border-radius: 16px;
background: #2a2823;
color: #d8d0c0;
font-size: 13px;
}
.missing strong { display: block; margin-bottom: 8px; color: var(--accent); font-weight: 560; }
.missing a { color: var(--accent); }
.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; flex-wrap: wrap; }
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; }
button:disabled { opacity: 0.45; cursor: not-allowed; }
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; }
.note {
color: #8d877b;
font-size: 12px;
max-width: 62ch;
}
.note code { color: #d8d0c0; font-size: 11px; }
[hidden] { display: none !important; }
@media (max-width: 860px) {
main { grid-template-columns: 1fr; }
.stage { position: static; }
}
</style>
</head>
<body>
<header>
<h1>Character Engine</h1>
<p>非官方弹簧角色机芯 · 几何不随仓库分发</p>
</header>
<main>
<aside class="stage">
<div id="ready-stage">
<div class="stage-disks">
<div class="disk" id="disk">
<svg id="bot" role="img" aria-label="Character preview"></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>
<div class="missing" id="missing-geo" hidden>
<strong>几何未加载</strong>
本仓库不包含 <code>geometry-data.js</code>。把符合
<a href="geometry.schema.md">geometry.schema.md</a>
的数据放到同目录后刷新,即可本地预览。不要提交或再分发从商业应用抽出的造型。
</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">—</b></div>
<div>眼睛 <b id="r-eye">—</b></div>
<div>旋转 <b id="r-spin">—</b></div>
<div>挤压 <b id="r-squash">—</b></div>
<div>overlay <b id="r-ov">—</b></div>
</div>
</aside>
<div>
<section>
<h2>情绪状态 · 39 态</h2>
<div id="states"></div>
</section>
<section id="shapes-section" hidden>
<h2>身形</h2>
<div class="shapes" id="shapes"></div>
</section>
<section id="swatches-section" hidden>
<h2>墨色</h2>
<div class="swatches" id="swatches"></div>
</section>
<section>
<h2>这是机芯,不是素材包</h2>
<p class="note">
引擎拆在 <code>src/</code>:math / tables / pose / tricks / eyes / fx / character。
有几何时,右侧 64px 带登录包装;thinking / orbit / writing / loading 等会切 overlay。
首屏从 curious 起播,点「登录轮换」则按 onboarding 从 idle 重来。
大预览默认开指针跟随。
</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>
function hasUsableGeo(geo) {
return !!(
geo &&
typeof geo.Re === "number" &&
Array.isArray(geo.eyes) &&
geo.eyes.length > 0 &&
geo.shapes &&
Object.keys(geo.shapes).length > 0
);
}
const ready = hasUsableGeo(window.GROK_GEO);
document.getElementById("missing-geo").hidden = ready;
document.getElementById("ready-stage").hidden = !ready;
document.querySelectorAll("#mode-onboard, #mode-hold, #opt-follow, #opt-emphasis, #act-spin, #act-bounce, #act-burst")
.forEach((el) => { el.disabled = !ready; });
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");
}
const groups = window.GROK_META?.groups || window.GROK_TABLES?.GROUPS || [];
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.disabled = !ready;
b.addEventListener("click", () => {
if (!window.__bots) return;
window.__bots.forEach((el) => {
el.setMode("hold");
el.setState(name, { resetEyes: false });
});
syncModes("hold");
});
row.appendChild(b);
});
stateRoot.appendChild(row);
});
if (!ready) {
// Engine tables still render as documentation. Live character stays off.
} else {
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;
bots.forEach((el) => {
el.moodN = 1;
el.setState("curious", { resetEyes: true });
});
setInterval(() => paintReadout(bot.snapshot()), 80);
const shapeRoot = document.getElementById("shapes");
document.getElementById("shapes-section").hidden = false;
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");
document.getElementById("swatches-section").hidden = false;
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 = () => {
if (!window.__bots) return;
window.__bots.forEach((el) => el.setMode("onboarding"));
syncModes("onboarding");
};
document.getElementById("mode-hold").onclick = () => {
if (!window.__bots) return;
window.__bots.forEach((el) => el.setMode("hold"));
syncModes("hold");
};
document.getElementById("opt-follow").onclick = () => {
if (!window.__bots) return;
const on = document.getElementById("opt-follow").getAttribute("aria-pressed") !== "true";
document.getElementById("opt-follow").setAttribute("aria-pressed", on ? "true" : "false");
window.__bots.forEach((el) => el.setFollowPointer(on));
};
document.getElementById("opt-emphasis").onclick = () => {
if (!window.__bots) return;
const on = document.getElementById("opt-emphasis").getAttribute("aria-pressed") !== "true";
document.getElementById("opt-emphasis").setAttribute("aria-pressed", on ? "true" : "false");
window.__bots.forEach((el) => el.setEmphasis(on));
};
document.getElementById("act-spin").onclick = () => window.__bots?.forEach((el) => el.spinOnce(1));
document.getElementById("act-bounce").onclick = () => window.__bots?.forEach((el) => el.bounceOnce());
document.getElementById("act-burst").onclick = () => window.__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);