feat:加上 Vue 学习站与可嵌入机芯
站点按 bloub 三栏做自定义、动画时间线和设置,支持序列导出。 形体只留常见几种并配中文名;动画页右栏拉满时间线旁空位,滚动条隐藏。 机芯补 GROK.create / <grok-bot>,以及暂停冻结。
This commit is contained in:
@@ -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>
|
||||
<!-- 任意页面 -->
|
||||
<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>
|
||||
|
||||
<grok-bot state="idle" shape="blob" color="black" size="64"></grok-bot>
|
||||
|
||||
<script>
|
||||
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] },
|
||||
});
|
||||
</script>
|
||||
</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
@@ -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
@@ -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));
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user