Spine Web Components
The spine-webcomponents
package provides web components (custom HTML elements) for embedding Spine animations directly into a web page.
When the <spine-skeleton>
tag is added to an HTML page, the library creates a shared WebGL canvas overlay where multiple Spine skeletons are rendered. This design overcomes browser limitations on the number of WebGL contexts.
Unlike the Spine player, the web components do not include a built-in UI for controlling playback. Instead, they expose a wide range of attributes that enable fine-grained configuration directly through HTML.
Exporting
Spine web components use the same export format as the Spine player. In addition, it supports using multiple skeletons in the same JSON file.
Set up
Adding a <spine-skeleton>
web component to a website involves only a few straightforward steps, outlined below.
Adding the JavaScript
The spine-webcomponents
package includes the JavaScript file spine-webcomponents.js
, which defines the two HTML custom elements <spine-skeleton>
and <spine-overlay>
, along with a set of related utility functions.
In the above example, the the file is loaded from UNPKG, a fast NPM CDN. The URL contains a version number (4.2
) which must match the Spine editor version used to export the skeleton. The asterisk (*
) for the patch version ensures the latest JavaScript code for the major.minor
version.
Use the .min.js
file extension for a minified file from the UNPKG CDN:
Alternatively, the file can be self-hosted by downloading it from UNPKG or by building it from the sources available on the GitHub repository. The repository also includes instructions for using the spine-webcomponents
with NPM or Yarn.
Using a spine-skeleton
After importing the JavaScript file, the web component can be used directly in HTML without additional JavaScript:
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
></spine-skeleton>
The <spine-skeleton>
element loads the skeleton data from /files/spineboy/export/spineboy-pro.skel
and the atlas from /files/spineboy/export/spineboy.atlas
. The atlas references an image file (spineboy.png
), which is loaded relative to the .atlas
file, from /files/spineboy/export/spineboy.png
.
A <spine-overlay>
element is automatically added at the bottom of the DOM. This component creates a transparent WebGL canvas that spans the entire page and is used to render all <spine-skeleton>
components in the correct positions within their parent containers. Most users don't need to be concerned with the overlay.
The web component renders the skeleton scaled to fit its parent element.
1 | 2 |
3 | 4 |
<tr>
<td>1</td>
<td>2
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>
</td>
</tr>
<tr>
<td>3
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>
</td>
<td>4</td>
</tr>
</table>!!
Configuration
The <spine-skeleton>
element provides many configuration attributes, allowing it to be tailored to specific requirements.
JSON, binary, and atlas URL
The two mandatory attributes, skeleton
and atlas
, define the source paths for the skeleton .json
or binary .skel
file and the .atlas
file, respectively. These paths can be either relative or absolute URLs.
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel">
</spine-skeleton>
<spine-skeleton
atlas="https://esotericsoftware.com/assets/spineboy-pma.atlas"
skeleton="https://esotericsoftware.com/assets/spineboy-pro.skel">
</spine-skeleton>
When using absolute URLs to another domain, it is possible that web browsers won't be able to load the assets. This can be solved by enabling CORS on the server that hosts the assets.
Embedding data
Instead of loading data from URLs, the .json
/.skel
, .atlas
, and .png
files can be embedded directly using the raw-data
attribute. This attribute accepts a stringified JSON object where keys are asset names and values are their Base64-encoded contents. The skeleton
and atlas
attributes should then reference the corresponding asset names used in this object. The raw-data
attribute is used to enable this setup.
atlas="/assets/inline.atlas"
skeleton="/assets/inline.skel"
animation="animation"
raw-data='{
"/assets/inline.atlas":"aW5saW5lLnBuZwpzaXplOjE2LDE2CmZpbHRlcjpMaW5lYXIsTGluZWFyCnBtYTp0cnVlCmRvdApib3VuZHM6MCwwLDEsMQo=",
"/assets/inline.skel":"/B8S/IqaXgYHNC4yLjM5wkgAAMJIAABCyAAAQsgAAELIAAAAAQRkb3QCBXJvb3QAAAAAAAAAAAAAAAA/gAAAP4AAAAAAAAAAAAAAAAAAAAAABGRvdAAAAAAAAAAAAAAAAABCyAAAQsgAAAAAAAAAAAAAAAAAAAAAAQRkb3QB//////////8BAAAAAAABAAEBACWwfdcAAAAAP4AAAD+AAAA/gAAAP4AAAAAAAQphbmltYXRpb24BAQABAQMAAAAAAP////8/gAAA/wAA/wBAAAAA/////wAAAAAAAAAAAA==",
"/assets/inline.png":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRF////p8QbyAAAAApJREFUeJxjZAAAAAQAAiFkrWoAAAAASUVORK5CYII="
}'
></spine-skeleton>
Style, width, height
By default, the web component renders the skeleton to fill its parent's size, but its actual dimensions are zero width and height. To manually set the size of the web component, use the standard style
or class
attributes. These attributes can be used to apply any desired styles.
.custom-class {
width: 150px;
height: 150px;
border: 1px solid green;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(0, 255, 0, 0.3);
margin-right: 10px;
}
</style>
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
class="custom-class"
></spine-skeleton>
<spine-skeleton
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
style="
width: 150px;
height: 150px;
border: 1px solid red;
border-radius: 10px;
box-shadow: -5px 5px 3px rgba(255, 0, 0, 0.3);
"
></spine-skeleton>
JSON skeleton key
To minimize requests for resources, multiple skeletons can be embedded in a single JSON file. When using such JSON, specify which skeleton to display by setting the json-skeleton-key
attribute on the web component. The spine-webcomponents
asset manager efficiently loads each asset only once, even if used on the page multiple times.
atlas="/files/spine-widget/assets/atlas2.atlas"
skeleton="/files/spine-widget/assets/demos.json"
json-skeleton-key="armorgirl"
animation="animation"
></spine-skeleton>
<spine-skeleton
atlas="/files/spine-widget/assets/atlas2.atlas"
skeleton="/files/spine-widget/assets/demos.json"
json-skeleton-key="greengirl"
animation="animation"
></spine-skeleton>
Animation
By default, the web component will show the setup pose. An animation can be set using the animation
attribute:
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
></spine-skeleton>
Default skin
By default, the web component will use the default skin of the skeleton, which only has attachments that are not in a skin in the Spine editor. The active skin can be set explicitly in the configuration via the skin
property:
atlas="/files/spine-widget/assets/mix-and-match-pma.atlas"
skeleton="/files/spine-widget/assets/mix-and-match-pro.skel"
animation="dance"
skin="full-skins/girl-spring-dress"
></spine-skeleton>
skin
accepts a comma-separated list of skin names. The skins will be combined into a new one, in the order provided. If multiple skins affect the same slot, the last one in the list takes precedence.
atlas="/files/spine-widget/assets/mix-and-match-pma.atlas"
skeleton="/files/spine-widget/assets/mix-and-match-pro.skel"
animation="dance"
skin="nose/short,skin-base,eyes/violet,hair/brown,clothes/hoodie-orange,legs/pants-jeans,accessories/bag,accessories/hat-red-yellow,eyelids/girly"
></spine-skeleton>
Fit mode
The web component tries to fit the skeleton animation within its container element depending on the fit
attribute. Here are some examples:
contain
: as large as possible while still containing the skeleton entirely within the container element (Default)fill
: fill the container element by distorting the skeleton's aspect ratioscaleDown
: scale the skeleton down to ensure that the skeleton fits within the container elementnone
: display the skeleton without regard to the container element size (here the [scale](#scale) attribute is set to `0.05`).Additional fit
modes are:
width
: fill the container element width, regardless of whether the skeleton overflows the container element vertically.height
: fill the container element height, regardless of whether the skeleton overflows the container element horizontally.cover
: as small as possible while still covering the entire container element.origin
: the skeleton origin is centered with the container element regardless of the bounds.
Bounds
The web component uses the skeleton bounds to fit inside the container element. The skeleton bounds are the bounding box (AABB) of the animation (or multiple animations), or of the setup pose if no animation is specified. To ensure the bounds fit the parent element according to the specified fit mode, the skeleton's scaleX
and scaleY
are set. This means these properties cannot be changed manually. To control scaleX
and scaleY
directly, set fit="none"
or fit="origin"
and access the skeleton object via JavaScript as explained below.
Scale
The Skeleton loader scale is set through the scale
attribute. Read more about scaling in the Spine Runtimes Guide.
In this example we set the fit
mode to none
to effectively view the scale change (otherwise scaleX
and scaleY
would be modified using the default fit mode).
scale="0.3"
scale="0.2"
scale="0.1"
Axis
Use x-axis
and y-axis
to shift the skeleton horizontally or vertically by percentage of the container element's width and height.
fit="none"
scale=".2"
x-axis=".25"
fit="origin"
scale=".2"
fit="origin"
scale=".2"
y-axis="-.5"
Offset
Use offset-x
and offset-y
to shift the skeleton horizontally or vertically by the specified number of pixels.
offset-x="0"
offset-y="0"
offset-x="-100"
offset-y="50"
Padding
Add virtual padding to the container element using pad-left
, pad-right
, pad-top
, and pad-bottom
. These values are percentages of the container's width for left and right, and percentages of the container's height for top and bottom.
pad-left="0"
pad-right="0"
pad-top="0"
pad-top="0"
pad-left=".25"
pad-right=".25"
pad-top=".25"
pad-top=".25"
Identifier
Assign an identifier to the web component to retrieve it using the spine.getSkeleton
function. This makes it easy to access the Skeleton
and AnimationState
objects in JavaScript code.
<spine-skeleton>
executes some asynchronous operations to retrieve assets. The whenReady
method is used to know when to access the Skeleton
and AnimationState
objects.
atlas="/files/spine-widget/assets/raptor-pma.atlas"
skeleton="/files/spine-widget/assets/raptor-pro.skel"
identifier="raptor"
></spine-skeleton>
raptor.skeleton.color.set(1, 0, 0, 1);
Clip
The clip
attribute will hide everything that is outside the container element.
Beware that this will break batching across skeletons.
atlas="/files/spine-widget/assets/tank-pma.atlas"
skeleton="/files/spine-widget/assets/tank-pro.skel"
animation="drive"
fit="height"
pad-top="0.3"
pad-bottom="0.3"
></spine-skeleton>
<spine-skeleton
atlas="/files/spine-widget/assets/tank-pma.atlas"
skeleton="/files/spine-widget/assets/tank-pro.skel"
animation="drive"
fit="height"
pad-top="0.3"
pad-bottom="0.3"
clip
></spine-skeleton>
Custom bounds
Custom bounds can be specified to focus on specific details of the animation, to zoom out, simulate camera movement, etc.
The bounds-x
, bounds-y
, bounds-width
, and bounds-height
attributes define custom bounds.
This example focuses on Celeste's face. To prevent the skeleton from overflowing the container element, we set the clip
attribute.
atlas="/files/spine-widget/assets/celestial-circus-pma.atlas"
skeleton="/files/spine-widget/assets/celestial-circus-pro.skel"
animation="wings-and-feet"
bounds-x="-155"
bounds-y="650"
bounds-width="300"
bounds-height="350"
clip
></spine-skeleton>
Auto calculate bounds
The animation can be changed by modifying the animation
attribute. The web component will switch to the new animation as if it were freshly created. However, new bounds are not recalculated unless the auto-calculate-bounds
attribute is set. This default behavior helps maintain consistent skeleton dimensions across animations. It might be useful to combine it with the animation-bounds attribute.
auto-calculate-bounds
set auto-calculate-bounds
identifier="spineboy-auto-bounds-1"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="jump"
></spine-skeleton>
<spine-skeleton
identifier="spineboy-auto-bounds-2"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="jump"
auto-calculate-bounds
></spine-skeleton>
spine.getSkeleton("spineboy-auto-bounds-1").whenReady,
spine.getSkeleton("spineboy-auto-bounds-2").whenReady,
]);
let toogleAnimation = false;
setInterval(() => {
const newAnimation = toogleAnimation ? "jump" : "death";
wc1.setAttribute("animation", newAnimation)
wc2.setAttribute("animation", newAnimation)
toogleAnimation = !toogleAnimation;
}, 4000);
Default mix
The default-mix
attribute defines the default mix duration for the AnimationState
. This is the default time in seconds to mix between animations when the animation changes, whether by using the animations
attribute or by JavaScript code using the AnimationState
object.
default-mix="0" (default)
default-mix="1"
identifier="spineboy-default-mix-1"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="idle"
default-mix="0"
></spine-skeleton>
<spine-skeleton
identifier="spineboy-default-mix-2"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="idle"
default-mix="1"
></spine-skeleton>
spine.getSkeleton("spineboy-default-mix-1").whenReady,
spine.getSkeleton("spineboy-default-mix-2").whenReady,
]);
let toogleAnimation = false;
setInterval(() => {
const newAnimation = toogleAnimation ? "idle" : "run";
wc1.setAttribute("animation", newAnimation)
wc2.setAttribute("animation", newAnimation)
toogleAnimation = !toogleAnimation;
}, 4000);
Animations
To display a sequence of animations without code, the animations
attribute can be used. Here we reproduced the example above with just web component attributes:
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation-bounds="walk,run"
default-mix="1"
animations="
[loop, 0, 3.5]
[0, idle, true]
[0, run, true, 4]
"
></spine-skeleton>
The animations
attribute accepts a string composed of groups enclosed in square brackets, for example: [...][...][...]
.
Each group defines an animation to be played, with parameters provided as a comma-separated list:
- track: the track number on which the animation will play
- animation name: the name of the animation
- loop:
true
to loop the animation (false
if omitted) - delay: seconds to wait after the previous animation starts, or
0
to wait until the previous animation ends (0
if omitted) - mixDuration: seconds to mix from the previous animation to this animation (
default-mix
if omitted, not applicable for the first animation on a track)
To enable looping of a track after the last animation, a special group [loop, trackNumber, repeatDelay]
can be added, where:
- loop: identifies this as a loop instruction
- trackNumber: the track number to loop
- repeatDelay: the number of seconds to wait after the last animation is completed before repeating the loop (
0
if omitted)
The first group for each track number is passed to the setAnimation method. Subsequent groups for the same track are passed to addAnimation.
To use setEmptyAnimation or addEmptyAnimation, the animation name #EMPTY#
must be specified. In this case, the loop
parameter is ignored.
Refer to the two examples below for clarification.
Spineboy uses this value for the animations
attribute:
[0, idle, true]
[0, run, false, 2, 0.25]
[0, run]
[0, run]
[0, run-to-idle, false, 0, 0.15]
[0, idle, true]
[0, jump, false, 0, 0.15]
[0, walk, false, 0, 0.05]
[0, death, false, 0, 0.05]
All animations are played on a single track. Here's a breakdown of the sequence:
[loop, 0]
: instructs track 0 to loop back to the beginning upon reaching the end[0, idle, true]
: sets the idle animation to loop[0, run, false, 2, 0.25]
: queues the run animation to start after 2 seconds with a 0.25 second mix[0, run]
: queues an additional run animation, without looping[0, run]
: queues another run animation[0, run-to-idle, false, 0, 0.15]
: queues the run-to-idle transition with no delay and a 0.15 second mix[0, idle, true]
: queues the idle animation to loop again[0, jump, false, 0, 0.15]
: queues the jump animation with no delay and a 0.15 second mix[0, walk, false, 0, 0.05]
: queues the walk animation with no delay and a 0.05 second mix[0, death, false, 0, 0.05]
: queues the death animation with no delay and a 0.05 second mix
Celeste uses the following value for the animations
attribute:
[loop, 1]
[1, #EMPTY#]
[1, eyeblink, false, 2]
This example uses two tracks. Track 0 plays the wings-and-feet
animation. Track 1 loops, playing an empty animation followed by the eyeblink
animation with a 2 second delay.
The textarea above can be modified for experimentation, then click Update animation
. For example, changing the delay from 2
to 0.5
results in more frequent blinking. To start the swing
animation on track 0 after 5 seconds with a 0.5 second mix, append: [0, swing, true, 5, 0.5]
Animations bounds
To define bounds based on multiple animations, the animation-bounds
attribute can be used. This attribute accepts a list of animations and calculates bounds that encompass all of them.
This approach helps maintain a consistent scale across animations and prevents the skeleton from overflowing its container when switching to an animation with larger bounds.
animation-bounds
animation-bounds="walk,jump"
Spinner
spinner
attribute allows to show a spinner while assets are loading. By default, nothing is shown during assets load. Click the buttons below to simulate a 1 second loading delay and to toggle the spinner
attribute.
identifier="spineboy-loading"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
spinner
></spine-skeleton>
<input type="button" value="Toggle spinner: OFF" onclick="toggleSpinner(this)" />
<input type="button" value="Reload" onclick="reloadWidget(this)" />
async function reloadWidget(element) {
element.disabled = true;
await wcLoading.whenReady;
wcLoading.loading = true;
setTimeout(() => {
element.disabled = false;
wcLoading.loading = false;
}, 1000)
}
function toggleSpinner(element) {
wcLoading.spinner = !wcLoading.spinner;
element.value = wcLoading.spinner ? "Toggle spinner: OFF" : "Toggle spinner: ON";
}
Offscreen behavior
Web components that are off-screen are not rendered. While off-screen, by default the AnimationState
update, Skeleton
update, skeleton.apply
, and skeleton.updateWorldTransform
functions are not invoked. This corresponds to offscreen=pause
.
To ensure that update functions are invoked even when off-screen, set offscreen=update
.
To ensure that all functions are invoked regardless of visibility, set offscreen=pose
.
pause
update
pose
When this page is refreshed with all three skeletons visible in the viewport, their animations start in sync. However, the first skeleton has offscreen="pause"
, which causes its state to pause when scrolled out of view. Upon returning to the viewport, its animation resumes without advancing the paused time, resulting in desynchronization with the other two skeletons. While the other two skeletons remain in sync, slight differences may occur, particularly when physics is involved.
To prevent a skeleton from being paused while off-screen, it is recommended to use the update
behavior. This avoids invoking updateWorldTransform
, which is typically the most CPU-intensive function.
Custom update
A web component's skeleton
and state
are updated and applied similarly to other runtimes.
Custom logic can be injected before and after updateWorldTransform
is called by setting beforeUpdateWorldTransforms
and afterUpdateWorldTransforms
, respectively.
To fully replace the default update behavior, assign a function to the update
property. This replaces both state and skeleton updates, including off-screen optimizations. In this case, managing the update
, apply
, and updateWorldTransform
invocation becomes the developer's responsibility. The onScreen
property, which is true
when the component is visible on screen, can be used for covenience.
All three functions follow the same signature: (delta: number, skeleton: Skeleton, state: AnimationState) => void
Drag
Setting the drag
attribute enables dragging for the web component. This may increase CPU usage, so it is advisable to use this feature only when draggable behavior is required.
Pointer position
To determine the pointer position in various coordinate spaces, the following properties can be used:
For spine-skeleton
:
pointerWorldX
andpointerWorldY
: the x and y coordinates of the pointer relative to the skeleton origin (Spine world coordinates).worldX
andworldY
: the x and y coordinates of the skeleton origin relative to the canvas/WebGL context origin (Spine world coordinates).
For spine-overlay
:
pointerCanvasX
andpointerCanvasY
: the x and y coordinates of the pointer relative to the top-left corner of the canvas (screen coordinates).pointerWorldX
andpointerWorldY
: the x and y coordinates of the pointer relative to the canvas/WebGL context origin (Spine world coordinates).
These properties enable interactive behavior with the web component. For example, in the examples below the owl's eyes follow the pointer.
identifier="owl-pointer"
atlas="/files/spine-widget/assets/owl-pma.atlas"
skeleton="/files/spine-widget/assets/owl-pro.skel"
animations="[0, idle, true][1, blink, true]"
></spine-skeleton>
const controlBone = wc.skeleton.findBone("control");
const tempVector = new spine.Vector3();
wc.afterUpdateWorldTransforms = () => {
controlBone.parent.worldToLocal(tempVector.set(wc.pointerWorldX, wc.pointerWorldY));
controlBone.x = controlBone.data.x + tempVector.x / wc.overlay.canvas.width * 30;
controlBone.y = controlBone.data.y + tempVector.y / wc.overlay.canvas.height * 30;
}
Interaction callbacks
Callbacks can be attached to the web components to handle pointer interactions by setting the interactive
attribute.
Callbacks can respond to interactions either within the web component's bounds or with specific slots. Supported events (PointerEventType
) include down
, up
, enter
, leave
, move
, and drag
.
To add callbacks:
- Set
pointerEventCallback: (event: PointerEventType) => void
to handle pointer actions within the web component bounds. - Call
addPointerSlotEventCallback (slotRef: number | string | Slot, slotFunction: (slot: Slot, event: PointerEventType) => void)
to handle pointer actions within the attachment bounds of the specified slot.
In the example below:
pointerEventCallback
triggers thejump
animation onenter
, and thewave
animation onleave
.addPointerSlotEventCallback
adds a callback for thehead-base
slot (the face). When the attachment receives adown
event, the normal and dark tint are updated based on the selected colors in the two tint selectors.
identifier="interactive0"
atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
skeleton="/files/spine-widget/assets/chibi-stickers.skel"
skin="mario"
animation="emotes/wave"
animation-bounds="emotes/wave,emotes/hooray"
pages="0,4"
interactive
></spine-skeleton>
<spine-skeleton
identifier="interactive1"
atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
skeleton="/files/spine-widget/assets/chibi-stickers.skel"
skin="nate"
animation="emotes/wave"
animation-bounds="emotes/wave,emotes/hooray"
pages="0,6"
interactive
></spine-skeleton>
Tint normal: <input type="color" id="color-picker" value="#ff0000" style="margin: 0;" />
Tint black: <input type="color" id="dark-picker" value="#000000" style="margin: 0;"/>
const darkPicker = document.getElementById("dark-picker");
[0, 1].forEach(async (i) => {
const wc = await spine.getSkeleton(`interactive${i}`).whenReady;
wc.pointerEventCallback = (event) => {
if (event === "enter") wc.state.setAnimation(0, "emotes/hooray", true).mixDuration = .15;
if (event === "leave") wc.state.setAnimation(0, "emotes/wave", true).mixDuration = .25;
}
const tempColor = new spine.Color();
const slot = wc.skeleton.findSlot("head-base");
slot.darkColor = new spine.Color(0, 0, 0, 1);
wc.addPointerSlotEventCallback(slot, (slot, event) => {
if (event === "down") {
slot.darkColor.setFromColor(spine.Color.fromString(darkPicker.value, tempColor));
slot.color.setFromColor(spine.Color.fromString(colorPicker.value, tempColor));
}
});
})
Debug mode
Debug mode can be enabled by setting the debug
attribute. This displays the following visual indicators:
- The skeleton's world origin (green)
- The root bone position (red)
- The bounds rectangle and its center (blue)
- The draggable area (semi-transparent red), if the web component is set as draggable.
In this example, the root is shifted to avoid overlapping with the origin.
style="width: 200px; height: 200px;"
identifier="sack-debug"
atlas="/files/spine-widget/assets/sack-pma.atlas"
skeleton="/files/spine-widget/assets/sack-pro.skel"
animation="cape-follow-example"
drag
offscreen="pose"
debug
></spine-skeleton>
.then(({ skeleton }) => skeleton.getRootBone().x += 50);
Page
When using multiple atlas pages (for example, one page per skin) and only a subset of pages need to be displayed, the pages
attribute can be used to specify which atlas pages to load. Provide a comma-separated list of the desired page indices.
pages="0,6"
pages="0,4"
pages="0,1"
To load textures programmatically, set the pages
attribute to an empty value: pages=""
. This loads the skeleton and atlas data without loading any textures, allowing textures to be loaded manually at a later time.
identifier="dragon"
style="flex: 0.8; height: 100%;"
atlas="/files/spine-widget/assets/dragon-pma.atlas"
skeleton="/files/spine-widget/assets/dragon-ess.skel"
animation="flying"
pages=""
></spine-skeleton>
<input type="button" value="Load page 0" onclick="loadPageDragon(0)" />
<input type="button" value="Load page 1" onclick="loadPageDragon(1)" />
<input type="button" value="Load page 2" onclick="loadPageDragon(2)" />
<input type="button" value="Load page 3" onclick="loadPageDragon(3)" />
<input type="button" value="Load page 4" onclick="loadPageDragon(4)" />
const dragon = await spine.getSkeleton("dragon").whenReady;
if (!dragon.pages.includes(pageIndex)) {
dragon.pages.push(pageIndex);
dragon.loadTexturesInPagesAttribute();
}
}
Follow slot
An HTMLElement can be made to follow a slot. This is useful for integrating dynamic content such as text into animations.
Call the followSlot
function with these parameters:
-
The
Slot
instance or slot name to follow -
The
HTMLElement
that will follow the slot -
An "options" object with these properties:
followOpacity
: links the element's opacity to the slot's alphafollowScale
: links the element's scale to the slot's scalefollowRotation
: links the element's rotation to the slot's rotationfollowVisibility
: shows or hides the element based on whether the slot has an attachment visiblehideAttachment
: hides the slot's attachment, as if the element visually replaces it
style="width: 200px; height: 200px;"
identifier="potty"
atlas="/files/spine-widget/assets/cloud-pot-pma.atlas"
skeleton="/files/spine-widget/assets/cloud-pot.skel"
animation="playing-in-the-rain"
></spine-skeleton>
<div id="rain/rain-color" style="font-size: 50px; display: none;">A</div>
<div id="rain/rain-white" style="font-size: 50px; display: none;">B</div>
<div id="rain/rain-blue" style="font-size: 50px; display: none;">C</div>
<div id="rain/rain-green" style="font-size: 50px; display: none;">D</div>
const options = { followVisibility: false, hideAttachment: true };
wc.followSlot("rain/rain-color", document.getElementById("rain/rain-color"), options);
wc.followSlot("rain/rain-white", document.getElementById("rain/rain-white"), options);
wc.followSlot("rain/rain-blue", document.getElementById("rain/rain-blue"), options);
wc.followSlot("rain/rain-green", document.getElementById("rain/rain-green"), options);
followSlot
works even with other spine web components! It works even when it is dragged!
style="width: 200px; height: 200px;"
identifier="potty2"
atlas="/files/spine-widget/assets/cloud-pot-pma.atlas"
skeleton="/files/spine-widget/assets/cloud-pot.skel"
animation="rain"
drag
offscreen="pose"
></spine-skeleton>
<spine-skeleton identifier="potty2-1" atlas="/files/spine-widget/assets/raptor-pma.atlas" skeleton="/files/spine-widget/assets/raptor-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-2" atlas="/files/spine-widget/assets/spineboy-pma.atlas" skeleton="/files/spine-widget/assets/spineboy-pro.skel" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-3" atlas="/files/spine-widget/assets/celestial-circus-pma.atlas" skeleton="/files/spine-widget/assets/celestial-circus-pro.skel" animation="wings-and-feet" style="height:200px; width: 200px;"></spine-skeleton>
<spine-skeleton identifier="potty2-4" atlas="/files/spine-widget/assets/goblins-pma.atlas" skeleton="/files/spine-widget/assets/goblins-pro.skel" skin="goblingirl" animation="walk" style="height:200px; width: 200px;"></spine-skeleton>
const options = { followVisibility: false, hideAttachment: true };
wc.followSlot("rain/rain-color", spine.getSkeleton("potty2-1"), options);
wc.followSlot("rain/rain-white", spine.getSkeleton("potty2-2"), options);
wc.followSlot("rain/rain-blue", spine.getSkeleton("potty2-3"), options);
wc.followSlot("rain/rain-green", spine.getSkeleton("potty2-4"), options);
Advanced usage
Programmatic creation
A <spine-skeleton>
element can be created programmatically using the createSkeleton
function. This function accepts an object where each property corresponds to a camelCase version of the web component's attributes.
<script>
["soeren", "sinisa", "luke"].forEach(skin => {
["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"].forEach(animation => {
const wc = spine.createSkeleton({
atlasPath: "/files/spine-widget/assets/chibi-stickers-pma.atlas",
skeletonPath: "/files/spine-widget/assets/chibi-stickers.skel",
animation,
skin,
pages: [0, 3, 7, 8],
});
wc.style.width = "25%";
wc.style.height = "100px";
document.currentScript.parentElement.appendChild(wc);
})
})
</script>
</div>
Alternatively, the web component can be directly appended to the DOM as HTML using standard DOM manipulation methods.
<script>
["harri", "misaki", "spineboy"].forEach(skin => {
["emotes/wave", "movement/trot-left", "emotes/idea", "emotes/hooray"].forEach(animation => {
document.currentScript.parentElement.insertAdjacentHTML('beforeend', `<spine-skeleton
style="width: 25%; height: 100px;"
atlas="/files/spine-widget/assets/chibi-stickers-pma.atlas"
skeleton="/files/spine-widget/assets/chibi-stickers.skel"
animation="${animation}"
skin="${skin}"
pages="0,2,5,9"
></spine-skeleton>
`);
});
});
</script>
</div>
By default, assets load immediately.
By default, assets are loaded immediately. To delay asset loading, set manualStart: "false"
. After creation, append the element to the DOM using the asynchronous appendTo
method. At the appropriate time, call start()
on the element to begin loading. Any interaction with the Skeleton
or AnimationState
must be deferred until whenReady
resolves.
Dispose
Removing a web component from the DOM does not automatically dispose of it, as it may be intended for reuse elsewhere. To explicitly dispose it, call its dispose()
method. This operation is safe and will not release resources still in use by other web components.
To dispose of all spine-webcomponents
resources, call dispose()
on the overlay instance.
The dispose.html
shows how to use the dispose
function.
Manual overlays
When a <spine-skeleton>
is added to the page, a <spine-overlay>
is automatically appended to contain the WebGL canvas where skeletons are rendered. This overlay spans the entire browser viewport.
Alternatively, a <spine-overlay>
can be manually appended to a specific HTML element. In this case, it inherits the size of its parent. To render a <spine-skeleton>
into a manually defined overlay, assign the same overlay-id
to both the skeleton and the overlay.
Regardless of where the overlay is placed within an HTML element, it will always reposition itself as the last child. This ensures the overlay remains on top of other elements. To prevent unnecessary DOM detachments and reattachments, it is recommended to place the overlay as the last element inside the desired container.
Manual overlay creation is useful in these scenarios:
- Scrollable containers
- The skeleton may overflow the container until the container element is fully visible.
- The skeleton may scroll with some lag, especially on displays with low refresh rates.
- Fixed/sticky containers
- The skeleton may scroll in a jerky or uneven manner.
- Custom overlay placement
- Useful when a smaller overlay is needed, or when the overlay should not be appended directly to the
<body>
, but to a specific container node.
- Useful when a smaller overlay is needed, or when the overlay should not be appended directly to the
These issues can be observed in the example below. The first scrollable list uses the default overlay, while the second uses a dedicated overlay inside the scrollable <div>
. Clicking the button demonstrates the jerky scrolling behavior by setting the position of the container <div>
to fixed.
<button id="popup-overlay-button-open">Set fixed position</button>
<div style="height: 250px; display: flex;">
<div id="fixed" style="display: flex;">
<div style="overflow-y: auto; width: 150px; height: 250px; border: 1px solid black; padding: 1px; background: white;">
<script>
for (let i = 0; i < 6; i++)
document.currentScript.parentElement.insertAdjacentHTML('beforeend', `
<spine-skeleton style="height:80px; width: 120px; border: 1px solid black;"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
></spine-skeleton>`);
</script>
</div>
<div style="overflow-y: auto; width: 150px; height: 250px; border: 1px solid black; padding: 1px; background: white;">
<spine-overlay overlay-id="scroll"></spine-overlay>
<script>
for (let i = 0; i < 6; i++)
document.currentScript.parentElement.insertAdjacentHTML('beforeend', `
<spine-skeleton style="height:80px; width: 120px; border: 1px solid black;"
overlay-id="scroll"
atlas="/files/spine-widget/assets/spineboy-pma.atlas"
skeleton="/files/spine-widget/assets/spineboy-pro.skel"
animation="walk"
></spine-skeleton>`);
</script>
</div>
</div>
</div>
</div>
const openPopupButton = document.getElementById('popup-overlay-button-open');
const popupOverlay = document.getElementById('fixed');
openPopupButton.addEventListener('click', function() {
if (positionFixed) {
popupOverlay.style.position = "";
popupOverlay.style.top = "";
popupOverlay.style.background = "";
openPopupButton.innerText = "Set fixed position";
} else {
popupOverlay.style.position = 'fixed';
popupOverlay.style.top = 'calc(50% - 125px)';
popupOverlay.style.background = 'white';
openPopupButton.innerText = "Unset fixed position";
}
positionFixed = !positionFixed;
});
Notice that each overlay creates its own WebGL contex, which counts against the maximum number of allowed WebGL contexts.