docs:发布不含几何的角色机芯学习仓
以 kivo@9337f7e 的 replica 机芯为基准,单独开源弹簧/状态机/眼睛编排。 不包含 geometry-data、官方图标和抽出的应用主包。
This commit is contained in:
+10
@@ -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/
|
||||||
@@ -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.
|
||||||
@@ -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) 只覆盖本仓库里的原创机芯、页面和文档。
|
||||||
|
它不授权任何第三方角色造型、商标或应用源码;那些东西也不在这个仓库里。
|
||||||
@@ -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` 忽略。
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Character Engine Playground</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--paper: #ece7dc;
|
||||||
|
--ink: #161513;
|
||||||
|
--mute: #6d675d;
|
||||||
|
--line: #d4cfc3;
|
||||||
|
--disk: #f3efe6;
|
||||||
|
--bg: #1a1916;
|
||||||
|
--panel: #22211d;
|
||||||
|
--accent: #e8d7a4;
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--paper); }
|
||||||
|
body {
|
||||||
|
font: 14px/1.5 "SF Pro Text", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
padding: 28px 32px 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 560;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
header p { margin: 0; color: var(--mute); font-size: 12px; }
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(280px, 420px) 1fr;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 12px 32px 48px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.stage {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid #2e2c27;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 28px 24px 20px;
|
||||||
|
}
|
||||||
|
.disk {
|
||||||
|
width: min(100%, 280px);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--disk);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
box-shadow: inset 0 -18px 40px rgba(40, 30, 10, 0.08), 0 24px 60px rgba(0,0,0,.35);
|
||||||
|
}
|
||||||
|
.disk svg { width: 68%; height: 68%; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; }
|
||||||
|
.stage-disks { display: flex; align-items: flex-end; justify-content: center; gap: 28px; }
|
||||||
|
.disk-login {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--disk);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
box-shadow: inset 0 -8px 18px rgba(40, 30, 10, 0.08), 0 12px 28px rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
.disk-login svg { width: 64px; height: 64px; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; }
|
||||||
|
.missing {
|
||||||
|
width: min(100%, 280px);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 22px 18px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #2a2823;
|
||||||
|
color: #d8d0c0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.missing strong { display: block; margin-bottom: 8px; color: var(--accent); font-weight: 560; }
|
||||||
|
.missing a { color: var(--accent); }
|
||||||
|
.readout {
|
||||||
|
margin-top: 18px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px 16px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #b7b1a4;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.readout b { color: var(--paper); font-weight: 560; }
|
||||||
|
.modes { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
|
||||||
|
button {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid #3a372f;
|
||||||
|
background: #2a2823;
|
||||||
|
color: var(--paper);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
button[aria-pressed="true"] { background: var(--accent); color: #1a1916; border-color: var(--accent); }
|
||||||
|
button:hover { border-color: #6a6458; }
|
||||||
|
button:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--mute);
|
||||||
|
font-weight: 560;
|
||||||
|
}
|
||||||
|
section + section { margin-top: 22px; }
|
||||||
|
.states { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||||
|
.states button { font-size: 12px; padding: 5px 10px; }
|
||||||
|
.shapes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.shape {
|
||||||
|
background: #2a2823;
|
||||||
|
border: 1px solid #3a372f;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 6px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.shape[aria-pressed="true"] { border-color: var(--accent); background: #353228; }
|
||||||
|
.shape svg { width: 48px; height: 48px; display: block; margin: 0 auto 4px; --fg: #efe8d8; --bg: #2a2823; }
|
||||||
|
.shape span { display: block; font-size: 10px; color: #b7b1a4; }
|
||||||
|
.swatches { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
|
.swatch {
|
||||||
|
width: 28px; height: 28px; border-radius: 50%;
|
||||||
|
border: 2px solid #111; cursor: pointer; padding: 0;
|
||||||
|
}
|
||||||
|
.swatch[aria-pressed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
.note {
|
||||||
|
color: #8d877b;
|
||||||
|
font-size: 12px;
|
||||||
|
max-width: 62ch;
|
||||||
|
}
|
||||||
|
.note code { color: #d8d0c0; font-size: 11px; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
main { grid-template-columns: 1fr; }
|
||||||
|
.stage { position: static; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Character Engine</h1>
|
||||||
|
<p>非官方弹簧角色机芯 · 几何不随仓库分发</p>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<aside class="stage">
|
||||||
|
<div id="ready-stage">
|
||||||
|
<div class="stage-disks">
|
||||||
|
<div class="disk" id="disk">
|
||||||
|
<svg id="bot" role="img" aria-label="Character preview"></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="disk-login">
|
||||||
|
<svg id="bot64" role="img" aria-label="Login size 64px"></svg>
|
||||||
|
</div>
|
||||||
|
<span style="display:block;margin-top:8px;font-size:10px;color:#8d877b;text-align:center">登录 sizePx: 64</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="missing" id="missing-geo" hidden>
|
||||||
|
<strong>几何未加载</strong>
|
||||||
|
本仓库不包含 <code>geometry-data.js</code>。把符合
|
||||||
|
<a href="geometry.schema.md">geometry.schema.md</a>
|
||||||
|
的数据放到同目录后刷新,即可本地预览。不要提交或再分发从商业应用抽出的造型。
|
||||||
|
</div>
|
||||||
|
<div class="modes">
|
||||||
|
<button id="mode-onboard" aria-pressed="true">登录轮换</button>
|
||||||
|
<button id="mode-hold" aria-pressed="false">锁定状态</button>
|
||||||
|
</div>
|
||||||
|
<div class="modes">
|
||||||
|
<button id="opt-follow" aria-pressed="true">跟随指针</button>
|
||||||
|
<button id="opt-emphasis" aria-pressed="false">emphasis</button>
|
||||||
|
<button id="act-spin">转一圈</button>
|
||||||
|
<button id="act-bounce">跳一下</button>
|
||||||
|
<button id="act-burst">粒子</button>
|
||||||
|
</div>
|
||||||
|
<div class="readout">
|
||||||
|
<div>状态 <b id="r-state">—</b></div>
|
||||||
|
<div>眼睛 <b id="r-eye">—</b></div>
|
||||||
|
<div>旋转 <b id="r-spin">—</b></div>
|
||||||
|
<div>挤压 <b id="r-squash">—</b></div>
|
||||||
|
<div>overlay <b id="r-ov">—</b></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<h2>情绪状态 · 39 态</h2>
|
||||||
|
<div id="states"></div>
|
||||||
|
</section>
|
||||||
|
<section id="shapes-section" hidden>
|
||||||
|
<h2>身形</h2>
|
||||||
|
<div class="shapes" id="shapes"></div>
|
||||||
|
</section>
|
||||||
|
<section id="swatches-section" hidden>
|
||||||
|
<h2>墨色</h2>
|
||||||
|
<div class="swatches" id="swatches"></div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>这是机芯,不是素材包</h2>
|
||||||
|
<p class="note">
|
||||||
|
引擎拆在 <code>src/</code>:math / tables / pose / tricks / eyes / fx / character。
|
||||||
|
有几何时,右侧 64px 带登录包装;thinking / orbit / writing / loading 等会切 overlay。
|
||||||
|
首屏从 curious 起播,点「登录轮换」则按 onboarding 从 idle 重来。
|
||||||
|
大预览默认开指针跟随。
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script src="geometry-data.js"></script>
|
||||||
|
<script src="src/math.js"></script>
|
||||||
|
<script src="src/tables.js"></script>
|
||||||
|
<script src="src/pose.js"></script>
|
||||||
|
<script src="src/tricks.js"></script>
|
||||||
|
<script src="src/fx.js"></script>
|
||||||
|
<script src="src/eyes.js"></script>
|
||||||
|
<script src="src/character.js"></script>
|
||||||
|
<script>
|
||||||
|
function hasUsableGeo(geo) {
|
||||||
|
return !!(
|
||||||
|
geo &&
|
||||||
|
typeof geo.Re === "number" &&
|
||||||
|
Array.isArray(geo.eyes) &&
|
||||||
|
geo.eyes.length > 0 &&
|
||||||
|
geo.shapes &&
|
||||||
|
Object.keys(geo.shapes).length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ready = hasUsableGeo(window.GROK_GEO);
|
||||||
|
document.getElementById("missing-geo").hidden = ready;
|
||||||
|
document.getElementById("ready-stage").hidden = !ready;
|
||||||
|
document.querySelectorAll("#mode-onboard, #mode-hold, #opt-follow, #opt-emphasis, #act-spin, #act-bounce, #act-burst")
|
||||||
|
.forEach((el) => { el.disabled = !ready; });
|
||||||
|
|
||||||
|
const readout = {
|
||||||
|
state: document.getElementById("r-state"),
|
||||||
|
eye: document.getElementById("r-eye"),
|
||||||
|
spin: document.getElementById("r-spin"),
|
||||||
|
squash: document.getElementById("r-squash"),
|
||||||
|
ov: document.getElementById("r-ov"),
|
||||||
|
};
|
||||||
|
const stateRoot = document.getElementById("states");
|
||||||
|
function paintReadout(s) {
|
||||||
|
readout.state.textContent = s.state;
|
||||||
|
readout.eye.textContent = `${s.eyeFrom} → ${s.eyeTo}`;
|
||||||
|
readout.spin.textContent = `${s.spin.toFixed(2)}°`;
|
||||||
|
readout.squash.textContent = s.squash.toFixed(3);
|
||||||
|
if (readout.ov) readout.ov.textContent = s.overlay || "—";
|
||||||
|
}
|
||||||
|
function markState(name) {
|
||||||
|
stateRoot.querySelectorAll("button").forEach((b) => {
|
||||||
|
b.setAttribute("aria-pressed", b.dataset.state === name ? "true" : "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function syncModes(mode) {
|
||||||
|
document.getElementById("mode-onboard").setAttribute("aria-pressed", mode === "onboarding");
|
||||||
|
document.getElementById("mode-hold").setAttribute("aria-pressed", mode === "hold");
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = window.GROK_META?.groups || window.GROK_TABLES?.GROUPS || [];
|
||||||
|
groups.forEach((group) => {
|
||||||
|
const h = document.createElement("h2");
|
||||||
|
h.textContent = group.label;
|
||||||
|
h.style.marginTop = "8px";
|
||||||
|
stateRoot.appendChild(h);
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = "states";
|
||||||
|
group.states.forEach((name) => {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.textContent = name;
|
||||||
|
b.dataset.state = name;
|
||||||
|
b.disabled = !ready;
|
||||||
|
b.addEventListener("click", () => {
|
||||||
|
if (!window.__bots) return;
|
||||||
|
window.__bots.forEach((el) => {
|
||||||
|
el.setMode("hold");
|
||||||
|
el.setState(name, { resetEyes: false });
|
||||||
|
});
|
||||||
|
syncModes("hold");
|
||||||
|
});
|
||||||
|
row.appendChild(b);
|
||||||
|
});
|
||||||
|
stateRoot.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
// Engine tables still render as documentation. Live character stays off.
|
||||||
|
} else {
|
||||||
|
const shared = { mode: "onboarding", shape: "blob", color: "black", scheme: "light", loginWrap: true };
|
||||||
|
const bot = new GrokCharacter(document.getElementById("bot"), {
|
||||||
|
...shared,
|
||||||
|
followPointer: true,
|
||||||
|
onChange(snap) { paintReadout(snap); markState(snap.state); },
|
||||||
|
});
|
||||||
|
const bot64 = new GrokCharacter(document.getElementById("bot64"), { ...shared, sizePx: 64 });
|
||||||
|
const bots = [bot, bot64];
|
||||||
|
window.__bots = bots;
|
||||||
|
bots.forEach((el) => {
|
||||||
|
el.moodN = 1;
|
||||||
|
el.setState("curious", { resetEyes: true });
|
||||||
|
});
|
||||||
|
setInterval(() => paintReadout(bot.snapshot()), 80);
|
||||||
|
|
||||||
|
const shapeRoot = document.getElementById("shapes");
|
||||||
|
document.getElementById("shapes-section").hidden = false;
|
||||||
|
Object.entries(GROK_GEO.shapes).forEach(([id, shape]) => {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.className = "shape";
|
||||||
|
b.dataset.shape = id;
|
||||||
|
b.innerHTML = `<svg viewBox="-15 -15 259 259"><path d="${shape.path}" style="fill:var(--fg)"></path></svg><span>${shape.label}</span>`;
|
||||||
|
b.addEventListener("click", () => {
|
||||||
|
bots.forEach((el) => el.setShape(id));
|
||||||
|
shapeRoot.querySelectorAll("button").forEach((el) => {
|
||||||
|
el.setAttribute("aria-pressed", el === b ? "true" : "false");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (id === "blob") b.setAttribute("aria-pressed", "true");
|
||||||
|
shapeRoot.appendChild(b);
|
||||||
|
});
|
||||||
|
|
||||||
|
const swatchRoot = document.getElementById("swatches");
|
||||||
|
document.getElementById("swatches-section").hidden = false;
|
||||||
|
Object.entries(GROK_GEO.palette).forEach(([id, pal]) => {
|
||||||
|
const b = document.createElement("button");
|
||||||
|
b.className = "swatch";
|
||||||
|
b.title = id;
|
||||||
|
b.style.background = pal.light;
|
||||||
|
b.addEventListener("click", () => {
|
||||||
|
bots.forEach((el) => el.setColor(id, "light"));
|
||||||
|
swatchRoot.querySelectorAll("button").forEach((el) => {
|
||||||
|
el.setAttribute("aria-pressed", el === b ? "true" : "false");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (id === "black") b.setAttribute("aria-pressed", "true");
|
||||||
|
swatchRoot.appendChild(b);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("mode-onboard").onclick = () => {
|
||||||
|
if (!window.__bots) return;
|
||||||
|
window.__bots.forEach((el) => el.setMode("onboarding"));
|
||||||
|
syncModes("onboarding");
|
||||||
|
};
|
||||||
|
document.getElementById("mode-hold").onclick = () => {
|
||||||
|
if (!window.__bots) return;
|
||||||
|
window.__bots.forEach((el) => el.setMode("hold"));
|
||||||
|
syncModes("hold");
|
||||||
|
};
|
||||||
|
document.getElementById("opt-follow").onclick = () => {
|
||||||
|
if (!window.__bots) return;
|
||||||
|
const on = document.getElementById("opt-follow").getAttribute("aria-pressed") !== "true";
|
||||||
|
document.getElementById("opt-follow").setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
window.__bots.forEach((el) => el.setFollowPointer(on));
|
||||||
|
};
|
||||||
|
document.getElementById("opt-emphasis").onclick = () => {
|
||||||
|
if (!window.__bots) return;
|
||||||
|
const on = document.getElementById("opt-emphasis").getAttribute("aria-pressed") !== "true";
|
||||||
|
document.getElementById("opt-emphasis").setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
window.__bots.forEach((el) => el.setEmphasis(on));
|
||||||
|
};
|
||||||
|
document.getElementById("act-spin").onclick = () => window.__bots?.forEach((el) => el.spinOnce(1));
|
||||||
|
document.getElementById("act-bounce").onclick = () => window.__bots?.forEach((el) => el.bounceOnce());
|
||||||
|
document.getElementById("act-burst").onclick = () => window.__bots?.forEach((el) => el.burstOnce());
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Executable
+214
@@ -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);
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user