Files
Edom 851f2b8c6c fix:修导出时钟与切页卡顿
导出与播放共用模拟时钟;换循环、重排播放头、<grok-bot> 重挂和构建产物对齐。
自定义/动画面板留下不重画,角色高度对齐,表情走弹簧。
2026-08-19 02:01:29 +08:00

415 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 {
--paper: #ece7dc;
--ink: #161513;
--mute: #6d675d;
--line: #d4cfc3;
--disk: #f3efe6;
--bg: #1a1916;
--panel: #22211d;
--accent: #e8d7a4;
}
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--paper); }
body {
font: 14px/1.5 "SF Pro Text", "PingFang SC", "Hiragino Sans GB", sans-serif;
letter-spacing: 0.01em;
}
header {
padding: 28px 32px 8px;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 16px;
}
header h1 {
margin: 0;
font-size: 15px;
font-weight: 560;
letter-spacing: 0.08em;
text-transform: uppercase;
}
header p { margin: 0; color: var(--mute); font-size: 12px; }
main {
display: grid;
grid-template-columns: minmax(280px, 420px) 1fr;
gap: 28px;
padding: 12px 32px 48px;
align-items: start;
}
.stage {
position: sticky;
top: 20px;
background: var(--panel);
border: 1px solid #2e2c27;
border-radius: 20px;
padding: 28px 24px 20px;
}
.disk {
width: min(100%, 280px);
aspect-ratio: 1;
margin: 0 auto;
border-radius: 50%;
background: var(--disk);
display: grid;
place-items: center;
box-shadow: inset 0 -18px 40px rgba(40, 30, 10, 0.08), 0 24px 60px rgba(0,0,0,.35);
}
.disk svg { width: 68%; height: 68%; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; }
.stage-disks { display: flex; align-items: flex-end; justify-content: center; gap: 28px; }
.disk-login {
width: 96px;
height: 96px;
border-radius: 50%;
background: var(--disk);
display: grid;
place-items: center;
box-shadow: inset 0 -8px 18px rgba(40, 30, 10, 0.08), 0 12px 28px rgba(0,0,0,.3);
}
.missing {
width: min(100%, 280px);
margin: 12px auto 0;
padding: 22px 18px;
border-radius: 16px;
background: #2a2823;
color: #d8d0c0;
font-size: 13px;
}
.missing strong { display: block; margin-bottom: 8px; color: var(--accent); font-weight: 560; }
.missing a { color: var(--accent); }
.disk-login svg { width: 64px; height: 64px; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; }
.disk-login span {
display: block;
margin-top: 8px;
font-size: 10px;
color: #8d877b;
text-align: center;
}
.readout, .hud {
margin-top: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
font-variant-numeric: tabular-nums;
color: #b7b1a4;
font-size: 12px;
}
.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;
background: #2a2823;
color: var(--paper);
border-radius: 999px;
padding: 6px 12px;
font: inherit;
cursor: pointer;
}
button[aria-pressed="true"] { background: var(--accent); color: #1a1916; border-color: var(--accent); }
button:hover { border-color: #6a6458; }
h2 {
margin: 0 0 10px;
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--mute);
font-weight: 560;
}
section + section { margin-top: 22px; }
.states { display: flex; flex-wrap: wrap; gap: 6px; }
.states button { font-size: 12px; padding: 5px 10px; }
.shapes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
gap: 8px;
}
.shape {
background: #2a2823;
border: 1px solid #3a372f;
border-radius: 10px;
padding: 8px 6px 6px;
cursor: pointer;
color: inherit;
text-align: center;
}
.shape[aria-pressed="true"] { border-color: var(--accent); background: #353228; }
.shape svg { width: 48px; height: 48px; display: block; margin: 0 auto 4px; --fg: #efe8d8; --bg: #2a2823; }
.shape span { display: block; font-size: 10px; color: #b7b1a4; }
.swatches { display: flex; flex-wrap: wrap; gap: 8px; }
.swatch {
width: 28px; height: 28px; border-radius: 50%;
border: 2px solid #111; cursor: pointer; padding: 0;
}
.swatch[aria-pressed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
.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; }
@media (max-width: 860px) {
main { grid-template-columns: 1fr; }
.stage { position: static; }
}
</style>
</head>
<body>
<header>
<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 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>
<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>
<button id="mode-hold" aria-pressed="false">锁定状态</button>
</div>
<div class="modes">
<button id="opt-follow" aria-pressed="true">跟随指针</button>
<button id="opt-emphasis" aria-pressed="false">emphasis</button>
<button id="act-spin">转一圈</button>
<button id="act-bounce">跳一下</button>
<button id="act-burst">粒子</button>
</div>
<div class="readout">
<div>状态 <b id="r-state">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>
<div id="states"></div>
</section>
<section>
<h2>身形 · Jo 几何生成器</h2>
<div class="shapes" id="shapes"></div>
</section>
<section>
<h2>墨色 · snt 调色板</h2>
<div class="swatches" id="swatches"></div>
</section>
<section>
<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>$_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>
</main>
<script src="geometry-data.js"></script>
<script src="src/math.js"></script>
<script src="src/tables.js"></script>
<script src="src/pose.js"></script>
<script src="src/tricks.js"></script>
<script src="src/fx.js"></script>
<script src="src/eyes.js"></script>
<script src="src/character.js"></script>
<script src="src/api.js"></script>
<script>
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) {
readout.state.textContent = s.state;
readout.eye.textContent = `${s.eyeFrom} → ${s.eyeTo}`;
readout.spin.textContent = `${s.spin.toFixed(2)}°`;
readout.squash.textContent = s.squash.toFixed(3);
if (readout.ov) readout.ov.textContent = s.overlay || "—";
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) => {
b.setAttribute("aria-pressed", b.dataset.state === name ? "true" : "false");
});
}
function syncModes(mode) {
document.getElementById("mode-onboard").setAttribute("aria-pressed", mode === "onboarding");
document.getElementById("mode-hold").setAttribute("aria-pressed", mode === "hold");
}
if (!window.GROK_GEO?.shapes || typeof window.GROK?.create !== "function") {
document.getElementById("missing-geo").hidden = false;
} else {
GROK_META.groups.forEach((group) => {
const h = document.createElement("h2");
h.textContent = group.label;
h.style.marginTop = "8px";
stateRoot.appendChild(h);
const row = document.createElement("div");
row.className = "states";
group.states.forEach((name) => {
const b = document.createElement("button");
b.textContent = name;
b.dataset.state = name;
b.addEventListener("click", () => {
bots.forEach((el) => {
el.setMode("hold");
el.setState(name, { resetEyes: false });
});
syncModes("hold");
});
row.appendChild(b);
});
stateRoot.appendChild(row);
});
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");
Object.entries(GROK_GEO.shapes).forEach(([id, shape]) => {
const b = document.createElement("button");
b.className = "shape";
b.dataset.shape = id;
b.innerHTML = `<svg viewBox="-15 -15 259 259"><path d="${shape.path}" style="fill:var(--fg)"></path></svg><span>${shape.label}</span>`;
b.addEventListener("click", () => {
bots.forEach((el) => el.setShape(id));
shapeRoot.querySelectorAll("button").forEach((el) => {
el.setAttribute("aria-pressed", el === b ? "true" : "false");
});
});
if (id === "blob") b.setAttribute("aria-pressed", "true");
shapeRoot.appendChild(b);
});
const swatchRoot = document.getElementById("swatches");
Object.entries(GROK_GEO.palette).forEach(([id, pal]) => {
const b = document.createElement("button");
b.className = "swatch";
b.title = id;
b.style.background = pal.light;
b.addEventListener("click", () => {
bots.forEach((el) => el.setColor(id, "light"));
swatchRoot.querySelectorAll("button").forEach((el) => {
el.setAttribute("aria-pressed", el === b ? "true" : "false");
});
});
if (id === "black") b.setAttribute("aria-pressed", "true");
swatchRoot.appendChild(b);
});
document.getElementById("mode-onboard").onclick = () => {
bots.forEach((el) => el.setMode("onboarding"));
syncModes("onboarding");
};
document.getElementById("mode-hold").onclick = () => {
bots.forEach((el) => el.setMode("hold"));
syncModes("hold");
};
document.getElementById("opt-follow").onclick = () => {
const on = document.getElementById("opt-follow").getAttribute("aria-pressed") !== "true";
document.getElementById("opt-follow").setAttribute("aria-pressed", on ? "true" : "false");
bots.forEach((el) => el.setFollowPointer(on));
};
document.getElementById("opt-emphasis").onclick = () => {
const on = document.getElementById("opt-emphasis").getAttribute("aria-pressed") !== "true";
document.getElementById("opt-emphasis").setAttribute("aria-pressed", on ? "true" : "false");
bots.forEach((el) => el.setEmphasis(on));
};
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>