openmascot 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +75 -2
- package/dist/animator.d.ts +33 -0
- package/dist/animator.js +88 -0
- package/dist/element.d.ts +61 -0
- package/dist/element.js +175 -0
- package/dist/embed.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/player.d.ts +40 -0
- package/dist/player.js +127 -0
- package/dist/react.d.ts +36 -0
- package/dist/react.js +34 -0
- package/dist/renderer.d.ts +33 -0
- package/dist/renderer.js +114 -0
- package/dist/triggers.d.ts +33 -0
- package/dist/triggers.js +91 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +1 -0
- package/package.json +68 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dheeraj Mahra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# openmascot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pixel mascots for any website. Draw a character and its moves in the [OpenMascot](https://github.com/DheerajMahra/openmascot) studio, download its JSON, and put it on a page with a `<Mascot>` React component or one script tag. It reacts to page load, clicks, hovers, timers and your own code.
|
|
4
|
+
|
|
5
|
+
- No dependencies. The script tag is about 3 KB gzipped.
|
|
6
|
+
- The mascot JSON is treated as data: nothing in it can run code or inject markup.
|
|
7
|
+
- Respects `prefers-reduced-motion`.
|
|
8
|
+
|
|
9
|
+
## React
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install openmascot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Mascot, emit } from "openmascot/react";
|
|
17
|
+
import moss from "./moss.json";
|
|
18
|
+
|
|
19
|
+
export const App = () => (
|
|
20
|
+
<>
|
|
21
|
+
<Mascot doc={moss} position="bottom-right" />
|
|
22
|
+
<button onClick={() => emit("success")}>Save</button>
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Needs React 19. Works with server rendering (Next.js and others): the doc is passed through a ref, so it survives hydration.
|
|
28
|
+
|
|
29
|
+
## Script tag
|
|
30
|
+
|
|
31
|
+
```html
|
|
32
|
+
<script src="https://cdn.jsdelivr.net/npm/openmascot@0/dist/embed.js" defer></script>
|
|
33
|
+
<open-mascot src="/moss.json" position="bottom-right"></open-mascot>
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
Mascot.emit("success");
|
|
37
|
+
</script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Put `moss.json` next to your page. Tags added or removed later start and stop on their own.
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
| `<Mascot>` prop | `<open-mascot>` attribute | Meaning |
|
|
45
|
+
| ------------------ | ------------------------- | --------------------------------------------------------------------------------------- |
|
|
46
|
+
| `doc` | (property) | The mascot JSON as an object. |
|
|
47
|
+
| `src` | `src` | URL of the mascot JSON, when `doc` isn't given. |
|
|
48
|
+
| `position` | `position` | `"bottom-right"` or `"bottom-left"` pins it to a corner. Leave it out to render inline. |
|
|
49
|
+
| `scale` | `scale` | Screen pixels per mascot pixel. Default 4. |
|
|
50
|
+
| `name` | `name` | A handle for targeting one mascot. |
|
|
51
|
+
| `triggers={false}` | `triggers="off"` | Turns off load, click, hover and timer moves. `emit` and `play` still work. |
|
|
52
|
+
|
|
53
|
+
## Playing moves from your code
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
emit("success"); // fires the mascot's "success" event: the studio decides which move plays
|
|
57
|
+
play("wave"); // plays a move by name
|
|
58
|
+
emit("success", { to: "inbox" }); // only the mascot named "inbox"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Both return `true` if a mascot played something. In the script tag they are `Mascot.emit` and `Mascot.play`.
|
|
62
|
+
|
|
63
|
+
To drive one mascot directly, take a ref (React) or the element (HTML). It has `play()`, `emit()`, `pause()` and `resume()`.
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
const mascot = useRef<MascotElement>(null);
|
|
67
|
+
|
|
68
|
+
<Mascot ref={mascot} doc={moss} />;
|
|
69
|
+
mascot.current?.play("wave");
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`MascotElement` is exported as a type from `openmascot/react`.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MascotDoc, MascotFrame } from "./types.js";
|
|
2
|
+
export declare const REST_FRAME: MascotFrame;
|
|
3
|
+
/** `{ pose, hold: 3 }` becomes three identical frames so playback is one index per tick. */
|
|
4
|
+
export declare const expandSequence: (seq: MascotFrame[] | undefined) => MascotFrame[];
|
|
5
|
+
/**
|
|
6
|
+
* Playback state, and nothing else: which sequence is current and which frame is showing.
|
|
7
|
+
* No DOM and no timers, so the player drives it and tests can step it directly.
|
|
8
|
+
* `index === -1` means resting on the default pose.
|
|
9
|
+
*/
|
|
10
|
+
export declare class Animator {
|
|
11
|
+
current: string | null;
|
|
12
|
+
index: number;
|
|
13
|
+
private seqs;
|
|
14
|
+
private frames;
|
|
15
|
+
/**
|
|
16
|
+
* Swap the document. Keeps the current sequence when it still exists.
|
|
17
|
+
* Returns true when the playing sequence vanished, so the caller can stop its timer.
|
|
18
|
+
*/
|
|
19
|
+
load(doc: MascotDoc): boolean;
|
|
20
|
+
/** The frame to draw right now. */
|
|
21
|
+
get frame(): MascotFrame;
|
|
22
|
+
/** True while a sequence is playing or held on one of its frames. */
|
|
23
|
+
get busy(): boolean;
|
|
24
|
+
frameCount(name: string): number;
|
|
25
|
+
/** Switch to a sequence from its first frame. False when it is missing or empty. */
|
|
26
|
+
play(name: string): boolean;
|
|
27
|
+
/** Leave the current sequence and rest on the default pose. */
|
|
28
|
+
stop(): void;
|
|
29
|
+
/** Jump to one frame of a sequence. */
|
|
30
|
+
seek(name: string, index: number): boolean;
|
|
31
|
+
/** Move to the next frame. Every sequence plays once, then rests on the default pose. */
|
|
32
|
+
advance(): void;
|
|
33
|
+
}
|
package/dist/animator.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const REST_FRAME = { pose: "default", hold: 1 };
|
|
2
|
+
/** `{ pose, hold: 3 }` becomes three identical frames so playback is one index per tick. */
|
|
3
|
+
export const expandSequence = (seq) => {
|
|
4
|
+
const frames = [];
|
|
5
|
+
for (const f of seq ?? []) {
|
|
6
|
+
const n = Math.max(1, Math.floor(f.hold) || 1);
|
|
7
|
+
for (let i = 0; i < n; i++)
|
|
8
|
+
frames.push(f);
|
|
9
|
+
}
|
|
10
|
+
return frames;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Playback state, and nothing else: which sequence is current and which frame is showing.
|
|
14
|
+
* No DOM and no timers, so the player drives it and tests can step it directly.
|
|
15
|
+
* `index === -1` means resting on the default pose.
|
|
16
|
+
*/
|
|
17
|
+
export class Animator {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.current = null;
|
|
20
|
+
this.index = -1;
|
|
21
|
+
this.seqs = {};
|
|
22
|
+
this.frames = [];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Swap the document. Keeps the current sequence when it still exists.
|
|
26
|
+
* Returns true when the playing sequence vanished, so the caller can stop its timer.
|
|
27
|
+
*/
|
|
28
|
+
load(doc) {
|
|
29
|
+
this.seqs = {};
|
|
30
|
+
for (const name of Object.keys(doc.sequences)) {
|
|
31
|
+
this.seqs[name] = expandSequence(doc.sequences[name]);
|
|
32
|
+
}
|
|
33
|
+
const keep = this.current ? this.seqs[this.current] : undefined;
|
|
34
|
+
if (keep && keep.length) {
|
|
35
|
+
this.frames = keep;
|
|
36
|
+
this.index = Math.min(this.index, keep.length - 1);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (this.current !== null) {
|
|
40
|
+
this.stop();
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/** The frame to draw right now. */
|
|
46
|
+
get frame() {
|
|
47
|
+
return this.index >= 0 ? (this.frames[this.index] ?? REST_FRAME) : REST_FRAME;
|
|
48
|
+
}
|
|
49
|
+
/** True while a sequence is playing or held on one of its frames. */
|
|
50
|
+
get busy() {
|
|
51
|
+
return this.current !== null;
|
|
52
|
+
}
|
|
53
|
+
frameCount(name) {
|
|
54
|
+
return this.seqs[name]?.length ?? 0;
|
|
55
|
+
}
|
|
56
|
+
/** Switch to a sequence from its first frame. False when it is missing or empty. */
|
|
57
|
+
play(name) {
|
|
58
|
+
const frames = this.seqs[name];
|
|
59
|
+
if (!frames || frames.length === 0)
|
|
60
|
+
return false;
|
|
61
|
+
this.current = name;
|
|
62
|
+
this.frames = frames;
|
|
63
|
+
this.index = 0;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/** Leave the current sequence and rest on the default pose. */
|
|
67
|
+
stop() {
|
|
68
|
+
this.current = null;
|
|
69
|
+
this.frames = [];
|
|
70
|
+
this.index = -1;
|
|
71
|
+
}
|
|
72
|
+
/** Jump to one frame of a sequence. */
|
|
73
|
+
seek(name, index) {
|
|
74
|
+
const frames = this.seqs[name];
|
|
75
|
+
if (!frames || frames.length === 0)
|
|
76
|
+
return false;
|
|
77
|
+
this.current = name;
|
|
78
|
+
this.frames = frames;
|
|
79
|
+
this.index = Math.max(0, Math.min(frames.length - 1, index));
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** Move to the next frame. Every sequence plays once, then rests on the default pose. */
|
|
83
|
+
advance() {
|
|
84
|
+
this.index++;
|
|
85
|
+
if (this.index >= this.frames.length)
|
|
86
|
+
this.stop();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MascotDoc, MascotPosition } from "./types.js";
|
|
2
|
+
import { MascotPlayer } from "./player.js";
|
|
3
|
+
export declare const MASCOT_TAG = "open-mascot";
|
|
4
|
+
/** Which mascots a page-level call reaches. Without `to`, every mascot on the page. */
|
|
5
|
+
export interface MascotTarget {
|
|
6
|
+
/** Matches the element's `name` attribute. */
|
|
7
|
+
to?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const Base: typeof HTMLElement;
|
|
10
|
+
/**
|
|
11
|
+
* `<open-mascot>`: a mascot that mounts itself when it enters the page and cleans up when it
|
|
12
|
+
* leaves. Give it a `doc` property (the JSON as an object) or a `src` attribute (a URL to it).
|
|
13
|
+
*
|
|
14
|
+
* <open-mascot src="/moss.json" position="bottom-right" scale="5" name="guide"></open-mascot>
|
|
15
|
+
*/
|
|
16
|
+
export declare class MascotElement extends Base {
|
|
17
|
+
static observedAttributes: string[];
|
|
18
|
+
private mounted;
|
|
19
|
+
private docValue;
|
|
20
|
+
private fetched;
|
|
21
|
+
/** Bumped on every unmount so a fetch that lands late is dropped. */
|
|
22
|
+
private loads;
|
|
23
|
+
/** Between connect and disconnect. Attribute callbacks during an upgrade come before it. */
|
|
24
|
+
private live;
|
|
25
|
+
/** The live player, or null while loading or off the page. */
|
|
26
|
+
get player(): MascotPlayer | null;
|
|
27
|
+
/** The mascot JSON as an object. Takes precedence over `src`. */
|
|
28
|
+
get doc(): MascotDoc | null;
|
|
29
|
+
set doc(doc: MascotDoc | null);
|
|
30
|
+
get src(): string | null;
|
|
31
|
+
set src(src: string | null);
|
|
32
|
+
/** Screen pixels per mascot cell. Default 4. */
|
|
33
|
+
get scale(): number;
|
|
34
|
+
set scale(scale: number);
|
|
35
|
+
/** Fixed corner placement. Unset renders inline where the element sits. */
|
|
36
|
+
get position(): MascotPosition | null;
|
|
37
|
+
set position(position: MascotPosition | null);
|
|
38
|
+
/** Wire the doc's triggers (load, click, hover, intervals). `triggers="off"` turns them off. */
|
|
39
|
+
get triggers(): boolean;
|
|
40
|
+
set triggers(on: boolean);
|
|
41
|
+
connectedCallback(): void;
|
|
42
|
+
disconnectedCallback(): void;
|
|
43
|
+
attributeChangedCallback(name: string, old: string | null, value: string | null): void;
|
|
44
|
+
/** Play a move by name. False if it doesn't exist or the mascot hasn't loaded. */
|
|
45
|
+
play(name: string): boolean;
|
|
46
|
+
/** Fire the doc's `custom:<name>` trigger. */
|
|
47
|
+
emit(name: string): boolean;
|
|
48
|
+
pause(): void;
|
|
49
|
+
resume(): void;
|
|
50
|
+
private remount;
|
|
51
|
+
private start;
|
|
52
|
+
private unmount;
|
|
53
|
+
private reflect;
|
|
54
|
+
}
|
|
55
|
+
/** Register `<open-mascot>`. Safe to call more than once, and a no-op on the server. */
|
|
56
|
+
export declare const defineMascotElement: () => void;
|
|
57
|
+
/** Fire `custom:<name>` on every mascot on the page, or on the ones named `to`. */
|
|
58
|
+
export declare const emit: (name: string, target?: MascotTarget) => boolean;
|
|
59
|
+
/** Play a move on every mascot on the page, or on the ones named `to`. */
|
|
60
|
+
export declare const play: (name: string, target?: MascotTarget) => boolean;
|
|
61
|
+
export {};
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { MascotPlayer } from "./player.js";
|
|
2
|
+
export const MASCOT_TAG = "open-mascot";
|
|
3
|
+
/** The styles the player writes on its host for a corner mascot. */
|
|
4
|
+
const CORNER_STYLES = ["position", "zIndex", "bottom", "left", "right"];
|
|
5
|
+
// On the server there is no HTMLElement; the class still has to evaluate so imports don't throw.
|
|
6
|
+
const Base = (typeof HTMLElement === "undefined" ? Object : HTMLElement);
|
|
7
|
+
/**
|
|
8
|
+
* `<open-mascot>`: a mascot that mounts itself when it enters the page and cleans up when it
|
|
9
|
+
* leaves. Give it a `doc` property (the JSON as an object) or a `src` attribute (a URL to it).
|
|
10
|
+
*
|
|
11
|
+
* <open-mascot src="/moss.json" position="bottom-right" scale="5" name="guide"></open-mascot>
|
|
12
|
+
*/
|
|
13
|
+
export class MascotElement extends Base {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
this.mounted = null;
|
|
17
|
+
this.docValue = null;
|
|
18
|
+
this.fetched = null;
|
|
19
|
+
/** Bumped on every unmount so a fetch that lands late is dropped. */
|
|
20
|
+
this.loads = 0;
|
|
21
|
+
/** Between connect and disconnect. Attribute callbacks during an upgrade come before it. */
|
|
22
|
+
this.live = false;
|
|
23
|
+
}
|
|
24
|
+
/** The live player, or null while loading or off the page. */
|
|
25
|
+
get player() {
|
|
26
|
+
return this.mounted;
|
|
27
|
+
}
|
|
28
|
+
/** The mascot JSON as an object. Takes precedence over `src`. */
|
|
29
|
+
get doc() {
|
|
30
|
+
return this.docValue;
|
|
31
|
+
}
|
|
32
|
+
set doc(doc) {
|
|
33
|
+
if (doc === this.docValue)
|
|
34
|
+
return;
|
|
35
|
+
this.docValue = doc;
|
|
36
|
+
this.remount();
|
|
37
|
+
}
|
|
38
|
+
get src() {
|
|
39
|
+
return this.getAttribute("src");
|
|
40
|
+
}
|
|
41
|
+
set src(src) {
|
|
42
|
+
this.reflect("src", src);
|
|
43
|
+
}
|
|
44
|
+
/** Screen pixels per mascot cell. Default 4. */
|
|
45
|
+
get scale() {
|
|
46
|
+
const scale = parseInt(this.getAttribute("scale") ?? "", 10);
|
|
47
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 4;
|
|
48
|
+
}
|
|
49
|
+
set scale(scale) {
|
|
50
|
+
this.setAttribute("scale", String(scale));
|
|
51
|
+
}
|
|
52
|
+
/** Fixed corner placement. Unset renders inline where the element sits. */
|
|
53
|
+
get position() {
|
|
54
|
+
const position = this.getAttribute("position");
|
|
55
|
+
return position === "bottom-left" || position === "bottom-right" ? position : null;
|
|
56
|
+
}
|
|
57
|
+
set position(position) {
|
|
58
|
+
this.reflect("position", position);
|
|
59
|
+
}
|
|
60
|
+
/** Wire the doc's triggers (load, click, hover, intervals). `triggers="off"` turns them off. */
|
|
61
|
+
get triggers() {
|
|
62
|
+
return this.getAttribute("triggers") !== "off";
|
|
63
|
+
}
|
|
64
|
+
set triggers(on) {
|
|
65
|
+
this.reflect("triggers", on ? null : "off");
|
|
66
|
+
}
|
|
67
|
+
connectedCallback() {
|
|
68
|
+
// A doc assigned before the element was defined is an own property hiding the setter.
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(this, "doc")) {
|
|
70
|
+
const doc = this.doc;
|
|
71
|
+
delete this.doc;
|
|
72
|
+
this.docValue = doc;
|
|
73
|
+
}
|
|
74
|
+
this.live = true;
|
|
75
|
+
this.remount();
|
|
76
|
+
}
|
|
77
|
+
disconnectedCallback() {
|
|
78
|
+
this.live = false;
|
|
79
|
+
this.unmount();
|
|
80
|
+
}
|
|
81
|
+
attributeChangedCallback(name, old, value) {
|
|
82
|
+
if (old === value || !this.live)
|
|
83
|
+
return;
|
|
84
|
+
if (name === "scale" && this.mounted)
|
|
85
|
+
this.mounted.setScale(this.scale);
|
|
86
|
+
else if (name !== "src" || !this.docValue)
|
|
87
|
+
this.remount();
|
|
88
|
+
}
|
|
89
|
+
/** Play a move by name. False if it doesn't exist or the mascot hasn't loaded. */
|
|
90
|
+
play(name) {
|
|
91
|
+
return this.mounted?.play(name) ?? false;
|
|
92
|
+
}
|
|
93
|
+
/** Fire the doc's `custom:<name>` trigger. */
|
|
94
|
+
emit(name) {
|
|
95
|
+
return this.mounted?.emit(name) ?? false;
|
|
96
|
+
}
|
|
97
|
+
pause() {
|
|
98
|
+
this.mounted?.pause();
|
|
99
|
+
}
|
|
100
|
+
resume() {
|
|
101
|
+
this.mounted?.resume();
|
|
102
|
+
}
|
|
103
|
+
remount() {
|
|
104
|
+
this.unmount();
|
|
105
|
+
if (!this.isConnected)
|
|
106
|
+
return;
|
|
107
|
+
if (this.docValue)
|
|
108
|
+
return this.start(this.docValue);
|
|
109
|
+
const src = this.src;
|
|
110
|
+
if (!src)
|
|
111
|
+
return;
|
|
112
|
+
if (this.fetched?.src === src)
|
|
113
|
+
return this.start(this.fetched.doc);
|
|
114
|
+
const load = this.loads;
|
|
115
|
+
fetch(src)
|
|
116
|
+
.then(r => {
|
|
117
|
+
if (!r.ok)
|
|
118
|
+
throw new Error(`HTTP ${r.status}`);
|
|
119
|
+
return r.json();
|
|
120
|
+
})
|
|
121
|
+
.then(doc => {
|
|
122
|
+
this.fetched = { src, doc };
|
|
123
|
+
if (load === this.loads)
|
|
124
|
+
this.start(doc);
|
|
125
|
+
})
|
|
126
|
+
.catch((err) => console.warn("mascot: could not load", src, err));
|
|
127
|
+
}
|
|
128
|
+
start(doc) {
|
|
129
|
+
this.mounted = new MascotPlayer(this, doc, {
|
|
130
|
+
scale: this.scale,
|
|
131
|
+
position: this.position,
|
|
132
|
+
triggers: this.triggers
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
unmount() {
|
|
136
|
+
this.loads++;
|
|
137
|
+
if (!this.mounted)
|
|
138
|
+
return;
|
|
139
|
+
this.mounted.destroy();
|
|
140
|
+
this.mounted = null;
|
|
141
|
+
for (const prop of CORNER_STYLES)
|
|
142
|
+
this.style[prop] = "";
|
|
143
|
+
}
|
|
144
|
+
reflect(name, value) {
|
|
145
|
+
if (value === null)
|
|
146
|
+
this.removeAttribute(name);
|
|
147
|
+
else
|
|
148
|
+
this.setAttribute(name, value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
MascotElement.observedAttributes = ["src", "scale", "position", "triggers"];
|
|
152
|
+
/** Register `<open-mascot>`. Safe to call more than once, and a no-op on the server. */
|
|
153
|
+
export const defineMascotElement = () => {
|
|
154
|
+
if (typeof customElements === "undefined" || customElements.get(MASCOT_TAG))
|
|
155
|
+
return;
|
|
156
|
+
customElements.define(MASCOT_TAG, MascotElement);
|
|
157
|
+
// :where() keeps this at zero specificity, so any page style wins.
|
|
158
|
+
const style = document.createElement("style");
|
|
159
|
+
style.textContent = `:where(${MASCOT_TAG}){display:inline-block;line-height:0}`;
|
|
160
|
+
document.head.append(style);
|
|
161
|
+
};
|
|
162
|
+
const mascots = (to) => {
|
|
163
|
+
if (typeof document === "undefined")
|
|
164
|
+
return [];
|
|
165
|
+
const all = Array.from(document.querySelectorAll(MASCOT_TAG));
|
|
166
|
+
return to === undefined ? all : all.filter(el => el.getAttribute("name") === to);
|
|
167
|
+
};
|
|
168
|
+
/** Fire `custom:<name>` on every mascot on the page, or on the ones named `to`. */
|
|
169
|
+
export const emit = (name, target = {}) => mascots(target.to)
|
|
170
|
+
.map(el => el.emit?.(name) ?? false)
|
|
171
|
+
.some(Boolean);
|
|
172
|
+
/** Play a move on every mascot on the page, or on the ones named `to`. */
|
|
173
|
+
export const play = (name, target = {}) => mascots(target.to)
|
|
174
|
+
.map(el => el.play?.(name) ?? false)
|
|
175
|
+
.some(Boolean);
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(()=>{var w={pose:"default",hold:1},H=i=>{let e=[];for(let t of i!=null?i:[]){let s=Math.max(1,Math.floor(t.hold)||1);for(let r=0;r<s;r++)e.push(t)}return e},d=class{constructor(){this.current=null;this.index=-1;this.seqs={};this.frames=[]}load(e){this.seqs={};for(let s of Object.keys(e.sequences))this.seqs[s]=H(e.sequences[s]);let t=this.current?this.seqs[this.current]:void 0;return t&&t.length?(this.frames=t,this.index=Math.min(this.index,t.length-1),!1):this.current!==null?(this.stop(),!0):!1}get frame(){var e;return this.index>=0&&(e=this.frames[this.index])!=null?e:w}get busy(){return this.current!==null}frameCount(e){var t,s;return(s=(t=this.seqs[e])==null?void 0:t.length)!=null?s:0}play(e){let t=this.seqs[e];return!t||t.length===0?!1:(this.current=e,this.frames=t,this.index=0,!0)}stop(){this.current=null,this.frames=[],this.index=-1}seek(e,t){let s=this.seqs[e];return!s||s.length===0?!1:(this.current=e,this.frames=s,this.index=Math.max(0,Math.min(s.length-1,t)),!0)}advance(){this.index++,this.index>=this.frames.length&&this.stop()}};var R=i=>[parseInt(i.slice(1,3),16),parseInt(i.slice(3,5),16),parseInt(i.slice(5,7),16)],k=(i,e)=>{var c,h;let{w:t,h:s}=i.grid,r=(c=i.poses[e])!=null?c:"",o=document.createElement("canvas");o.width=t,o.height=s;let a=o.getContext("2d");if(!a)return o;let n=a.createImageData(t,s);for(let l=0;l<t*s;l++){let y=r[l];if(!y||y===".")continue;let L=(h=i.palette[parseInt(y,36)])!=null?h:"#000000",[P,q,A]=R(L);n.data[l*4]=P,n.data[l*4+1]=q,n.data[l*4+2]=A,n.data[l*4+3]=255}return a.putImageData(n,0,0),o};var F=i=>{var t,s,r;let e={left:1,right:1,up:0,down:1};for(let o of Object.values((t=i.sequences)!=null?t:{}))for(let a of o){let n=Math.round((s=a.x)!=null?s:0),c=Math.round((r=a.offset)!=null?r:0);e.left=Math.max(e.left,-n),e.right=Math.max(e.right,n),e.up=Math.max(e.up,-c),e.down=Math.max(e.down,c)}return e},m=class{constructor(e){this.scale=e;this.poses=new Map;this.pad={left:1,right:1,up:0,down:1};this.canvas=document.createElement("canvas"),this.canvas.style.imageRendering="pixelated",this.canvas.style.display="block",this.canvas.setAttribute("role","img");let t=this.canvas.getContext("2d");if(!t)throw new Error("mascot: canvas 2d context unavailable");this.ctx=t}setDoc(e){this.doc=e,this.poses.clear();for(let t of Object.keys(e.poses))this.poses.set(t,k(e,t));this.pad=F(e),this.canvas.setAttribute("aria-label",`${e.name} mascot`),this.resize()}setScale(e){this.scale=e,this.resize()}draw(e){var c,h,l;let{w:t,h:s}=this.doc.grid,r=this.scale,o=(c=this.poses.get(e.pose))!=null?c:this.poses.get("default"),a=(((h=e.offset)!=null?h:0)+this.pad.up)*r,n=(((l=e.x)!=null?l:0)+this.pad.left)*r;this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),o&&this.ctx.drawImage(o,n,a,t*r,s*r)}destroy(){this.canvas.remove()}resize(){let{w:e,h:t}=this.doc.grid,s=this.scale,{left:r,right:o,up:a,down:n}=this.pad;this.canvas.width=(e+r+o)*s,this.canvas.height=(t+a+n)*s,this.canvas.style.width=`${this.canvas.width}px`,this.canvas.style.height=`${this.canvas.height}px`,this.ctx.imageSmoothingEnabled=!1}};var I=i=>i==="load"||i==="click"||i==="hover"?{type:i}:i.startsWith("interval:")?{type:"interval",seconds:parseFloat(i.slice(9))||5}:i.startsWith("custom:")?{type:"custom",name:i.slice(7)}:null,u=class{constructor(e,t){this.host=e;this.sequence=t}},M=class extends u{constructor(){super(...arguments);this.onClick=()=>void this.host.play(this.sequence)}attach(){this.host.el.addEventListener("click",this.onClick),this.host.el.style.cursor="pointer"}detach(){this.host.el.removeEventListener("click",this.onClick),this.host.el.style.cursor=""}},b=class extends u{constructor(){super(...arguments);this.onEnter=()=>{this.host.isBusy()||this.host.play(this.sequence)}}attach(){this.host.el.addEventListener("mouseenter",this.onEnter)}detach(){this.host.el.removeEventListener("mouseenter",this.onEnter)}},x=class extends u{constructor(t,s,r){super(t,s);this.seconds=r;this.id=null}attach(){this.id=setInterval(()=>{this.host.isBusy()||this.host.play(this.sequence)},this.seconds*1e3)}detach(){this.id!==null&&clearInterval(this.id),this.id=null}},p=class{constructor(e,t){this.host=e;this.triggers=[];for(let[s,r]of Object.entries(t)){let o=I(s);(o==null?void 0:o.type)==="click"?this.triggers.push(new M(e,r)):(o==null?void 0:o.type)==="hover"?this.triggers.push(new b(e,r)):(o==null?void 0:o.type)==="interval"&&this.triggers.push(new x(e,r,o.seconds))}}attach(){this.host.el.style.cursor="";for(let e of this.triggers)e.attach()}detach(){for(let e of this.triggers)e.detach()}};var S=()=>typeof window!="undefined"&&typeof window.matchMedia=="function"&&window.matchMedia("(prefers-reduced-motion: reduce)").matches,g=class{constructor(e,t,s={}){this.animator=new d;this.triggers=null;this.timer=null;var r,o,a,n;if(this.el=e,this.reduced=(r=s.reducedMotion)!=null?r:S(),this.renderer=new m((o=s.scale)!=null?o:4),s.position&&(e.style.position="fixed",e.style.zIndex="9999",e.style.bottom="16px",e.style[s.position==="bottom-left"?"left":"right"]="16px"),e.style.lineHeight="0",e.appendChild(this.renderer.canvas),this.setDoc(t),s.triggers!==!1){this.triggers=new p(this,(a=t.triggers)!=null?a:{}),this.triggers.attach();let c=(n=t.triggers)==null?void 0:n.load;c&&this.play(c)}}get canvas(){return this.renderer.canvas}get current(){return this.animator.current}setDoc(e){this.doc=e;let t=this.animator.load(e);this.renderer.setDoc(e),t&&this.stopTimer(),this.draw()}setScale(e){this.renderer.setScale(e),this.draw()}play(e){return this.animator.play(e)?(this.stopTimer(),this.reduced?(this.animator.stop(),this.draw()):this.step(),!0):!1}showFrame(e,t){this.animator.seek(e,t)&&(this.stopTimer(),this.draw())}frameCount(e){return this.animator.frameCount(e)}currentIndex(){return this.animator.index}isPlaying(){return this.timer!==null}pause(){this.stopTimer()}resume(){this.timer===null&&this.animator.current&&this.step()}emit(e){var s;let t=(s=this.doc.triggers)==null?void 0:s[`custom:${e}`];return t?this.play(t):!1}isBusy(){return this.animator.busy}destroy(){var e;this.stopTimer(),(e=this.triggers)==null||e.detach(),this.triggers=null,this.renderer.destroy()}stopTimer(){this.timer!==null&&clearTimeout(this.timer),this.timer=null}step(){this.draw(),this.animator.current!==null&&(this.timer=setTimeout(()=>{this.timer=null,this.animator.advance(),this.step()},this.doc.tick||60))}draw(){this.renderer.draw(this.animator.frame)}};var f="open-mascot",O=["position","zIndex","bottom","left","right"],V=typeof HTMLElement=="undefined"?Object:HTMLElement,v=class extends V{constructor(){super(...arguments);this.mounted=null;this.docValue=null;this.fetched=null;this.loads=0;this.live=!1}get player(){return this.mounted}get doc(){return this.docValue}set doc(t){t!==this.docValue&&(this.docValue=t,this.remount())}get src(){return this.getAttribute("src")}set src(t){this.reflect("src",t)}get scale(){var s;let t=parseInt((s=this.getAttribute("scale"))!=null?s:"",10);return Number.isFinite(t)&&t>0?t:4}set scale(t){this.setAttribute("scale",String(t))}get position(){let t=this.getAttribute("position");return t==="bottom-left"||t==="bottom-right"?t:null}set position(t){this.reflect("position",t)}get triggers(){return this.getAttribute("triggers")!=="off"}set triggers(t){this.reflect("triggers",t?null:"off")}connectedCallback(){if(Object.prototype.hasOwnProperty.call(this,"doc")){let t=this.doc;delete this.doc,this.docValue=t}this.live=!0,this.remount()}disconnectedCallback(){this.live=!1,this.unmount()}attributeChangedCallback(t,s,r){s===r||!this.live||(t==="scale"&&this.mounted?this.mounted.setScale(this.scale):(t!=="src"||!this.docValue)&&this.remount())}play(t){var s,r;return(r=(s=this.mounted)==null?void 0:s.play(t))!=null?r:!1}emit(t){var s,r;return(r=(s=this.mounted)==null?void 0:s.emit(t))!=null?r:!1}pause(){var t;(t=this.mounted)==null||t.pause()}resume(){var t;(t=this.mounted)==null||t.resume()}remount(){var r;if(this.unmount(),!this.isConnected)return;if(this.docValue)return this.start(this.docValue);let t=this.src;if(!t)return;if(((r=this.fetched)==null?void 0:r.src)===t)return this.start(this.fetched.doc);let s=this.loads;fetch(t).then(o=>{if(!o.ok)throw new Error(`HTTP ${o.status}`);return o.json()}).then(o=>{this.fetched={src:t,doc:o},s===this.loads&&this.start(o)}).catch(o=>console.warn("mascot: could not load",t,o))}start(t){this.mounted=new g(this,t,{scale:this.scale,position:this.position,triggers:this.triggers})}unmount(){if(this.loads++,!!this.mounted){this.mounted.destroy(),this.mounted=null;for(let t of O)this.style[t]=""}}reflect(t,s){s===null?this.removeAttribute(t):this.setAttribute(t,s)}};v.observedAttributes=["src","scale","position","triggers"];var T=()=>{if(typeof customElements=="undefined"||customElements.get(f))return;customElements.define(f,v);let i=document.createElement("style");i.textContent=`:where(${f}){display:inline-block;line-height:0}`,document.head.append(i)},E=i=>{if(typeof document=="undefined")return[];let e=Array.from(document.querySelectorAll(f));return i===void 0?e:e.filter(t=>t.getAttribute("name")===i)},D=(i,e={})=>E(e.to).map(t=>{var s,r;return(r=(s=t.emit)==null?void 0:s.call(t,i))!=null?r:!1}).some(Boolean),C=(i,e={})=>E(e.to).map(t=>{var s,r;return(r=(s=t.play)==null?void 0:s.call(t,i))!=null?r:!1}).some(Boolean);window.Mascot={emit:D,play:C};T();})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Animator, expandSequence } from "./animator.js";
|
|
2
|
+
export { MASCOT_TAG, MascotElement, defineMascotElement, emit, play } from "./element.js";
|
|
3
|
+
export type { MascotTarget } from "./element.js";
|
|
4
|
+
export { MascotPlayer, prefersReducedMotion } from "./player.js";
|
|
5
|
+
export { EMPTY_CELL, poseToDataUrl, rasterizePose, sequencePadding } from "./renderer.js";
|
|
6
|
+
export type { MascotPadding } from "./renderer.js";
|
|
7
|
+
export { parseTrigger } from "./triggers.js";
|
|
8
|
+
export type { ParsedTrigger } from "./triggers.js";
|
|
9
|
+
export type { MascotDoc, MascotFrame, MascotOptions, MascotPosition, MascotSequence } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Animator, expandSequence } from "./animator.js";
|
|
2
|
+
export { MASCOT_TAG, MascotElement, defineMascotElement, emit, play } from "./element.js";
|
|
3
|
+
export { MascotPlayer, prefersReducedMotion } from "./player.js";
|
|
4
|
+
export { EMPTY_CELL, poseToDataUrl, rasterizePose, sequencePadding } from "./renderer.js";
|
|
5
|
+
export { parseTrigger } from "./triggers.js";
|
package/dist/player.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MascotDoc, MascotOptions } from "./types.js";
|
|
2
|
+
export declare const prefersReducedMotion: () => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Plays a mascot inside an element. A thin facade: the Animator holds playback state,
|
|
5
|
+
* the Renderer draws it, the TriggerManager reacts to events. This class owns the timer
|
|
6
|
+
* and the DOM mount.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MascotPlayer {
|
|
9
|
+
readonly el: HTMLElement;
|
|
10
|
+
private doc;
|
|
11
|
+
private readonly animator;
|
|
12
|
+
private readonly renderer;
|
|
13
|
+
private triggers;
|
|
14
|
+
private timer;
|
|
15
|
+
private reduced;
|
|
16
|
+
constructor(el: HTMLElement, doc: MascotDoc, opts?: MascotOptions);
|
|
17
|
+
get canvas(): HTMLCanvasElement;
|
|
18
|
+
/** Name of the sequence that is playing, or null at rest. */
|
|
19
|
+
get current(): string | null;
|
|
20
|
+
/** Swap the document in place. Keeps the current sequence playing when it still exists. */
|
|
21
|
+
setDoc(doc: MascotDoc): void;
|
|
22
|
+
setScale(scale: number): void;
|
|
23
|
+
/** Start a sequence. It plays once, then the mascot rests on its default pose. */
|
|
24
|
+
play(name: string): boolean;
|
|
25
|
+
/** Jump to one frame of a sequence and hold it. For scrubbing in an editor. */
|
|
26
|
+
showFrame(name: string, index: number): void;
|
|
27
|
+
frameCount(name: string): number;
|
|
28
|
+
/** Index into the expanded frames of the current sequence, or -1 at rest. */
|
|
29
|
+
currentIndex(): number;
|
|
30
|
+
isPlaying(): boolean;
|
|
31
|
+
pause(): void;
|
|
32
|
+
resume(): void;
|
|
33
|
+
/** Fire a `custom:<name>` trigger. */
|
|
34
|
+
emit(name: string): boolean;
|
|
35
|
+
isBusy(): boolean;
|
|
36
|
+
destroy(): void;
|
|
37
|
+
private stopTimer;
|
|
38
|
+
private step;
|
|
39
|
+
private draw;
|
|
40
|
+
}
|
package/dist/player.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Animator } from "./animator.js";
|
|
2
|
+
import { Renderer } from "./renderer.js";
|
|
3
|
+
import { TriggerManager } from "./triggers.js";
|
|
4
|
+
export const prefersReducedMotion = () => typeof window !== "undefined" &&
|
|
5
|
+
typeof window.matchMedia === "function" &&
|
|
6
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
7
|
+
/**
|
|
8
|
+
* Plays a mascot inside an element. A thin facade: the Animator holds playback state,
|
|
9
|
+
* the Renderer draws it, the TriggerManager reacts to events. This class owns the timer
|
|
10
|
+
* and the DOM mount.
|
|
11
|
+
*/
|
|
12
|
+
export class MascotPlayer {
|
|
13
|
+
constructor(el, doc, opts = {}) {
|
|
14
|
+
this.animator = new Animator();
|
|
15
|
+
this.triggers = null;
|
|
16
|
+
this.timer = null;
|
|
17
|
+
this.el = el;
|
|
18
|
+
this.reduced = opts.reducedMotion ?? prefersReducedMotion();
|
|
19
|
+
this.renderer = new Renderer(opts.scale ?? 4);
|
|
20
|
+
if (opts.position) {
|
|
21
|
+
el.style.position = "fixed";
|
|
22
|
+
el.style.zIndex = "9999";
|
|
23
|
+
el.style.bottom = "16px";
|
|
24
|
+
el.style[opts.position === "bottom-left" ? "left" : "right"] = "16px";
|
|
25
|
+
}
|
|
26
|
+
el.style.lineHeight = "0";
|
|
27
|
+
el.appendChild(this.renderer.canvas);
|
|
28
|
+
this.setDoc(doc);
|
|
29
|
+
if (opts.triggers !== false) {
|
|
30
|
+
this.triggers = new TriggerManager(this, doc.triggers ?? {});
|
|
31
|
+
this.triggers.attach();
|
|
32
|
+
const load = doc.triggers?.load;
|
|
33
|
+
if (load)
|
|
34
|
+
this.play(load);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
get canvas() {
|
|
38
|
+
return this.renderer.canvas;
|
|
39
|
+
}
|
|
40
|
+
/** Name of the sequence that is playing, or null at rest. */
|
|
41
|
+
get current() {
|
|
42
|
+
return this.animator.current;
|
|
43
|
+
}
|
|
44
|
+
/** Swap the document in place. Keeps the current sequence playing when it still exists. */
|
|
45
|
+
setDoc(doc) {
|
|
46
|
+
this.doc = doc;
|
|
47
|
+
const lost = this.animator.load(doc);
|
|
48
|
+
this.renderer.setDoc(doc);
|
|
49
|
+
if (lost)
|
|
50
|
+
this.stopTimer();
|
|
51
|
+
this.draw();
|
|
52
|
+
}
|
|
53
|
+
setScale(scale) {
|
|
54
|
+
this.renderer.setScale(scale);
|
|
55
|
+
this.draw();
|
|
56
|
+
}
|
|
57
|
+
/** Start a sequence. It plays once, then the mascot rests on its default pose. */
|
|
58
|
+
play(name) {
|
|
59
|
+
if (!this.animator.play(name))
|
|
60
|
+
return false;
|
|
61
|
+
this.stopTimer();
|
|
62
|
+
if (this.reduced) {
|
|
63
|
+
this.animator.stop();
|
|
64
|
+
this.draw();
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.step();
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/** Jump to one frame of a sequence and hold it. For scrubbing in an editor. */
|
|
72
|
+
showFrame(name, index) {
|
|
73
|
+
if (!this.animator.seek(name, index))
|
|
74
|
+
return;
|
|
75
|
+
this.stopTimer();
|
|
76
|
+
this.draw();
|
|
77
|
+
}
|
|
78
|
+
frameCount(name) {
|
|
79
|
+
return this.animator.frameCount(name);
|
|
80
|
+
}
|
|
81
|
+
/** Index into the expanded frames of the current sequence, or -1 at rest. */
|
|
82
|
+
currentIndex() {
|
|
83
|
+
return this.animator.index;
|
|
84
|
+
}
|
|
85
|
+
isPlaying() {
|
|
86
|
+
return this.timer !== null;
|
|
87
|
+
}
|
|
88
|
+
pause() {
|
|
89
|
+
this.stopTimer();
|
|
90
|
+
}
|
|
91
|
+
resume() {
|
|
92
|
+
if (this.timer === null && this.animator.current)
|
|
93
|
+
this.step();
|
|
94
|
+
}
|
|
95
|
+
/** Fire a `custom:<name>` trigger. */
|
|
96
|
+
emit(name) {
|
|
97
|
+
const seq = this.doc.triggers?.[`custom:${name}`];
|
|
98
|
+
return seq ? this.play(seq) : false;
|
|
99
|
+
}
|
|
100
|
+
isBusy() {
|
|
101
|
+
return this.animator.busy;
|
|
102
|
+
}
|
|
103
|
+
destroy() {
|
|
104
|
+
this.stopTimer();
|
|
105
|
+
this.triggers?.detach();
|
|
106
|
+
this.triggers = null;
|
|
107
|
+
this.renderer.destroy();
|
|
108
|
+
}
|
|
109
|
+
stopTimer() {
|
|
110
|
+
if (this.timer !== null)
|
|
111
|
+
clearTimeout(this.timer);
|
|
112
|
+
this.timer = null;
|
|
113
|
+
}
|
|
114
|
+
step() {
|
|
115
|
+
this.draw();
|
|
116
|
+
if (this.animator.current === null)
|
|
117
|
+
return;
|
|
118
|
+
this.timer = setTimeout(() => {
|
|
119
|
+
this.timer = null;
|
|
120
|
+
this.animator.advance();
|
|
121
|
+
this.step();
|
|
122
|
+
}, this.doc.tick || 60);
|
|
123
|
+
}
|
|
124
|
+
draw() {
|
|
125
|
+
this.renderer.draw(this.animator.frame);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MascotElement } from "./element.js";
|
|
2
|
+
import type { MascotDoc, MascotPosition } from "./types.js";
|
|
3
|
+
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
4
|
+
export { emit, play } from "./element.js";
|
|
5
|
+
export type { MascotElement, MascotTarget } from "./element.js";
|
|
6
|
+
export type { MascotDoc, MascotPosition } from "./types.js";
|
|
7
|
+
/** What <Mascot> takes. Each prop matches the <open-mascot> property of the same name. */
|
|
8
|
+
export interface MascotProps extends DetailedHTMLProps<HTMLAttributes<MascotElement>, MascotElement> {
|
|
9
|
+
doc?: MascotDoc;
|
|
10
|
+
src?: string;
|
|
11
|
+
scale?: number;
|
|
12
|
+
position?: MascotPosition;
|
|
13
|
+
/** A handle for `emit(name, { to })` and `play(name, { to })`. */
|
|
14
|
+
name?: string;
|
|
15
|
+
triggers?: boolean;
|
|
16
|
+
}
|
|
17
|
+
declare global {
|
|
18
|
+
interface HTMLElementTagNameMap {
|
|
19
|
+
"open-mascot": MascotElement;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
declare module "react" {
|
|
23
|
+
namespace JSX {
|
|
24
|
+
interface IntrinsicElements {
|
|
25
|
+
"open-mascot": MascotProps;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `<open-mascot>` for React. React 19 skips non-string props on custom elements when it hydrates
|
|
31
|
+
* server-rendered HTML, so `doc` and `triggers` go in through a ref callback, which runs on
|
|
32
|
+
* hydration too. The rest render as attributes.
|
|
33
|
+
*
|
|
34
|
+
* <Mascot doc={moss} name="inbox" scale={3} />
|
|
35
|
+
*/
|
|
36
|
+
export declare const Mascot: ({ doc, triggers, ref, ...props }: MascotProps) => import("react").JSX.Element;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { defineMascotElement } from "./element.js";
|
|
4
|
+
export { emit, play } from "./element.js";
|
|
5
|
+
defineMascotElement();
|
|
6
|
+
/**
|
|
7
|
+
* `<open-mascot>` for React. React 19 skips non-string props on custom elements when it hydrates
|
|
8
|
+
* server-rendered HTML, so `doc` and `triggers` go in through a ref callback, which runs on
|
|
9
|
+
* hydration too. The rest render as attributes.
|
|
10
|
+
*
|
|
11
|
+
* <Mascot doc={moss} name="inbox" scale={3} />
|
|
12
|
+
*/
|
|
13
|
+
export const Mascot = ({ doc, triggers, ref, ...props }) => (_jsx("open-mascot", { ...props, ref: (el) => {
|
|
14
|
+
if (!el)
|
|
15
|
+
return;
|
|
16
|
+
// Triggers first: setting them after the doc would mount twice.
|
|
17
|
+
el.triggers = triggers ?? true;
|
|
18
|
+
el.doc = doc ?? null;
|
|
19
|
+
if (typeof ref === "function") {
|
|
20
|
+
const cleanup = ref(el);
|
|
21
|
+
return () => {
|
|
22
|
+
if (cleanup)
|
|
23
|
+
cleanup();
|
|
24
|
+
else
|
|
25
|
+
ref(null);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (ref)
|
|
29
|
+
ref.current = el;
|
|
30
|
+
return () => {
|
|
31
|
+
if (ref)
|
|
32
|
+
ref.current = null;
|
|
33
|
+
};
|
|
34
|
+
} }));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MascotDoc, MascotFrame } from "./types.js";
|
|
2
|
+
export declare const EMPTY_CELL = ".";
|
|
3
|
+
/** Turn one pose string into a canvas the size of the grid, one canvas pixel per cell. */
|
|
4
|
+
export declare const rasterizePose: (doc: MascotDoc, pose: string) => HTMLCanvasElement;
|
|
5
|
+
/** A scaled PNG of one pose, for thumbnails and exports. */
|
|
6
|
+
export declare const poseToDataUrl: (doc: MascotDoc, pose: string, scale?: number) => string;
|
|
7
|
+
/** Slack around the grid, in cells, so every frame's offset and slide stays on the canvas. */
|
|
8
|
+
export interface MascotPadding {
|
|
9
|
+
left: number;
|
|
10
|
+
right: number;
|
|
11
|
+
up: number;
|
|
12
|
+
down: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* How far frames push the drawing outside its grid. Always at least one column each side
|
|
16
|
+
* and one row below, so a mascot without any offsets draws the same as before.
|
|
17
|
+
*/
|
|
18
|
+
export declare const sequencePadding: (doc: MascotDoc) => MascotPadding;
|
|
19
|
+
/** Owns the canvas: sizing, a rasterized-pose cache, and drawing one frame. */
|
|
20
|
+
export declare class Renderer {
|
|
21
|
+
private scale;
|
|
22
|
+
readonly canvas: HTMLCanvasElement;
|
|
23
|
+
private ctx;
|
|
24
|
+
private doc;
|
|
25
|
+
private poses;
|
|
26
|
+
private pad;
|
|
27
|
+
constructor(scale: number);
|
|
28
|
+
setDoc(doc: MascotDoc): void;
|
|
29
|
+
setScale(scale: number): void;
|
|
30
|
+
draw(f: MascotFrame): void;
|
|
31
|
+
destroy(): void;
|
|
32
|
+
private resize;
|
|
33
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const EMPTY_CELL = ".";
|
|
2
|
+
const hexToRgb = (hex) => [
|
|
3
|
+
parseInt(hex.slice(1, 3), 16),
|
|
4
|
+
parseInt(hex.slice(3, 5), 16),
|
|
5
|
+
parseInt(hex.slice(5, 7), 16)
|
|
6
|
+
];
|
|
7
|
+
/** Turn one pose string into a canvas the size of the grid, one canvas pixel per cell. */
|
|
8
|
+
export const rasterizePose = (doc, pose) => {
|
|
9
|
+
const { w, h } = doc.grid;
|
|
10
|
+
const str = doc.poses[pose] ?? "";
|
|
11
|
+
const canvas = document.createElement("canvas");
|
|
12
|
+
canvas.width = w;
|
|
13
|
+
canvas.height = h;
|
|
14
|
+
const ctx = canvas.getContext("2d");
|
|
15
|
+
if (!ctx)
|
|
16
|
+
return canvas;
|
|
17
|
+
const img = ctx.createImageData(w, h);
|
|
18
|
+
for (let i = 0; i < w * h; i++) {
|
|
19
|
+
const ch = str[i];
|
|
20
|
+
if (!ch || ch === EMPTY_CELL)
|
|
21
|
+
continue;
|
|
22
|
+
const hex = doc.palette[parseInt(ch, 36)] ?? "#000000";
|
|
23
|
+
const [r, g, b] = hexToRgb(hex);
|
|
24
|
+
img.data[i * 4] = r;
|
|
25
|
+
img.data[i * 4 + 1] = g;
|
|
26
|
+
img.data[i * 4 + 2] = b;
|
|
27
|
+
img.data[i * 4 + 3] = 255;
|
|
28
|
+
}
|
|
29
|
+
ctx.putImageData(img, 0, 0);
|
|
30
|
+
return canvas;
|
|
31
|
+
};
|
|
32
|
+
/** A scaled PNG of one pose, for thumbnails and exports. */
|
|
33
|
+
export const poseToDataUrl = (doc, pose, scale = 8) => {
|
|
34
|
+
const src = rasterizePose(doc, pose);
|
|
35
|
+
const out = document.createElement("canvas");
|
|
36
|
+
out.width = src.width * scale;
|
|
37
|
+
out.height = src.height * scale;
|
|
38
|
+
const ctx = out.getContext("2d");
|
|
39
|
+
if (!ctx)
|
|
40
|
+
return "";
|
|
41
|
+
ctx.imageSmoothingEnabled = false;
|
|
42
|
+
ctx.drawImage(src, 0, 0, out.width, out.height);
|
|
43
|
+
return out.toDataURL("image/png");
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* How far frames push the drawing outside its grid. Always at least one column each side
|
|
47
|
+
* and one row below, so a mascot without any offsets draws the same as before.
|
|
48
|
+
*/
|
|
49
|
+
export const sequencePadding = (doc) => {
|
|
50
|
+
const pad = { left: 1, right: 1, up: 0, down: 1 };
|
|
51
|
+
for (const seq of Object.values(doc.sequences ?? {})) {
|
|
52
|
+
for (const f of seq) {
|
|
53
|
+
const x = Math.round(f.x ?? 0);
|
|
54
|
+
const y = Math.round(f.offset ?? 0);
|
|
55
|
+
pad.left = Math.max(pad.left, -x);
|
|
56
|
+
pad.right = Math.max(pad.right, x);
|
|
57
|
+
pad.up = Math.max(pad.up, -y);
|
|
58
|
+
pad.down = Math.max(pad.down, y);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return pad;
|
|
62
|
+
};
|
|
63
|
+
/** Owns the canvas: sizing, a rasterized-pose cache, and drawing one frame. */
|
|
64
|
+
export class Renderer {
|
|
65
|
+
constructor(scale) {
|
|
66
|
+
this.scale = scale;
|
|
67
|
+
this.poses = new Map();
|
|
68
|
+
this.pad = { left: 1, right: 1, up: 0, down: 1 };
|
|
69
|
+
this.canvas = document.createElement("canvas");
|
|
70
|
+
this.canvas.style.imageRendering = "pixelated";
|
|
71
|
+
this.canvas.style.display = "block";
|
|
72
|
+
this.canvas.setAttribute("role", "img");
|
|
73
|
+
const ctx = this.canvas.getContext("2d");
|
|
74
|
+
if (!ctx)
|
|
75
|
+
throw new Error("mascot: canvas 2d context unavailable");
|
|
76
|
+
this.ctx = ctx;
|
|
77
|
+
}
|
|
78
|
+
setDoc(doc) {
|
|
79
|
+
this.doc = doc;
|
|
80
|
+
this.poses.clear();
|
|
81
|
+
for (const name of Object.keys(doc.poses))
|
|
82
|
+
this.poses.set(name, rasterizePose(doc, name));
|
|
83
|
+
this.pad = sequencePadding(doc);
|
|
84
|
+
this.canvas.setAttribute("aria-label", `${doc.name} mascot`);
|
|
85
|
+
this.resize();
|
|
86
|
+
}
|
|
87
|
+
setScale(scale) {
|
|
88
|
+
this.scale = scale;
|
|
89
|
+
this.resize();
|
|
90
|
+
}
|
|
91
|
+
draw(f) {
|
|
92
|
+
const { w, h } = this.doc.grid;
|
|
93
|
+
const s = this.scale;
|
|
94
|
+
const pose = this.poses.get(f.pose) ?? this.poses.get("default");
|
|
95
|
+
const offset = ((f.offset ?? 0) + this.pad.up) * s;
|
|
96
|
+
const x = ((f.x ?? 0) + this.pad.left) * s;
|
|
97
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
98
|
+
if (pose)
|
|
99
|
+
this.ctx.drawImage(pose, x, offset, w * s, h * s);
|
|
100
|
+
}
|
|
101
|
+
destroy() {
|
|
102
|
+
this.canvas.remove();
|
|
103
|
+
}
|
|
104
|
+
resize() {
|
|
105
|
+
const { w, h } = this.doc.grid;
|
|
106
|
+
const s = this.scale;
|
|
107
|
+
const { left, right, up, down } = this.pad;
|
|
108
|
+
this.canvas.width = (w + left + right) * s; // slack each side for slides
|
|
109
|
+
this.canvas.height = (h + up + down) * s; // slack above for hops, below for crouches
|
|
110
|
+
this.canvas.style.width = `${this.canvas.width}px`;
|
|
111
|
+
this.canvas.style.height = `${this.canvas.height}px`;
|
|
112
|
+
this.ctx.imageSmoothingEnabled = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ParsedTrigger = {
|
|
2
|
+
type: "load" | "click" | "hover";
|
|
3
|
+
} | {
|
|
4
|
+
type: "interval";
|
|
5
|
+
seconds: number;
|
|
6
|
+
} | {
|
|
7
|
+
type: "custom";
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
/** The one place trigger keys ("click", "interval:30", "custom:win") are decoded. */
|
|
11
|
+
export declare const parseTrigger: (key: string) => ParsedTrigger | null;
|
|
12
|
+
/** What a trigger needs from the player. */
|
|
13
|
+
export interface TriggerHost {
|
|
14
|
+
el: HTMLElement;
|
|
15
|
+
play(name: string): boolean;
|
|
16
|
+
isBusy(): boolean;
|
|
17
|
+
}
|
|
18
|
+
/** An event source that starts one sequence. Owns its listeners and cleans them up. */
|
|
19
|
+
export declare abstract class Trigger {
|
|
20
|
+
protected readonly host: TriggerHost;
|
|
21
|
+
protected readonly sequence: string;
|
|
22
|
+
constructor(host: TriggerHost, sequence: string);
|
|
23
|
+
abstract attach(): void;
|
|
24
|
+
abstract detach(): void;
|
|
25
|
+
}
|
|
26
|
+
/** Builds triggers from a doc's trigger map. `load` and `custom:*` are handled by the player. */
|
|
27
|
+
export declare class TriggerManager {
|
|
28
|
+
private readonly host;
|
|
29
|
+
private triggers;
|
|
30
|
+
constructor(host: TriggerHost, map: Record<string, string>);
|
|
31
|
+
attach(): void;
|
|
32
|
+
detach(): void;
|
|
33
|
+
}
|
package/dist/triggers.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** The one place trigger keys ("click", "interval:30", "custom:win") are decoded. */
|
|
2
|
+
export const parseTrigger = (key) => {
|
|
3
|
+
if (key === "load" || key === "click" || key === "hover")
|
|
4
|
+
return { type: key };
|
|
5
|
+
if (key.startsWith("interval:"))
|
|
6
|
+
return { type: "interval", seconds: parseFloat(key.slice(9)) || 5 };
|
|
7
|
+
if (key.startsWith("custom:"))
|
|
8
|
+
return { type: "custom", name: key.slice(7) };
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
11
|
+
/** An event source that starts one sequence. Owns its listeners and cleans them up. */
|
|
12
|
+
export class Trigger {
|
|
13
|
+
constructor(host, sequence) {
|
|
14
|
+
this.host = host;
|
|
15
|
+
this.sequence = sequence;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class ClickTrigger extends Trigger {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
this.onClick = () => void this.host.play(this.sequence);
|
|
22
|
+
}
|
|
23
|
+
attach() {
|
|
24
|
+
this.host.el.addEventListener("click", this.onClick);
|
|
25
|
+
this.host.el.style.cursor = "pointer";
|
|
26
|
+
}
|
|
27
|
+
detach() {
|
|
28
|
+
this.host.el.removeEventListener("click", this.onClick);
|
|
29
|
+
this.host.el.style.cursor = "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Hover does not interrupt a move that is already playing. */
|
|
33
|
+
class HoverTrigger extends Trigger {
|
|
34
|
+
constructor() {
|
|
35
|
+
super(...arguments);
|
|
36
|
+
this.onEnter = () => {
|
|
37
|
+
if (!this.host.isBusy())
|
|
38
|
+
this.host.play(this.sequence);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
attach() {
|
|
42
|
+
this.host.el.addEventListener("mouseenter", this.onEnter);
|
|
43
|
+
}
|
|
44
|
+
detach() {
|
|
45
|
+
this.host.el.removeEventListener("mouseenter", this.onEnter);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class IntervalTrigger extends Trigger {
|
|
49
|
+
constructor(host, sequence, seconds) {
|
|
50
|
+
super(host, sequence);
|
|
51
|
+
this.seconds = seconds;
|
|
52
|
+
this.id = null;
|
|
53
|
+
}
|
|
54
|
+
attach() {
|
|
55
|
+
this.id = setInterval(() => {
|
|
56
|
+
if (!this.host.isBusy())
|
|
57
|
+
this.host.play(this.sequence);
|
|
58
|
+
}, this.seconds * 1000);
|
|
59
|
+
}
|
|
60
|
+
detach() {
|
|
61
|
+
if (this.id !== null)
|
|
62
|
+
clearInterval(this.id);
|
|
63
|
+
this.id = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Builds triggers from a doc's trigger map. `load` and `custom:*` are handled by the player. */
|
|
67
|
+
export class TriggerManager {
|
|
68
|
+
constructor(host, map) {
|
|
69
|
+
this.host = host;
|
|
70
|
+
this.triggers = [];
|
|
71
|
+
for (const [key, sequence] of Object.entries(map)) {
|
|
72
|
+
const parsed = parseTrigger(key);
|
|
73
|
+
if (parsed?.type === "click")
|
|
74
|
+
this.triggers.push(new ClickTrigger(host, sequence));
|
|
75
|
+
else if (parsed?.type === "hover")
|
|
76
|
+
this.triggers.push(new HoverTrigger(host, sequence));
|
|
77
|
+
else if (parsed?.type === "interval") {
|
|
78
|
+
this.triggers.push(new IntervalTrigger(host, sequence, parsed.seconds));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
attach() {
|
|
83
|
+
this.host.el.style.cursor = "";
|
|
84
|
+
for (const t of this.triggers)
|
|
85
|
+
t.attach();
|
|
86
|
+
}
|
|
87
|
+
detach() {
|
|
88
|
+
for (const t of this.triggers)
|
|
89
|
+
t.detach();
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** One step of a sequence. `hold` is how many ticks the pose stays on screen. */
|
|
2
|
+
export interface MascotFrame {
|
|
3
|
+
pose: string;
|
|
4
|
+
hold: number;
|
|
5
|
+
/** Rows to push the drawing down. Reads as a crouch. */
|
|
6
|
+
offset?: number;
|
|
7
|
+
/** Columns to push the drawing right (negative = left). Reads as a slide. */
|
|
8
|
+
x?: number;
|
|
9
|
+
}
|
|
10
|
+
export type MascotSequence = MascotFrame[];
|
|
11
|
+
/**
|
|
12
|
+
* The whole mascot. Poses are strings with one character per cell, row by row:
|
|
13
|
+
* "." is transparent, any other character is a base-36 palette index.
|
|
14
|
+
*/
|
|
15
|
+
export interface MascotDoc {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
version: number;
|
|
19
|
+
grid: {
|
|
20
|
+
w: number;
|
|
21
|
+
h: number;
|
|
22
|
+
};
|
|
23
|
+
/** Milliseconds per animation tick. */
|
|
24
|
+
tick: number;
|
|
25
|
+
palette: string[];
|
|
26
|
+
poses: Record<string, string>;
|
|
27
|
+
sequences: Record<string, MascotSequence>;
|
|
28
|
+
/**
|
|
29
|
+
* Event → sequence name. Keys: "load", "click", "hover", "interval:<seconds>",
|
|
30
|
+
* "custom:<name>". Every sequence plays once; between them the mascot rests on its default pose.
|
|
31
|
+
*/
|
|
32
|
+
triggers: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
export type MascotPosition = "bottom-right" | "bottom-left";
|
|
35
|
+
export interface MascotOptions {
|
|
36
|
+
/** Screen pixels per mascot cell. Default 4. */
|
|
37
|
+
scale?: number;
|
|
38
|
+
/** Fixed corner placement. Omit to render inline where the element sits. */
|
|
39
|
+
position?: MascotPosition | null;
|
|
40
|
+
/** Force reduced motion on or off. Defaults to the OS preference. */
|
|
41
|
+
reducedMotion?: boolean;
|
|
42
|
+
/** Wire the doc's triggers (click, hover, intervals, load). Default true. */
|
|
43
|
+
triggers?: boolean;
|
|
44
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openmascot",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pixel mascots for any website: a <Mascot> React component and a one-line script tag.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"animation",
|
|
7
|
+
"canvas",
|
|
8
|
+
"custom-element",
|
|
9
|
+
"mascot",
|
|
10
|
+
"pixel-art",
|
|
11
|
+
"react",
|
|
12
|
+
"sprite",
|
|
13
|
+
"web-component"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/DheerajMahra/openmascot#readme",
|
|
16
|
+
"bugs": "https://github.com/DheerajMahra/openmascot/issues",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Dheeraj Mahra",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/DheerajMahra/openmascot.git",
|
|
22
|
+
"directory": "packages/embed"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"unpkg": "dist/embed.js",
|
|
29
|
+
"jsdelivr": "dist/embed.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./react": {
|
|
36
|
+
"types": "./dist/react.d.ts",
|
|
37
|
+
"default": "./dist/react.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19",
|
|
46
|
+
"@types/react-dom": "^19",
|
|
47
|
+
"esbuild": "^0.25.0",
|
|
48
|
+
"jsdom": "^30.1.3",
|
|
49
|
+
"react": "19.3.0",
|
|
50
|
+
"react-dom": "19.3.0",
|
|
51
|
+
"typescript": "^5",
|
|
52
|
+
"vitest": "^5.0.5"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=19"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"react": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "rm -rf dist && pnpm build:lib && pnpm build:script",
|
|
64
|
+
"build:lib": "tsc -p tsconfig.build.json && node scripts/add-js-extensions.mjs dist",
|
|
65
|
+
"build:script": "esbuild src/embed.ts --bundle --format=iife --target=es2018 --minify --outfile=dist/embed.js",
|
|
66
|
+
"demo": "pnpm build && echo 'Demo: http://localhost:4311/packages/embed/demo/vanilla/' && python3 -m http.server 4311 --directory ../..",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"typecheck": "tsc --noEmit"
|
|
69
|
+
},
|
|
70
|
+
"types": "./dist/index.d.ts"
|
|
6
71
|
}
|