<lumorig-player> is a web component that draws your mascot as live SVG and animates it with the same sampler the exporters use. One module script (about 8 KB gzipped), one element, no framework. It loads a bundle: the mascot's spec, every preset fitted to its rig, every saved animation and every canvas, so switching motions never needs another request.
Try it#
This is Pip rendered in the page. Change the controls and the snippet below the stage updates to match.
<lumorig-player
src="https://lumorig.com/api/v1/mascots/pip/bundle"
animation="wave"
trigger="auto"
follow-cursor
></lumorig-player>Install#
Load the module from /player.js on your Lumorig origin, then drop the element anywhere. It registers itself as lumorig-player when the script runs. Point src at the mascot's bundle_url.
<script type="module" src="https://lumorig.com/player.js"></script>
<lumorig-player
src="https://lumorig.com/api/v1/mascots/msc_…/bundle"
animation="idle"
trigger="hover"
follow-cursor
style="width: 200px; height: 200px"
></lumorig-player>The element is 240 px wide and square by default; size it with CSS. Get a ready snippet with lumorig embed msc_… -a wave or the get_embed_code MCP tool.
Serving the bundle yourself#
The bundle endpoint sends open CORS headers, so it works from any origin. If you would rather not depend on it at runtime, fetch the JSON once, host it with your site, and point src at your copy, or set the bundle property directly.
curl https://lumorig.com/api/v1/mascots/msc_…/bundle -H "Authorization: Bearer $LUMORIG_API_KEY" -o public/pip.bundle.jsonAttributes#
| Name | Type | Description |
|---|---|---|
src | URL | Bundle to load. Changing it loads the new one. |
animation | string | Clip to play: a preset id (wave, think…) or a saved animation's name. Unknown names fall back to the bundle's first clip. |
trigger | auto | hover | click | none | auto (default) plays at once. hover plays while the pointer is over it and rewinds on leave. click toggles play and pause. none waits for play(). |
speed | number | Playback rate, default 1. Must be positive. |
loop | true | false | Override the clip's own looping. loop="false" plays once and fires ended; any other value loops. Omit it to use the clip's setting. |
palette | JSON | Recolor in the browser by token, e.g. palette='{"fur":"#3A5BFF"}'. Token names are in mascot.palette. |
follow-cursor | boolean | The pupils (a part named pupils) or else the head drift toward the pointer. |
canvas | string | Run a canvas from the bundle by name. It takes over from animation while set. |
no-shadow | boolean | Hide the soft floor shadow. Read when the mascot is built, so set it before the bundle loads. |
Methods and properties#
| Member | Does |
|---|---|
play(name?) | Play from the current time. With a name, switches animation first. Restarts a finished one-shot clip. |
pause() | Stop at the current frame. |
seek(t) | Jump to t seconds and draw that frame. |
setInput(name, value) | Set a canvas input (boolean or number). |
fire(name) | Fire a canvas trigger input. |
bundle | Get or set the loaded bundle object. Setting it skips the network entirely. |
currentTime · duration · paused | Read-only timeline state, in seconds. |
state | The current canvas state id, or null when no canvas is running. |
const pip = document.querySelector("lumorig-player");
pip.play("celebrate");
pip.pause();
pip.seek(0.4);
// Load a bundle you already have, no request:
pip.bundle = await fetch("/pip.bundle.json").then((r) => r.json());Events#
| Event | When | detail |
|---|---|---|
ready | The bundle loaded and the mascot is drawn. | — |
ended | A non-looping clip reached its end. | — |
statechange | A canvas moved to a new state. | { state } |
error | The bundle failed to load, or the canvas name isn't in the bundle. | The Error |
pip.addEventListener("ready", () => pip.play("wave"));
pip.addEventListener("statechange", (e) => console.log("now", e.detail.state));
pip.addEventListener("error", (e) => console.warn("mascot failed", e.detail));React#
The repository ships a thin React wrapper, MascotPlayer, that renders the element and registers it on mount, so it is safe to server-render. Props map one-to-one onto the attributes (followCursor for follow-cursor, palette as an object), plus bundle, onReady and onEnded.
import { MascotPlayer } from "@lumorig/player/react";
export function Helper() {
return (
<MascotPlayer
src="https://lumorig.com/api/v1/mascots/msc_…/bundle"
animation="idle"
trigger="hover"
followCursor
style={{ width: 200, height: 200 }}
onReady={(el) => el.play("wave")}
/>
);
}import { useEffect } from "react";
export function Helper() {
useEffect(() => {
import(/* webpackIgnore: true */ "https://lumorig.com/player.js");
}, []);
return <lumorig-player src="https://lumorig.com/api/v1/mascots/msc_…/bundle" animation="idle" trigger="hover" />;
}Private mascots#
Bundles of public mascots load with no key. Private ones need a signed, expiring share link (never put an API key in a web page).
# Option 1: make it public (permanent embed)
curl -X PATCH https://lumorig.com/api/v1/mascots/msc_… -H "Authorization: Bearer $LUMORIG_API_KEY" \
-H "Content-Type: application/json" -d '{ "public": true }'
# Option 2: a signed link, here for 7 days (default 1 hour, max 30 days)
curl -X POST https://lumorig.com/api/v1/mascots/msc_…/share -H "Authorization: Bearer $LUMORIG_API_KEY" \
-H "Content-Type: application/json" -d '{ "ttl_seconds": 604800 }'
# → { "token": "…", "expires_at": "…", "bundle_url": "/api/v1/mascots/msc_…/bundle?token=…" }Share links stop working when they expire (403 forbidden, “Share token expired.”), so refresh them server-side before then, or make the mascot public. Public bundles are cached for 60 seconds, so a new version reaches embeds within a minute.
Accessibility and motion#
- The SVG carries
role="img"and the mascot's name as its label. If the mascot is decorative, addaria-hidden="true"to the element. - Under
prefers-reduced-motion: reducenothing plays on its own:autoandhovershow the first frame. A click plays the animation once and returns to the first frame, so motion only happens when the visitor asks for it. - Nothing animates while it isn't playing: the render loop stops when the clip pauses, ends, or the element leaves the page.