lively-mascot 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * lively-mascot · core SDK (v0.2.0)
3
+ *
4
+ * Requires: src/core/emotions.js, src/core/rig.js (loaded before this script)
5
+ * Character files (src/characters/*.js) register themselves after this script.
6
+ *
7
+ * @license MIT
8
+ */
9
+ (function (root, factory) {
10
+ if (typeof module === "object" && typeof module.exports === "object") {
11
+ var emo = require("./core/emotions.js");
12
+ var api = factory(emo.groups, emo.emotions);
13
+ // Character modules are optional in the browser, where callers choose
14
+ // which scripts to include. The npm entry loads all bundled characters.
15
+ if (typeof globalThis !== "undefined") globalThis.LivelyMascot = api;
16
+ require("./core/rig.js");
17
+ require("./characters/sprout.js");
18
+ require("./characters/cat.js");
19
+ require("./characters/robot.js");
20
+ require("./characters/ghost.js");
21
+ require("./characters/jelly.js");
22
+ module.exports = api;
23
+ } else {
24
+ root.LivelyMascot = factory(
25
+ root.LivelyEmotionGroups || {},
26
+ root.LivelyEmotions || {}
27
+ );
28
+ }
29
+ })(typeof self !== "undefined" ? self : this, function (LivelyEmotionGroups, LivelyEmotions) {
30
+ "use strict";
31
+
32
+ // --- DOM Helpers ---
33
+ function el(tag, attrs, children) {
34
+ var node = document.createElement(tag);
35
+ if (attrs) for (var k in attrs) {
36
+ if (Object.prototype.hasOwnProperty.call(attrs, k)) {
37
+ var v = attrs[k];
38
+ if (v === undefined || v === null || v === "") continue;
39
+ if (k === "class") node.className = String(v);
40
+ else if (k === "text") node.textContent = String(v);
41
+ else node.setAttribute(k, String(v));
42
+ }
43
+ }
44
+ if (children) for (var i = 0; i < children.length; i++) node.appendChild(children[i]);
45
+ return node;
46
+ }
47
+
48
+ var SVG_NS = "http://www.w3.org/2000/svg";
49
+ function svg(tag, attrs, children) {
50
+ var node = document.createElementNS(SVG_NS, tag);
51
+ if (attrs) for (var k in attrs) {
52
+ if (Object.prototype.hasOwnProperty.call(attrs, k)) {
53
+ var v = attrs[k];
54
+ if (v === undefined || v === null || v === "") continue;
55
+ node.setAttribute(k, String(v));
56
+ }
57
+ }
58
+ if (children) for (var i = 0; i < children.length; i++) node.appendChild(children[i]);
59
+ return node;
60
+ }
61
+ function hEl(tag, attrs, children) {
62
+ var node = document.createElement(tag);
63
+ if (attrs) for (var k in attrs) {
64
+ if (Object.prototype.hasOwnProperty.call(attrs, k)) {
65
+ var v = attrs[k];
66
+ if (v === undefined || v === null || v === "") continue;
67
+ if (k === "class") node.className = String(v);
68
+ else if (k === "text") node.textContent = String(v);
69
+ else node.setAttribute(k, String(v));
70
+ }
71
+ }
72
+ if (children) for (var i = 0; i < children.length; i++) node.appendChild(children[i]);
73
+ return node;
74
+ }
75
+
76
+ /**
77
+ * Shared face builder. Every character reuses the same eye/pupil/mouth
78
+ * library so emotion CSS works uniformly across sprout, cat, robot, etc.
79
+ *
80
+ * @param {object} api the rig.api object (registers eyes / pupils / face)
81
+ * @returns {{ wrap: HTMLElement, face: SVGElement }} wrap = .lively-face-wrap
82
+ */
83
+ function buildFaceSvg(api) {
84
+ var clipId = "lively-eye-clip-" + Math.random().toString(36).slice(2, 9);
85
+
86
+ function buildEye(cx, cy) {
87
+ var wrapper = svg("g", { transform: "translate(" + cx + " " + cy + ")" });
88
+ var eye = svg("g", { class: "lively-face__eye" });
89
+ api.registerEye(eye);
90
+ eye.appendChild(svg("ellipse", { rx: 10, ry: 11.5 }));
91
+ var clip = svg("g", { "clip-path": "url(#" + clipId + ")" });
92
+ var pupil = svg("g");
93
+ api.registerPupil(pupil, { maxX: 8, maxY: 6 });
94
+ pupil.appendChild(svg("circle", { class: "lively-face__pupil", r: 6.2 }));
95
+ pupil.appendChild(svg("circle", { class: "lively-face__shine", cx: 2.2, cy: -2.4, r: 2.1 }));
96
+ clip.appendChild(pupil);
97
+ eye.appendChild(clip);
98
+ eye.appendChild(svg("path", { class: "lively-face__happy", d: "M-6.5 1.5 Q0 -6 6.5 1.5" }));
99
+ wrapper.appendChild(eye);
100
+ return wrapper;
101
+ }
102
+
103
+ var face = svg("g", { class: "lively-face" });
104
+ api.registerFace(face);
105
+
106
+ // Blush
107
+ face.appendChild(svg("ellipse", { class: "lively-face__blush", cx: 20, cy: 57, rx: 7, ry: 4 }));
108
+ face.appendChild(svg("ellipse", { class: "lively-face__blush", cx: 80, cy: 57, rx: 7, ry: 4 }));
109
+
110
+ // Eyes
111
+ face.appendChild(buildEye(34, 43));
112
+ face.appendChild(buildEye(66, 43));
113
+
114
+ // Default mouth
115
+ face.appendChild(svg("path", { class: "lively-face__mouth", d: "M42 63 Q50 70 58 63" }));
116
+ // Happy mouth
117
+ face.appendChild(svg("path", { class: "lively-face__happy-mouth", d: "M40 61 Q50 74 60 61 Q50 66 40 61 Z" }));
118
+ // Sad mouth
119
+ face.appendChild(svg("path", { class: "lively-face__sad-mouth", d: "M43 67 Q50 62 57 67" }));
120
+ // Open mouth
121
+ face.appendChild(svg("ellipse", { class: "lively-face__open-mouth", cx: 50, cy: 65, rx: 5, ry: 6 }));
122
+ // Flat mouth
123
+ face.appendChild(svg("line", { class: "lively-face__flat-mouth", x1: 43, y1: 65, x2: 57, y2: 65 }));
124
+ // Bored mouth: almost level, with a restrained lift at one corner.
125
+ face.appendChild(svg("path", { class: "lively-face__bored-mouth", d: "M42.5 65.2 C47.5 65.9 53 65.7 57.5 64.2" }));
126
+ // Sleep eyes
127
+ face.appendChild(svg("line", { class: "lively-face__sleep-eye lively-face__sleep-eye--l", x1: 27, y1: 43, x2: 41, y2: 43 }));
128
+ face.appendChild(svg("line", { class: "lively-face__sleep-eye lively-face__sleep-eye--r", x1: 59, y1: 43, x2: 73, y2: 43 }));
129
+ // Angry brows
130
+ face.appendChild(svg("line", { class: "lively-face__angry-brow lively-face__angry-brow--l", x1: 25, y1: 32, x2: 38, y2: 35 }));
131
+ face.appendChild(svg("line", { class: "lively-face__angry-brow lively-face__angry-brow--r", x1: 75, y1: 32, x2: 62, y2: 35 }));
132
+ // X eyes
133
+ face.appendChild(svg("g", { class: "lively-face__x-eye lively-face__x-eye--l", transform: "translate(34,43)" }, [
134
+ svg("line", { x1: -6, y1: -7, x2: 6, y2: 7 }),
135
+ svg("line", { x1: 6, y1: -7, x2: -6, y2: 7 })
136
+ ]));
137
+ face.appendChild(svg("g", { class: "lively-face__x-eye lively-face__x-eye--r", transform: "translate(66,43)" }, [
138
+ svg("line", { x1: -6, y1: -7, x2: 6, y2: 7 }),
139
+ svg("line", { x1: 6, y1: -7, x2: -6, y2: 7 })
140
+ ]));
141
+ // Heart eyes
142
+ face.appendChild(svg("path", { class: "lively-face__heart-eye lively-face__heart-eye--l", transform: "translate(34,42)", d: "M0 3 C0 0, -5 -4, -7 -1 C-9 2, 0 9, 0 9 C0 9, 9 2, 7 -1 C5 -4, 0 0, 0 3Z" }));
143
+ face.appendChild(svg("path", { class: "lively-face__heart-eye lively-face__heart-eye--r", transform: "translate(66,42)", d: "M0 3 C0 0, -5 -4, -7 -1 C-9 2, 0 9, 0 9 C0 9, 9 2, 7 -1 C5 -4, 0 0, 0 3Z" }));
144
+ // Cry eyes
145
+ face.appendChild(svg("path", { class: "lively-face__cry-eye lively-face__cry-eye--l", transform: "translate(34,43)", d: "M-6 2 Q0 -4 6 2" }));
146
+ face.appendChild(svg("path", { class: "lively-face__cry-eye lively-face__cry-eye--r", transform: "translate(66,43)", d: "M-6 2 Q0 -4 6 2" }));
147
+ // Tears
148
+ face.appendChild(svg("g", { class: "lively-face__tear lively-face__tear--l", transform: "translate(40,52)" }, [
149
+ svg("ellipse", { cx: 0, cy: 0, rx: 2.5, ry: 3.5, fill: "#6ec7ff", opacity: "0.8" }),
150
+ svg("ellipse", { cx: 0, cy: 8, rx: 1.8, ry: 2.5, fill: "#6ec7ff", opacity: "0.5" })
151
+ ]));
152
+ face.appendChild(svg("g", { class: "lively-face__tear lively-face__tear--r", transform: "translate(60,52)" }, [
153
+ svg("ellipse", { cx: 0, cy: 0, rx: 2.5, ry: 3.5, fill: "#6ec7ff", opacity: "0.8" }),
154
+ svg("ellipse", { cx: 0, cy: 8, rx: 1.8, ry: 2.5, fill: "#6ec7ff", opacity: "0.5" })
155
+ ]));
156
+ // Sweat drop
157
+ face.appendChild(svg("g", { class: "lively-face__sweat", transform: "translate(82,30)" }, [
158
+ svg("path", { d: "M0 0 C-3 5, -3 10, 0 12 C3 10, 3 5, 0 0Z", fill: "#6ec7ff", opacity: "0.7" })
159
+ ]));
160
+ // Star eyes
161
+ face.appendChild(svg("g", { class: "lively-face__star-eye lively-face__star-eye--l", transform: "translate(34,43)" }, [
162
+ svg("polygon", { points: "0,-8 2,-3 7,-3 3,1 5,7 0,3 -5,7 -3,1 -7,-3 -2,-3", fill: "#ffe066" })
163
+ ]));
164
+ face.appendChild(svg("g", { class: "lively-face__star-eye lively-face__star-eye--r", transform: "translate(66,43)" }, [
165
+ svg("polygon", { points: "0,-8 2,-3 7,-3 3,1 5,7 0,3 -5,7 -3,1 -7,-3 -2,-3", fill: "#ffe066" })
166
+ ]));
167
+ // Sparkles
168
+ face.appendChild(svg("g", { class: "lively-face__sparkle lively-face__sparkle--1", transform: "translate(15,20)" }, [
169
+ svg("line", { x1: 0, y1: -4, x2: 0, y2: 4, stroke: "#ffe066", "stroke-width": "1.5" }),
170
+ svg("line", { x1: -4, y1: 0, x2: 4, y2: 0, stroke: "#ffe066", "stroke-width": "1.5" })
171
+ ]));
172
+ face.appendChild(svg("g", { class: "lively-face__sparkle lively-face__sparkle--2", transform: "translate(85,18)" }, [
173
+ svg("line", { x1: 0, y1: -3, x2: 0, y2: 3, stroke: "#ffe066", "stroke-width": "1.2" }),
174
+ svg("line", { x1: -3, y1: 0, x2: 3, y2: 0, stroke: "#ffe066", "stroke-width": "1.2" })
175
+ ]));
176
+ // Waiting dots
177
+ face.appendChild(svg("g", { class: "lively-face__waiting-dots", transform: "translate(42,68)" }, [
178
+ svg("circle", { cx: 0, cy: 0, r: 2, fill: "#888" }),
179
+ svg("circle", { cx: 8, cy: 0, r: 2, fill: "#888" }),
180
+ svg("circle", { cx: 16, cy: 0, r: 2, fill: "#888" })
181
+ ]));
182
+
183
+ var faceSvg = svg("svg", { viewBox: "0 0 100 100", "aria-hidden": "true" });
184
+ var defs = svg("defs");
185
+ var clipPath = svg("clipPath", { id: clipId });
186
+ clipPath.appendChild(svg("ellipse", { rx: 10, ry: 11.5 }));
187
+ defs.appendChild(clipPath);
188
+ faceSvg.appendChild(defs);
189
+ faceSvg.appendChild(face);
190
+ var wrap = hEl("div", { class: "lively-face-wrap" });
191
+ wrap.appendChild(faceSvg);
192
+
193
+ return { wrap: wrap, face: face };
194
+ }
195
+
196
+ // --- Character Registry ---
197
+ // Characters register themselves via LivelyMascot.registerCharacter() from
198
+ // their own script files (e.g. src/characters/sprout.js, src/characters/cat.js).
199
+ var characters = {};
200
+
201
+ function registerCharacter(id, render, name, viewBox) {
202
+ characters[id] = { id: id, name: name || id, viewBox: viewBox || "0 0 100 100", render: render };
203
+ }
204
+ function getCharacter(type) { return characters[type] || characters.sprout; }
205
+
206
+ function normalizeViewMode(mode) {
207
+ return String(mode || "3d").toLowerCase() === "2d" ? "2d" : "3d";
208
+ }
209
+
210
+ function normalizeOutlineVisible(value) {
211
+ return value !== false && String(value).toLowerCase() !== "false";
212
+ }
213
+
214
+ // --- Core API ---
215
+ function createMascot(target, options) {
216
+ options = options || {};
217
+ var type = options.type || "sprout";
218
+ var character = getCharacter(type);
219
+ var viewMode = normalizeViewMode(options.viewMode || options.mode);
220
+ var outlineVisible = normalizeOutlineVisible(options.outlineVisible);
221
+ var root = el("div", {
222
+ class: "lively-mascot lively-mascot--" + character.id + " lively-mascot--" + viewMode + (!outlineVisible ? " lively-mascot--outline-hidden" : "") + (options.animated === false ? " lively-mascot--static" : ""),
223
+ style: "width:" + (options.size || 106) + "px;height:" + (options.size || 106) + "px",
224
+ "aria-hidden": "true",
225
+ });
226
+ var rigEl = el("div", { class: "lively-mascot__rig" });
227
+ // gaze wrapper holds all character parts so the whole face turns with the
228
+ // body posture (sway / lean) instead of staying dead-on.
229
+ var gazeEl = el("div", { class: "lively-mascot__gaze" });
230
+ rigEl.appendChild(gazeEl);
231
+ var theme = { body: options.color || "", outline: options.outline || "", accent: options.accent || "" };
232
+ function applyTheme() {
233
+ root.style.setProperty("--lively-body", theme.body || null);
234
+ root.style.setProperty("--lively-outline", theme.outline || null);
235
+ root.style.setProperty("--lively-accent", theme.accent || null);
236
+ }
237
+ applyTheme();
238
+ var rig = createRig(root, rigEl, {
239
+ followCursor: options.followCursor,
240
+ hopInterval: options.hopInterval,
241
+ animated: options.animated
242
+ }, { onClick: options.onClick });
243
+ character.render(rig.api, gazeEl);
244
+ rig.api.registerGazeWrap(gazeEl);
245
+ // Loading ring overlay — shared by ALL characters (shown via .is-emotion-28).
246
+ gazeEl.appendChild(el("div", { class: "lively__loading-overlay", "aria-hidden": "true" }));
247
+ root.appendChild(rigEl);
248
+ target.appendChild(root);
249
+
250
+ return {
251
+ el: root,
252
+ type: character.id,
253
+ get viewMode() { return viewMode; },
254
+ setViewMode: function (mode) {
255
+ viewMode = normalizeViewMode(mode);
256
+ root.classList.toggle("lively-mascot--2d", viewMode === "2d");
257
+ root.classList.toggle("lively-mascot--3d", viewMode === "3d");
258
+ return viewMode;
259
+ },
260
+ get outlineVisible() { return outlineVisible; },
261
+ setOutlineVisible: function (visible) {
262
+ outlineVisible = normalizeOutlineVisible(visible);
263
+ root.classList.toggle("lively-mascot--outline-hidden", !outlineVisible);
264
+ return outlineVisible;
265
+ },
266
+ setTheme: function (p) {
267
+ p = p || {};
268
+ if (p.body) theme.body = p.body;
269
+ if (p.outline) theme.outline = p.outline;
270
+ if (p.accent) theme.accent = p.accent;
271
+ applyTheme();
272
+ },
273
+ setEmotion: function (emotionId) {
274
+ root.className = root.className.replace(/is-emotion-\d+/g, "").trim();
275
+ root.classList.add("is-emotion-" + emotionId);
276
+ rig.api.setEmotionState(String(emotionId));
277
+ },
278
+ clearEmotion: function () {
279
+ root.className = root.className.replace(/is-emotion-\d+/g, "").trim();
280
+ root.classList.add("is-emotion-02");
281
+ rig.api.setEmotionState("02");
282
+ },
283
+ destroy: function () { rig.destroy(); root.remove(); }
284
+ };
285
+ }
286
+
287
+ function defineMascotElement(tag) {
288
+ tag = tag || "lively-mascot";
289
+ if (typeof customElements === "undefined" || customElements.get(tag)) return;
290
+ var _create = createMascot;
291
+ var LivelyMascotElement = (function () {
292
+ function LivelyMascotElement() {
293
+ var _newTarget = this.constructor;
294
+ var _this = Reflect.construct(HTMLElement, [], _newTarget);
295
+ return _this;
296
+ }
297
+ LivelyMascotElement.prototype = Object.create(HTMLElement.prototype, {
298
+ constructor: { value: LivelyMascotElement, writable: true, configurable: true }
299
+ });
300
+ LivelyMascotElement.observedAttributes = ["type", "color", "size", "view-mode", "mode", "show-outline"];
301
+ LivelyMascotElement.prototype.connectedCallback = function () { this._render(); };
302
+ LivelyMascotElement.prototype.disconnectedCallback = function () { if (this._inst) this._inst.destroy(); this._inst = null; this.textContent = ""; };
303
+ LivelyMascotElement.prototype.attributeChangedCallback = function () { if (this.isConnected) this._render(); };
304
+ LivelyMascotElement.prototype._render = function () {
305
+ if (this._inst) this._inst.destroy();
306
+ this.textContent = "";
307
+ this._inst = _create(this, {
308
+ type: this.getAttribute("type"),
309
+ color: this.getAttribute("color"),
310
+ size: this.getAttribute("size") ? Number(this.getAttribute("size")) : undefined,
311
+ viewMode: this.getAttribute("view-mode") || this.getAttribute("mode") || undefined,
312
+ outlineVisible: this.getAttribute("show-outline") !== "false"
313
+ });
314
+ };
315
+ return LivelyMascotElement;
316
+ })();
317
+ customElements.define(tag, LivelyMascotElement);
318
+ }
319
+
320
+ return {
321
+ createMascot: createMascot,
322
+ registerCharacter: registerCharacter,
323
+ defineMascotElement: defineMascotElement,
324
+ buildFaceSvg: buildFaceSvg,
325
+ characters: characters,
326
+ emotions: LivelyEmotions,
327
+ emotionGroups: LivelyEmotionGroups,
328
+ version: "0.2.0"
329
+ };
330
+ });
@@ -0,0 +1,66 @@
1
+ export type ViewMode = "2d" | "3d";
2
+
3
+ export interface MascotOptions {
4
+ type?: string;
5
+ color?: string;
6
+ outline?: string;
7
+ accent?: string;
8
+ size?: number;
9
+ followCursor?: boolean;
10
+ viewMode?: ViewMode;
11
+ mode?: ViewMode;
12
+ outlineVisible?: boolean;
13
+ animated?: boolean;
14
+ hopInterval?: [number, number] | null;
15
+ onClick?: () => void;
16
+ }
17
+
18
+ export interface MascotInstance {
19
+ readonly el: HTMLElement;
20
+ readonly type: string;
21
+ readonly viewMode: ViewMode;
22
+ readonly outlineVisible: boolean;
23
+ setViewMode(mode: ViewMode): ViewMode;
24
+ setOutlineVisible(visible: boolean): boolean;
25
+ setTheme(theme: { body?: string; outline?: string; accent?: string }): void;
26
+ setEmotion(id: string | number): void;
27
+ clearEmotion(): void;
28
+ destroy(): void;
29
+ }
30
+
31
+ export interface EmotionDefinition {
32
+ id: string;
33
+ name: string;
34
+ group: string;
35
+ desc: string;
36
+ bodyAnim?: string;
37
+ bodyFilter?: string;
38
+ leafAnim?: string;
39
+ footAnim?: string;
40
+ blink?: boolean | "fast";
41
+ gaze?: boolean;
42
+ hop?: boolean;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ export const createMascot: (target: Element, options?: MascotOptions) => MascotInstance;
47
+ export const registerCharacter: (id: string, render: Function, name?: string, viewBox?: string) => void;
48
+ export const defineMascotElement: (tag?: string) => void;
49
+ export const buildFaceSvg: (api: object) => { wrap: HTMLElement; face: SVGElement };
50
+ export const characters: Record<string, object>;
51
+ export const emotions: Record<string, EmotionDefinition>;
52
+ export const emotionGroups: Record<string, { name: string; order: number }>;
53
+ export const version: string;
54
+
55
+ declare const LivelyMascot: {
56
+ createMascot: typeof createMascot;
57
+ registerCharacter: typeof registerCharacter;
58
+ defineMascotElement: typeof defineMascotElement;
59
+ buildFaceSvg: typeof buildFaceSvg;
60
+ characters: typeof characters;
61
+ emotions: typeof emotions;
62
+ emotionGroups: typeof emotionGroups;
63
+ version: typeof version;
64
+ };
65
+
66
+ export default LivelyMascot;