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