com.esotericsoftware.spine.SkeletonViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.esotericsoftware.spine.SkeletonViewer.java

Source

/******************************************************************************
 * Spine Runtimes Software License
 * Version 2.1
 * 
 * Copyright (c) 2013, Esoteric Software
 * All rights reserved.
 * 
 * You are granted a perpetual, non-exclusive, non-sublicensable and
 * non-transferable license to install, execute and perform the Spine Runtimes
 * Software (the "Software") solely for internal use. Without the written
 * permission of Esoteric Software (typically granted by licensing Spine), you
 * may not (a) modify, translate, adapt or otherwise create derivative works,
 * improvements of the Software or develop new applications using the Software
 * or (b) remove, delete, alter or obscure any trademarks or any copyright,
 * trademark, patent or other intellectual property or proprietary rights
 * notices on or in the Software, including any copy thereof. Redistributions
 * in binary or source form must include this license and terms.
 * 
 * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL ESOTERIC SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************/

package com.esotericsoftware.spine;

import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;

import java.awt.FileDialog;
import java.awt.Frame;
import java.io.File;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.InputMultiplexer;
import com.badlogic.gdx.Preferences;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.List;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.ui.Slider;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup;
import com.badlogic.gdx.scenes.scene2d.ui.Window;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.esotericsoftware.spine.AnimationState.TrackEntry;

public class SkeletonViewer extends ApplicationAdapter {
    static final float checkModifiedInterval = 0.250f;
    static final float reloadDelay = 1;

    UI ui;

    PolygonSpriteBatch batch;
    SkeletonRenderer renderer;
    SkeletonRendererDebug debugRenderer;
    SkeletonData skeletonData;
    Skeleton skeleton;
    AnimationState state;
    int skeletonX, skeletonY;
    FileHandle skeletonFile;
    long lastModified;
    float lastModifiedCheck, reloadTimer;

    public void create() {
        ui = new UI();
        batch = new PolygonSpriteBatch();
        renderer = new SkeletonRenderer();
        debugRenderer = new SkeletonRendererDebug();
        skeletonX = (int) (ui.window.getWidth() + (Gdx.graphics.getWidth() - ui.window.getWidth()) / 2);
        skeletonY = Gdx.graphics.getHeight() / 4;

        loadSkeleton(Gdx.files.internal(
                Gdx.app.getPreferences("spine-skeletontest").getString("lastFile", "spineboy/spineboy.json")),
                false);
    }

    void loadSkeleton(FileHandle skeletonFile, boolean reload) {
        if (skeletonFile == null)
            return;

        // A regular texture atlas would normally usually be used. This returns a white image for images not found in the atlas.
        Pixmap pixmap = new Pixmap(32, 32, Format.RGBA8888);
        pixmap.setColor(new Color(1, 1, 1, 0.33f));
        pixmap.fill();
        final AtlasRegion fake = new AtlasRegion(new Texture(pixmap), 0, 0, 32, 32);
        pixmap.dispose();

        String atlasFileName = skeletonFile.nameWithoutExtension();
        if (atlasFileName.endsWith(".json"))
            atlasFileName = new FileHandle(atlasFileName).nameWithoutExtension();
        FileHandle atlasFile = skeletonFile.sibling(atlasFileName + ".atlas");
        if (!atlasFile.exists())
            atlasFile = skeletonFile.sibling(atlasFileName + ".atlas.txt");
        TextureAtlasData data = !atlasFile.exists() ? null
                : new TextureAtlasData(atlasFile, atlasFile.parent(), false);
        TextureAtlas atlas = new TextureAtlas(data) {
            public AtlasRegion findRegion(String name) {
                AtlasRegion region = super.findRegion(name);
                return region != null ? region : fake;
            }
        };

        try {
            String extension = skeletonFile.extension();
            if (extension.equalsIgnoreCase("json") || extension.equalsIgnoreCase("txt")) {
                SkeletonJson json = new SkeletonJson(atlas);
                json.setScale(ui.scaleSlider.getValue());
                skeletonData = json.readSkeletonData(skeletonFile);
            } else {
                SkeletonBinary binary = new SkeletonBinary(atlas);
                binary.setScale(ui.scaleSlider.getValue());
                skeletonData = binary.readSkeletonData(skeletonFile);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            ui.toast("Error loading skeleton: " + skeletonFile.name());
            lastModifiedCheck = 5;
            return;
        }

        skeleton = new Skeleton(skeletonData);
        skeleton.setToSetupPose();
        skeleton = new Skeleton(skeleton);
        skeleton.updateWorldTransform();

        state = new AnimationState(new AnimationStateData(skeletonData));

        this.skeletonFile = skeletonFile;
        Preferences prefs = Gdx.app.getPreferences("spine-skeletontest");
        prefs.putString("lastFile", skeletonFile.path());
        prefs.flush();
        lastModified = skeletonFile.lastModified();
        lastModifiedCheck = checkModifiedInterval;

        // Populate UI.

        ui.skeletonLabel.setText(skeletonFile.name());
        {
            Array<String> items = new Array();
            for (Skin skin : skeletonData.getSkins())
                items.add(skin.getName());
            ui.skinList.setItems(items);
        }
        {
            Array<String> items = new Array();
            for (Animation animation : skeletonData.getAnimations())
                items.add(animation.getName());
            ui.animationList.setItems(items);
        }

        // Configure skeleton from UI.

        skeleton.setSkin(ui.skinList.getSelected());
        state.setAnimation(0, ui.animationList.getSelected(), ui.loopCheckbox.isChecked());

        if (reload)
            ui.toast("Reloaded.");
    }

    public void render() {
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        float delta = Gdx.graphics.getDeltaTime();

        if (skeleton != null) {
            if (reloadTimer <= 0) {
                lastModifiedCheck -= delta;
                if (lastModifiedCheck < 0) {
                    lastModifiedCheck = checkModifiedInterval;
                    long time = skeletonFile.lastModified();
                    if (time != 0 && lastModified != time)
                        reloadTimer = reloadDelay;
                }
            } else {
                reloadTimer -= delta;
                if (reloadTimer <= 0)
                    loadSkeleton(skeletonFile, true);
            }

            state.getData().setDefaultMix(ui.mixSlider.getValue());
            renderer.setPremultipliedAlpha(ui.premultipliedCheckbox.isChecked());

            delta = Math.min(delta, 0.032f) * ui.speedSlider.getValue();
            skeleton.update(delta);
            skeleton.setFlip(ui.flipXCheckbox.isChecked(), ui.flipYCheckbox.isChecked());
            if (!ui.pauseButton.isChecked()) {
                state.update(delta);
                state.apply(skeleton);
            }
            skeleton.setPosition(skeletonX, skeletonY);
            // skeleton.setPosition(0, 0);
            // skeleton.getRootBone().setX(skeletonX);
            // skeleton.getRootBone().setY(skeletonY);
            skeleton.updateWorldTransform();

            batch.begin();
            renderer.draw(batch, skeleton);
            batch.end();

            debugRenderer.setBones(ui.debugBonesCheckbox.isChecked());
            debugRenderer.setRegionAttachments(ui.debugRegionsCheckbox.isChecked());
            debugRenderer.setBoundingBoxes(ui.debugBoundingBoxesCheckbox.isChecked());
            debugRenderer.setMeshHull(ui.debugMeshHullCheckbox.isChecked());
            debugRenderer.setMeshTriangles(ui.debugMeshTrianglesCheckbox.isChecked());
            debugRenderer.draw(skeleton);
        }

        ui.stage.act();
        ui.stage.draw();

        // Draw indicator for timeline position.
        if (state != null) {
            ShapeRenderer shapes = debugRenderer.getShapeRenderer();
            TrackEntry entry = state.getCurrent(0);
            if (entry != null) {
                float percent = entry.getTime() / entry.getEndTime();
                if (entry.getLoop())
                    percent %= 1;
                float x = ui.window.getRight() + (Gdx.graphics.getWidth() - ui.window.getRight()) * percent;
                shapes.setColor(Color.CYAN);
                shapes.begin(ShapeType.Line);
                shapes.line(x, 0, x, 20);
                shapes.end();
            }
        }
    }

    public void resize(int width, int height) {
        batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
        debugRenderer.getShapeRenderer().setProjectionMatrix(batch.getProjectionMatrix());
        ui.stage.getViewport().update(width, height, true);
        if (!ui.minimizeButton.isChecked())
            ui.window.setHeight(height);
    }

    class UI {
        Stage stage = new Stage(new ScreenViewport());
        com.badlogic.gdx.scenes.scene2d.ui.Skin skin = new com.badlogic.gdx.scenes.scene2d.ui.Skin(
                Gdx.files.internal("skin/skin.json"));

        Window window = new Window("Skeleton", skin);
        Table root = new Table(skin);
        TextButton browseButton = new TextButton("Browse", skin);
        Label skeletonLabel = new Label("", skin);
        List<String> animationList = new List(skin);
        List<String> skinList = new List(skin);
        CheckBox loopCheckbox = new CheckBox(" Loop", skin);
        CheckBox premultipliedCheckbox = new CheckBox(" Premultiplied", skin);
        Slider mixSlider = new Slider(0f, 2, 0.01f, false, skin);
        Label mixLabel = new Label("0.3", skin);
        Slider speedSlider = new Slider(0.1f, 3, 0.01f, false, skin);
        Label speedLabel = new Label("1.0", skin);
        CheckBox flipXCheckbox = new CheckBox(" X", skin);
        CheckBox flipYCheckbox = new CheckBox(" Y", skin);
        CheckBox debugBonesCheckbox = new CheckBox(" Bones", skin);
        CheckBox debugRegionsCheckbox = new CheckBox(" Regions", skin);
        CheckBox debugBoundingBoxesCheckbox = new CheckBox(" Bounds", skin);
        CheckBox debugMeshHullCheckbox = new CheckBox(" Mesh Hull", skin);
        CheckBox debugMeshTrianglesCheckbox = new CheckBox(" Mesh Triangles", skin);
        Slider scaleSlider = new Slider(0.1f, 3, 0.01f, false, skin);
        Label scaleLabel = new Label("1.0", skin);
        TextButton pauseButton = new TextButton("Pause", skin, "toggle");
        TextButton minimizeButton = new TextButton("-", skin);
        TextButton bonesSetupPoseButton = new TextButton("Bones", skin);
        TextButton slotsSetupPoseButton = new TextButton("Slots", skin);
        TextButton setupPoseButton = new TextButton("Both", skin);
        WidgetGroup toasts = new WidgetGroup();

        public UI() {
            // Configure widgets.

            premultipliedCheckbox.setChecked(true);

            loopCheckbox.setChecked(true);

            scaleSlider.setValue(1);
            scaleSlider.setSnapToValues(new float[] { 1 }, 0.1f);

            mixSlider.setValue(0.3f);

            speedSlider.setValue(1);
            speedSlider.setSnapToValues(new float[] { 1 }, 0.1f);

            window.setMovable(false);
            window.setResizable(false);

            minimizeButton.padTop(-2).padLeft(5);
            minimizeButton.getColor().a = 0.66f;
            window.getButtonTable().add(minimizeButton).size(20, 20);

            ScrollPane skinScroll = new ScrollPane(skinList, skin);
            skinScroll.setFadeScrollBars(false);

            ScrollPane animationScroll = new ScrollPane(animationList, skin);
            animationScroll.setFadeScrollBars(false);

            // Layout.

            root.pad(2, 4, 4, 4).defaults().space(6);
            root.columnDefaults(0).top().right();
            root.columnDefaults(1).left();
            root.row().padTop(6);
            root.add("Skeleton:");
            {
                Table table = table();
                table.add(skeletonLabel).fillX().expandX();
                table.add(browseButton);
                root.add(table).fill().row();
            }
            root.add("Scale:");
            {
                Table table = table();
                table.add(scaleLabel).width(29);
                table.add(scaleSlider).fillX().expandX();
                root.add(table).fill().row();
            }
            root.add("Flip:");
            root.add(table(flipXCheckbox, flipYCheckbox)).row();
            root.add("Debug:");
            root.add(table(debugBonesCheckbox, debugRegionsCheckbox, debugBoundingBoxesCheckbox)).row();
            root.add();
            root.add(table(debugMeshHullCheckbox, debugMeshTrianglesCheckbox)).row();
            root.add("Alpha:");
            root.add(premultipliedCheckbox).row();
            root.add("Skin:");
            root.add(skinScroll).expand().fill().minHeight(75).row();
            root.add("Setup Pose:");
            root.add(table(bonesSetupPoseButton, slotsSetupPoseButton, setupPoseButton)).row();
            root.add("Animation:");
            root.add(animationScroll).expand().fill().minHeight(75).row();
            root.add("Mix:");
            {
                Table table = table();
                table.add(mixLabel).width(29);
                table.add(mixSlider).fillX().expandX();
                root.add(table).fill().row();
            }
            root.add("Speed:");
            {
                Table table = table();
                table.add(speedLabel).width(29);
                table.add(speedSlider).fillX().expandX();
                root.add(table).fill().row();
            }
            root.add("Playback:");
            root.add(table(pauseButton, loopCheckbox)).row();

            window.add(root).expand().fill();
            window.pack();
            stage.addActor(window);

            {
                Table table = new Table(skin);
                table.setFillParent(true);
                table.setTouchable(Touchable.disabled);
                stage.addActor(table);
                table.pad(10).bottom().right();
                table.add(toasts);
                table.debug();
            }

            // Events.

            window.addListener(new InputListener() {
                public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                    event.cancel();
                    return true;
                }
            });

            browseButton.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    FileDialog fileDialog = new FileDialog((Frame) null, "Choose skeleton file");
                    fileDialog.setMode(FileDialog.LOAD);
                    fileDialog.setVisible(true);
                    String name = fileDialog.getFile();
                    String dir = fileDialog.getDirectory();
                    if (name == null || dir == null)
                        return;
                    loadSkeleton(new FileHandle(new File(dir, name).getAbsolutePath()), false);
                }
            });

            setupPoseButton.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    if (skeleton != null)
                        skeleton.setToSetupPose();
                }
            });
            bonesSetupPoseButton.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    if (skeleton != null)
                        skeleton.setBonesToSetupPose();
                }
            });
            slotsSetupPoseButton.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    if (skeleton != null)
                        skeleton.setSlotsToSetupPose();
                }
            });

            minimizeButton.addListener(new ClickListener() {
                public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
                    event.cancel();
                    return super.touchDown(event, x, y, pointer, button);
                }

                public void clicked(InputEvent event, float x, float y) {
                    if (minimizeButton.isChecked()) {
                        window.getCells().get(0).setActor(null);
                        window.setHeight(20);
                        minimizeButton.setText("+");
                    } else {
                        window.getCells().get(0).setActor(root);
                        ui.window.setHeight(Gdx.graphics.getHeight());
                        minimizeButton.setText("-");
                    }
                }
            });

            scaleSlider.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    scaleLabel.setText(Float.toString((int) (scaleSlider.getValue() * 100) / 100f));
                    if (!scaleSlider.isDragging())
                        loadSkeleton(skeletonFile, false);
                }
            });

            speedSlider.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    speedLabel.setText(Float.toString((int) (speedSlider.getValue() * 100) / 100f));
                }
            });

            mixSlider.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    mixLabel.setText(Float.toString((int) (mixSlider.getValue() * 100) / 100f));
                    if (state != null)
                        state.getData().setDefaultMix(mixSlider.getValue());
                }
            });

            animationList.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    if (state != null)
                        state.setAnimation(0, animationList.getSelected(), loopCheckbox.isChecked());
                }
            });

            skinList.addListener(new ChangeListener() {
                public void changed(ChangeEvent event, Actor actor) {
                    if (skeleton != null) {
                        skeleton.setSkin(skinList.getSelected());
                        skeleton.setSlotsToSetupPose();
                    }
                }
            });

            Gdx.input.setInputProcessor(new InputMultiplexer(stage, new InputAdapter() {
                public boolean touchDown(int screenX, int screenY, int pointer, int button) {
                    touchDragged(screenX, screenY, pointer);
                    return false;
                }

                public boolean touchDragged(int screenX, int screenY, int pointer) {
                    skeletonX = screenX;
                    skeletonY = Gdx.graphics.getHeight() - screenY;
                    return false;
                }
            }));
        }

        private Table table(Actor... actors) {
            Table table = new Table();
            table.defaults().space(6);
            table.add(actors);
            return table;
        }

        void toast(String text) {
            Table table = new Table();
            table.add(new Label(text, skin));
            table.getColor().a = 0;
            table.pack();
            table.setPosition(-table.getWidth(), -3 - table.getHeight());
            table.addAction(sequence( //
                    parallel(moveBy(0, table.getHeight(), 0.3f), fadeIn(0.3f)), //
                    delay(5f), //
                    parallel(moveBy(0, table.getHeight(), 0.3f), fadeOut(0.3f)), //
                    removeActor() //
            ));
            for (Actor actor : toasts.getChildren())
                actor.addAction(moveBy(0, table.getHeight(), 0.3f));
            toasts.addActor(table);
            toasts.getParent().toFront();
        }
    }

    static public void main(String[] args) throws Exception {
        LwjglApplicationConfiguration.disableAudio = true;
        LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
        config.width = 800;
        config.height = 600;
        config.title = "Skeleton Viewer";
        config.allowSoftwareMode = true;
        new LwjglApplication(new SkeletonViewer(), config);
    }
}