From 5502b2d97af12ed1e542060cabd197008e78e4b6 Mon Sep 17 00:00:00 2001
From: Edom <463882408@qq.com>
Date: Sat, 15 Aug 2026 22:45:18 +0800
Subject: [PATCH] =?UTF-8?q?docs:=E5=8F=91=E5=B8=83=E4=B8=8D=E5=90=AB?=
=?UTF-8?q?=E5=87=A0=E4=BD=95=E7=9A=84=E8=A7=92=E8=89=B2=E6=9C=BA=E8=8A=AF?=
=?UTF-8?q?=E5=AD=A6=E4=B9=A0=E4=BB=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
以 kivo@9337f7e 的 replica 机芯为基准,单独开源弹簧/状态机/眼睛编排。
不包含 geometry-data、官方图标和抽出的应用主包。
---
.gitignore | 10 +
LICENSE | 26 ++
README.md | 55 +++
replica/geometry.schema.md | 56 +++
replica/index.html | 384 ++++++++++++++++
replica/snap.mjs | 214 +++++++++
replica/src/character.js | 857 ++++++++++++++++++++++++++++++++++
replica/src/eyes.js | 218 +++++++++
replica/src/fx.js | 908 +++++++++++++++++++++++++++++++++++++
replica/src/math.js | 247 ++++++++++
replica/src/pose.js | 409 +++++++++++++++++
replica/src/tables.js | 133 ++++++
replica/src/tricks.js | 115 +++++
13 files changed, 3632 insertions(+)
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 replica/geometry.schema.md
create mode 100644 replica/index.html
create mode 100755 replica/snap.mjs
create mode 100644 replica/src/character.js
create mode 100644 replica/src/eyes.js
create mode 100644 replica/src/fx.js
create mode 100644 replica/src/math.js
create mode 100644 replica/src/pose.js
create mode 100644 replica/src/tables.js
create mode 100644 replica/src/tricks.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c6c28e
--- /dev/null
+++ b/.gitignore
@@ -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/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..756ccde
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ff78c2
--- /dev/null
+++ b/README.md
@@ -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) 只覆盖本仓库里的原创机芯、页面和文档。
+它不授权任何第三方角色造型、商标或应用源码;那些东西也不在这个仓库里。
diff --git a/replica/geometry.schema.md b/replica/geometry.schema.md
new file mode 100644
index 0000000..9d9dbf5
--- /dev/null
+++ b/replica/geometry.schema.md
@@ -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` 忽略。
diff --git a/replica/index.html b/replica/index.html
new file mode 100644
index 0000000..f11fdf0
--- /dev/null
+++ b/replica/index.html
@@ -0,0 +1,384 @@
+
+
+
+
+
+Character Engine Playground
+
+
+
+
+ Character Engine
+ 非官方弹簧角色机芯 · 几何不随仓库分发
+
+
+
+
+
+
+
+
+ 这是机芯,不是素材包
+
+ 引擎拆在 src/:math / tables / pose / tricks / eyes / fx / character。
+ 有几何时,右侧 64px 带登录包装;thinking / orbit / writing / loading 等会切 overlay。
+ 首屏从 curious 起播,点「登录轮换」则按 onboarding 从 idle 重来。
+ 大预览默认开指针跟随。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/replica/snap.mjs b/replica/snap.mjs
new file mode 100755
index 0000000..982d435
--- /dev/null
+++ b/replica/snap.mjs
@@ -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);
+});
diff --git a/replica/src/character.js b/replica/src/character.js
new file mode 100644
index 0000000..ae3b580
--- /dev/null
+++ b/replica/src/character.js
@@ -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);
diff --git a/replica/src/eyes.js b/replica/src/eyes.js
new file mode 100644
index 0000000..3a105a1
--- /dev/null
+++ b/replica/src/eyes.js
@@ -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);
diff --git a/replica/src/fx.js b/replica/src/fx.js
new file mode 100644
index 0000000..8af2abd
--- /dev/null
+++ b/replica/src/fx.js
@@ -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);
diff --git a/replica/src/math.js b/replica/src/math.js
new file mode 100644
index 0000000..cb67b8a
--- /dev/null
+++ b/replica/src/math.js
@@ -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);
diff --git a/replica/src/pose.js b/replica/src/pose.js
new file mode 100644
index 0000000..9bc5874
--- /dev/null
+++ b/replica/src/pose.js
@@ -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);
diff --git a/replica/src/tables.js b/replica/src/tables.js
new file mode 100644
index 0000000..617b2fd
--- /dev/null
+++ b/replica/src/tables.js
@@ -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);
diff --git a/replica/src/tricks.js b/replica/src/tricks.js
new file mode 100644
index 0000000..1eafa9a
--- /dev/null
+++ b/replica/src/tricks.js
@@ -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);