feat:加上 Vue 学习站与可嵌入机芯

站点按 bloub 三栏做自定义、动画时间线和设置,支持序列导出。
形体只留常见几种并配中文名;动画页右栏拉满时间线旁空位,滚动条隐藏。
机芯补 GROK.create / <grok-bot>,以及暂停冻结。
This commit is contained in:
Edom
2026-08-19 01:29:13 +08:00
parent 891f6ef7e7
commit 1ba126c92b
55 changed files with 8998 additions and 224 deletions
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Grok Bot · 最小嵌入</title>
<style>
:root { --disk: #f3efe6; --ink: #161513; --mute: #6d675d; --paper: #ece7dc; }
html, body { margin: 0; min-height: 100%; background: #1a1916; color: var(--paper); }
body {
font: 14px/1.55 "SF Pro Text", "PingFang SC", sans-serif;
padding: 40px 28px 64px;
max-width: 640px;
}
h1 { margin: 0 0 6px; font-size: 14px; letter-spacing: 0.08em; text-transform: uppercase; }
p { margin: 0 0 22px; color: var(--mute); font-size: 13px; }
a { color: #e8d7a4; }
.disk {
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);
}
grok-bot { color-scheme: light; }
pre {
margin: 22px 0 0;
padding: 14px 16px;
background: #22211d;
border: 1px solid #2e2c27;
border-radius: 12px;
overflow: auto;
font: 12px/1.5 ui-monospace, Menlo, monospace;
color: #d8d0c0;
}
</style>
</head>
<body>
<h1>最小嵌入</h1>
<p>
登录页原参数:64px · blob · black · <code>loginWrap</code>。
<a href="index.html">回 playground</a>
</p>
<div class="disk">
<grok-bot state="idle" shape="blob" color="black" size="64" mode="hold"></grok-bot>
</div>
<pre>
&lt;!-- 任意页面 --&gt;
&lt;script src="geometry-data.js"&gt;&lt;/script&gt;
&lt;script src="src/math.js"&gt;&lt;/script&gt;
&lt;script src="src/tables.js"&gt;&lt;/script&gt;
&lt;script src="src/pose.js"&gt;&lt;/script&gt;
&lt;script src="src/tricks.js"&gt;&lt;/script&gt;
&lt;script src="src/fx.js"&gt;&lt;/script&gt;
&lt;script src="src/eyes.js"&gt;&lt;/script&gt;
&lt;script src="src/character.js"&gt;&lt;/script&gt;
&lt;script src="src/api.js"&gt;&lt;/script&gt;
&lt;grok-bot state="idle" shape="blob" color="black" size="64"&gt;&lt;/grok-bot&gt;
&lt;script&gt;
const el = document.querySelector("grok-bot");
el.setState("playful");
el.spin(1);
// 或命令式
const { setState, spin, destroy } = GROK.create("#host", {
sizePx: 64, shape: "blob", color: "black", loginWrap: true, mode: "hold",
});
// 换皮:只覆盖你有的部分
GROK.create("#other", {
eyes: MY_EYES,
shapes: { blob: MY_BLOB },
playlist: { idle: [0], playful: [1] },
});
&lt;/script&gt;
</pre>
<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 src="src/api.js"></script>
</body>
</html>
+135 -127
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Character Engine Playground</title>
<title>Grok Bot 角色 · 源码复刻</title>
<style>
:root {
--paper: #ece7dc;
@@ -73,18 +73,14 @@
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;
.disk-login span {
display: block;
margin-top: 8px;
font-size: 10px;
color: #8d877b;
text-align: center;
}
.missing strong { display: block; margin-bottom: 8px; color: var(--accent); font-weight: 560; }
.missing a { color: var(--accent); }
.readout {
.readout, .hud {
margin-top: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
@@ -93,8 +89,15 @@
color: #b7b1a4;
font-size: 12px;
}
.readout b { color: var(--paper); font-weight: 560; }
.modes { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
.hud {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #2e2c27;
grid-template-columns: 1fr 1fr;
font-size: 11px;
}
.readout b, .hud b { color: var(--paper); font-weight: 560; }
.modes { display: flex; gap: 8px; margin-top: 16px; }
button {
appearance: none;
border: 1px solid #3a372f;
@@ -107,7 +110,6 @@
}
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;
@@ -142,13 +144,19 @@
border: 2px solid #111; cursor: pointer; padding: 0;
}
.swatch[aria-pressed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
.icons { display: flex; gap: 16px; align-items: center; }
.icons figure { margin: 0; text-align: center; }
.icons img {
width: 72px; height: 72px; border-radius: 16px;
background: #111; display: block;
}
.icons figcaption { margin-top: 6px; font-size: 11px; color: var(--mute); }
.note {
color: #8d877b;
font-size: 12px;
max-width: 62ch;
}
.note code { color: #d8d0c0; font-size: 11px; }
[hidden] { display: none !important; }
@media (max-width: 860px) {
main { grid-template-columns: 1fr; }
.stage { position: static; }
@@ -157,29 +165,21 @@
</head>
<body>
<header>
<h1>Character Engine</h1>
<p>非官方弹簧角色机芯 · 几何不随仓库分发</p>
<h1>Grok Bot Character</h1>
<p>从 Grok Bot.app v0.18.0 的 app.asar 抽出 · 个人学习用 · <a href="showcase.html" style="color:#e8d7a4">录屏展示</a> · <a href="embed.html" style="color:#e8d7a4">最小嵌入</a></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 class="stage-disks">
<div class="disk" id="disk">
<svg id="bot" role="img" aria-label="Grok Bot character"></svg>
</div>
<div>
<div class="disk-login">
<svg id="bot64" role="img" aria-label="Login size 64px"></svg>
</div>
<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>
<span style="display:block;margin-top:8px;font-size:10px;color:#8d877b;text-align:center">登录 sizePx: 64</span>
</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>
@@ -193,33 +193,57 @@
<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>状态 <b id="r-state">idle</b></div>
<div>眼睛 <b id="r-eye">0 → 0</b></div>
<div>旋转 <b id="r-spin">0.00°</b></div>
<div>挤压 <b id="r-squash">1.000</b></div>
<div>overlay <b id="r-ov">—</b></div>
</div>
<div class="hud" id="hud">
<div>overlay.x <b id="h-ox">0</b></div>
<div>trick <b id="h-trick">—</b></div>
<div>shapeSpring <b id="h-shape">1</b></div>
<div>notify <b id="h-notify">0</b></div>
<div>blink <b id="h-blink">1</b></div>
<div>badge <b id="h-badge">—</b></div>
</div>
</aside>
<div>
<section>
<h2>情绪状态 · 39 态</h2>
<h2>情绪状态 · 源码 39 态</h2>
<div id="states"></div>
</section>
<section id="shapes-section" hidden>
<h2>身形</h2>
<section>
<h2>身形 · Jo 几何生成器</h2>
<div class="shapes" id="shapes"></div>
</section>
<section id="swatches-section" hidden>
<h2>墨色</h2>
<section>
<h2>墨色 · snt 调色板</h2>
<div class="swatches" id="swatches"></div>
</section>
<section>
<h2>这是机芯,不是素材包</h2>
<h2>Dock 静态商标(位图,不是角色引擎)</h2>
<div class="icons">
<figure>
<img src="../assets/icon.iconset/[email protected]" alt="Dock icon 256" />
<figcaption>icon.icns 256</figcaption>
</figure>
<figure>
<img src="../assets/app-icon-256.png" alt="In-app icon" />
<figcaption>asar app-icon</figcaption>
</figure>
</div>
</section>
<section>
<h2>这不是截图描摹</h2>
<p class="note">
引擎拆在 <code>src/</code>:math / tables / pose / tricks / eyes / fx / character。
有几何时,右侧 64px 带登录包装;thinking / orbit / writing / loading 等会切 overlay。
首屏从 curious 起播,点「登录轮换」则按 onboarding 从 idle 重来。
大预览默认开指针跟随。
引擎按源码 <code>$_t</code> + 登录包装 <code>sd()</code> 拆在
<code>src/</code>:math / tables / pose / tricks / eyes / fx / character。
右侧 64px 带 <code>pose.scale ≈ 1.04</code>。thinking / orbit / writing / loading
等会切 overlay;celebrate 会 <code>spinWild</code>;换身形走弹簧并轮换一记特技。
大预览从 curious 起播;右侧 64px 登录盘按 <code>pjn(0)=idle</code>。点「登录轮换」两颗都从 idle 重来。
大预览默认开指针跟随;源码 <code>sd().isFollowingPointer</code> 默认关。
别的项目请看 <a href="embed.html"><code>embed.html</code></a> 和 <code>GROK.create</code>。
</p>
</section>
</div>
@@ -232,30 +256,20 @@
<script src="src/fx.js"></script>
<script src="src/eyes.js"></script>
<script src="src/character.js"></script>
<script src="src/api.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"),
ox: document.getElementById("h-ox"),
trick: document.getElementById("h-trick"),
shape: document.getElementById("h-shape"),
notify: document.getElementById("h-notify"),
blink: document.getElementById("h-blink"),
badge: document.getElementById("h-badge"),
};
const stateRoot = document.getElementById("states");
function paintReadout(s) {
@@ -264,6 +278,13 @@
readout.spin.textContent = `${s.spin.toFixed(2)}°`;
readout.squash.textContent = s.squash.toFixed(3);
if (readout.ov) readout.ov.textContent = s.overlay || "—";
if (!readout.ox) return;
readout.ox.textContent = (s.overlayX ?? 0).toFixed(3);
readout.trick.textContent = s.trick || "—";
readout.shape.textContent = (s.shapeSpring ?? 1).toFixed(3);
readout.notify.textContent = (s.notify ?? 0).toFixed(3);
readout.blink.textContent = (s.blink ?? 1).toFixed(3);
readout.badge.textContent = s.badge ? `${s.badge.x.toFixed(1)}, ${s.badge.y.toFixed(1)}` : "—";
}
function markState(name) {
stateRoot.querySelectorAll("button").forEach((b) => {
@@ -275,8 +296,7 @@
document.getElementById("mode-hold").setAttribute("aria-pressed", mode === "hold");
}
const groups = window.GROK_META?.groups || window.GROK_TABLES?.GROUPS || [];
groups.forEach((group) => {
GROK_META.groups.forEach((group) => {
const h = document.createElement("h2");
h.textContent = group.label;
h.style.marginTop = "8px";
@@ -287,10 +307,8 @@
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) => {
bots.forEach((el) => {
el.setMode("hold");
el.setState(name, { resetEyes: false });
});
@@ -301,84 +319,74 @@
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 shared = { mode: "onboarding", shape: "blob", color: "black", scheme: "light", loginWrap: true };
const stage = GROK.create(document.getElementById("bot"), {
...shared,
followPointer: true,
onChange(snap) { paintReadout(snap); markState(snap.state); },
});
const login = GROK.create(document.getElementById("bot64"), { ...shared, sizePx: 64 });
const bot = stage.bot;
const bot64 = login.bot;
const bots = [bot, bot64];
window.__bots = bots;
bot.moodN = 1;
bot.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");
});
const shapeRoot = document.getElementById("shapes");
Object.entries(GROK_GEO.shapes).forEach(([id, shape]) => {
const b = document.createElement("button");
b.className = "shape";
b.dataset.shape = id;
b.innerHTML = `<svg viewBox="-15 -15 259 259"><path d="${shape.path}" style="fill:var(--fg)"></path></svg><span>${shape.label}</span>`;
b.addEventListener("click", () => {
bots.forEach((el) => el.setShape(id));
shapeRoot.querySelectorAll("button").forEach((el) => {
el.setAttribute("aria-pressed", el === b ? "true" : "false");
});
if (id === "blob") b.setAttribute("aria-pressed", "true");
shapeRoot.appendChild(b);
});
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");
});
const swatchRoot = document.getElementById("swatches");
Object.entries(GROK_GEO.palette).forEach(([id, pal]) => {
const b = document.createElement("button");
b.className = "swatch";
b.title = id;
b.style.background = pal.light;
b.addEventListener("click", () => {
bots.forEach((el) => el.setColor(id, "light"));
swatchRoot.querySelectorAll("button").forEach((el) => {
el.setAttribute("aria-pressed", el === b ? "true" : "false");
});
if (id === "black") b.setAttribute("aria-pressed", "true");
swatchRoot.appendChild(b);
});
}
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"));
bots.forEach((el) => el.setMode("onboarding"));
syncModes("onboarding");
};
document.getElementById("mode-hold").onclick = () => {
if (!window.__bots) return;
window.__bots.forEach((el) => el.setMode("hold"));
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));
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));
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());
document.getElementById("act-spin").onclick = () => bots.forEach((el) => el.spinOnce(1));
document.getElementById("act-bounce").onclick = () => bots.forEach((el) => el.bounceOnce());
document.getElementById("act-burst").onclick = () => bots.forEach((el) => el.burstOnce());
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
Executable → Regular
+232 -4
View File
@@ -146,14 +146,21 @@ async function main() {
if (ready) break;
await sleep(100);
}
const report = [];
report.push(await cdp.eval(`(() => {
const [stage, login] = window.__bots || [];
return {
label: "boot-modes",
stage: { state: stage?.state, mode: stage?.mode, moodN: stage?.moodN },
login: { state: login?.state, mode: login?.mode, moodN: login?.moodN },
};
})()`));
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",
"playful", "laughing", "sleeping", "waking", "idle", "celebrate",
"thinking", "writing", "orbit", "humming", "loading", "notifying",
];
for (const name of states) {
await click(cdp, `button[data-state="${name}"]`);
@@ -201,6 +208,227 @@ async function main() {
await shot(cdp, `${shape}-spin`);
}
// Gr() Lt/Mt: locals default to 1 each frame (not prev)
await cdp.eval(`(() => {
const b = window.__bots[0];
b.setMode("hold");
b.setState("laughing", { resetEyes: false });
b.poseOut.lid = 0.7;
b.poseOut.eyeBoost = 1.12;
return true;
})()`);
await sleep(80);
await click(cdp, 'button[data-state="loading"]');
await sleep(80);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "loading-after-laughing",
lid: b.poseOut.lid,
eyeBoost: b.poseOut.eyeBoost,
trick: b.trick?.kind || null,
spinTurn: !!b.spinTurn,
};
})()`));
await click(cdp, 'button[data-state="excited"]');
await sleep(80);
await click(cdp, 'button[data-state="waking"]');
await sleep(1600);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "[email protected]",
dt: (performance.now() - b.stateAt) / 1000,
lid: b.poseOut.lid,
eyeBoost: b.poseOut.eyeBoost,
ty: b.poseOut.ty,
squash: b.poseOut.squash,
};
})()`));
await click(cdp, 'button[data-state="dragging"]');
await sleep(50);
const drag = await cdp.eval(`(() => {
const b = window.__bots[0];
const now = performance.now();
const dt = (now - b.stateAt) / 1000;
const a = window.GROK_POSE.applyPose("dragging", 0, 0.05, now, b.ctx, { prev: { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1.06 } });
const mid = window.GROK_POSE.applyPose("dragging", 0, 1.2, now, b.ctx, { prev: { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1 } });
const late = window.GROK_POSE.applyPose("dragging", 0, 2.5, now, b.ctx, { prev: { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1.06 } });
return { label: "dragging-Lt", dt, a: a.eyeBoost, mid: mid.eyeBoost, late: late.eyeBoost };
})()`);
report.push(drag);
// hold 切态 must not tear rn/ys
await click(cdp, 'button[data-state="idle"]');
await sleep(60);
await click(cdp, "#act-spin");
await sleep(40);
const beforeHold = await cdp.eval(`({ spinTurn: !!window.__bots[0].spinTurn, trick: window.__bots[0].trick?.kind || null })`);
await click(cdp, 'button[data-state="thinking"]');
await sleep(40);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "hold-keeps-pn",
before: ${JSON.stringify(beforeHold)},
state: b.state,
spinTurn: !!b.spinTurn,
trick: b.trick?.kind || null,
};
})()`));
// notifying badge = rest ring 7/8
await click(cdp, 'button[data-state="notifying"]');
await sleep(400);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
const R = b.geo.Re;
const ring = window.GROK_FX.shapeRing(b.geo.shapes[b.shapeName].path, R);
const Yl = ring[Math.round(ring.length * 7 / 8) % ring.length];
const badge = b.badge;
const vis = badge && badge.style.display !== "none";
const cx = vis ? +badge.getAttribute("cx") : null;
const cy = vis ? +badge.getAttribute("cy") : null;
return {
label: "notifying-Yl",
vis,
cx, cy,
Yl,
dx: vis ? Math.abs(cx - Yl[0]) : null,
dy: vis ? Math.abs(cy - Yl[1]) : null,
notifyX: b.notify.x,
};
})()`));
// excited pn must survive 1200ms onboard cut to idle
await cdp.eval(`(() => {
const b = window.__bots[0];
b.setMode("hold");
b.moodN = 7;
b.setState("excited", { resetEyes: false });
b._pn(1);
b.mode = "onboarding";
b.stateAt = performance.now() - 1190;
return true;
})()`);
await sleep(80);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "onboard-excited-to-idle",
state: b.state,
moodN: b.moodN,
spinTurn: !!b.spinTurn,
trick: b.trick?.kind || null,
};
})()`));
await click(cdp, 'button[data-state="idle"]');
await sleep(40);
await cdp.eval(`(() => {
const b = window.__bots[0];
b.trick = null;
b.spinOnce(1);
return true;
})()`);
await sleep(50);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
const rn = b.spinTurn;
const turn = b.extras.turn;
return {
label: "pn-post-spring",
hasRn: !!rn,
turn,
rnX: rn ? rn.x : null,
match: rn ? Math.abs(turn - rn.x) < 1e-9 : turn == null,
};
})()`));
await cdp.eval(`(() => {
const b = window.__bots[0];
b.setFollowPointer(true);
b.pointerRaw = { x: 0, y: 0 };
b.pointer.tx = 12;
b.pointer.ty = 8;
b.pointer.x = 12;
b.pointer.y = 8;
return true;
})()`);
await sleep(30);
await cdp.eval(`(() => {
const b = window.__bots[0];
b.pointerRaw = null;
return true;
})()`);
await sleep(80);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "pointer-leave-decay",
tracking: !!(b.gazeTarget || (b.followPointer && b.pointerRaw)),
x: b.pointer.x,
tx: b.pointer.tx,
decaying: b.pointer.tx === 0 && b.pointer.x > 0.05 && b.pointer.x < 12,
};
})()`));
await click(cdp, 'button[data-state="writing"]');
await sleep(400);
await cdp.eval(`(() => {
const b = window.__bots[0];
b.fx.ink = [[10, 10], [20, 20]];
b.fx.recvTick = 3;
b.fx.recvDir = 1.2;
b.setState("writing", { resetEyes: false });
return true;
})()`);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "setState-resets-ink-recv",
ink: b.fx.ink.length,
recvTick: b.fx.recvTick,
recvDir: b.fx.recvDir,
};
})()`));
await cdp.eval(`(() => {
const b = window.__bots[0];
b.setMode("hold");
b.trick = null;
b.spinTurn = null;
b.hopAt = -1;
b.particles.clear();
b.overlay.x = 0; b.overlay.v = 0; b.overlay.t = 0;
b.overlayTurn.x = 0; b.overlayTurn.v = 0; b.overlayTurn.t = 0;
b.overlayMix.x = 1; b.overlayMix.v = 0; b.overlayMix.t = 1;
b.shapeSpring.x = 1; b.shapeSpring.v = 0; b.shapeSpring.t = 1;
b.eyeMorph.x = 1; b.eyeMorph.v = 0; b.eyeMorph.t = 1;
b.ovOn = false;
b.ovKind = null;
b.ovTarget = null;
b.ovPrev = null;
b.setState("sleeping", { resetEyes: false });
b.setPaused(true);
return true;
})()`);
await sleep(200);
report.push(await cdp.eval(`(() => {
const b = window.__bots[0];
return {
label: "pause-settle-snap",
raf: b._raf,
eyeFrom: b.eyeFrom,
eyeTo: b.eyeTo,
poseOut: b.poseOut,
fromPolys: b._fromPolys,
};
})()`));
await cdp.eval(`window.__bots[0].setPaused(false)`);
await writeFile(join(OUT, "report.json"), JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
+136
View File
@@ -0,0 +1,136 @@
/* L2 — public factory + <grok-bot>. Skin: eyes / shapes / playlist / palette. */
(function (g) {
const ATTRS = ["state", "shape", "color", "size", "paused", "follow", "mode", "emphasis"];
function mergeSkin(opts = {}) {
const base = opts.geo || g.GROK_GEO;
return {
...base,
eyes: opts.eyes || base.eyes,
shapes: opts.shapes ? { ...base.shapes, ...opts.shapes } : base.shapes,
palette: opts.palette ? { ...base.palette, ...opts.palette } : base.palette,
solids: opts.solids || base.solids,
};
}
function resolveSvg(target) {
if (!target) throw new Error("GROK.create: missing target");
if (typeof target === "string") {
const el = document.querySelector(target);
if (!el) throw new Error("GROK.create: no match for " + target);
target = el;
}
if (target.tagName === "SVG" || target instanceof SVGSVGElement) return target;
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
target.appendChild(svg);
return svg;
}
function createCharacter(target, opts = {}) {
const svg = resolveSvg(target);
const geo = mergeSkin(opts);
const playlist = opts.playlist
? { ...g.GROK_TABLES.EYE_PLAYLIST, ...opts.playlist }
: g.GROK_TABLES.EYE_PLAYLIST;
const bot = new g.GrokCharacter(svg, { mode: "hold", ...opts, geo, playlist });
return {
bot,
svg,
setState: (name, o) => bot.setState(name, o),
setShape: (name) => bot.setShape(name),
setColor: (id, scheme) => bot.setColor(id, scheme),
setPaused: (v) => bot.setPaused(v),
setMode: (m) => bot.setMode(m),
setFollowPointer: (v) => bot.setFollowPointer(v),
setEmphasis: (v) => bot.setEmphasis(v),
spin: (n) => bot.spinOnce(n),
bounce: () => bot.bounceOnce(),
burst: () => bot.burstOnce(),
destroy: () => bot.destroy(),
snapshot: () => bot.snapshot(),
};
}
class GrokBotElement extends HTMLElement {
static get observedAttributes() { return ATTRS; }
connectedCallback() {
if (this._handle) return;
const root = this.attachShadow({ mode: "open" });
const box = document.createElement("div");
box.setAttribute("part", "disk");
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("role", "img");
svg.setAttribute("aria-label", this.getAttribute("label") || "character");
box.appendChild(svg);
const style = document.createElement("style");
style.textContent = `:host{display:inline-block;line-height:0}div{width:100%;height:100%}svg{width:100%;height:100%;overflow:visible}`;
root.append(style, box);
this._applySize();
this._handle = createCharacter(svg, this._opts());
this.bot = this._handle.bot;
}
disconnectedCallback() {
this._handle?.destroy();
this._handle = null;
this.bot = null;
}
attributeChangedCallback(name, prev, next) {
if (!this._handle || prev === next) return;
this._applySize();
if (name === "state") this._handle.setState(next || "idle");
else if (name === "shape") this._handle.setShape(next || "blob");
else if (name === "color") this._handle.setColor(next || "black");
else if (name === "paused") this._handle.setPaused(this.hasAttribute("paused"));
else if (name === "follow") this._handle.setFollowPointer(next === "true");
else if (name === "emphasis") this._handle.setEmphasis(this.hasAttribute("emphasis"));
else if (name === "mode") this._handle.setMode(next || "hold");
}
_applySize() {
const n = Number(this.getAttribute("size"));
if (n > 0) {
this.style.width = `${n}px`;
this.style.height = `${n}px`;
}
}
_opts() {
const size = Number(this.getAttribute("size"));
return {
state: this.getAttribute("state") || "idle",
shape: this.getAttribute("shape") || "blob",
color: this.getAttribute("color") || "black",
sizePx: size > 0 ? size : null,
paused: this.hasAttribute("paused"),
followPointer: this.getAttribute("follow") === "true",
emphasis: this.hasAttribute("emphasis"),
mode: this.getAttribute("mode") || "hold",
loginWrap: this.getAttribute("login") !== "false",
};
}
setState(name) { this._handle?.setState(name); }
setShape(name) { this._handle?.setShape(name); }
setColor(id) { this._handle?.setColor(id); }
setPaused(v) { this._handle?.setPaused(v); }
spin(n) { this._handle?.spin(n); }
bounce() { this._handle?.bounce(); }
destroy() { this._handle?.destroy(); }
}
function define(name = "grok-bot") {
if (!customElements.get(name)) customElements.define(name, GrokBotElement);
return name;
}
define();
g.GROK = {
create: createCharacter,
define,
mergeSkin,
Character: g.GrokCharacter,
};
})(window);
+223 -52
View File
@@ -17,15 +17,31 @@
VIEW, VIEW_HALF, VIEW_MID, inkFg, inkCss, EYE_BG,
} = T;
const snapSpring = (s, v) => {
s.x = s.t = v;
s.v = 0;
};
const HOLD_GAZE = {
curious: { x: 12, y: -6 }, sad: { x: 2, y: 8 }, shy: { x: 10, y: 7 },
proud: { x: 0, y: -6 }, bored: { x: 12, y: 6 }, suspicious: { x: -15, y: 3 },
confused: { x: 12, y: 3 }, thinking: { x: 12, y: -7 }, angry: { x: 0, y: 2 },
drowsy: { x: 4, y: 7 }, searching: { x: 14, y: -4 }, working: { x: 3, y: 7 },
notifying: { x: 7, y: -3 }, playful: { x: 10, y: -4 }, happy: { x: -6, y: -4 },
laughing: { x: 4, y: -4 }, listening: { x: 5, y: -2 }, scared: { x: 12, y: 2 },
};
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.mode = opts.mode || "hold";
this.state = opts.state || "idle";
this.onChange = opts.onChange || (() => {});
this.frameHalf = opts.frameHalf || VIEW_HALF;
this.fitBox = !!opts.fitBox;
this.loginWrap = opts.loginWrap !== false;
this.eyeTopology = opts.eyeTopology ?? this.loginWrap;
this.faceTune = opts.faceTune ?? (this.loginWrap ? FACE_TUNE : null);
@@ -36,12 +52,16 @@
this.emphasis = !!opts.emphasis;
this.followPointer = !!opts.followPointer;
this.gazeTarget = opts.gazeTarget || null;
this.frozen = opts.paused === "hold-pose";
this.driven = !!opts.driven;
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.geo = opts.geo || g.GROK_GEO;
this.playlists = opts.playlist || EYE_PLAYLIST;
this.spin = spring(0);
this.tx = spring(0);
@@ -102,19 +122,27 @@
this.ovTarget = null;
this.ovRest = false;
this.ovRestAt = 0;
this.pxW = 190;
this.pxW = opts.sizePx || 190;
this.pxAt = 0;
this.partScale = 1;
this.celebrateAt = -1;
this.gazeSpin = 0;
this.extras = { turn: null, Kr: 0, yi: 0, ki: 0, Yr: 0, Zr: 0, wi: 0, hop: 0 };
this.poseOut = { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1 };
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));
if (!this.driven) this._bindPointer();
if (this.frozen) {
this.holdFrame();
} else if (this.driven) {
this._paint(this.t0);
} else {
this._paint(this.t0);
this._armTick();
}
}
destroy() {
@@ -125,7 +153,7 @@
_freshCtx(now) {
return {
nodUntil: now + 1800,
nodUntil: now + rand(1200, 2200),
nodEnd: 0,
angryShakeUntil: 0,
impulseAt: now + rand(500, 1200),
@@ -156,7 +184,28 @@
}
setPaused(v) {
if (v === "hold-pose") {
this.holdFrame();
return;
}
this.frozen = false;
this.paused = !!v;
if (!this.paused) this._armTick();
}
_armTick() {
if (this.frozen || this.driven || this._raf) return;
this.last = performance.now();
this._raf = requestAnimationFrame((t) => this._tick(t));
}
_wake() {
if (this.frozen || this.driven) return;
if (this.paused) this._armTick();
}
step(dt) {
this._tick(this.last + Math.max(0, dt) * 1000);
}
setEmphasis(v) {
@@ -176,10 +225,10 @@
}
setShape(name) {
if (!g.GROK_GEO.shapes[name] || name === this.shapeName) return;
const R = g.GROK_GEO.Re;
if (!this.geo.shapes[name] || name === this.shapeName) return;
const R = this.geo.Re;
const k = K2(clamp(this.shapeSpring.x, 0, 1));
const rest = FX.shapeMetrics(g.GROK_GEO.shapes[this.shapeName], R);
const rest = FX.shapeMetrics(this.geo.shapes[this.shapeName], R);
if (k >= 1 || !this.prevFace || !this.prevRing) {
this.prevFace = rest.face;
this.prevRing = rest.ring;
@@ -199,6 +248,7 @@
if (this.loginWrap) this.eyeScaleProp = shapeEyeScale(name);
this._applyPoseScale();
this._cycleShapeTrick();
this._wake();
}
setColor(id, scheme) {
@@ -209,7 +259,7 @@
} else if (this.loginWrap) {
this.svg.style.setProperty("--fg", inkFg(id));
} else {
const pal = g.GROK_GEO.palette[id] || g.GROK_GEO.palette.black;
const pal = this.geo.palette[id] || this.geo.palette.black;
this.svg.style.setProperty("--fg", this.scheme === "dark" ? pal.dark : pal.light);
}
this.svg.style.setProperty("--ink", inkCss(id));
@@ -227,10 +277,10 @@
}
setState(name, { resetEyes = false } = {}) {
if (!EYE_PLAYLIST[name]) return;
if (!this.playlists[name]) return;
this.state = name;
this.stateAt = performance.now();
const list = EYE_PLAYLIST[name];
const list = this.playlists[name];
this.eyeIdx = 0;
if (resetEyes) {
this.eyeFrom = list[0];
@@ -255,14 +305,12 @@
: 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();
this.fx?.resetInk();
this.fx?.resetRecv();
if (name !== "waking" && name !== "sleeping" && name !== "drowsy") {
EY.queueBlink(this.blinkQueue, this.stateAt);
}
this._wake();
try {
this.onChange(this.snapshot());
} catch (_) { /* host UI may not be ready */ }
@@ -283,17 +331,80 @@
squash: this.squash.x,
blink: this.blink.x,
overlay: this.ovKind,
overlayX: this.overlay.x,
notify: this.notify.x,
trick: this.trick?.kind || (this.spinTurn ? "pn" : null),
shapeSpring: this.shapeSpring.x,
badge: this.badge && this.badge.style.display !== "none"
? { x: +this.badge.getAttribute("cx"), y: +this.badge.getAttribute("cy") }
: null,
};
}
spinOnce(turns = 1) {
this._wake();
this._pn(turns);
}
bounceOnce() {
this._wake();
this._hop(performance.now());
}
burstOnce() {
if (!this.reduceMotion) this.particles.burst(22, 1.1, 0.3);
if (!this.reduceMotion && !this.paused) this.particles.burst(22, 1.1, 0.3);
}
orbitGaze(ms = 1500) {
if (this.reduceMotion || this.frozen) return;
this.gazeOrbit = { at: performance.now(), dur: Math.max(ms, 1), from: Math.PI * 2 };
this._armTick();
}
holdFrame(at = 900) {
this.frozen = true;
this.paused = true;
if (this._raf) {
cancelAnimationFrame(this._raf);
this._raf = 0;
}
const now = this.t0 + at;
const mt = at / 1000;
const pose = applyPose(this.state, mt, mt, now, this.ctx, {
eyeTo: this.eyeTo,
eyeMorphX: 1,
blinkX: this.blink.x,
playlist: this.playlists,
prev: this.poseOut,
});
this.ctx.wantPn = null;
this.ctx.wantBurst = null;
this.ctx.tyKick = 0;
this.ctx.spinKick = 0;
this.poseOut = pose;
snapSpring(this.spin, pose.spin);
snapSpring(this.tx, pose.tx);
snapSpring(this.ty, pose.ty);
snapSpring(this.squash, pose.squash);
snapSpring(this.eyeScale, pose.eyeBoost);
snapSpring(this.blink, pose.lid);
this._stepOverlay(now);
if (this.ovKind) {
snapSpring(this.overlay, this.overlay.t);
snapSpring(this.overlayMix, 1);
snapSpring(this.overlayTurn, this.overlayTurn.t);
this.fx.overlayAt = this.t0;
}
snapSpring(this.notify, this.state === "notifying" ? 1 : 0);
snapSpring(this.humDots, this.state === "humming" ? 1 : 0);
if (this.state === "humming" || this.state === "loading") this.ovSpin = 2.1;
const gz = HOLD_GAZE[this.state] || { x: 0, y: 0 };
snapSpring(this.gazeX, gz.x);
snapSpring(this.gazeY, gz.y);
this.eyeMorph.x = 1;
this.eyeMorph.v = 0;
this.eyeMorph.t = 1;
this._fromPolys = null;
this._paint(now);
return this.snapshot();
}
_applyPoseScale() {
@@ -303,7 +414,8 @@
this.svg.style.width = `${this.sizePx}px`;
this.svg.style.height = `${this.sizePx}px`;
}
if (Math.abs(sc - 1) > 0.001) {
const boxed = this.fitBox || (this.sizePx && this.sizePx < 120);
if (!boxed && Math.abs(sc - 1) > 0.001) {
this.svg.style.transform = `scale(${sc})`;
this.svg.style.transformOrigin = "50% 50%";
} else {
@@ -329,7 +441,7 @@
}
_build() {
const geo = g.GROK_GEO;
const geo = this.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");
@@ -400,7 +512,7 @@
}
_currentPolys(t) {
const eyes = g.GROK_GEO.eyes;
const eyes = this.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)];
@@ -412,7 +524,8 @@
}
_hop(now) {
if (this.hopAt < 0) this.hopAt = now;
if (this.reduceMotion || this.paused || this.hopAt >= 0) return;
this.hopAt = now;
}
_cycleShapeTrick() {
@@ -423,9 +536,11 @@
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 {
} else if (this.trickCycle === 2) {
if (!this.trick) this.trick = TR.startTrick("spinBounce", this.reduceMotion);
} else if (this.trickCycle === 3) {
if (!this.trick) this.trick = TR.startTrick("spinDizzy", this.reduceMotion);
} else {
this._pn(1);
this.particles.burst(16, 0.95, 0.3);
}
@@ -499,21 +614,17 @@
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) {
if (this.frozen) {
this._raf = 0;
return;
}
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) {
if (!this.paused && this.mode === "onboarding" && now - this.stateAt >= ONBOARDING_MS) {
this.moodN += 1;
this.setState(onboardMood(this.moodN));
}
@@ -524,7 +635,10 @@
eyeTo: this.eyeTo,
eyeMorphX: this.eyeMorph.x,
blinkX: this.blink.x,
playlist: this.playlists,
prev: this.poseOut,
});
this.poseOut = pose;
this.spin.t = pose.spin;
this.tx.t = pose.tx;
this.ty.t = pose.ty;
@@ -567,8 +681,8 @@
this._stepOverlay(now);
if (this.celebrateAt > 0 && now >= this.celebrateAt && !this.trick && !this.spinTurn) {
this.trick = TR.startTrick("spinWild", this.reduceMotion);
if (this.celebrateAt > 0 && now >= this.celebrateAt && !this.trick) {
this.trick = TR.startTrick("spinWild", this.reduceMotion || this.paused);
this.celebrateAt = now + 6200;
}
@@ -577,10 +691,10 @@
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 this.trick = TR.startTrick("spinBounce", this.reduceMotion || this.paused);
} else if (z < 0.34) this.trick = TR.startTrick("spinBounce", this.reduceMotion || this.paused);
else if (z < 0.62) this._hop(now);
else if (z < 0.86) this.trick = TR.startTrick("spinDizzy", this.reduceMotion);
else if (z < 0.86) this.trick = TR.startTrick("spinDizzy", this.reduceMotion || this.paused);
else this._pn(1);
}
this.trickAt = now + rand(9000, 18000);
@@ -594,17 +708,12 @@
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 };
this.extras = { ...tf, turn: 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];
const list = this.playlists[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);
@@ -649,7 +758,7 @@
}
if (this.reduceMotion) {
this._morphEyes(EYE_PLAYLIST[this.state][0]);
this._morphEyes(this.playlists[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;
}
@@ -679,6 +788,25 @@
this.overlayTurn.x = this.overlayTurn.t;
this.overlay.x = this.overlay.t;
}
if (this.spinTurn) {
if (TR.spinTurnSettled(this.spinTurn)) {
this.spinTurn = null;
this.wildWide = false;
} else {
this.extras = { ...this.extras, turn: (this.extras.turn ?? 0) + this.spinTurn.x };
}
}
if (this.gazeOrbit) {
const u = clamp((now - this.gazeOrbit.at) / this.gazeOrbit.dur, 0, 1);
const ease = u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;
this.gazeSpin = this.gazeOrbit.from * (1 - ease);
if (u >= 1) {
this.gazeOrbit = null;
this.gazeSpin = 0;
}
} else {
this.gazeSpin = 0;
}
this.notify.t = this.state === "notifying" ? 1 : 0;
this.humDots.t = this.state === "humming" ? 1 : 0;
@@ -703,11 +831,52 @@
this._updatePointer(now);
this._paint(now);
if (this.driven) {
this._raf = 0;
return;
}
if (
this.paused
&& !this.trick
&& !this.spinTurn
&& this.hopAt < 0
&& !this.particles.hasLife()
&& Math.abs(this.overlay.x - this.overlay.t) < 0.001
&& Math.abs(this.overlayTurn.t - this.overlayTurn.x) < 0.01
&& this.overlayMix.x > 0.996
&& Math.abs(this.shapeSpring.x - this.shapeSpring.t) < 0.001
&& Math.abs(this.eyeMorph.x - this.eyeMorph.t) < 0.001
) {
for (const s of [this.spin, this.tx, this.ty, this.gazeX, this.gazeY]) {
s.x = 0; s.v = 0; s.t = 0;
}
for (const s of [this.squash, this.blink, this.eyeScale]) {
s.x = 1; s.v = 0; s.t = 1;
}
this.shapeSpring.x = this.shapeSpring.t;
this.shapeSpring.v = 0;
this.blinkQueue = [];
this.winkAt = -1e9;
const home = this.playlists[this.state]?.[0] ?? 0;
this.eyeFrom = home;
this.eyeTo = home;
this._fromPolys = null;
this.eyeMorph.x = 1;
this.eyeMorph.v = 0;
this.eyeMorph.t = 1;
this.poseOut = { spin: 0, tx: 0, ty: 0, squash: 1, lid: 1, eyeBoost: 1 };
this._paint(0);
this._raf = 0;
return;
}
this._raf = requestAnimationFrame((t) => this._tick(t));
}
_paint(now) {
const geo = g.GROK_GEO;
const geo = this.geo;
const R = geo.Re;
const shape = geo.shapes[this.shapeName];
const morphK = K2(clamp(this.shapeSpring.x, 0, 1));
@@ -744,7 +913,7 @@
: FX.shapeRing(shape.path, R);
let liveRing = restRing;
let turned = false;
const turnAt = !morphing && spinning ? FX.turnAtOf(this.shapeName, shape.path, R) : null;
const turnAt = !morphing && spinning ? FX.turnAtOf(this.shapeName, shape.path, R, geo.solids) : null;
if (turnAt) {
liveRing = turnAt(spinAmt);
turned = true;
@@ -778,7 +947,7 @@
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;
const half = this.frameHalf / 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);
@@ -787,8 +956,9 @@
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;
if (this.gazeSpin) cyl = (cyl ?? 0) + this.gazeSpin;
const ringHint = morphing || turned ? liveRing : null;
const hasPtr = !!(this.gazeTarget || (this.followPointer && this.pointerRaw));
const tracking = !!(this.gazeTarget || (this.followPointer && this.pointerRaw));
EY.paintEyes({
now,
polys,
@@ -806,7 +976,8 @@
winkEye: this.winkEye,
turn: cyl,
cr,
pointer: hasPtr ? this.pointer : null,
pointer: this.pointer,
tracking,
notifyX: this.notify.x,
overlayX: this.overlay.x,
eyeEls: this.eyeEls,
+4 -1
View File
@@ -69,7 +69,7 @@
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 gazeW = opt.tracking ? 0.2 : 1;
const badgeRing = opt.badgeRing || ringHint;
const Yl = badgeRing
? badgeRing[Math.round(badgeRing.length * 7 / 8) % badgeRing.length]
@@ -138,6 +138,9 @@
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) {
const Zl = Rn(0.16);
pointer.x += (pointer.tx - pointer.x) * Zl;
pointer.y += (pointer.ty - pointer.y) * Zl;
Kj += pointer.x * (1 - 0.6 * Vn) + pullX;
Ko += pointer.y * (1 - 0.6 * Vn) + pullY;
} else {
+3 -2
View File
@@ -838,12 +838,13 @@
}
resetInk() { this.ink = []; }
resetRecv() { this.recvTick = -1; this.recvDir = -0.7; }
}
const turnAtCache = new Map();
function turnAtOf(name, path, R) {
function turnAtOf(name, path, R, solids) {
if (turnAtCache.has(name)) return turnAtCache.get(name);
const solid = global.GROK_GEO.solids?.[name];
const solid = (solids || global.GROK_GEO.solids)?.[name];
if (!solid || !global.GROK_MATH?.makeTurnAt) {
turnAtCache.set(name, null);
return null;
+4 -2
View File
@@ -5,9 +5,10 @@
function applyPose(state, mt, dtState, now, ctx, extra = {}) {
const pt = 0;
const prev = extra.prev || { spin: pt, tx: 0, ty: 0, squash: 1 };
let { spin, tx, ty, squash } = prev;
let lid = 1;
let eyeBoost = 1;
let spin = pt, tx = 0, ty = 0, squash = 1;
switch (state) {
case "sleeping": {
@@ -17,7 +18,8 @@
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)) {
const sleepEyes = extra.playlist?.sleeping || EYE_PLAYLIST.sleeping;
if (sleepEyes.includes(extra.eyeTo)) {
lid = extra.eyeMorphX > 0.85 ? 1 : 0.08;
} else if (dtState < 1.2) {
const dn = Math.min(1, dtState / 1);