[ts] Added dress-up example

Shows how to render skins to thumbnails which can then be used in an HTML UI.
This commit is contained in:
badlogic 2021-09-07 15:38:23 +02:00
parent 68413182fb
commit ad41761293
4 changed files with 249 additions and 4 deletions

View File

@ -27,6 +27,7 @@
<li><a href="/spine-webgl/example">Example</a></li> <li><a href="/spine-webgl/example">Example</a></li>
<li><a href="/spine-webgl/example/barebones.html">Barebones</a></li> <li><a href="/spine-webgl/example/barebones.html">Barebones</a></li>
<li><a href="/spine-webgl/example/mix-and-match.html">Mix &amp; match</a></li> <li><a href="/spine-webgl/example/mix-and-match.html">Mix &amp; match</a></li>
<li><a href="/spine-webgl/example/dress-up.html">Dress-up</a></li>
<li><a href="/spine-webgl/demos/additiveblending.html">Additive blending</a></li> <li><a href="/spine-webgl/demos/additiveblending.html">Additive blending</a></li>
<li><a href="/spine-webgl/demos/clipping.html">Clipping</a></li> <li><a href="/spine-webgl/demos/clipping.html">Clipping</a></li>
<li><a href="/spine-webgl/demos/hoverboard.html">Hoverboard</a></li> <li><a href="/spine-webgl/demos/hoverboard.html">Hoverboard</a></li>

View File

@ -11,8 +11,10 @@
<canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas> <canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
<script> <script>
class App { class App {
skeleton; constructor() {
animationState; this.skeleton = null;
this.animationState = null;
}
loadAssets(canvas) { loadAssets(canvas) {
// Load the skeleton file. // Load the skeleton file.

View File

@ -0,0 +1,239 @@
<html>
<script src="../dist/iife/spine-webgl.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
}
#container {
display: flex;
width: 100%;
height: 100vh;
}
#canvas {
flex-grow: 1;
}
#skins {
flex-shrink: 0;
overflow: scroll;
}
#skins>img {
display: block;
}
</style>
<body>
<div id="container">
<div id="skins"></div>
<canvas id="canvas"></canvas>
</div>
<script>
// Define the class running in the Spine canvas
class App {
constructor() {
this.canvas = null;
this.atlas = null;
this.skeletonData = null;
this.skeleton = null;
this.state = null;
this.selectedSkins = [];
this.skinThumbnails = {};
}
loadAssets(canvas) {
canvas.assetManager.AnimationState
canvas.assetManager.loadTextureAtlas("mix-and-match-pma.atlas");
canvas.assetManager.loadBinary("mix-and-match-pro.skel");
}
initialize(canvas) {
this.canvas = canvas;
let assetManager = canvas.assetManager;
// Create the atlas
this.atlas = canvas.assetManager.require("mix-and-match-pma.atlas");
let atlasLoader = new spine.AtlasAttachmentLoader(this.atlas);
// Create the skeleton
let skeletonBinary = new spine.SkeletonBinary(atlasLoader);
this.skeletonData = skeletonBinary.readSkeletonData(assetManager.require("mix-and-match-pro.skel"));
this.skeleton = new spine.Skeleton(this.skeletonData);
// Create the animation state
let stateData = new spine.AnimationStateData(this.skeletonData);
this.state = new spine.AnimationState(stateData);
this.state.setAnimation(0, "dance", true);
// Create the user interface to selecting skins
this.createUI(canvas);
// Create a default skin.
this.addSkin("skin-base");
this.addSkin("nose/short");
this.addSkin("eyelids/girly");
this.addSkin("eyes/violet");
this.addSkin("hair/brown");
this.addSkin("clothes/hoodie-orange");
this.addSkin("legs/pants-jeans");
this.addSkin("accessories/bag");
this.addSkin("accessories/hat-red-yellow");
}
addSkin(skinName) {
if (this.selectedSkins.indexOf(skinName) != -1) return;
this.selectedSkins.push(skinName);
let thumbnail = this.skinThumbnails[skinName];
thumbnail.isSet = true;
thumbnail.style.filter = "none";
this.updateSkin();
}
removeSkin(skinName) {
let index = this.selectedSkins.indexOf(skinName);
if (index == -1) return;
this.selectedSkins.splice(index, 1);
let thumbnail = this.skinThumbnails[skinName];
thumbnail.isSet = false;
thumbnail.style.filter = "grayscale(1)";
this.updateSkin();
}
updateSkin() {
// Create a new skin from all the selected skins.
let newSkin = new spine.Skin("custom-skin");
for (var skinName of this.selectedSkins) {
newSkin.addSkin(this.skeletonData.findSkin(skinName));
}
this.skeleton.setSkin(newSkin);
this.skeleton.setToSetupPose();
// Center and zoom the camera
let offset = new spine.Vector2(), size = new spine.Vector2();
this.skeleton.getBounds(offset, size);
let camera = this.canvas.renderer.camera;
camera.position.x = offset.x + size.x / 2;
camera.position.y = offset.y + size.y / 2;
camera.zoom = size.x > size.y ? size.x / this.canvas.htmlCanvas.width * 1.1 : size.y / this.canvas.htmlCanvas.height * 1.1;
camera.update();
}
createUI(canvas) {
const THUMBNAIL_SIZE = 300;
// We'll reuse the webgl context used to render the skeleton for
// thumbnail generation. We temporarily resize it to 300x300 pixels
// Note: we passed `preserveDrawingBuffer: true` to the SpineCanvas
// constructor. Without it, we could not fetch the pixel data from
// the canvas after rendering.
let oldWidth = canvas.htmlCanvas.width;
let oldHeight = canvas.htmlCanvas.height;
canvas.htmlCanvas.width = canvas.htmlCanvas.height = THUMBNAIL_SIZE;
canvas.gl.viewport(0, 0, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
// For each skin, generate at thumbnail as follows
// 1. Set it on the skeleton
// 2. Determine its bounds
// 3. Center and scale it to the offscreen canvas and render it
// 4. Fetch the rendered image from the canvas and store it.
let images = [];
for (var skin of this.skeletonData.skins) {
// Skip the empty default skin
if (skin.name === "default") continue;
// Set the skin, then update the skeleton
// to the setup pose and calculate the world transforms
this.skeleton.setSkin(skin);
this.skeleton.setToSetupPose();
this.skeleton.updateWorldTransform();
// Calculate the bounding box enclosing the skeleton.
let offset = new spine.Vector2(), size = new spine.Vector2();
this.skeleton.getBounds(offset, size);
// Position the renderer camera on the center of the bounds, and
// set the zoom so the full skin is visible within the 300x300
// rendering area. We leave 10% of empty space around a skin in the
// thumbnail, hence the multiplication of 1.1 for the zoom factor.
canvas.renderer.camera.position.x = offset.x + size.x / 2;
canvas.renderer.camera.position.y = offset.y + size.y / 2;
canvas.renderer.camera.zoom = size.x > size.y ? size.x / THUMBNAIL_SIZE * 1.1 : size.y / THUMBNAIL_SIZE * 1.1;
canvas.renderer.camera.setViewport(THUMBNAIL_SIZE, THUMBNAIL_SIZE);
canvas.renderer.camera.update();
// Clear the canvas and render the skeleton
canvas.clear(0.5, 0.5, 0.5, 1);
canvas.renderer.begin();
canvas.renderer.drawSkeleton(this.skeleton, true);
canvas.renderer.end();
// Get the image data and convert it to an img element
let image = new Image();
image.src = canvas.htmlCanvas.toDataURL();
image.skinName = skin.name;
image.isSet = false;
image.style.filter = "grayscale(1)";
// Set up a click listener that will add/remove the skin
image.onclick = () => {
if (image.isSet) this.removeSkin(image.skinName);
else this.addSkin(image.skinName);
}
// Store the thumbail image in the list of all skin
// thumbnails.
images.push(image);
this.skinThumbnails[image.skinName] = image;
}
// Sort the list of skin thumbnails by name, so items
// from the same folder end up next to each other.
images.sort((a, b) => {
return a.skinName > b.skinName ? 1 : -1;
});
// Add the thumbnails to the skins <div>
let skinsDiv = document.getElementById("skins");
for (var thumbnail of images) {
skinsDiv.appendChild(thumbnail);
}
// Restore the canvas size and camera of the renderer
canvas.htmlCanvas.width = oldWidth;
canvas.htmlCanvas.height = oldHeight;
canvas.renderer.resize(spine.ResizeMode.Expand);
canvas.renderer.camera.position.x = 0;
canvas.renderer.camera.position.y = 0;
canvas.renderer.camera.zoom = 1;
}
update(canvas, delta) {
this.state.update(delta);
this.state.apply(this.skeleton);
this.skeleton.updateWorldTransform();
}
render(canvas) {
let renderer = canvas.renderer;
renderer.resize(spine.ResizeMode.Expand);
canvas.clear(0.2, 0.2, 0.2, 1);
renderer.begin();
renderer.drawSkeleton(this.skeleton, true);
renderer.end();
}
}
// Create the Spine canvas which runs the app
new spine.SpineCanvas(document.getElementById("canvas"), {
pathPrefix: "assets/",
app: new App()
});
</script>
</body>
</html>

View File

@ -52,7 +52,9 @@ export interface SpineCanvasConfig {
/* The {@link SpineCanvasApp} to be run in the canvas. */ /* The {@link SpineCanvasApp} to be run in the canvas. */
app: SpineCanvasApp; app: SpineCanvasApp;
/* The path prefix to be used by the {@link AssetManager}. */ /* The path prefix to be used by the {@link AssetManager}. */
pathPrefix: string; pathPrefix?: string;
/* The WebGL context configuration */
webglConfig?: any;
} }
/** Manages the life-cycle and WebGL context of a {@link SpineCanvasApp}. The app loads /** Manages the life-cycle and WebGL context of a {@link SpineCanvasApp}. The app loads
@ -83,9 +85,10 @@ export class SpineCanvas {
render: () => { }, render: () => { },
error: () => { }, error: () => { },
} }
if (config.webglConfig === undefined) config.webglConfig = { alpha: true };
this.htmlCanvas = canvas; this.htmlCanvas = canvas;
this.context = new ManagedWebGLRenderingContext(canvas, { alpha: true }); this.context = new ManagedWebGLRenderingContext(canvas, config.webglConfig);
this.renderer = new SceneRenderer(canvas, this.context); this.renderer = new SceneRenderer(canvas, this.context);
this.gl = this.context.gl; this.gl = this.context.gl;
this.assetManager = new AssetManager(this.context, config.pathPrefix); this.assetManager = new AssetManager(this.context, config.pathPrefix);