142 lines
5.1 KiB
JavaScript
142 lines
5.1 KiB
JavaScript
/* 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),
|
|
setPose: (v) => bot.setPose(v),
|
|
setManualOffset: (v) => bot.setManualOffset(v),
|
|
setEyeTune: (v) => bot.setEyeTune(v),
|
|
setManualHold: (v) => bot.setManualHold(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.shadowRoot || this.attachShadow({ mode: "open" });
|
|
root.replaceChildren();
|
|
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);
|