mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-20 17:26:01 +08:00
* [ts] poc wegl overlay * [ts] poc wegl overlay * canvas4 * overlay 4 * overlay * overlay fix scroll cut * Zoom fix - WIP horizontal scroll bug see comment at line 495 * Scroll should be resolved * web component * Loading * mostly work - when overflow top/bottom...are > 0, widgets slightly jump on up and down fast scroll. * Renamed * Add OffScreenUpdateBehaviour * Fixed loading spinner example * Translate is done each frame through requestAnimationFrame. * Added clip. * Removed SpineCanvasApp dependency. * Removed code duplication for clipping into div * Refactor drag logic. Drag works with clip too. * WIP - Doc before refactor * WIP - Refactor + animation and skin attribute change will reinit the widget * Modified export script to copy chibi * Fix zoom bug due to screen size remaining the same at different zoom level. * Removed useless changes on other classes. * Removed custom LoadingSpinnerWindget class. Added a new specific method in LoadingSpinner. * Fix 1 pixel misalignment in non clip mode. * Cleaned webcomponent example. * Made span fps display configurable. * Made some changes to make it work on old browsers. * Manage lifecycle for Input and SpineWebComponentOverlay. Missing SpineWebComponentWidget. * Managed lifecycle for SpineWebComponentWidget. * Fixed input remove listener. Run tsfmt. * Fixed infinite requestAnimationFrame calls. * Fix x-axis and y-axis not working in clip mode. * Fix widget using overlay before overlay webcomponent is fully upgraded. * Restore resize observer since the window resize event does not fire on body resize, but on window resize. It's supported by 93% of the browsers. * Add padding attributes/properties. * Exposed parameters to set bounds. Deeply changed how bounds work, especially for the fact that they are not auto recalculated anymore if the animation is changed (unless autoRecalculateBounds is set to true). * Changes to make the widget more dynamic while changing attributes. See now webcomponent-gui.html. * Initial support of spine-widget into scrollable containers, and overlay-id for multiple spine-overlay. working * WIP - Added/Changed: - overlay loading: now overlay is moved as the last element from where it is inserted to avoid widgets covered by backgrounds of html elements after it. - default overlayId - widget position in overlay coordinates (worldX, worldY) (experimental) - cursor position on widget world (cursorWorldX, cursorWorldY) (experimental) - jsonSkeletonKey: allow to load a specific skeleton in json containing multiple skeletons - onViewportManualStart: start the widget when in manual-start and enters the viewport the first time - overlayAssignedPromise: a promise that resolves when the overlay is assigned to the widget. Reads the comment on it - appendTo: to append the widget created using js and wait for the overlayAssignedPromise to resolve - changed how loadingPromise works - added cursorCanvasX, cursorCanvasY, cursorWorldX, cursorWorldY to overlay (experimental) * Docs and minor fixes. * Fixed example. * Allow multiple widgets for the same HTMLElement * Simplified clip to div by using scissor rather than changing viewport+camera. In this way we can treat coordinates equally for cliped and not clipped widgets! * Simplified drag calculation and drag debug removing an additional div. * WIP - Add interactivity events. isdraggable is currently broken. * Add interactivity events. * Fixed a bug where an infinite loop occurred in Firefox when compareDocumentPosition results in DOCUMENT_POSITION_DISCONNECTED. In both Chrome and Firefox, when an element is inside a webcomponent the comparison results in DOCUMENT_POSITION_DISCONNECTED. But in Firefox the element result in DOCUMENT_POSITION_FOLLOWING too, leading to an infinite loop. * Fix physicsTranslate y opposite direction while dragging. * Add followSlot method * Add animations, animations-bound and default-mix attributes. * Original event is passed to bounds and slot callbacks. * Made overlay canvas size consistent across different browsers. Base size is not anymore the screen size due to browser limitations, but the window size. This will trigger additional canvas resize on window resize. * Reduce DPI if canvas is too big to avoid page crash - this happen on webpage on mobile with high dpi and missing meta viewport tag with width=device-width. * Pma properties on atlas is used to detect pma textures. * Add rawData attribute to pass s stringified JSON object for inline base64 assets. * format * Prevent useless resize. * Prevent error on disconnected callback * Update tutorial. * Fixed multiple click events on mobile touches. Add team example. * Fixed slot interaction issue. * Resize overlay when follow slot element is added. * Add interactive widget example. * Temporarily add windmill only manually to webgl assets * Move getBounding in scrollable case. * formatter * Fixed overlay disconnectedCallback. * Overlay should load not for DOMContentLoaded event only if document has already complete loading. * Overlay parent bounding box determined only if necessary. * Overlay needs to consider border if it's scrollable. * Fixed issues with slot events on mobile. * food app and cleanup * Make overlayAssignedPromise private. * Change scrollable to appendedToBody, make it private and determine it at dom connect. * scrollable-tweak-off changed to no-auto-parent-transform. * Update bounds and slot interaction method names. * getHTMLElementReference to getHostElement. * recalculateBounds and autoRecalculateBounds to calculateBounds and autoCalculateBounds. * dpi to dpr and transform DPR to DevicePixelRatio when needed. Add @internal tag to internal fields. * Properties rename. * Better comments and variable names for canvas resize related stuff. * callbacks renamed consistently. * for each to for of. * Update other names. * onScreenManualStart to startWhenVisible. startWhenVisible set manualStart to true. * fix return to continue in for of. * Refcounter for asset manager and gl resources disposal for webcomponent. * Fixes to asset manager ref counter. * Add dispose example. * Fix overlay init not completed in some scenario. DOMContentLoaded is invoked only when document.readyState is interactive. * Cleaned up examples. * spinner to no-spinner. * Move webcomponent to spine-widget folder. * start won't reset bounds. * Remove test file. * Moved team example in its own page. * Improved tutorial explanations. * Moved some examples in their own pages. * Formatter. * Widgets dragged can be dragged even if host is offscreen. * General refactor. spine-widget to spine-skeleton. * spine-widget package renamed to spine-webcomponents. * Moved assets to a single assets folder. * Run formatter. * Add beta notice. * Changed widget occurrences to webcomponents.
383 lines
13 KiB
HTML
383 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
</head>
|
|
<script src="../dist/iife/spine-webgl.js"></script>
|
|
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body,
|
|
html {
|
|
height: 100%
|
|
}
|
|
|
|
canvas {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
</style>
|
|
|
|
<body>
|
|
<canvas id="canvas"></canvas>
|
|
<center>
|
|
<div style="color: #fff; position: fixed; top: 0; width: 100%">
|
|
<span>Format:</span><select id="formatList"></select>
|
|
<span>Skeleton:</span><select id="skeletonList"></select>
|
|
<span>Animation:</span><select id="animationList"></select>
|
|
<span>Skin:</span><select id="skinList"></select>
|
|
<span>Debug:</span><input type="checkbox" id="debug">
|
|
</div>
|
|
</center>
|
|
<script>
|
|
|
|
let canvas;
|
|
let ctx;
|
|
let shader;
|
|
let batcher;
|
|
let mvp = new spine.Matrix4();
|
|
let skeletonRenderer;
|
|
let assetManager;
|
|
|
|
let debugRenderer;
|
|
let shapes;
|
|
|
|
let lastFrameTime;
|
|
let skeletons = {};
|
|
let format = "JSON";
|
|
let activeSkeleton = "spineboy";
|
|
|
|
function init() {
|
|
// Create the managed WebGL context. Managed contexts will restore resources like shaders
|
|
// and buffers automatically if the WebGL context is lost.
|
|
canvas = document.getElementById("canvas");
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
let config = { alpha: false };
|
|
ctx = new spine.ManagedWebGLRenderingContext(canvas, config);
|
|
if (!ctx.gl) {
|
|
alert('WebGL is unavailable.');
|
|
return;
|
|
}
|
|
|
|
// Create a simple shader, mesh, model-view-projection matrix, SkeletonRenderer, and AssetManager.
|
|
shader = spine.Shader.newTwoColoredTextured(ctx);
|
|
batcher = new spine.PolygonBatcher(ctx);
|
|
mvp.ortho2d(0, 0, canvas.width - 1, canvas.height - 1);
|
|
skeletonRenderer = new spine.SkeletonRenderer(ctx);
|
|
assetManager = new spine.AssetManager(ctx, "/assets/");
|
|
|
|
// Create a debug renderer and the ShapeRenderer it needs to render lines.
|
|
debugRenderer = new spine.SkeletonDebugRenderer(ctx);
|
|
debugRenderer.drawRegionAttachments = true;
|
|
debugRenderer.drawBoundingBoxes = true;
|
|
debugRenderer.drawMeshHull = true;
|
|
debugRenderer.drawMeshTriangles = true;
|
|
debugRenderer.drawPaths = true;
|
|
debugShader = spine.Shader.newColored(ctx);
|
|
shapes = new spine.ShapeRenderer(ctx);
|
|
|
|
// Tell AssetManager to load the resources for each skeleton, including the exported data file, the .atlas file and the .png
|
|
// file for the atlas. We then wait until all resources are loaded in the load() method.
|
|
assetManager.loadBinary("spineboy-pro.skel");
|
|
assetManager.loadText("spineboy-pro.json");
|
|
assetManager.loadTextureAtlas("spineboy-pma.atlas");
|
|
assetManager.loadBinary("raptor-pro.skel");
|
|
assetManager.loadText("raptor-pro.json");
|
|
assetManager.loadTextureAtlas("raptor-pma.atlas");
|
|
assetManager.loadBinary("tank-pro.skel");
|
|
assetManager.loadText("tank-pro.json");
|
|
assetManager.loadTextureAtlas("tank-pma.atlas");
|
|
assetManager.loadBinary("goblins-pro.skel");
|
|
assetManager.loadText("goblins-pro.json");
|
|
assetManager.loadTextureAtlas("goblins-pma.atlas");
|
|
assetManager.loadBinary("vine-pro.skel");
|
|
assetManager.loadText("vine-pro.json");
|
|
assetManager.loadTextureAtlas("vine-pma.atlas");
|
|
assetManager.loadBinary("stretchyman-pro.skel");
|
|
assetManager.loadText("stretchyman-pro.json");
|
|
assetManager.loadTextureAtlas("stretchyman-pma.atlas");
|
|
assetManager.loadBinary("coin-pro.skel");
|
|
assetManager.loadText("coin-pro.json");
|
|
assetManager.loadTextureAtlas("coin-pma.atlas");
|
|
assetManager.loadBinary("mix-and-match-pro.skel");
|
|
assetManager.loadText("mix-and-match-pro.json");
|
|
assetManager.loadTextureAtlas("mix-and-match-pma.atlas");
|
|
requestAnimationFrame(load);
|
|
}
|
|
|
|
function load() {
|
|
// Wait until the AssetManager has loaded all resources, then load the skeletons.
|
|
if (assetManager.isLoadingComplete()) {
|
|
skeletons = {
|
|
coin: {
|
|
Binary: loadSkeleton("coin-pro.skel", "animation", true),
|
|
JSON: loadSkeleton("coin-pro.json", "animation", true)
|
|
},
|
|
goblins: {
|
|
Binary: loadSkeleton("goblins-pro.skel", "walk", true, "goblin"),
|
|
JSON: loadSkeleton("goblins-pro.json", "walk", true, "goblin")
|
|
},
|
|
"mix-and-match-pro": {
|
|
Binary: loadSkeleton("mix-and-match-pro.skel", "dance", true, "full-skins/girl-blue-cape"),
|
|
JSON: loadSkeleton("mix-and-match-pro.json", "dance", true, "full-skins/girl-blue-cape")
|
|
},
|
|
raptor: {
|
|
Binary: loadSkeleton("raptor-pro.skel", "walk", true),
|
|
JSON: loadSkeleton("raptor-pro.json", "walk", true)
|
|
},
|
|
spineboy: {
|
|
Binary: loadSkeleton("spineboy-pro.skel", "run", true),
|
|
JSON: loadSkeleton("spineboy-pro.json", "run", true)
|
|
},
|
|
stretchyman: {
|
|
Binary: loadSkeleton("stretchyman-pro.skel", "sneak", true),
|
|
JSON: loadSkeleton("stretchyman-pro.json", "sneak", true)
|
|
},
|
|
tank: {
|
|
Binary: loadSkeleton("tank-pro.skel", "drive", true),
|
|
JSON: loadSkeleton("tank-pro.json", "drive", true)
|
|
},
|
|
vine: {
|
|
Binary: loadSkeleton("vine-pro.skel", "grow", true),
|
|
JSON: loadSkeleton("vine-pro.json", "grow", true)
|
|
}
|
|
};
|
|
setupUI();
|
|
lastFrameTime = Date.now() / 1000;
|
|
requestAnimationFrame(render); // Loading is done, call render every frame.
|
|
} else
|
|
requestAnimationFrame(load);
|
|
}
|
|
|
|
function loadSkeleton(name, initialAnimation, premultipliedAlpha, skin) {
|
|
if (skin === undefined) skin = "default";
|
|
|
|
// Load the texture atlas using name.atlas from the AssetManager.
|
|
let atlas = assetManager.require(name.replace(/(?:-ess|-pro)\.(skel|json)/, "") + (premultipliedAlpha ? "-pma" : "") + ".atlas");
|
|
|
|
// Create an AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
|
|
let atlasLoader = new spine.AtlasAttachmentLoader(atlas);
|
|
|
|
// Create a skeleton loader instance for parsing the skeleton data file.
|
|
let skeletonLoader = name.endsWith(".skel") ? new spine.SkeletonBinary(atlasLoader) : new spine.SkeletonJson(atlasLoader);
|
|
|
|
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
|
|
skeletonLoader.scale = 1;
|
|
let skeletonData = skeletonLoader.readSkeletonData(assetManager.require(name));
|
|
let skeleton = new spine.Skeleton(skeletonData);
|
|
skeleton.setSkinByName(skin);
|
|
let bounds = calculateSetupPoseBounds(skeleton);
|
|
|
|
// Create an AnimationState, and set the initial animation in looping mode.
|
|
let animationStateData = new spine.AnimationStateData(skeleton.data);
|
|
let animationState = new spine.AnimationState(animationStateData);
|
|
if (name == "spineboy-pro.skel" || name == "spineboy-pro.json") {
|
|
animationStateData.setMix("walk", "run", 1.5)
|
|
animationStateData.setMix("run", "jump", 0.2)
|
|
animationStateData.setMix("jump", "run", 0.4);
|
|
animationState.setEmptyAnimation(0, 0);
|
|
let entry = animationState.addAnimation(0, "walk", true, 0);
|
|
entry.mixDuration = 1;
|
|
animationState.addAnimation(0, "run", true, 1.5);
|
|
animationState.addAnimation(0, "jump", false, 2);
|
|
animationState.addAnimation(0, "run", true, 0);
|
|
animationState.addEmptyAnimation(0, 1, 1);
|
|
entry = animationState.addAnimation(0, "walk", true, 1.5);
|
|
entry.mixDuration = 1;
|
|
} else
|
|
animationState.setAnimation(0, initialAnimation, true);
|
|
|
|
function log(message) {
|
|
if ($('#debug').is(':checked')) console.log(message);
|
|
}
|
|
animationState.addListener({
|
|
start: function (track) {
|
|
log("Animation on track " + track.trackIndex + " started");
|
|
},
|
|
interrupt: function (track) {
|
|
log("Animation on track " + track.trackIndex + " interrupted");
|
|
},
|
|
end: function (track) {
|
|
log("Animation on track " + track.trackIndex + " ended");
|
|
},
|
|
disposed: function (track) {
|
|
log("Animation on track " + track.trackIndex + " disposed");
|
|
},
|
|
complete: function (track) {
|
|
log("Animation on track " + track.trackIndex + " completed");
|
|
},
|
|
event: function (track, event) {
|
|
log("Event on track " + track.trackIndex + ": " + JSON.stringify(event));
|
|
}
|
|
})
|
|
|
|
// Pack everything up and return to caller.
|
|
return { skeleton: skeleton, state: animationState, bounds: bounds, premultipliedAlpha: premultipliedAlpha };
|
|
}
|
|
|
|
function calculateSetupPoseBounds(skeleton) {
|
|
skeleton.setToSetupPose();
|
|
skeleton.updateWorldTransform(spine.Physics.update);
|
|
let offset = new spine.Vector2();
|
|
let size = new spine.Vector2();
|
|
skeleton.getBounds(offset, size, []);
|
|
return { offset: offset, size: size };
|
|
}
|
|
|
|
function setupUI() {
|
|
let formatList = $("#formatList");
|
|
formatList.append($("<option>Binary</option>"));
|
|
formatList.append($("<option selected>JSON</option>"));
|
|
let skeletonList = $("#skeletonList");
|
|
for (let skeletonName in skeletons) {
|
|
let option = $("<option></option>");
|
|
option.attr("value", skeletonName).text(skeletonName);
|
|
if (skeletonName === activeSkeleton) option.attr("selected", "selected");
|
|
skeletonList.append(option);
|
|
}
|
|
let setupAnimationUI = function () {
|
|
let animationList = $("#animationList");
|
|
animationList.empty();
|
|
let skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
let state = skeletons[activeSkeleton][format].state;
|
|
let activeAnimation = state.tracks[0].animation.name;
|
|
for (let i = 0; i < skeleton.data.animations.length; i++) {
|
|
let name = skeleton.data.animations[i].name;
|
|
let option = $("<option></option>");
|
|
option.attr("value", name).text(name);
|
|
if (name === activeAnimation) option.attr("selected", "selected");
|
|
animationList.append(option);
|
|
}
|
|
|
|
animationList.change(function () {
|
|
let state = skeletons[activeSkeleton][format].state;
|
|
let skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
let animationName = $("#animationList option:selected").text();
|
|
skeleton.setToSetupPose();
|
|
state.setAnimation(0, animationName, true);
|
|
})
|
|
}
|
|
|
|
let setupSkinUI = function () {
|
|
let skinList = $("#skinList");
|
|
skinList.empty();
|
|
let skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
let activeSkin = skeleton.skin == null ? "default" : skeleton.skin.name;
|
|
for (let i = 0; i < skeleton.data.skins.length; i++) {
|
|
let name = skeleton.data.skins[i].name;
|
|
let option = $("<option></option>");
|
|
option.attr("value", name).text(name);
|
|
if (name === activeSkin) option.attr("selected", "selected");
|
|
skinList.append(option);
|
|
}
|
|
|
|
skinList.change(function () {
|
|
let skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
let skinName = $("#skinList option:selected").text();
|
|
skeleton.setSkinByName(skinName);
|
|
skeleton.setSlotsToSetupPose();
|
|
})
|
|
}
|
|
|
|
skeletonList.change(function () {
|
|
activeSkeleton = $("#skeletonList option:selected").text();
|
|
setupAnimationUI();
|
|
setupSkinUI();
|
|
})
|
|
|
|
formatList.change(function () {
|
|
format = $("#formatList option:selected").text();
|
|
setupAnimationUI();
|
|
setupSkinUI();
|
|
})
|
|
|
|
setupAnimationUI();
|
|
setupSkinUI();
|
|
}
|
|
|
|
function render() {
|
|
let gl = ctx.gl;
|
|
let now = Date.now() / 1000;
|
|
let delta = now - lastFrameTime;
|
|
lastFrameTime = now;
|
|
|
|
// Update the MVP matrix to adjust for canvas size changes
|
|
resize();
|
|
|
|
gl.clearColor(0.3, 0.3, 0.3, 1);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
// Apply the animation state based on the delta time.
|
|
let skeleton = skeletons[activeSkeleton][format].skeleton;
|
|
let state = skeletons[activeSkeleton][format].state;
|
|
let bounds = skeletons[activeSkeleton][format].bounds;
|
|
let premultipliedAlpha = skeletons[activeSkeleton][format].premultipliedAlpha;
|
|
state.update(delta);
|
|
state.apply(skeleton);
|
|
skeleton.updateWorldTransform(spine.Physics.update);
|
|
|
|
// Bind the shader and set the texture and model-view-projection matrix.
|
|
shader.bind();
|
|
shader.setUniformi(spine.Shader.SAMPLER, 0);
|
|
shader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp.values);
|
|
|
|
// Start the batch and tell the SkeletonRenderer to render the active skeleton.
|
|
batcher.begin(shader);
|
|
|
|
skeletonRenderer.premultipliedAlpha = premultipliedAlpha;
|
|
skeletonRenderer.draw(batcher, skeleton);
|
|
batcher.end();
|
|
|
|
shader.unbind();
|
|
|
|
// Draw debug information.
|
|
let debug = $('#debug').is(':checked');
|
|
if (debug) {
|
|
debugShader.bind();
|
|
debugShader.setUniform4x4f(spine.Shader.MVP_MATRIX, mvp.values);
|
|
debugRenderer.premultipliedAlpha = premultipliedAlpha;
|
|
shapes.begin(debugShader);
|
|
debugRenderer.draw(shapes, skeleton);
|
|
shapes.end();
|
|
debugShader.unbind();
|
|
}
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
function resize() {
|
|
let w = canvas.clientWidth;
|
|
let h = canvas.clientHeight;
|
|
if (canvas.width != w || canvas.height != h) {
|
|
canvas.width = w;
|
|
canvas.height = h;
|
|
}
|
|
|
|
// Calculations to center the skeleton in the canvas.
|
|
let bounds = skeletons[activeSkeleton][format].bounds;
|
|
let centerX = bounds.offset.x + bounds.size.x / 2;
|
|
let centerY = bounds.offset.y + bounds.size.y / 2;
|
|
let scaleX = bounds.size.x / canvas.width;
|
|
let scaleY = bounds.size.y / canvas.height;
|
|
let scale = Math.max(scaleX, scaleY) * 2;
|
|
if (scale < 1) scale = 1;
|
|
let width = canvas.width * scale;
|
|
let height = canvas.height * scale;
|
|
|
|
mvp.ortho2d(centerX - width / 2, centerY - height / 2, width, height);
|
|
ctx.gl.viewport(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
init();
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html> |