commit 5502b2d97af12ed1e542060cabd197008e78e4b6 Author: Edom <463882408@qq.com> Date: Sat Aug 15 22:45:18 2026 +0800 docs:发布不含几何的角色机芯学习仓 以 kivo@9337f7e 的 replica 机芯为基准,单独开源弹簧/状态机/眼睛编排。 不包含 geometry-data、官方图标和抽出的应用主包。 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

+

非官方弹簧角色机芯 · 几何不随仓库分发

+
+
+ +
+
+

情绪状态 · 39 态

+
+
+ + +
+

这是机芯,不是素材包

+

+ 引擎拆在 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);