├── settings.gradle ├── .gitignore ├── src ├── test │ ├── resources │ │ ├── test.png │ │ ├── test.json │ │ ├── test.scml │ │ └── test.scon │ └── java │ │ └── com │ │ └── brashmokey │ │ └── spriter │ │ ├── JsonReaderTest.java │ │ └── SCONReaderTest.java └── main │ ├── java │ └── com │ │ └── brashmonkey │ │ └── spriter │ │ ├── SpriterException.java │ │ ├── JsonReader.java │ │ ├── File.java │ │ ├── FileReference.java │ │ ├── Dimension.java │ │ ├── Meta.java │ │ ├── IKObject.java │ │ ├── Folder.java │ │ ├── Interpolator.java │ │ ├── CCDResolver.java │ │ ├── Point.java │ │ ├── IKResolver.java │ │ ├── Loader.java │ │ ├── Rectangle.java │ │ ├── Box.java │ │ ├── PlayerTweener.java │ │ ├── Data.java │ │ ├── Curve.java │ │ ├── Calculator.java │ │ ├── Mainline.java │ │ ├── Entity.java │ │ ├── Animation.java │ │ ├── TweenedAnimation.java │ │ ├── Timeline.java │ │ ├── Drawer.java │ │ ├── Spriter.java │ │ ├── SCMLReader.java │ │ └── SCONReader.java │ └── resources │ └── com │ └── brashmonkey │ └── spriter │ └── spriter.gwt.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── LICENSE.txt ├── gradlew.bat ├── pom.xml ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spriter' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build 4 | out 5 | target -------------------------------------------------------------------------------- /src/test/resources/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trixt0r/spriter/HEAD/src/test/resources/test.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trixt0r/spriter/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/SpriterException.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * An Exception which will be thrown if a Spriter specific issue happens at runtime. 5 | * @author Trixt0r 6 | * 7 | */ 8 | public class SpriterException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | public SpriterException(String message){ 13 | super(message); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/com/brashmonkey/spriter/spriter.gwt.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/JsonReader.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import org.json.*; 4 | 5 | import java.io.*; 6 | 7 | /** Lightweight JSON parser. Uses org.json library. 8 | * @author mrdlink */ 9 | public class JsonReader { 10 | 11 | private JsonReader(){} 12 | 13 | public static JSONObject parse (String json) { 14 | return new JSONObject(json); 15 | } 16 | 17 | public static JSONObject parse (InputStream input) throws IOException{ 18 | StringBuilder textBuilder = new StringBuilder(); 19 | Reader reader = new BufferedReader(new InputStreamReader(input)); 20 | int c = 0; 21 | while ((c = reader.read()) != -1) { 22 | textBuilder.append((char) c); 23 | } 24 | reader.close(); 25 | 26 | return new JSONObject(textBuilder.toString()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /************************************************************************** 2 | * Copyright 2016 by Trixt0r 3 | * (https://github.com/Trixt0r, Heinrich Reich, e-mail: h.reich90@gmail.com) 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***************************************************************************/ 17 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/File.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * Represents a file in a Spriter SCML file. 5 | * A file has an {@link #id}, a {@link #name}. 6 | * A {@link #size} and a {@link #pivot} point, i.e. origin of an image do not have to be set since a file can be a sound file. 7 | * @author Trixt0r 8 | * 9 | */ 10 | public class File { 11 | 12 | public final int id; 13 | public final String name; 14 | public final Dimension size; 15 | public final Point pivot; 16 | 17 | File(int id, String name, Dimension size, Point pivot){ 18 | this.id = id; 19 | this.name = name; 20 | this.size = size; 21 | this.pivot = pivot; 22 | } 23 | 24 | /** 25 | * Returns whether this file is a sprite, i.e. an image which is going to be animated, or not. 26 | * @return whether this file is a sprite or not. 27 | */ 28 | public boolean isSprite(){ 29 | return pivot != null && size != null; 30 | } 31 | 32 | public String toString(){ 33 | return getClass().getSimpleName()+"|[id: "+id+", name: "+name+", size: "+size+", pivot: "+pivot; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/FileReference.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * Represents a reference to a specific file. 5 | * A file reference consists of a folder and file index. 6 | * @author Trixt0r 7 | * 8 | */ 9 | public class FileReference { 10 | 11 | public int folder, file; 12 | 13 | public FileReference(int folder, int file){ 14 | this.set(folder, file); 15 | } 16 | 17 | @Override 18 | public int hashCode(){ 19 | return folder*10000+file;//We can have 10000 files per folder 20 | } 21 | 22 | @Override 23 | public boolean equals(Object ref){ 24 | if(ref instanceof FileReference){ 25 | return this.file == ((FileReference)ref).file && this.folder == ((FileReference)ref).folder; 26 | } else return false; 27 | } 28 | 29 | public void set(int folder, int file){ 30 | this.folder = folder; 31 | this.file = file; 32 | } 33 | 34 | public void set(FileReference ref){ 35 | this.set(ref.folder, ref.file); 36 | } 37 | 38 | public boolean hasFile(){ 39 | return this.file != -1; 40 | } 41 | 42 | public boolean hasFolder(){ 43 | return this.folder != -1; 44 | } 45 | 46 | public String toString(){ 47 | return "[folder: "+folder+", file: "+file+"]"; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Dimension.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * Represents a dimension in a 2D space. 5 | * A dimension has a width and a height. 6 | * @author Trixt0r 7 | * 8 | */ 9 | public class Dimension { 10 | 11 | public float width, height; 12 | 13 | /** 14 | * Creates a new dimension with the given size. 15 | * @param width the width of the dimension 16 | * @param height the height of the dimension 17 | */ 18 | public Dimension(float width, float height){ 19 | this.set(width, height); 20 | } 21 | 22 | /** 23 | * Creates a new dimension with the given size. 24 | * @param size the size 25 | */ 26 | public Dimension(Dimension size){ 27 | this.set(size); 28 | } 29 | 30 | /** 31 | * Sets the size of this dimension to the given size. 32 | * @param width the width of the dimension 33 | * @param height the height of the dimension 34 | */ 35 | public void set(float width, float height){ 36 | this.width = width; 37 | this.height = height; 38 | } 39 | 40 | /** 41 | * Sets the size of this dimension to the given size. 42 | * @param size the size 43 | */ 44 | public void set(Dimension size){ 45 | this.set(size.width, size.height); 46 | } 47 | 48 | public String toString(){ 49 | return "["+width+"x"+height+"]"; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/brashmokey/spriter/JsonReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.brashmokey.spriter; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import com.brashmonkey.spriter.JsonReader; 6 | import org.json.JSONObject; 7 | import org.junit.Test; 8 | 9 | import java.io.*; 10 | 11 | public class JsonReaderTest { 12 | 13 | @Test 14 | public void should_load_json_object_from_json_string () throws IOException { 15 | StringBuilder textBuilder = new StringBuilder(); 16 | BufferedReader reader = new BufferedReader(new FileReader(new File("src/test/resources/test.scon"))); 17 | int c = 0; 18 | while ((c = reader.read()) != -1) { 19 | textBuilder.append((char) c); 20 | } 21 | reader.close(); 22 | 23 | JSONObject object = JsonReader.parse(textBuilder.toString()); 24 | 25 | assertTrue(object instanceof JSONObject); 26 | assertTrue(object.toMap().keySet().contains("scon_version")); 27 | } 28 | 29 | @Test 30 | public void should_load_json_object_from_input_stream () throws IOException { 31 | JSONObject object = JsonReader.parse(new FileInputStream(new File("src/test/resources/test.scon"))); 32 | 33 | assertTrue(object instanceof JSONObject); 34 | assertTrue(object.toMap().keySet().contains("scon_version")); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Meta.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | public class Meta { 4 | 5 | Var[] vars; 6 | 7 | static class Var { 8 | int id; 9 | String name; 10 | Value def; 11 | Key[] keys; 12 | 13 | static class Key { 14 | int id; 15 | long time; 16 | Value value; 17 | 18 | public Value getValue() { 19 | return value; 20 | } 21 | 22 | public Class getType() { 23 | return this.value.getClass(); 24 | } 25 | 26 | public int getId() { 27 | return this.id; 28 | } 29 | 30 | public long getTime() { 31 | return this.time; 32 | } 33 | } 34 | 35 | static class Value { 36 | Object value; 37 | 38 | public int getInt() { 39 | return (Integer)value; 40 | } 41 | 42 | public long getLong() { 43 | return (Long)value; 44 | } 45 | 46 | public String getString() { 47 | return (String)value; 48 | } 49 | } 50 | 51 | public Key get(long time) { 52 | for (Key key: this.keys) 53 | if (key.time == time) 54 | return key; 55 | return null; 56 | } 57 | 58 | public boolean has(long time) { 59 | return this.get(time) != null; 60 | } 61 | 62 | public String getName() { 63 | return this.name; 64 | } 65 | 66 | public int getId() { 67 | return this.id; 68 | } 69 | 70 | public Value getDefault() { 71 | return this.def; 72 | } 73 | } 74 | 75 | public Var getVar(long time) { 76 | for (Var var: this.vars) 77 | if (var.get(time) != null) 78 | return var; 79 | return null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/IKObject.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * An inverse kinematics objects which defines a constraint for a {@link IKResolver}. 5 | * 6 | * @author Trixt0r 7 | * 8 | */ 9 | public class IKObject extends Point { 10 | 11 | int chainLength, iterations; 12 | 13 | /** 14 | * Creates a new IKObject with the given constraints. 15 | * @param x x coordinate constraint 16 | * @param y y coordinate constraint 17 | * @param length the chain length constraint. 18 | * @param iterations the number of iterations. 19 | */ 20 | public IKObject(float x, float y, int length, int iterations) { 21 | super(x, y); 22 | this.setLength(length); 23 | this.setIterations(iterations); 24 | } 25 | 26 | /** 27 | * Sets the chain length of this ik object. 28 | * The chain length indicates how many parent bones should get affected, when a {@link IKResolver} resolves the constraints. 29 | * @param chainLength the chain length 30 | * @return this ik object for chained operations 31 | * @throws SpriterException if the chain length is smaller than 0 32 | */ 33 | public IKObject setLength(int chainLength){ 34 | if(chainLength < 0) throw new SpriterException("The chain has to be at least 0!"); 35 | this.chainLength = chainLength; 36 | return this; 37 | } 38 | 39 | /** 40 | * Sets the number of iterations. 41 | * The more iterations a {@link IKResolver} is asked to do, the more precise the result will be. 42 | * @param iterations number of iterations 43 | * @return this ik object for chained operations 44 | * @throws SpriterException if the number of iterations is smaller than 0 45 | */ 46 | public IKObject setIterations(int iterations){ 47 | if(iterations < 0) throw new SpriterException("The number of iterations has to be at least 1!"); 48 | this.iterations = iterations; 49 | return this; 50 | } 51 | 52 | /** 53 | * Returns the current set chain length. 54 | * @return the chain length 55 | */ 56 | public int getChainLength(){ 57 | return this.chainLength; 58 | } 59 | 60 | /** 61 | * Returns the current set number of iterations. 62 | * @return the number of iterations 63 | */ 64 | public int getIterations(){ 65 | return this.iterations; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Folder.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * Represents a folder in a Spriter SCML file. 5 | * A folder has at least an {@link #id}, {@link #name} and {@link #files} may be empty. 6 | * An instance of this class holds an array of {@link File} instances. 7 | * Specific {@link File} instances can be accessed via the corresponding methods, i.e getFile(). 8 | * @author Trixt0r 9 | * 10 | */ 11 | public class Folder { 12 | 13 | final File[] files; 14 | private int filePointer = 0; 15 | public final int id; 16 | public final String name; 17 | 18 | Folder(int id, String name, int files){ 19 | this.id = id; 20 | this.name = name; 21 | this.files = new File[files]; 22 | } 23 | 24 | /** 25 | * Adds a {@link File} instance to this folder. 26 | * @param file the file to add 27 | */ 28 | void addFile(File file){ 29 | this.files[filePointer++] = file; 30 | } 31 | 32 | /** 33 | * Returns a {@link File} instance with the given index. 34 | * @param index the index of the file 35 | * @return the file with the given name 36 | */ 37 | public File getFile(int index){ 38 | return files[index]; 39 | } 40 | 41 | /** 42 | * Returns a {@link File} instance with the given name. 43 | * @param name the name of the file 44 | * @return the file with the given name or null if no file with the given name exists 45 | */ 46 | public File getFile(String name){ 47 | int index = getFileIndex(name); 48 | if(index >= 0) return getFile(index); 49 | else return null; 50 | } 51 | 52 | /** 53 | * Returns a file index with the given name. 54 | * @param name the name of the file 55 | * @return the file index with the given name or -1 if no file with the given name exists 56 | */ 57 | int getFileIndex(String name){ 58 | for(File file: this.files) 59 | if(file.name.equals(name)) return file.id; 60 | return -1; 61 | } 62 | 63 | public String toString(){ 64 | String toReturn = getClass().getSimpleName()+"|[id: "+id+", name: "+name; 65 | for(File file: files) 66 | toReturn += "\n"+file; 67 | toReturn += "]"; 68 | return toReturn; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/brashmokey/spriter/SCONReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.brashmokey.spriter; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import com.brashmonkey.spriter.Data; 6 | import com.brashmonkey.spriter.Player; 7 | import com.brashmonkey.spriter.SCMLReader; 8 | import com.brashmonkey.spriter.SCONReader; 9 | import org.junit.Test; 10 | 11 | import java.io.*; 12 | 13 | public class SCONReaderTest { 14 | 15 | @Test 16 | public void should_load_scon_data_from_json_string () throws IOException { 17 | StringBuilder textBuilder = new StringBuilder(); 18 | BufferedReader reader = new BufferedReader(new FileReader(new File("src/test/resources/test.scon"))); 19 | int c = 0; 20 | while ((c = reader.read()) != -1) { 21 | textBuilder.append((char) c); 22 | } 23 | reader.close(); 24 | 25 | SCONReader sconReader = new SCONReader(textBuilder.toString()); 26 | Data data = sconReader.getData(); 27 | 28 | assertTrue(data.scmlVersion.equals("1.0")); 29 | assertTrue(data.getEntity(0).name.equals("entity_000")); 30 | } 31 | 32 | @Test 33 | public void should_load_scon_data_from_input_stream () throws IOException { 34 | SCONReader sconReader = new SCONReader(new FileInputStream(new File("src/test/resources/test.scon"))); 35 | Data data = sconReader.getData(); 36 | 37 | assertTrue(data.scmlVersion.equals("1.0")); 38 | assertTrue(data.getEntity(0).name.equals("entity_000")); 39 | } 40 | 41 | @Test 42 | public void should_be_as_fast_as_scml_reader () throws IOException { 43 | long sconBegin = System.nanoTime(); 44 | SCONReader sconReader = new SCONReader(new FileInputStream(new File("src/test/resources/Roy/roy.scon"))); 45 | Data sconData = sconReader.getData(); 46 | long sconEnd = System.nanoTime(); 47 | 48 | long scmlBegin = System.nanoTime(); 49 | SCMLReader scmlReader = new SCMLReader(new FileInputStream(new File("src/test/resources/Roy/roy.scml"))); 50 | Data scmlData = scmlReader.getData(); 51 | long scmlEnd = System.nanoTime(); 52 | 53 | long sconTime = sconEnd - sconBegin; 54 | long scmlTime = scmlEnd - scmlBegin; 55 | 56 | assertTrue(sconTime <= scmlTime); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Interpolator.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | 4 | 5 | /** 6 | * Utility class for various interpolation techniques, Spriter is using. 7 | * @author Trixt0r 8 | * 9 | */ 10 | public class Interpolator { 11 | 12 | public static float linear(float a, float b, float t){ 13 | return a+(b-a)*t; 14 | } 15 | 16 | public static float linearAngle(float a, float b, float t){ 17 | return a + Calculator.angleDifference(b, a)*t; 18 | } 19 | 20 | public static float quadratic(float a, float b, float c, float t){ 21 | return linear(linear(a, b, t), linear(b, c, t), t); 22 | } 23 | 24 | public static float quadraticAngle(float a, float b, float c, float t){ 25 | return linearAngle(linearAngle(a, b, t), linearAngle(b, c, t), t); 26 | } 27 | 28 | public static float cubic(float a, float b, float c, float d, float t){ 29 | return linear(quadratic(a, b, c, t), quadratic(b, c, d, t), t); 30 | } 31 | 32 | public static float cubicAngle(float a, float b, float c, float d, float t){ 33 | return linearAngle(quadraticAngle(a, b, c, t), quadraticAngle(b, c, d, t), t); 34 | } 35 | 36 | public static float quartic(float a, float b, float c, float d, float e, float t){ 37 | return linear(cubic(a, b, c, d, t), cubic(b, c, d, e, t), t); 38 | } 39 | 40 | public static float quarticAngle(float a, float b, float c, float d, float e, float t){ 41 | return linearAngle(cubicAngle(a, b, c, d, t), cubicAngle(b, c, d, e, t), t); 42 | } 43 | 44 | public static float quintic(float a, float b, float c, float d, float e, float f, float t){ 45 | return linear(quartic(a, b, c, d, e, t), quartic(b, c, d, e, f, t), t); 46 | } 47 | 48 | public static float quinticAngle(float a, float b, float c, float d, float e, float f, float t){ 49 | return linearAngle(quarticAngle(a, b, c, d, e, t), quarticAngle(b, c, d, e, f, t), t); 50 | } 51 | 52 | public static float bezier(float t, float x1, float x2, float x3,float x4){ 53 | return bezier0(t)*x1 + bezier1(t)*x2 + bezier2(t)*x3 + bezier3(t)*x4; 54 | } 55 | 56 | private static float bezier0(float t){ 57 | float temp = t*t; 58 | return -temp*t + 3*temp - 3*t + 1; 59 | } 60 | 61 | private static float bezier1(float t){ 62 | float temp = t*t; 63 | return 3*t*temp - 6*temp + 3*t; 64 | } 65 | 66 | private static float bezier2(float t){ 67 | float temp = t*t; 68 | return -3*temp*t+3*temp; 69 | } 70 | 71 | private static float bezier3(float t){ 72 | return t*t*t; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/CCDResolver.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import com.brashmonkey.spriter.Mainline.Key.BoneRef; 4 | import com.brashmonkey.spriter.Timeline.Key.Bone; 5 | 6 | /** 7 | * An inverse kinematics resolver implementation. 8 | * An instance of this class uses the CCD (Cyclic Coordinate Descent) algorithm to resolve the constraints. 9 | * @see ccd-algorithm 10 | * and cyclic-coordinate-descent-in-2d . 11 | * @author Trixt0r 12 | * 13 | */ 14 | public class CCDResolver extends IKResolver { 15 | 16 | public CCDResolver(Player player) { 17 | super(player); 18 | } 19 | 20 | @Override 21 | public void resolve(float x, float y, int chainLength, BoneRef effectorRef) { 22 | //player.unmapObjects(null); 23 | Timeline timeline = player.animation.getTimeline(effectorRef.timeline); 24 | Timeline.Key key = player.tweenedKeys[effectorRef.timeline]; 25 | Timeline.Key unmappedKey = player.unmappedTweenedKeys[effectorRef.timeline]; 26 | Bone effector = key.object(); 27 | Bone unmappedffector = unmappedKey.object(); 28 | float width = (timeline.objectInfo != null) ? timeline.objectInfo.size.width: 200; 29 | width *= unmappedffector.scale.x; 30 | float xx = unmappedffector.position.x+(float)Math.cos(Math.toRadians(unmappedffector.angle))*width, 31 | yy = unmappedffector.position.y+(float)Math.sin(Math.toRadians(unmappedffector.angle))*width; 32 | if(Calculator.distanceBetween(xx, yy, x, y) <= this.tolerance) 33 | return; 34 | 35 | effector.angle = Calculator.angleBetween(unmappedffector.position.x, unmappedffector.position.y, x, y); 36 | if(Math.signum(player.root.scale.x) == -1) effector.angle += 180f; 37 | BoneRef parentRef = effectorRef.parent; 38 | Bone parent = null, unmappedParent = null; 39 | if(parentRef != null){ 40 | parent = player.tweenedKeys[parentRef.timeline].object(); 41 | unmappedParent = player.unmappedTweenedKeys[parentRef.timeline].object(); 42 | effector.angle -= unmappedParent.angle; 43 | } 44 | player.unmapObjects(null); 45 | for(int i = 0; i < chainLength && parentRef != null; i++){ 46 | if(Calculator.distanceBetween(xx, yy, x, y) <= this.tolerance) 47 | return; 48 | parent.angle += Calculator.angleDifference(Calculator.angleBetween(unmappedParent.position.x, unmappedParent.position.y, x, y), 49 | Calculator.angleBetween(unmappedParent.position.x, unmappedParent.position.y, xx, yy)); 50 | parentRef = parentRef.parent; 51 | if(parentRef != null && i < chainLength-1){ 52 | parent = player.tweenedKeys[parentRef.timeline].object(); 53 | unmappedParent = player.unmappedTweenedKeys[parentRef.timeline].object(); 54 | parent.angle -= unmappedParent.angle; 55 | } 56 | else parent = null; 57 | player.unmapObjects(null); 58 | xx = unmappedffector.position.x+(float)Math.cos(Math.toRadians(unmappedffector.angle))*width; 59 | yy = unmappedffector.position.y+(float)Math.sin(Math.toRadians(unmappedffector.angle))*width; 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/resources/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "frames": { 3 | "0.png": { 4 | "frame": { 5 | "h": 224, 6 | "w": 256, 7 | "x": 768, 8 | "y": 0 9 | }, 10 | "rotated": "false", 11 | "sourceSize": { 12 | "h": 224, 13 | "w": 256 14 | }, 15 | "spriteSourceSize": { 16 | "h": 224, 17 | "w": 256, 18 | "x": 0, 19 | "y": 0 20 | }, 21 | "trimmed": "true" 22 | }, 23 | "1.png": { 24 | "frame": { 25 | "h": 224, 26 | "w": 256, 27 | "x": 0, 28 | "y": 0 29 | }, 30 | "rotated": "false", 31 | "sourceSize": { 32 | "h": 224, 33 | "w": 256 34 | }, 35 | "spriteSourceSize": { 36 | "h": 224, 37 | "w": 256, 38 | "x": 0, 39 | "y": 0 40 | }, 41 | "trimmed": "true" 42 | }, 43 | "2.png": { 44 | "frame": { 45 | "h": 224, 46 | "w": 256, 47 | "x": 768, 48 | "y": 224 49 | }, 50 | "rotated": "false", 51 | "sourceSize": { 52 | "h": 224, 53 | "w": 256 54 | }, 55 | "spriteSourceSize": { 56 | "h": 224, 57 | "w": 256, 58 | "x": 0, 59 | "y": 0 60 | }, 61 | "trimmed": "true" 62 | }, 63 | "3.png": { 64 | "frame": { 65 | "h": 224, 66 | "w": 256, 67 | "x": 256, 68 | "y": 224 69 | }, 70 | "rotated": "false", 71 | "sourceSize": { 72 | "h": 224, 73 | "w": 256 74 | }, 75 | "spriteSourceSize": { 76 | "h": 224, 77 | "w": 256, 78 | "x": 0, 79 | "y": 0 80 | }, 81 | "trimmed": "true" 82 | }, 83 | "4.png": { 84 | "frame": { 85 | "h": 224, 86 | "w": 256, 87 | "x": 512, 88 | "y": 224 89 | }, 90 | "rotated": "false", 91 | "sourceSize": { 92 | "h": 224, 93 | "w": 256 94 | }, 95 | "spriteSourceSize": { 96 | "h": 224, 97 | "w": 256, 98 | "x": 0, 99 | "y": 0 100 | }, 101 | "trimmed": "true" 102 | }, 103 | "5.png": { 104 | "frame": { 105 | "h": 224, 106 | "w": 256, 107 | "x": 512, 108 | "y": 0 109 | }, 110 | "rotated": "false", 111 | "sourceSize": { 112 | "h": 224, 113 | "w": 256 114 | }, 115 | "spriteSourceSize": { 116 | "h": 224, 117 | "w": 256, 118 | "x": 0, 119 | "y": 0 120 | }, 121 | "trimmed": "true" 122 | }, 123 | "6.png": { 124 | "frame": { 125 | "h": 224, 126 | "w": 256, 127 | "x": 0, 128 | "y": 224 129 | }, 130 | "rotated": "false", 131 | "sourceSize": { 132 | "h": 224, 133 | "w": 256 134 | }, 135 | "spriteSourceSize": { 136 | "h": 224, 137 | "w": 256, 138 | "x": 0, 139 | "y": 0 140 | }, 141 | "trimmed": "true" 142 | }, 143 | "7.png": { 144 | "frame": { 145 | "h": 224, 146 | "w": 256, 147 | "x": 256, 148 | "y": 0 149 | }, 150 | "rotated": "false", 151 | "sourceSize": { 152 | "h": 224, 153 | "w": 256 154 | }, 155 | "spriteSourceSize": { 156 | "h": 224, 157 | "w": 256, 158 | "x": 0, 159 | "y": 0 160 | }, 161 | "trimmed": "true" 162 | } 163 | }, 164 | "meta": { 165 | "app": "Spriter", 166 | "format": "RGBA8888", 167 | "image": "test.png", 168 | "scale": 1, 169 | "size": { 170 | "h": 448, 171 | "w": 1024 172 | }, 173 | "version": "r11" 174 | } 175 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.brashmonkey.spriter 4 | spriter 5 | 1.0.1 6 | jar 7 | 8 | Spriter 9 | Generic Java implementation for Spriter. 10 | https://brashmonkey.com/ 11 | 12 | 13 | 14 | Apache License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | repo 17 | 18 | 19 | 20 | 21 | 22 | internal.repo 23 | Temporary Staging Repository 24 | file://${project.build.directory}/mvn-repo 25 | 26 | 27 | 28 | 29 | 30 | org.json 31 | json 32 | 20180130 33 | 34 | 35 | junit 36 | junit 37 | 4.12 38 | test 39 | 40 | 41 | 42 | 43 | 44 | 45 | maven-deploy-plugin 46 | 2.8.1 47 | 48 | internal.repo::default::file://${project.build.directory}/mvn 49 | 50 | 51 | 52 | com.github.github 53 | site-maven-plugin 54 | 0.12 55 | 56 | Maven artifacts for ${project.version} 57 | true 58 | ${project.build.directory}/mvn 59 | refs/heads/mvn 60 | **/* 61 | spriter 62 | Trixt0r 63 | 64 | 65 | 66 | 67 | 68 | site 69 | 70 | deploy 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | github 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Point.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * A utility class to keep the code short. 5 | * A point is essentially that what you would expect if you think about a point in a 2D space. 6 | * It holds an x and y value. You can {@link #translate(Point)}, {@link #scale(Point)}, {@link #rotate(float)} and {@link #set(Point)} a point. 7 | * @author Trixt0r 8 | * 9 | */ 10 | public class Point { 11 | 12 | /** 13 | * The x coordinates of this point. 14 | */ 15 | public float x; 16 | /** 17 | * The y coordinates of this point. 18 | */ 19 | public float y; 20 | 21 | /** 22 | * Creates a point at (0,0). 23 | */ 24 | public Point(){ 25 | this(0,0); 26 | } 27 | 28 | /** 29 | * Creates a point at the position of the given point. 30 | * @param point the point to set this point at 31 | */ 32 | public Point(Point point){ 33 | this(point.x, point.y); 34 | } 35 | 36 | /** 37 | * Creates a point at (x, y). 38 | * @param x the x coordinate 39 | * @param y the y coordinate 40 | */ 41 | public Point(float x, float y){ 42 | this.set(x, y); 43 | } 44 | 45 | /** 46 | * Sets this point to the given coordinates. 47 | * @param x the x coordinate 48 | * @param y the y coordinate 49 | * @return this point for chained operations 50 | */ 51 | public Point set(float x, float y){ 52 | this.x = x; 53 | this.y = y; 54 | return this; 55 | } 56 | 57 | /** 58 | * Adds the given amount to this point. 59 | * @param x the amount in x direction to add 60 | * @param y the amount in y direction to add 61 | * @return this point for chained operations 62 | */ 63 | public Point translate(float x, float y){ 64 | return this.set(this.x+x, this.y+y); 65 | } 66 | 67 | /** 68 | * Scales this point by the given amount. 69 | * @param x the scale amount in x direction 70 | * @param y the scale amount in y direction 71 | * @return this point for chained operations 72 | */ 73 | public Point scale(float x, float y){ 74 | return this.set(this.x*x, this.y*y); 75 | } 76 | 77 | /** 78 | * Sets this point to the given point. 79 | * @param point the new coordinates 80 | * @return this point for chained operations 81 | */ 82 | public Point set(Point point){ 83 | return this.set(point.x, point.y); 84 | } 85 | 86 | /** 87 | * Adds the given amount to this point. 88 | * @param amount the amount to add 89 | * @return this point for chained operations 90 | */ 91 | public Point translate(Point amount){ 92 | return this.translate(amount.x, amount.y); 93 | } 94 | 95 | /** 96 | * Scales this point by the given amount. 97 | * @param amount the amount to scale 98 | * @return this point for chained operations 99 | */ 100 | public Point scale(Point amount){ 101 | return this.scale(amount.x, amount.y); 102 | } 103 | 104 | /** 105 | * Rotates this point around (0,0) by the given amount of degrees. 106 | * @param degrees the angle to rotate this point 107 | * @return this point for chained operations 108 | */ 109 | public Point rotate(float degrees){ 110 | if(x != 0 || y != 0){ 111 | float cos = Calculator.cosDeg(degrees); 112 | float sin = Calculator.sinDeg(degrees); 113 | 114 | float xx = x*cos-y*sin; 115 | float yy = x*sin+y*cos; 116 | 117 | this.x = xx; 118 | this.y = yy; 119 | } 120 | return this; 121 | } 122 | 123 | /** 124 | * Returns a copy of this point with the current set values. 125 | * @return a copy of this point 126 | */ 127 | public Point copy(){ 128 | return new Point(x,y); 129 | } 130 | 131 | public String toString(){ 132 | return "["+x+","+y+"]"; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/IKResolver.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map.Entry; 5 | 6 | import com.brashmonkey.spriter.Mainline.Key.BoneRef; 7 | import com.brashmonkey.spriter.Timeline.Key.Bone; 8 | 9 | /** 10 | * A IKResolver is responsible for resolving previously set constraints. 11 | * @see Inverse kinematics 12 | * @author Trixt0r 13 | * 14 | */ 15 | public abstract class IKResolver { 16 | 17 | /** 18 | * Resolves the inverse kinematics constraint with a specific algtorithm 19 | * @param x the target x value 20 | * @param y the target y value 21 | * @param chainLength number of parents which are affected 22 | * @param effector the actual effector where the resolved information has to be stored in. 23 | */ 24 | protected abstract void resolve(float x, float y, int chainLength, BoneRef effector); 25 | 26 | protected HashMap ikMap; 27 | protected float tolerance; 28 | protected Player player; 29 | 30 | /** 31 | * Creates a resolver with a default tolerance of 5f. 32 | */ 33 | public IKResolver(Player player) { 34 | this.tolerance = 5f; 35 | this.ikMap = new HashMap(); 36 | this.setPlayer(player); 37 | } 38 | 39 | /** 40 | * Sets the player for this resolver. 41 | * @param player the player which gets affected. 42 | * @throws SpriterException if player is null 43 | */ 44 | public void setPlayer(Player player){ 45 | if(player == null) throw new SpriterException("player cannot be null!"); 46 | this.player = player; 47 | } 48 | 49 | /** 50 | * Returns the current set player. 51 | * @return the current player. 52 | */ 53 | public Player getPlayer(){ 54 | return this.player; 55 | } 56 | 57 | /** 58 | * Resolves the inverse kinematics constraints with the implemented algorithm in {@link #resolve(float, float, int, BoneRef)}. 59 | */ 60 | public void resolve(){ 61 | for(Entry entry: this.ikMap.entrySet()){ 62 | for(int j = 0; j < entry.getKey().iterations; j++) 63 | this.resolve(entry.getKey().x, entry.getKey().y, entry.getKey().chainLength, entry.getValue()); 64 | } 65 | } 66 | 67 | /** 68 | * Adds the given object to the internal IKObject - Bone map. 69 | * This means, the values of the given ik object affect the mapped bone. 70 | * @param ikObject the ik object 71 | * @param boneRef the bone reference which gets affected 72 | */ 73 | public void mapIKObject(IKObject ikObject, BoneRef boneRef){ 74 | this.ikMap.put(ikObject, boneRef); 75 | } 76 | 77 | /** 78 | * Adds the given object to the internal IKObject - Bone map. 79 | * This means, the values of the given ik object affect the mapped bone. 80 | * @param ikObject the ik object 81 | * @param bone the bone which gets affected 82 | */ 83 | public void mapIKObject(IKObject ikObject, Bone bone){ 84 | this.ikMap.put(ikObject, player.getBoneRef(bone)); 85 | } 86 | 87 | /** 88 | * Removes the given object from the internal map. 89 | * @param ikObject the ik object to remove 90 | */ 91 | public void unmapIKObject(IKObject ikObject){ 92 | this.ikMap.remove(ikObject); 93 | } 94 | 95 | /** 96 | * Returns the tolerance of this resolver. 97 | * @return the tolerance 98 | */ 99 | public float getTolerance() { 100 | return tolerance; 101 | } 102 | 103 | /** 104 | * Sets the tolerance distance of this resolver. 105 | * The resolver should stop the algorithm if the distance to the set ik object is less than the tolerance. 106 | * @param tolerance the tolerance 107 | */ 108 | public void setTolerance(float tolerance) { 109 | this.tolerance = tolerance; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Loader.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.util.HashMap; 4 | 5 | /** 6 | * A loader is responsible for loading all resources. 7 | * Since this library is meant to be as generic as possible, it cannot be assumed how to load a resource. Because of this this class has to be abstract. 8 | * This class takes care of loading all resources a {@link Data} instance contains. 9 | * To load all resources an instance relies on {@link #loadResource(FileReference)} which has to implemented with the backend specific methods. 10 | * 11 | * @author Trixt0r 12 | * 13 | * @param The backend specific resource. In general such a resource is called "sprite", "texture" or "image". 14 | */ 15 | public abstract class Loader { 16 | 17 | /** 18 | * Contains all loaded resources if not {@link #isDisposed()}. 19 | */ 20 | protected final HashMap resources; 21 | 22 | /** 23 | * The current set data containing {@link Folder}s and {@link File}s. 24 | */ 25 | protected Data data; 26 | 27 | /** 28 | * The root path to the previous loaded Spriter SCML file. 29 | */ 30 | protected String root = ""; 31 | 32 | private boolean disposed; 33 | 34 | /** 35 | * Creates a loader with the given Spriter data. 36 | * @param data the generated Spriter data 37 | */ 38 | public Loader(Data data){ 39 | this.data = data; 40 | this.resources = new HashMap(100); 41 | } 42 | 43 | /** 44 | * Loads a resource. 45 | * The path to the file can be resolved with {@link #root} and {@link #data}. 46 | * I recommend using {@link Data#getFile(FileReference)}. Then the path to the resource is {@link File#name} relative to {@link #root}. 47 | * @param ref the reference to load 48 | * @return the loaded resource 49 | */ 50 | protected abstract R loadResource(FileReference ref); 51 | 52 | /** 53 | * Called when all resources from {@link #data} have been loaded. 54 | */ 55 | protected void finishLoading(){} 56 | 57 | /** 58 | * Called before all resources get loaded. 59 | */ 60 | protected void beginLoading(){} 61 | 62 | /** 63 | * Loads all resources indicated by {@link #data}. 64 | * @param root the root folder of the previously loaded Spriter SCML file 65 | */ 66 | public void load(String root){ 67 | this.root = root; 68 | this.beginLoading(); 69 | for(Folder folder: data.folders){ 70 | for(File file: folder.files){ 71 | //if(new java.io.File(root+"/"+file.name).exists()){ 72 | FileReference ref = new FileReference(folder.id, file.id); 73 | this.resources.put(ref, this.loadResource(ref)); 74 | //} 75 | } 76 | } 77 | this.disposed = false; 78 | this.finishLoading(); 79 | } 80 | 81 | /** 82 | * Loads all resources indicated by {@link #data}. 83 | * @param file the previously loaded Spriter SCML file 84 | */ 85 | public void load(java.io.File file){ 86 | this.load(file.getParent()); 87 | } 88 | 89 | /** 90 | * Returns a resource the given reference is pointing to. 91 | * @param ref the reference pointing to a resource 92 | * @return the resource or null if the resource is not loaded yet. 93 | */ 94 | public R get(FileReference ref){ 95 | return this.resources.get(ref); 96 | } 97 | 98 | /** 99 | * Removes all loaded resources from the internal reference-resource map. 100 | * Override this method and dispose all your resources. After that call {@link #dispose()} of the super class. 101 | */ 102 | public void dispose(){ 103 | resources.clear(); 104 | data = null; 105 | root = ""; 106 | disposed = true; 107 | } 108 | 109 | /** 110 | * Returns whether this loader has been disposed or not. 111 | * @return true if this loader is disposed 112 | */ 113 | public boolean isDisposed(){ 114 | return disposed; 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Rectangle.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * Represents a 2D rectangle with left, top, right and bottom bounds. 5 | * A rectangle is responsible for calculating its own size and checking if a point is inside it or if it is intersecting with another rectangle. 6 | * @author Trixt0r 7 | * 8 | */ 9 | public class Rectangle { 10 | 11 | /** 12 | * Belongs to the bounds of this rectangle. 13 | */ 14 | public float left, top, right, bottom; 15 | /** 16 | * The size of this rectangle. 17 | */ 18 | public final Dimension size; 19 | 20 | /** 21 | * Creates a rectangle with the given bounds. 22 | * @param left left bounding 23 | * @param top top bounding 24 | * @param right right bounding 25 | * @param bottom bottom bounding 26 | */ 27 | public Rectangle(float left, float top, float right, float bottom){ 28 | this.set(left, top, right, bottom); 29 | this.size = new Dimension(0, 0); 30 | this.calculateSize(); 31 | } 32 | 33 | /** 34 | * Creates a rectangle with the bounds of the given rectangle. 35 | * @param rect rectangle containing the bounds. 36 | */ 37 | public Rectangle(Rectangle rect){ 38 | this(rect.left, rect.top, rect.right, rect.bottom); 39 | } 40 | 41 | /** 42 | * Returns whether the given point (x,y) is inside this rectangle. 43 | * @param x the x coordinate 44 | * @param y the y coordinate 45 | * @return true if (x,y) is inside 46 | */ 47 | public boolean isInside(float x, float y){ 48 | return x >= this.left && x <= this.right && y <= this.top && y >= this.bottom; 49 | } 50 | 51 | /** 52 | * Returns whether the given point is inside this rectangle. 53 | * @param point the point 54 | * @return true if the point is inside 55 | */ 56 | public boolean isInside(Point point){ 57 | return isInside(point.x, point.y); 58 | } 59 | 60 | /** 61 | * Calculates the size of this rectangle. 62 | */ 63 | public void calculateSize(){ 64 | this.size.set(right-left, top-bottom); 65 | } 66 | 67 | /** 68 | * Sets the bounds of this rectangle to the bounds of the given rectangle. 69 | * @param rect rectangle containing the bounds. 70 | */ 71 | public void set(Rectangle rect){ 72 | if(rect == null) return; 73 | this.bottom = rect.bottom; 74 | this.left = rect.left; 75 | this.right = rect.right; 76 | this.top = rect.top; 77 | this.calculateSize(); 78 | } 79 | 80 | /** 81 | * Sets the bounds of this rectangle to the given bounds. 82 | * @param left left bounding 83 | * @param top top bounding 84 | * @param right right bounding 85 | * @param bottom bottom bounding 86 | */ 87 | public void set(float left, float top, float right, float bottom){ 88 | this.left = left; 89 | this.top = top; 90 | this.right = right; 91 | this.bottom = bottom; 92 | } 93 | 94 | /** 95 | * Returns whether the given two rectangles are intersecting. 96 | * @param rect1 the first rectangle 97 | * @param rect2 the second rectangle 98 | * @return true if the rectangles are intersecting 99 | */ 100 | public static boolean areIntersecting(Rectangle rect1, Rectangle rect2){ 101 | return rect1.isInside(rect2.left, rect2.top) || rect1.isInside(rect2.right, rect2.top) 102 | || rect1.isInside(rect2.left, rect2.bottom) || rect1.isInside(rect2.right, rect2.bottom); 103 | } 104 | 105 | /** 106 | * Creates a bigger rectangle of the given two and saves it in the target. 107 | * @param rect1 the first rectangle 108 | * @param rect2 the second rectangle 109 | * @param target the target to save the new bounds. 110 | */ 111 | public static void setBiggerRectangle(Rectangle rect1, Rectangle rect2, Rectangle target){ 112 | target.left = Math.min(rect1.left, rect2.left); 113 | target.bottom = Math.min(rect1.bottom, rect2.bottom); 114 | target.right = Math.max(rect1.right, rect2.right); 115 | target.top = Math.max(rect1.top, rect2.top); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Box.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import com.brashmonkey.spriter.Entity.ObjectInfo; 4 | 5 | /** 6 | * Represents a box, which consists of four points: top-left, top-right, bottom-left and bottom-right. 7 | * A box is responsible for checking collisions and calculating a bounding box for a {@link Timeline.Key.Bone}. 8 | * @author Trixt0r 9 | * 10 | */ 11 | public class Box { 12 | public final Point[] points; 13 | private Rectangle rect; 14 | 15 | /** 16 | * Creates a new box with no witdh and height. 17 | */ 18 | public Box(){ 19 | this.points = new Point[4]; 20 | //this.temp = new Point[4]; 21 | for(int i = 0; i < 4; i++){ 22 | this.points[i] = new Point(0,0); 23 | //this.temp[i] = new Point(0,0); 24 | } 25 | this.rect = new Rectangle(0,0,0,0); 26 | } 27 | 28 | /** 29 | * Calculates its four points for the given bone or object with the given info. 30 | * @param boneOrObject the bone or object 31 | * @param info the info 32 | * @throws NullPointerException if info or boneOrObject is null 33 | */ 34 | public void calcFor(Timeline.Key.Bone boneOrObject, ObjectInfo info){ 35 | float width = info.size.width*boneOrObject.scale.x; 36 | float height = info.size.height*boneOrObject.scale.y; 37 | 38 | float pivotX = width*boneOrObject.pivot.x; 39 | float pivotY = height*boneOrObject.pivot.y; 40 | 41 | this.points[0].set(-pivotX,-pivotY); 42 | this.points[1].set(width-pivotX, -pivotY); 43 | this.points[2].set(-pivotX,height-pivotY); 44 | this.points[3].set(width-pivotX,height-pivotY); 45 | 46 | for(int i = 0; i < 4; i++) 47 | this.points[i].rotate(boneOrObject.angle); 48 | for(int i = 0; i < 4; i++) 49 | this.points[i].translate(boneOrObject.position); 50 | } 51 | 52 | /** 53 | * Returns whether the given coordinates lie inside the box of the given bone or object. 54 | * @param boneOrObject the bone or object 55 | * @param info the object info of the given bone or object 56 | * @param x the x coordinate 57 | * @param y the y coordinate 58 | * @return true if the given point lies in the box 59 | * @throws NullPointerException if info or boneOrObject is null 60 | */ 61 | public boolean collides(Timeline.Key.Bone boneOrObject, ObjectInfo info, float x, float y){ 62 | float width = info.size.width*boneOrObject.scale.x; 63 | float height = info.size.height*boneOrObject.scale.y; 64 | 65 | float pivotX = width*boneOrObject.pivot.x; 66 | float pivotY = height*boneOrObject.pivot.y; 67 | 68 | Point point = new Point(x-boneOrObject.position.x,y-boneOrObject.position.y); 69 | point.rotate(-boneOrObject.angle); 70 | 71 | return point.x >= -pivotX && point.x <= width-pivotX && point.y >= -pivotY && point.y <= height-pivotY; 72 | } 73 | 74 | /** 75 | * Returns whether this box is inside the given rectangle. 76 | * @param rect the rectangle 77 | * @return true if one of the four points is inside the rectangle 78 | */ 79 | public boolean isInside(Rectangle rect){ 80 | boolean inside = false; 81 | for(Point p: points) 82 | inside |= rect.isInside(p); 83 | return inside; 84 | } 85 | 86 | /** 87 | * Returns a bounding box for this box. 88 | * @return the bounding box 89 | */ 90 | public Rectangle getBoundingRect(){ 91 | this.rect.set(points[0].x,points[0].y,points[0].x,points[0].y); 92 | this.rect.left = Math.min(Math.min(Math.min(Math.min(points[0].x, points[1].x),points[2].x),points[3].x), this.rect.left); 93 | this.rect.right = Math.max(Math.max(Math.max(Math.max(points[0].x, points[1].x),points[2].x),points[3].x), this.rect.right); 94 | this.rect.top = Math.max(Math.max(Math.max(Math.max(points[0].y, points[1].y),points[2].y),points[3].y), this.rect.top); 95 | this.rect.bottom = Math.min(Math.min(Math.min(Math.min(points[0].y, points[1].y),points[2].y),points[3].y), this.rect.bottom); 96 | return this.rect; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/PlayerTweener.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * A player tweener is responsible for tweening to {@link Player} instances. 5 | * Such a 6 | * @author Trixt0r 7 | * 8 | */ 9 | public class PlayerTweener extends Player{ 10 | 11 | private TweenedAnimation anim; 12 | private Player player1, player2; 13 | /** 14 | * Indicates whether to update the {@link Player} instances this instance is holding. 15 | * If this variable is set to false, you will have to call {@link Player#update()} on your own. 16 | */ 17 | public boolean updatePlayers = true; 18 | 19 | /** 20 | * The name of root bone to start the tweening at. 21 | * Set it to null to tween the whole hierarchy. 22 | */ 23 | public String baseBoneName = null; 24 | 25 | /** 26 | * Creates a player tweener which will tween the given two players. 27 | * @param player1 the first player 28 | * @param player2 the second player 29 | */ 30 | public PlayerTweener(Player player1, Player player2){ 31 | super(player1.getEntity()); 32 | this.setPlayers(player1, player2); 33 | } 34 | 35 | /** 36 | * Creates a player tweener based on the entity. 37 | * The players to tween will be created by this instance. 38 | * @param entity the entity the players will animate 39 | */ 40 | public PlayerTweener(Entity entity){ 41 | this(new Player(entity), new Player(entity)); 42 | } 43 | 44 | /** 45 | * Tweens the current set players. 46 | * This method will update the set players if {@link #updatePlayers} is true. 47 | * @throws SpriterException if no bone with {@link #baseBoneName} exists 48 | */ 49 | @Override 50 | public void update(){ 51 | if(updatePlayers){ 52 | player1.update(); 53 | player2.update(); 54 | } 55 | anim.setAnimations(player1.animation, player2.animation); 56 | super.update(); 57 | if(baseBoneName != null){ 58 | int index = anim.onFirstMainLine()? player1.getBoneIndex(baseBoneName) : player2.getBoneIndex(baseBoneName); 59 | if(index == -1) throw new SpriterException("A bone with name \""+baseBoneName+"\" does no exist!"); 60 | anim.base = anim.getCurrentKey().getBoneRef(index); 61 | super.update(); 62 | } 63 | } 64 | 65 | /** 66 | * Sets the players for this tweener. 67 | * Both players have to hold the same {@link Entity} 68 | * @param player1 the first player 69 | * @param player2 the second player 70 | */ 71 | public void setPlayers(Player player1, Player player2){ 72 | if(player1.entity != player2.entity) 73 | throw new SpriterException("player1 and player2 have to hold the same entity!"); 74 | this.player1 = player1; 75 | this.player2 = player2; 76 | if(player1.entity == entity) return; 77 | this.anim = new TweenedAnimation(player1.getEntity()); 78 | anim.setAnimations(player1.animation, player2.animation); 79 | super.setEntity(player1.getEntity()); 80 | super.setAnimation(anim); 81 | } 82 | 83 | /** 84 | * Returns the first set player. 85 | * @return the first player 86 | */ 87 | public Player getFirstPlayer(){ 88 | return this.player1; 89 | } 90 | 91 | /** 92 | * Returns the second set player. 93 | * @return the second player 94 | */ 95 | public Player getSecondPlayer(){ 96 | return this.player2; 97 | } 98 | 99 | /** 100 | * Sets the interpolation weight of this tweener. 101 | * @param weight the interpolation weight between 0.0f and 1.0f 102 | */ 103 | public void setWeight(float weight){ 104 | this.anim.weight = weight; 105 | } 106 | 107 | /** 108 | * Returns the interpolation weight. 109 | * @return the interpolation weight between 0.0f and 1.0f 110 | */ 111 | public float getWeight(){ 112 | return this.anim.weight; 113 | } 114 | 115 | 116 | /** 117 | * Sets the base animation of this tweener. 118 | * Has only an effect if {@link #baseBoneName} is not null. 119 | * @param anim the base animation 120 | */ 121 | public void setBaseAnimation(Animation anim){ 122 | this.anim.baseAnimation = anim; 123 | } 124 | 125 | /** 126 | * Sets the base animation of this tweener by the given animation index. 127 | * Has only an effect if {@link #baseBoneName} is not null. 128 | * @param index the index of the base animation 129 | */ 130 | public void setBaseAnimation(int index){ 131 | this.setBaseAnimation(entity.getAnimation(index)); 132 | } 133 | 134 | /** 135 | * Sets the base animation of this tweener by the given name. 136 | * Has only an effect if {@link #baseBoneName} is not null. 137 | * @param name the name of the base animation 138 | */ 139 | public void setBaseAnimation(String name){ 140 | this.setBaseAnimation(entity.getAnimation(name)); 141 | } 142 | 143 | /** 144 | * Returns the base animation if this tweener. 145 | * @return the base animation 146 | */ 147 | public Animation getBaseAnimation(){ 148 | return this.anim.baseAnimation; 149 | } 150 | 151 | /** 152 | * Not supported by this class. 153 | */ 154 | @Override 155 | public void setAnimation(Animation anim){} 156 | 157 | /** 158 | * Not supported by this class. 159 | */ 160 | @Override 161 | public void setEntity(Entity entity){} 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Data.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | 4 | /** 5 | * Represents all the data which necessary to animate a Spriter generated SCML file. 6 | * An instance of this class holds {@link Folder}s and {@link Entity} instances. 7 | * Specific {@link Folder} and {@link Entity} instances can be accessed via the corresponding methods, i.e. getEntity() 8 | * and getFolder(). 9 | * @author Trixt0r 10 | * 11 | */ 12 | public class Data { 13 | 14 | /** 15 | * Represents the rendering mode stored in the spriter data root. 16 | */ 17 | public enum PixelMode { 18 | NONE, PIXEL_ART; 19 | 20 | /** 21 | * @param mode 22 | * @return The pixel mode for the given int value. Default is {@link PixelMode#NONE}. 23 | */ 24 | public static PixelMode get(int mode) { 25 | switch (mode) { 26 | case 1: return PIXEL_ART; 27 | default: return NONE; 28 | } 29 | } 30 | } 31 | 32 | final Folder[] folders; 33 | final Entity[] entities; 34 | private int folderPointer = 0, entityPointer = 0; 35 | public final String scmlVersion, generator, generatorVersion; 36 | public final PixelMode pixelMode; 37 | 38 | 39 | Data(String scmlVersion, String generator, String generatorVersion, PixelMode pixelMode, int folders, int entities){ 40 | this.scmlVersion = scmlVersion; 41 | this.generator = generator; 42 | this.generatorVersion = generatorVersion; 43 | this.pixelMode = pixelMode; 44 | this.folders = new Folder[folders]; 45 | this.entities = new Entity[entities]; 46 | } 47 | 48 | /** 49 | * Adds a folder to this data. 50 | * @param folder the folder to add 51 | */ 52 | void addFolder(Folder folder){ 53 | this.folders[folderPointer++] = folder; 54 | } 55 | 56 | /** 57 | * Adds an entity to this data. 58 | * @param entity the entity to add 59 | */ 60 | void addEntity(Entity entity){ 61 | this.entities[entityPointer++] = entity; 62 | } 63 | 64 | /** 65 | * Returns a {@link Folder} instance with the given name. 66 | * @param name the name of the folder 67 | * @return the folder with the given name or null if no folder with the given name exists 68 | */ 69 | public Folder getFolder(String name){ 70 | int index = getFolderIndex(name); 71 | if(index >= 0) return getFolder(index); 72 | else return null; 73 | } 74 | 75 | /** 76 | * Returns a folder index with the given name. 77 | * @param name name of the folder 78 | * @return the folder index of the Folder with the given name or -1 if no folder with the given name exists 79 | */ 80 | int getFolderIndex(String name){ 81 | for(Folder folder: this.folders) 82 | if(folder.name.equals(name)) return folder.id; 83 | return -1; 84 | } 85 | 86 | /** 87 | * Returns a {@link Folder} instance at the given index. 88 | * @param index the index of the folder 89 | * @return the {@link Folder} instance at the given index 90 | */ 91 | Folder getFolder(int index){ 92 | return this.folders[index]; 93 | } 94 | 95 | /** 96 | * Returns an {@link Entity} instance with the given index. 97 | * @param index index of the entity to return. 98 | * @return the entity with the given index 99 | */ 100 | public Entity getEntity(int index){ 101 | return this.entities[index]; 102 | } 103 | 104 | /** 105 | * Returns an {@link Entity} instance with the given name. 106 | * @param name the name of the entity 107 | * @return the entity with the given name or null if no entity with the given name exists 108 | */ 109 | public Entity getEntity(String name){ 110 | int index = getEntityIndex(name); 111 | if(index >= 0) return getEntity(index); 112 | else return null; 113 | } 114 | 115 | /** 116 | * Returns an entity index with the given name. 117 | * @param name name of the entity 118 | * @return the entity index of the entity with the given name or -1 if no entity with the given name exists 119 | */ 120 | int getEntityIndex(String name){ 121 | for(Entity entity: this.entities) 122 | if(entity.name.equals(name)) return entity.id; 123 | return -1; 124 | } 125 | 126 | /** 127 | * Returns a {@link File} instance in the given {@link Folder} instance at the given file index. 128 | * @param folder {@link Folder} instance to search in. 129 | * @param file index of the file 130 | * @return the {@link File} instance in the given folder at the given file index 131 | */ 132 | public File getFile(Folder folder, int file){ 133 | return folder.getFile(file); 134 | } 135 | 136 | /** 137 | * Returns a {@link File} instance in the given folder at the given file index. 138 | * @param folder index of the folder 139 | * @param file index of the file 140 | * @return the {@link File} instance in the given folder at the given file index 141 | */ 142 | public File getFile(int folder, int file){ 143 | return getFile(this.getFolder(folder), file); 144 | } 145 | 146 | /** 147 | * Returns a {@link File} instance for the given {@link FileReference} instance. 148 | * @param ref reference to the file 149 | * @return the {@link File} instance for the given reference 150 | */ 151 | public File getFile(FileReference ref){ 152 | return this.getFile(ref.folder, ref.file); 153 | } 154 | 155 | /** 156 | * @return The string representation of this spriter data 157 | */ 158 | public String toString(){ 159 | String toReturn = getClass().getSimpleName() + 160 | "|[Version: " + scmlVersion + 161 | ", Generator: " + generator + 162 | " (" + generatorVersion + ")]"; 163 | for(Folder folder: folders) 164 | toReturn += "\n"+folder; 165 | for(Entity entity: entities) 166 | toReturn += "\n"+entity; 167 | toReturn+="]"; 168 | return toReturn; 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Curve.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import static com.brashmonkey.spriter.Calculator.*; 4 | import static com.brashmonkey.spriter.Interpolator.*; 5 | 6 | /** 7 | * Represents a curve in a Spriter SCML file. 8 | * An instance of this class is responsible for tweening given data. 9 | * The most important method of this class is {@link #tween(float, float, float)}. 10 | * Curves can be changed with sub curves {@link Curve#subCurve}. 11 | * @author Trixt0r 12 | * 13 | */ 14 | public class Curve { 15 | 16 | /** 17 | * Represents a curve type in a Spriter SCML file. 18 | * @author Trixt0r 19 | * 20 | */ 21 | public static enum Type { 22 | Instant, Linear, Quadratic, Cubic, Quartic, Quintic, Bezier; 23 | } 24 | 25 | /** 26 | * Returns a curve type based on the given curve name. 27 | * @param name the name of the curve 28 | * @return the curve type. {@link Type#Linear} is returned as a default type. 29 | */ 30 | public static Type getType(String name){ 31 | if(name.equals("instant")) return Type.Instant; 32 | else if(name.equals("quadratic")) return Type.Quadratic; 33 | else if(name.equals("cubic")) return Type.Cubic; 34 | else if(name.equals("quartic")) return Type.Quartic; 35 | else if(name.equals("quintic")) return Type.Quintic; 36 | else if(name.equals("bezier")) return Type.Bezier; 37 | else return Type.Linear; 38 | } 39 | 40 | private Type type; 41 | /** 42 | * The sub curve of this curve, which can be null. 43 | */ 44 | public Curve subCurve; 45 | /** 46 | * The constraints of a curve which will affect a curve of the types different from {@link Type#Linear} and {@link Type#Instant}. 47 | */ 48 | public final Constraints constraints = new Constraints(0, 0, 0, 0); 49 | 50 | /** 51 | * Creates a new linear curve. 52 | */ 53 | public Curve(){ 54 | this(Type.Linear); 55 | } 56 | 57 | /** 58 | * Creates a new curve with the given type. 59 | * @param type the curve type 60 | */ 61 | public Curve(Type type){ 62 | this(type, null); 63 | } 64 | 65 | /** 66 | * Creates a new curve with the given type and sub cuve. 67 | * @param type the curve type 68 | * @param subCurve the sub curve. Can be null 69 | */ 70 | public Curve(Type type, Curve subCurve){ 71 | this.setType(type); 72 | this.subCurve = subCurve; 73 | } 74 | 75 | /** 76 | * Sets the type of this curve. 77 | * @param type the curve type. 78 | * @throws SpriterException if the type is null 79 | */ 80 | public void setType(Type type){ 81 | if(type == null) throw new SpriterException("The type of a curve cannot be null!"); 82 | this.type = type; 83 | } 84 | 85 | /** 86 | * Returns the type of this curve. 87 | * @return the curve type 88 | */ 89 | public Type getType(){ 90 | return this.type; 91 | } 92 | 93 | 94 | private float lastCubicSolution = 0f; 95 | /** 96 | * Returns a new value based on the given values. 97 | * Tweens the weight with the set sub curve. 98 | * @param a the start value 99 | * @param b the end value 100 | * @param t the weight which lies between 0.0 and 1.0 101 | * @return tweened value 102 | */ 103 | public float tween(float a, float b, float t){ 104 | t = tweenSub(0f,1f,t); 105 | switch(type){ 106 | case Instant: return a; 107 | case Linear: return linear(a, b, t); 108 | case Quadratic: return quadratic(a, linear(a, b, constraints.c1), b, t); 109 | case Cubic: return cubic(a, linear(a, b, constraints.c1), linear(a, b, constraints.c2), b, t); 110 | case Quartic: return quartic(a, linear(a, b, constraints.c1), linear(a, b, constraints.c2), linear(a, b, constraints.c3), b, t); 111 | case Quintic: return quintic(a, linear(a, b, constraints.c1), linear(a, b, constraints.c2), linear(a, b, constraints.c3), linear(a, b, constraints.c4), b, t); 112 | case Bezier: float cubicSolution = solveCubic(3f*(constraints.c1-constraints.c3) + 1f, 3f*(constraints.c3-2f*constraints.c1), 3f*constraints.c1, -t); 113 | if(cubicSolution == NO_SOLUTION) cubicSolution = lastCubicSolution; 114 | else lastCubicSolution = cubicSolution; 115 | return linear(a, b, bezier(cubicSolution, 0f, constraints.c2, constraints.c4, 1f)); 116 | default: return linear(a, b, t); 117 | } 118 | } 119 | 120 | /** 121 | * Interpolates the given two points with the given weight and saves the result in the target point. 122 | * @param a the start point 123 | * @param b the end point 124 | * @param t the weight which lies between 0.0 and 1.0 125 | * @param target the target point to save the result in 126 | */ 127 | public void tweenPoint(Point a, Point b, float t, Point target){ 128 | target.set(this.tween(a.x, b.x, t), this.tween(a.y, b.y, t)); 129 | } 130 | 131 | private float tweenSub(float a, float b, float t){ 132 | if(this.subCurve != null) return subCurve.tween(a, b, t); 133 | else return t; 134 | } 135 | 136 | /** 137 | * Returns a tweened angle based on the given angles, weight and the spin. 138 | * @param a the start angle 139 | * @param b the end angle 140 | * @param t the weight which lies between 0.0 and 1.0 141 | * @param spin the spin, which is either 0, 1 or -1 142 | * @return tweened angle 143 | */ 144 | public float tweenAngle(float a, float b, float t, int spin){ 145 | if(spin>0){ 146 | if(b-a < 0) 147 | b+=360; 148 | } 149 | else if(spin < 0){ 150 | if(b-a > 0) 151 | b-=360; 152 | } 153 | else return a; 154 | 155 | return tween(a, b, t); 156 | } 157 | 158 | /** 159 | * @see #tween(float, float, float) 160 | */ 161 | public float tweenAngle(float a, float b, float t){ 162 | t = tweenSub(0f,1f,t); 163 | switch(type){ 164 | case Instant: return a; 165 | case Linear: return linearAngle(a, b, t); 166 | case Quadratic: return quadraticAngle(a, linearAngle(a, b, constraints.c1), b, t); 167 | case Cubic: return cubicAngle(a, linearAngle(a, b, constraints.c1), linearAngle(a, b, constraints.c2), b, t); 168 | case Quartic: return quarticAngle(a, linearAngle(a, b, constraints.c1), linearAngle(a, b, constraints.c2), linearAngle(a, b, constraints.c3), b, t); 169 | case Quintic: return quinticAngle(a, linearAngle(a, b, constraints.c1), linearAngle(a, b, constraints.c2), linearAngle(a, b, constraints.c3), linearAngle(a, b, constraints.c4), b, t); 170 | case Bezier: float cubicSolution = solveCubic(3f*(constraints.c1-constraints.c3) + 1f, 3f*(constraints.c3-2f*constraints.c1), 3f*constraints.c1, -t); 171 | if(cubicSolution == NO_SOLUTION) cubicSolution = lastCubicSolution; 172 | else lastCubicSolution = cubicSolution; 173 | return linearAngle(a, b, bezier(cubicSolution, 0f, constraints.c2, constraints.c4, 1f)); 174 | default: return linearAngle(a, b, t); 175 | } 176 | } 177 | 178 | public String toString(){ 179 | return getClass().getSimpleName()+"|["+type+":"+constraints+", subCurve: "+subCurve+"]"; 180 | } 181 | 182 | /** 183 | * Represents constraints for a curve. 184 | * Constraints are important for curves which have a order higher than 1. 185 | * @author Trixt0r 186 | * 187 | */ 188 | public static class Constraints{ 189 | public float c1, c2, c3, c4; 190 | 191 | public Constraints(float c1, float c2, float c3, float c4){ 192 | this.set(c1, c2, c3, c4); 193 | } 194 | 195 | public void set(float c1, float c2, float c3, float c4){ 196 | this.c1 = c1; 197 | this.c2 = c2; 198 | this.c3 = c3; 199 | this.c4 = c4; 200 | } 201 | 202 | public String toString(){ 203 | return getClass().getSimpleName()+"| [c1:"+c1+", c2:"+c2+", c3:"+c3+", c4:"+c4+"]"; 204 | } 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Calculator.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import static java.lang.Math.*; 4 | 5 | /** 6 | * A utility class which provides methods to calculate Spriter specific issues, 7 | * like linear interpolation and rotation around a parent object. 8 | * Other interpolation types are coming with the next releases of Spriter. 9 | * 10 | * @author Trixt0r 11 | * 12 | */ 13 | 14 | public class Calculator { 15 | 16 | public final static float PI = (float)Math.PI; 17 | public final static float NO_SOLUTION = -1; 18 | 19 | /** 20 | * Calculates the smallest difference between angle a and b. 21 | * @param a first angle (in degrees) 22 | * @param b second angle (in degrees) 23 | * @return Smallest difference between a and b (between 180� and -180�). 24 | */ 25 | public static float angleDifference(float a, float b){ 26 | return ((((a - b) % 360) + 540) % 360) - 180; 27 | } 28 | 29 | /** 30 | * @param x1 x coordinate of first point. 31 | * @param y1 y coordinate of first point. 32 | * @param x2 x coordinate of second point. 33 | * @param y2 y coordinate of second point. 34 | * @return Angle between the two given points. 35 | */ 36 | public static float angleBetween(float x1, float y1, float x2, float y2){ 37 | return (float)toDegrees(atan2(y2-y1,x2-x1)); 38 | } 39 | 40 | /** 41 | * @param x1 x coordinate of first point. 42 | * @param y1 y coordinate of first point. 43 | * @param x2 x coordinate of second point. 44 | * @param y2 y coordinate of second point. 45 | * @return Distance between the two given points. 46 | */ 47 | public static float distanceBetween(float x1, float y1, float x2, float y2){ 48 | float xDiff = x2-x1; 49 | float yDiff = y2-y1; 50 | return (float)sqrt(xDiff*xDiff+yDiff*yDiff); 51 | } 52 | 53 | /** 54 | * Solves the equation a*x^3 + b*x^2 + c*x +d = 0. 55 | * @param a 56 | * @param b 57 | * @param c 58 | * @param d 59 | * @return the solution of the cubic function if it belongs [0, 1], {@link #NO_SOLUTION} otherwise. 60 | */ 61 | public static float solveCubic(float a, float b, float c, float d) { 62 | if (a == 0) return solveQuadratic(b, c, d); 63 | if (d == 0) return 0f; 64 | 65 | b /= a; 66 | c /= a; 67 | d /= a; 68 | float squaredB = squared(b); 69 | float q = (3f * c - squaredB) / 9f; 70 | float r = (-27f * d + b * (9f * c - 2f * squaredB)) / 54f; 71 | float disc = cubed(q) + squared(r); 72 | float term1 = b / 3f; 73 | 74 | if (disc > 0) { 75 | float sqrtDisc = sqrt(disc); 76 | float s = r + sqrtDisc; 77 | s = (s < 0) ? -cubicRoot(-s) : cubicRoot(s); 78 | float t = r - sqrtDisc; 79 | t = (t < 0) ? -cubicRoot(-t) : cubicRoot(t); 80 | 81 | float result = -term1 + s + t; 82 | if (result >= 0 && result <= 1) return result; 83 | } else if (disc == 0) { 84 | float r13 = (r < 0) ? -cubicRoot(-r) : cubicRoot(r); 85 | 86 | float result = -term1 + 2f * r13; 87 | if (result >= 0 && result <= 1) return result; 88 | 89 | result = -(r13 + term1); 90 | if (result >= 0 && result <= 1) return result; 91 | } else { 92 | q = -q; 93 | float dum1 = q * q * q; 94 | dum1 = acos(r / sqrt(dum1)); 95 | float r13 = 2f * sqrt(q); 96 | 97 | float result = -term1 + r13 * cos(dum1 / 3f); 98 | if (result >= 0 && result <= 1) return result; 99 | 100 | result = -term1 + r13 * cos((dum1 + 2f * PI) / 3f); 101 | if (result >= 0 && result <= 1) return result; 102 | 103 | result = -term1 + r13 * cos((dum1 + 4f * PI) / 3f); 104 | if (result >= 0 && result <= 1) return result; 105 | } 106 | 107 | return NO_SOLUTION; 108 | } 109 | 110 | /** 111 | * Solves the equation a*x^2 + b*x + c = 0 112 | * @param a 113 | * @param b 114 | * @param c 115 | * @return the solution for the quadratic function if it belongs [0, 1], {@link #NO_SOLUTION} otherwise. 116 | */ 117 | public static float solveQuadratic(float a, float b, float c) { 118 | float squaredB = squared(b); 119 | float twoA = 2 * a; 120 | float fourAC = 4 * a * c; 121 | float sqrt = sqrt(squaredB - fourAC); 122 | float result = (-b + sqrt) / twoA; 123 | if (result >= 0 && result <= 1) return result; 124 | 125 | result = (-b - sqrt) / twoA; 126 | if (result >= 0 && result <= 1) return result; 127 | 128 | return NO_SOLUTION; 129 | } 130 | 131 | /** 132 | * Returns the square of the given value. 133 | * @param f the value 134 | * @return the square of the value 135 | */ 136 | public static float squared(float f) { return f * f; } 137 | 138 | /** 139 | * Returns the cubed value of the given one. 140 | * @param f the value 141 | * @return the cubed value 142 | */ 143 | public static float cubed(float f) { return f * f * f; } 144 | 145 | /** 146 | * Returns the cubic root of the given value. 147 | * @param f the value 148 | * @return the cubic root 149 | */ 150 | public static float cubicRoot(float f) { return (float) pow(f, 1f / 3f); } 151 | 152 | /** 153 | * Returns the square root of the given value. 154 | * @param x the value 155 | * @return the square root 156 | */ 157 | public static float sqrt(float x){ return (float)Math.sqrt(x); } 158 | 159 | /** 160 | * Returns the arc cosine at the given value. 161 | * @param x the value 162 | * @return the arc cosine 163 | */ 164 | public static float acos(float x){ return (float)Math.acos(x); } 165 | 166 | static private final int SIN_BITS = 14; // 16KB. Adjust for accuracy. 167 | static private final int SIN_MASK = ~(-1 << SIN_BITS); 168 | static private final int SIN_COUNT = SIN_MASK + 1; 169 | 170 | static private final float radFull = PI * 2; 171 | static private final float degFull = 360; 172 | static private final float radToIndex = SIN_COUNT / radFull; 173 | static private final float degToIndex = SIN_COUNT / degFull; 174 | 175 | /** multiply by this to convert from radians to degrees */ 176 | static public final float radiansToDegrees = 180f / PI; 177 | static public final float radDeg = radiansToDegrees; 178 | /** multiply by this to convert from degrees to radians */ 179 | static public final float degreesToRadians = PI / 180; 180 | static public final float degRad = degreesToRadians; 181 | 182 | static private class Sin { 183 | static final float[] table = new float[SIN_COUNT]; 184 | static { 185 | for (int i = 0; i < SIN_COUNT; i++) 186 | table[i] = (float)Math.sin((i + 0.5f) / SIN_COUNT * radFull); 187 | for (int i = 0; i < 360; i += 90) 188 | table[(int)(i * degToIndex) & SIN_MASK] = (float)Math.sin(i * degreesToRadians); 189 | } 190 | } 191 | 192 | /** Returns the sine in radians from a lookup table. */ 193 | static public final float sin (float radians) { 194 | return Sin.table[(int)(radians * radToIndex) & SIN_MASK]; 195 | } 196 | 197 | /** Returns the cosine in radians from a lookup table. */ 198 | static public final float cos (float radians) { 199 | return Sin.table[(int)((radians + PI / 2) * radToIndex) & SIN_MASK]; 200 | } 201 | 202 | /** Returns the sine in radians from a lookup table. */ 203 | static public final float sinDeg (float degrees) { 204 | return Sin.table[(int)(degrees * degToIndex) & SIN_MASK]; 205 | } 206 | 207 | /** Returns the cosine in radians from a lookup table. */ 208 | static public final float cosDeg (float degrees) { 209 | return Sin.table[(int)((degrees + 90) * degToIndex) & SIN_MASK]; 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Mainline.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | /** 4 | * Represents a mainline in a Spriter SCML file. 5 | * A mainline holds only keys and occurs only once in an animation. 6 | * The mainline is responsible for telling which draw order the sprites have 7 | * and how the objects are related to each other, i.e. which bone is the root and which objects are the children. 8 | * @author Trixt0r 9 | * 10 | */ 11 | public class Mainline { 12 | 13 | final Key[] keys; 14 | private int keyPointer = 0; 15 | 16 | public Mainline(int keys){ 17 | this.keys = new Key[keys]; 18 | } 19 | 20 | public String toString(){ 21 | String toReturn = getClass().getSimpleName()+"|"; 22 | for(Key key: keys) 23 | toReturn += "\n"+key; 24 | toReturn+="]"; 25 | return toReturn; 26 | } 27 | 28 | public void addKey(Key key){ 29 | this.keys[keyPointer++] = key; 30 | } 31 | 32 | /** 33 | * Returns a {@link Key} at the given index. 34 | * @param index the index of the key 35 | * @return the key with the given index 36 | * @throws IndexOutOfBoundsException if index is out of range 37 | */ 38 | public Key getKey(int index){ 39 | return this.keys[index]; 40 | } 41 | 42 | /** 43 | * Returns a {@link Key} before the given time. 44 | * @param time the time a key has to be before 45 | * @return a key which has a time value before the given one. 46 | * The first key is returned if no key was found. 47 | */ 48 | public Key getKeyBeforeTime(int time){ 49 | Key found = this.keys[0]; 50 | for(Key key: this.keys){ 51 | if(key.time <= time) found = key; 52 | else break; 53 | } 54 | return found; 55 | } 56 | 57 | /** 58 | * Represents a mainline key in a Spriter SCML file. 59 | * A mainline key holds an {@link #id}, a {@link #time}, a {@link #curve} 60 | * and lists of bone and object references which build a tree hierarchy. 61 | * @author Trixt0r 62 | * 63 | */ 64 | public static class Key{ 65 | 66 | public final int id, time; 67 | final BoneRef[] boneRefs; 68 | final ObjectRef[] objectRefs; 69 | private int bonePointer = 0, objectPointer = 0; 70 | public final Curve curve; 71 | 72 | public Key(int id, int time, Curve curve, int boneRefs, int objectRefs){ 73 | this.id = id; 74 | this.time = time; 75 | this.curve = curve; 76 | this.boneRefs = new BoneRef[boneRefs]; 77 | this.objectRefs = new ObjectRef[objectRefs]; 78 | } 79 | 80 | /** 81 | * Adds a bone reference to this key. 82 | * @param ref the reference to add 83 | */ 84 | public void addBoneRef(BoneRef ref){ 85 | this.boneRefs[bonePointer++] = ref; 86 | } 87 | 88 | /** 89 | * Adds a object reference to this key. 90 | * @param ref the reference to add 91 | */ 92 | public void addObjectRef(ObjectRef ref){ 93 | this.objectRefs[objectPointer++] = ref; 94 | } 95 | 96 | /** 97 | * Returns a {@link BoneRef} with the given index. 98 | * @param index the index of the bone reference 99 | * @return the bone reference or null if no reference exists with the given index 100 | */ 101 | public BoneRef getBoneRef(int index){ 102 | if(index < 0 || index >= this.boneRefs.length) return null; 103 | else return this.boneRefs[index]; 104 | } 105 | 106 | /** 107 | * Returns a {@link ObjectRef} with the given index. 108 | * @param index the index of the object reference 109 | * @return the object reference or null if no reference exists with the given index 110 | */ 111 | public ObjectRef getObjectRef(int index){ 112 | if(index < 0 || index >= this.objectRefs.length) return null; 113 | else return this.objectRefs[index]; 114 | } 115 | 116 | /** 117 | * Returns a {@link BoneRef} for the given reference. 118 | * @param ref the reference to the reference in this key 119 | * @return a bone reference with the same time line as the given one 120 | */ 121 | public BoneRef getBoneRef(BoneRef ref){ 122 | return getBoneRefTimeline(ref.timeline); 123 | } 124 | 125 | /** 126 | * Returns a {@link BoneRef} with the given time line index. 127 | * @param timeline the time line index 128 | * @return the bone reference with the given time line index or null if no reference exists with the given time line index 129 | */ 130 | public BoneRef getBoneRefTimeline(int timeline){ 131 | for(BoneRef boneRef: this.boneRefs) 132 | if(boneRef.timeline == timeline) return boneRef; 133 | return null; 134 | } 135 | 136 | /** 137 | * Returns an {@link ObjectRef} for the given reference. 138 | * @param ref the reference to the reference in this key 139 | * @return an object reference with the same time line as the given one 140 | */ 141 | public ObjectRef getObjectRef(ObjectRef ref){ 142 | return getObjectRefTimeline(ref.timeline); 143 | } 144 | 145 | /** 146 | * Returns a {@link ObjectRef} with the given time line index. 147 | * @param timeline the time line index 148 | * @return the object reference with the given time line index or null if no reference exists with the given time line index 149 | */ 150 | public ObjectRef getObjectRefTimeline(int timeline){ 151 | for(ObjectRef objRef: this.objectRefs) 152 | if(objRef.timeline == timeline) return objRef; 153 | return null; 154 | } 155 | 156 | public String toString(){ 157 | String toReturn = getClass().getSimpleName()+"|[id:"+id+", time: "+time+", curve: ["+curve+"]"; 158 | for(BoneRef ref: boneRefs) 159 | toReturn += "\n"+ref; 160 | for(ObjectRef ref: objectRefs) 161 | toReturn += "\n"+ref; 162 | toReturn+="]"; 163 | return toReturn; 164 | } 165 | 166 | /** 167 | * Represents a bone reference in a Spriter SCML file. 168 | * A bone reference holds an {@link #id}, a {@link #timeline} and a {@link #key}. 169 | * A bone reference may have a parent reference. 170 | * @author Trixt0r 171 | * 172 | */ 173 | public static class BoneRef{ 174 | public final int id, key, timeline; 175 | public final BoneRef parent; 176 | 177 | public BoneRef(int id, int timeline, int key, BoneRef parent){ 178 | this.id = id; 179 | this.timeline = timeline; 180 | this.key = key; 181 | this.parent = parent; 182 | } 183 | 184 | public String toString(){ 185 | int parentId = (parent != null) ? parent.id:-1; 186 | return getClass().getSimpleName()+"|id: "+id+", parent:"+parentId+", timeline: "+timeline+", key: "+key; 187 | } 188 | } 189 | 190 | /** 191 | * Represents an object reference in a Spriter SCML file. 192 | * An object reference extends a {@link BoneRef} with a {@link #zIndex}, 193 | * which indicates when the object has to be drawn. 194 | * @author Trixt0r 195 | * 196 | */ 197 | public static class ObjectRef extends BoneRef implements Comparable{ 198 | public final int zIndex; 199 | 200 | public ObjectRef(int id, int timeline, int key, BoneRef parent, int zIndex){ 201 | super(id, timeline, key, parent); 202 | this.zIndex = zIndex; 203 | } 204 | 205 | public String toString(){ 206 | return super.toString()+", z_index: "+zIndex; 207 | } 208 | 209 | public int compareTo(ObjectRef o) { 210 | return (int)Math.signum(zIndex-o.zIndex); 211 | } 212 | } 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/test/resources/test.scml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Entity.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | 7 | /** 8 | * Represents an entity of a Spriter SCML file. 9 | * An entity holds {@link Animation}s, an {@link #id}, a {@link #name}. 10 | * {@link #characterMaps} and {@link #objectInfos} may be empty. 11 | * @author Trixt0r 12 | * 13 | */ 14 | public class Entity { 15 | 16 | public final int id; 17 | public final String name; 18 | private final Animation[] animations; 19 | private int animationPointer = 0; 20 | private final HashMap namedAnimations; 21 | private final CharacterMap[] characterMaps; 22 | private int charMapPointer = 0; 23 | private final ObjectInfo[] objectInfos; 24 | private int objInfoPointer = 0; 25 | 26 | Entity(int id, String name, int animations, int characterMaps, int objectInfos){ 27 | this.id = id; 28 | this.name = name; 29 | this.animations = new Animation[animations]; 30 | this.characterMaps = new CharacterMap[characterMaps]; 31 | this.objectInfos = new ObjectInfo[objectInfos]; 32 | this.namedAnimations = new HashMap(); 33 | } 34 | 35 | void addAnimation(Animation anim){ 36 | this.animations[animationPointer++] = anim; 37 | this.namedAnimations.put(anim.name, anim); 38 | } 39 | 40 | /** 41 | * Returns an {@link Animation} with the given index. 42 | * @param index the index of the animation 43 | * @return the animation with the given index 44 | * @throws IndexOutOfBoundsException if the index is out of range 45 | */ 46 | public Animation getAnimation(int index){ 47 | return this.animations[index]; 48 | } 49 | 50 | /** 51 | * Returns an {@link Animation} with the given name. 52 | * @param name the name of the animation 53 | * @return the animation with the given name or null if no animation exists with the given name 54 | */ 55 | public Animation getAnimation(String name){ 56 | return this.namedAnimations.get(name); 57 | } 58 | 59 | /** 60 | * Returns the number of animations this entity holds. 61 | * @return the number of animations 62 | */ 63 | public int animations(){ 64 | return this.animations.length; 65 | } 66 | 67 | /** 68 | * Returns whether this entity contains the given animation. 69 | * @param anim the animation to check 70 | * @return true if the given animation is in this entity, false otherwise. 71 | */ 72 | public boolean containsAnimation(Animation anim){ 73 | for(Animation a: this.animations) 74 | if(a == anim) return true; 75 | return false; 76 | } 77 | 78 | /** 79 | * Returns the animation with the most number of time lines in this entity. 80 | * @return animation with the maximum amount of time lines. 81 | */ 82 | public Animation getAnimationWithMostTimelines(){ 83 | Animation maxAnim = getAnimation(0); 84 | for(Animation anim: this.animations){ 85 | if(maxAnim.timelines() < anim.timelines()) maxAnim = anim; 86 | } 87 | return maxAnim; 88 | } 89 | 90 | /** 91 | * Returns a {@link CharacterMap} with the given name. 92 | * @param name name of the character map 93 | * @return the character map or null if no character map exists with the given name 94 | */ 95 | public CharacterMap getCharacterMap(String name){ 96 | for(CharacterMap map: this.characterMaps) 97 | if(map.name.equals(name)) return map; 98 | return null; 99 | } 100 | 101 | void addCharacterMap(CharacterMap map){ 102 | this.characterMaps[charMapPointer++] = map; 103 | } 104 | 105 | void addInfo(ObjectInfo info){ 106 | this.objectInfos[objInfoPointer++] = info; 107 | } 108 | 109 | /** 110 | * Returns an {@link ObjectInfo} with the given index. 111 | * @param index the index of the object info 112 | * @return the object info 113 | * @throws IndexOutOfBoundsException if index is out of range 114 | */ 115 | public ObjectInfo getInfo(int index){ 116 | return this.objectInfos[index]; 117 | } 118 | 119 | /** 120 | * Returns an {@link ObjectInfo} with the given name. 121 | * @param name name of the object info 122 | * @return object info or null if no object info exists with the given name 123 | */ 124 | public ObjectInfo getInfo(String name){ 125 | for(ObjectInfo info: this.objectInfos) 126 | if(info.name.equals(name)) return info; 127 | return null; 128 | } 129 | 130 | /** 131 | * Returns an {@link ObjectInfo} with the given name and the given {@link ObjectType} type. 132 | * @param name the name of the object info 133 | * @param type the type if the object info 134 | * @return the object info or null if no object info exists with the given name and type 135 | */ 136 | public ObjectInfo getInfo(String name, ObjectType type){ 137 | ObjectInfo info = this.getInfo(name); 138 | if(info != null && info.type == type) return info; 139 | else return null; 140 | } 141 | 142 | /** 143 | * Represents the object types Spriter supports. 144 | * @author Trixt0r 145 | * 146 | */ 147 | public static enum ObjectType{ 148 | Sprite, Bone, Box, Point, Skin; 149 | 150 | /** 151 | * Returns the object type for the given name 152 | * @param name the name of the type 153 | * @return the object type, Sprite is the default value 154 | */ 155 | public static ObjectType getObjectInfoFor(String name){ 156 | if(name.equals("bone")) return Bone; 157 | else if(name.equals("skin")) return Skin; 158 | else if(name.equals("box")) return Box; 159 | else if(name.equals("point")) return Point; 160 | else return Sprite; 161 | } 162 | } 163 | 164 | /** 165 | * Represents the object info in a Spriter SCML file. 166 | * An object info holds a {@link #type} and a {@link #name}. 167 | * If the type is a Sprite it holds a list of frames. Otherwise it has a {@link #size} for debug drawing purposes. 168 | * @author Trixt0r 169 | * 170 | */ 171 | public static class ObjectInfo{ 172 | public final ObjectType type; 173 | public final List frames; 174 | public final String name; 175 | public final Dimension size; 176 | 177 | ObjectInfo(String name, ObjectType type, Dimension size, List frames){ 178 | this.type = type; 179 | this.frames = frames; 180 | this.name = name; 181 | this.size = size; 182 | } 183 | 184 | ObjectInfo(String name, ObjectType type, Dimension size){ 185 | this(name, type, size, new ArrayList()); 186 | } 187 | 188 | ObjectInfo(String name, ObjectType type, List frames){ 189 | this(name, type, new Dimension(0,0), frames); 190 | } 191 | 192 | public String toString(){ 193 | return name + ": "+ type + ", size: "+size+"|frames:\n"+frames; 194 | } 195 | } 196 | 197 | /** 198 | * Represents a Spriter SCML character map. 199 | * A character map maps {@link FileReference}s to {@link FileReference}s. 200 | * It holds an {@link CharacterMap#id} and a {@link CharacterMap#name}. 201 | * @author Trixt0r 202 | * 203 | */ 204 | public static class CharacterMap extends HashMap{ 205 | private static final long serialVersionUID = 6062776450159802283L; 206 | 207 | public final int id; 208 | public final String name; 209 | 210 | public CharacterMap(int id, String name){ 211 | this.id = id; 212 | this.name = name; 213 | } 214 | 215 | /** 216 | * Returns the mapped reference for the given key. 217 | * @param key the key of the reference 218 | * @return The mapped reference if the key is in this map, otherwise the given key itself is returned. 219 | */ 220 | public FileReference get(FileReference key){ 221 | if(!super.containsKey(key)) return key; 222 | else return super.get(key); 223 | } 224 | } 225 | 226 | public String toString(){ 227 | String toReturn = getClass().getSimpleName()+"|[id: "+id+", name: "+name+"]"; 228 | toReturn +="Object infos:\n"; 229 | for(ObjectInfo info: this.objectInfos) 230 | toReturn += "\n"+info; 231 | toReturn +="Character maps:\n"; 232 | for(CharacterMap map: this.characterMaps) 233 | toReturn += "\n"+map; 234 | toReturn +="Animations:\n"; 235 | for(Animation animaton: this.animations) 236 | toReturn += "\n"+animaton; 237 | toReturn+="]"; 238 | return toReturn; 239 | } 240 | 241 | } 242 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Animation.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.util.HashMap; 4 | 5 | import com.brashmonkey.spriter.Mainline.Key; 6 | import com.brashmonkey.spriter.Mainline.Key.BoneRef; 7 | import com.brashmonkey.spriter.Mainline.Key.ObjectRef; 8 | import com.brashmonkey.spriter.Timeline.Key.Bone; 9 | import com.brashmonkey.spriter.Timeline.Key.Object; 10 | /** 11 | * Represents an animation of a Spriter SCML file. 12 | * An animation holds {@link Timeline}s and a {@link Mainline} to animate objects. 13 | * Furthermore it holds an {@link #id}, a {@link #length}, a {@link #name} and whether it is {@link #looping} or not. 14 | * @author Trixt0r 15 | * 16 | */ 17 | public class Animation { 18 | 19 | public final Mainline mainline; 20 | private final Timeline[] timelines; 21 | private int timelinePointer = 0; 22 | private final HashMap nameToTimeline; 23 | public final int id, length; 24 | public final String name; 25 | public final boolean looping; 26 | Key currentKey; 27 | Timeline.Key[] tweenedKeys, unmappedTweenedKeys; 28 | private boolean prepared; 29 | 30 | public Animation(Mainline mainline, int id, String name, int length, boolean looping, int timelines){ 31 | this.mainline = mainline; 32 | this.id = id; 33 | this.name = name; 34 | this.length = length; 35 | this.looping = looping; 36 | this.timelines = new Timeline[timelines]; 37 | this.prepared = false; 38 | this.nameToTimeline = new HashMap(); 39 | //this.currentKey = mainline.getKey(0); 40 | } 41 | 42 | /** 43 | * Returns a {@link Timeline} with the given index. 44 | * @param index the index of the timeline 45 | * @return the timeline with the given index 46 | * @throws IndexOutOfBoundsException if the index is out of range 47 | */ 48 | public Timeline getTimeline(int index){ 49 | return this.timelines[index]; 50 | } 51 | 52 | /** 53 | * Returns a {@link Timeline} with the given name. 54 | * @param name the name of the time line 55 | * @return the time line with the given name or null if no time line exists with the given name. 56 | */ 57 | public Timeline getTimeline(String name){ 58 | return this.nameToTimeline.get(name); 59 | } 60 | 61 | void addTimeline(Timeline timeline){ 62 | this.timelines[timelinePointer++] = timeline; 63 | this.nameToTimeline.put(timeline.name, timeline); 64 | } 65 | 66 | /** 67 | * Returns the number of time lines this animation holds. 68 | * @return the number of time lines 69 | */ 70 | public int timelines(){ 71 | return timelines.length; 72 | } 73 | 74 | public String toString(){ 75 | String toReturn = getClass().getSimpleName()+"|[id: "+id+", "+name+", duration: "+length+", is looping: "+looping; 76 | toReturn +="Mainline:\n"; 77 | toReturn += mainline; 78 | toReturn += "Timelines\n"; 79 | for(Timeline timeline: this.timelines) 80 | toReturn += timeline; 81 | toReturn+="]"; 82 | return toReturn; 83 | } 84 | 85 | /** 86 | * Updates the bone and object structure with the given time to the given root bone. 87 | * @param time The time which has to be between 0 and {@link #length} to work properly. 88 | * @param root The root bone which is not allowed to be null. The whole animation runs relative to the root bone. 89 | */ 90 | public void update(int time, Bone root){ 91 | if(!this.prepared) throw new SpriterException("This animation is not ready yet to animate itself. Please call prepare()!"); 92 | if(root == null) throw new SpriterException("The root can not be null! Set a root bone to apply this animation relative to the root bone."); 93 | this.currentKey = mainline.getKeyBeforeTime(time); 94 | 95 | for(Timeline.Key timelineKey: this.unmappedTweenedKeys) 96 | timelineKey.active = false; 97 | for(BoneRef ref: currentKey.boneRefs) 98 | this.update(ref, root, time); 99 | for(ObjectRef ref: currentKey.objectRefs) 100 | this.update(ref, root, time); 101 | } 102 | 103 | protected void update(BoneRef ref, Bone root, int time){ 104 | boolean isObject = ref instanceof ObjectRef; 105 | //Get the timelines, the refs pointing to 106 | Timeline timeline = getTimeline(ref.timeline); 107 | Timeline.Key key = timeline.getKey(ref.key); 108 | Timeline.Key nextKey = timeline.getKey((ref.key+1)%timeline.keys.length); 109 | int currentTime = key.time; 110 | int nextTime = nextKey.time; 111 | if(nextTime < currentTime){ 112 | if(!looping) nextKey = key; 113 | else nextTime = length; 114 | } 115 | //Normalize the time 116 | float t = (float)(time - currentTime)/(float)(nextTime - currentTime); 117 | if(Float.isNaN(t) || Float.isInfinite(t)) t = 1f; 118 | if(currentKey.time > currentTime){ 119 | float tMid = (float)(currentKey.time - currentTime)/(float)(nextTime - currentTime); 120 | if(Float.isNaN(tMid) || Float.isInfinite(tMid)) tMid = 0f; 121 | t = (float)(time - currentKey.time)/(float)(nextTime - currentKey.time); 122 | if(Float.isNaN(t) || Float.isInfinite(t)) t = 1f; 123 | t = currentKey.curve.tween(tMid, 1f, t); 124 | } 125 | else 126 | t = currentKey.curve.tween(0f, 1f, t); 127 | //Tween bone/object 128 | Bone bone1 = key.object(); 129 | Bone bone2 = nextKey.object(); 130 | Bone tweenTarget = this.tweenedKeys[ref.timeline].object(); 131 | if(isObject) this.tweenObject((Object)bone1, (Object)bone2, (Object)tweenTarget, t, key.curve, key.spin); 132 | else this.tweenBone(bone1, bone2, tweenTarget, t, key.curve, key.spin); 133 | this.unmappedTweenedKeys[ref.timeline].active = true; 134 | this.unmapTimelineObject(ref.timeline, isObject,(ref.parent != null) ? 135 | this.unmappedTweenedKeys[ref.parent.timeline].object(): root); 136 | } 137 | 138 | void unmapTimelineObject(int timeline, boolean isObject, Bone root){ 139 | Bone tweenTarget = this.tweenedKeys[timeline].object(); 140 | Bone mapTarget = this.unmappedTweenedKeys[timeline].object(); 141 | if(isObject) ((Object)mapTarget).set((Object)tweenTarget); 142 | else mapTarget.set(tweenTarget); 143 | mapTarget.unmap(root); 144 | } 145 | 146 | protected void tweenBone(Bone bone1, Bone bone2, Bone target, float t, Curve curve, int spin){ 147 | target.angle = curve.tweenAngle(bone1.angle, bone2.angle, t, spin); 148 | curve.tweenPoint(bone1.position, bone2.position, t, target.position); 149 | curve.tweenPoint(bone1.scale, bone2.scale, t, target.scale); 150 | curve.tweenPoint(bone1.pivot, bone2.pivot, t, target.pivot); 151 | } 152 | 153 | protected void tweenObject(Object object1, Object object2, Object target, float t, Curve curve, int spin){ 154 | this.tweenBone(object1, object2, target, t, curve, spin); 155 | target.alpha = curve.tweenAngle(object1.alpha, object2.alpha, t); 156 | target.ref.set(object1.ref); 157 | } 158 | 159 | Timeline getSimilarTimeline(Timeline t){ 160 | Timeline found = getTimeline(t.name); 161 | if(found == null && t.id < this.timelines()) found = this.getTimeline(t.id); 162 | return found; 163 | } 164 | 165 | /*Timeline getSimilarTimeline(BoneRef ref, Collection coveredTimelines){ 166 | if(ref.parent == null) return null; 167 | for(BoneRef boneRef: this.currentKey.objectRefs){ 168 | Timeline t = this.getTimeline(boneRef.timeline); 169 | if(boneRef.parent != null && boneRef.parent.id == ref.parent.id && !coveredTimelines.contains(t)) 170 | return t; 171 | } 172 | return null; 173 | } 174 | 175 | Timeline getSimilarTimeline(ObjectRef ref, Collection coveredTimelines){ 176 | if(ref.parent == null) return null; 177 | for(ObjectRef objRef: this.currentKey.objectRefs){ 178 | Timeline t = this.getTimeline(objRef.timeline); 179 | if(objRef.parent != null && objRef.parent.id == ref.parent.id && !coveredTimelines.contains(t)) 180 | return t; 181 | } 182 | return null; 183 | }*/ 184 | 185 | /** 186 | * Prepares this animation to set this animation in any time state. 187 | * This method has to be called before {@link #update(int, Bone)}. 188 | */ 189 | public void prepare(){ 190 | if(this.prepared) return; 191 | this.tweenedKeys = new Timeline.Key[timelines.length]; 192 | this.unmappedTweenedKeys = new Timeline.Key[timelines.length]; 193 | 194 | for(int i = 0; i < this.tweenedKeys.length; i++){ 195 | this.tweenedKeys[i] = new Timeline.Key(i); 196 | this.unmappedTweenedKeys[i] = new Timeline.Key(i); 197 | this.tweenedKeys[i].setObject(new Timeline.Key.Object(new Point(0,0))); 198 | this.unmappedTweenedKeys[i].setObject(new Timeline.Key.Object(new Point(0,0))); 199 | } 200 | if(mainline.keys.length > 0) currentKey = mainline.getKey(0); 201 | this.prepared = true; 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/TweenedAnimation.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import com.brashmonkey.spriter.Mainline.Key.BoneRef; 4 | import com.brashmonkey.spriter.Mainline.Key.ObjectRef; 5 | import com.brashmonkey.spriter.Timeline.Key.Bone; 6 | import com.brashmonkey.spriter.Timeline.Key.Object; 7 | 8 | /** 9 | * A tweened animation is responsible for updating itself based on two given animations. 10 | * The values of the two given animations will get interpolated and save in this animation. 11 | * When tweening two animations, you have to make sure that they have the same structure. 12 | * The best result is achieved if bones of two different animations are named in the same way. 13 | * There are still issues with sprites, which are hard to resolve since Spriter does not save them in a useful order or naming convention. 14 | * @author Trixt0r 15 | * 16 | */ 17 | public class TweenedAnimation extends Animation{ 18 | 19 | /** 20 | * The weight of the interpolation. 0.5f is the default value. 21 | * Values closer to 0.0f mean the first animation will have more influence. 22 | */ 23 | public float weight = .5f; 24 | 25 | /** 26 | * Indicates when a sprite should be switched form the first animation object to the second one. 27 | * A value closer to 0.0f means that the sprites of the second animation will be drawn. 28 | */ 29 | public float spriteThreshold = .5f; 30 | 31 | /** 32 | * The curve which will tween the animations. 33 | * The default type of the curve is {@link Curve.Type#Linear}. 34 | */ 35 | public final Curve curve; 36 | 37 | /** 38 | * The entity the animations have be part of. 39 | * Animations of two different entities can not be tweened. 40 | */ 41 | public final Entity entity; 42 | private Animation anim1, anim2; 43 | 44 | /** 45 | * The base animation an object or bone will get if it will not be tweened. 46 | */ 47 | public Animation baseAnimation; 48 | BoneRef base = null; 49 | 50 | /** 51 | * Indicates whether to tween sprites or not. Default value is false. 52 | * Tweening sprites should be only enabled if they have exactly the same structure. 53 | * If all animations are bone based and sprites only change their references it is not recommended to tween sprites. 54 | */ 55 | public boolean tweenSprites = false; 56 | 57 | /** 58 | * Creates a tweened animation based on the given entity. 59 | * @param entity the entity animations have to be part of 60 | */ 61 | public TweenedAnimation(Entity entity) { 62 | super(new Mainline(0), -1, "__interpolatedAnimation__", 0, true, entity.getAnimationWithMostTimelines().timelines()); 63 | this.entity = entity; 64 | this.curve = new Curve(); 65 | this.setUpTimelines(); 66 | } 67 | 68 | /** 69 | * Returns the current mainline key. 70 | * @return the mainline key 71 | */ 72 | public Mainline.Key getCurrentKey(){ 73 | return this.currentKey; 74 | } 75 | 76 | @Override 77 | public void update(int time, Bone root){ 78 | super.currentKey = onFirstMainLine() ? anim1.currentKey: anim2.currentKey; 79 | for(Timeline.Key timelineKey: this.unmappedTweenedKeys) 80 | timelineKey.active = false; 81 | if(base != null){//TODO: Sprites not working properly because of different timeline naming 82 | Animation currentAnim = onFirstMainLine() ? anim1: anim2; 83 | Animation baseAnim = baseAnimation == null ? (onFirstMainLine() ? anim1:anim2) : baseAnimation; 84 | for(BoneRef ref: currentKey.boneRefs){ 85 | Timeline timeline = baseAnim.getSimilarTimeline(currentAnim.getTimeline(ref.timeline)); 86 | if(timeline == null) continue; 87 | Timeline.Key key, mappedKey; 88 | key = baseAnim.tweenedKeys[timeline.id]; 89 | mappedKey = baseAnim.unmappedTweenedKeys[timeline.id]; 90 | this.tweenedKeys[ref.timeline].active = key.active; 91 | this.tweenedKeys[ref.timeline].object().set(key.object()); 92 | this.unmappedTweenedKeys[ref.timeline].active = mappedKey.active; 93 | this.unmapTimelineObject(ref.timeline, false,(ref.parent != null) ? 94 | this.unmappedTweenedKeys[ref.parent.timeline].object(): root); 95 | } 96 | /*for(ObjectRef ref: baseAnim.currentKey.objectRefs){ 97 | Timeline timeline = baseAnim.getTimeline(ref.timeline);//getSimilarTimeline(ref, tempTimelines); 98 | if(timeline != null){ 99 | //tempTimelines.addLast(timeline); 100 | Timeline.Key key = baseAnim.tweenedKeys[timeline.id]; 101 | Timeline.Key mappedKey = baseAnim.mappedTweenedKeys[timeline.id]; 102 | Object obj = (Object) key.object(); 103 | 104 | this.tweenedKeys[ref.timeline].active = key.active; 105 | ((Object)this.tweenedKeys[ref.timeline].object()).set(obj); 106 | this.mappedTweenedKeys[ref.timeline].active = mappedKey.active; 107 | this.unmapTimelineObject(ref.timeline, true,(ref.parent != null) ? 108 | this.mappedTweenedKeys[ref.parent.timeline].object(): root); 109 | } 110 | }*/ 111 | //tempTimelines.clear(); 112 | } 113 | 114 | this.tweenBoneRefs(base, root); 115 | for(ObjectRef ref: super.currentKey.objectRefs){ 116 | //if(ref.parent == base) 117 | this.update(ref, root, 0); 118 | } 119 | } 120 | 121 | private void tweenBoneRefs(BoneRef base, Bone root){ 122 | int startIndex = base == null ? -1 : base.id-1; 123 | int length = super.currentKey.boneRefs.length; 124 | for(int i = startIndex+1; i < length; i++){ 125 | BoneRef ref = currentKey.boneRefs[i]; 126 | if(base == ref || ref.parent == base) this.update(ref, root, 0); 127 | if(base == ref.parent) this.tweenBoneRefs(ref, root); 128 | } 129 | } 130 | 131 | @Override 132 | protected void update(BoneRef ref, Bone root, int time){ 133 | boolean isObject = ref instanceof ObjectRef; 134 | //Tween bone/object 135 | Bone bone1 = null, bone2 = null, tweenTarget = null; 136 | Timeline t1 = onFirstMainLine() ? anim1.getTimeline(ref.timeline) : anim1.getSimilarTimeline(anim2.getTimeline(ref.timeline)); 137 | Timeline t2 = onFirstMainLine() ? anim2.getSimilarTimeline(t1) : anim2.getTimeline(ref.timeline); 138 | Timeline targetTimeline = super.getTimeline(onFirstMainLine() ? t1.id:t2.id); 139 | if(t1 != null) bone1 = anim1.tweenedKeys[t1.id].object(); 140 | if(t2 != null) bone2 = anim2.tweenedKeys[t2.id].object(); 141 | if(targetTimeline != null) tweenTarget = this.tweenedKeys[targetTimeline.id].object(); 142 | if(isObject && (t2 == null || !tweenSprites)){ 143 | if(!onFirstMainLine()) bone1 = bone2; 144 | else bone2 = bone1; 145 | } 146 | if(bone2 != null && tweenTarget != null && bone1 != null){ 147 | if(isObject) this.tweenObject((Object)bone1, (Object)bone2, (Object)tweenTarget, this.weight, this.curve); 148 | else this.tweenBone(bone1, bone2, tweenTarget, this.weight, this.curve); 149 | this.unmappedTweenedKeys[targetTimeline.id].active = true; 150 | } 151 | //Transform the bone relative to the parent bone or the root 152 | if(this.unmappedTweenedKeys[ref.timeline].active){ 153 | this.unmapTimelineObject(targetTimeline.id, isObject,(ref.parent != null) ? 154 | this.unmappedTweenedKeys[ref.parent.timeline].object(): root); 155 | } 156 | } 157 | 158 | private void tweenBone(Bone bone1, Bone bone2, Bone target, float t, Curve curve){ 159 | target.angle = curve.tweenAngle(bone1.angle, bone2.angle, t); 160 | curve.tweenPoint(bone1.position, bone2.position, t, target.position); 161 | curve.tweenPoint(bone1.scale, bone2.scale, t, target.scale); 162 | curve.tweenPoint(bone1.pivot, bone2.pivot, t, target.pivot); 163 | } 164 | 165 | private void tweenObject(Object object1, Object object2, Object target, float t, Curve curve){ 166 | this.tweenBone(object1, object2, target, t, curve); 167 | target.alpha = curve.tweenAngle(object1.alpha, object2.alpha, t); 168 | target.ref.set(object1.ref); 169 | } 170 | 171 | /** 172 | * Returns whether the current mainline key is the one from the first animation or from the second one. 173 | * @return true if the mainline key is the one from the first animation 174 | */ 175 | public boolean onFirstMainLine(){ 176 | return this.weight < this.spriteThreshold; 177 | } 178 | 179 | private void setUpTimelines(){ 180 | Animation maxAnim = this.entity.getAnimationWithMostTimelines(); 181 | int max = maxAnim.timelines(); 182 | for(int i = 0; i < max; i++){ 183 | Timeline t = new Timeline(i, maxAnim.getTimeline(i).name, maxAnim.getTimeline(i).objectInfo, 1); 184 | addTimeline(t); 185 | } 186 | prepare(); 187 | } 188 | 189 | /** 190 | * Sets the animations to tween. 191 | * @param animation1 the first animation 192 | * @param animation2 the second animation 193 | * @throws SpriterException if {@link #entity} does not contain one of the given animations. 194 | */ 195 | public void setAnimations(Animation animation1, Animation animation2){ 196 | boolean areInterpolated = animation1 instanceof TweenedAnimation || animation2 instanceof TweenedAnimation; 197 | if(animation1 == anim1 && animation2 == anim2) return; 198 | if((!this.entity.containsAnimation(animation1) || !this.entity.containsAnimation(animation2)) && !areInterpolated) 199 | throw new SpriterException("Both animations have to be part of the same entity!"); 200 | this.anim1 = animation1; 201 | this.anim2 = animation2; 202 | } 203 | 204 | /** 205 | * Returns the first animation. 206 | * @return the first animation 207 | */ 208 | public Animation getFirstAnimation(){ 209 | return this.anim1; 210 | } 211 | 212 | /** 213 | * Returns the second animation. 214 | * @return the second animation 215 | */ 216 | public Animation getSecondAnimation(){ 217 | return this.anim2; 218 | } 219 | 220 | } 221 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Timeline.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import com.brashmonkey.spriter.Entity.ObjectInfo; 4 | 5 | /** 6 | * Represents a time line in a Spriter SCML file. 7 | * A time line holds an {@link #id}, a {@link #name} and at least one {@link Key}. 8 | * @author Trixt0r 9 | * 10 | */ 11 | public class Timeline { 12 | 13 | public final Key[] keys; 14 | private int keyPointer = 0; 15 | public final int id; 16 | public final String name; 17 | public final ObjectInfo objectInfo; 18 | 19 | Timeline(int id, String name, ObjectInfo objectInfo, int keys){ 20 | this.id = id; 21 | this.name = name; 22 | this.objectInfo = objectInfo; 23 | this.keys = new Key[keys]; 24 | } 25 | 26 | void addKey(Key key){ 27 | this.keys[keyPointer++] = key; 28 | } 29 | 30 | /** 31 | * Returns a {@link Key} at the given index 32 | * @param index the index of the key. 33 | * @return the key with the given index. 34 | * @throws IndexOutOfBoundsException if the index is out of range 35 | */ 36 | public Key getKey(int index){ 37 | return this.keys[index]; 38 | } 39 | 40 | public String toString(){ 41 | String toReturn = getClass().getSimpleName()+"|[id:"+id+", name: "+name+", object_info: "+objectInfo; 42 | for(Key key: keys) 43 | toReturn += "\n"+key; 44 | toReturn+="]"; 45 | return toReturn; 46 | } 47 | 48 | /** 49 | * Represents a time line key in a Spriter SCML file. 50 | * A key holds an {@link #id}, a {@link #time}, a {@link #spin}, an {@link #object()} and a {@link #curve}. 51 | * @author Trixt0r 52 | * 53 | */ 54 | public static class Key{ 55 | 56 | public final int id, spin; 57 | public int time; 58 | public final Curve curve; 59 | public boolean active; 60 | private Object object; 61 | 62 | public Key(int id, int time, int spin, Curve curve){ 63 | this.id = id; 64 | this.time = time; 65 | this.spin = spin; 66 | this.curve = curve; 67 | } 68 | 69 | public Key(int id,int time, int spin){ 70 | this(id, time, 1, new Curve()); 71 | } 72 | 73 | public Key(int id, int time){ 74 | this(id, time, 1); 75 | } 76 | 77 | public Key(int id){ 78 | this(id, 0); 79 | } 80 | 81 | public void setObject(Object object){ 82 | if(object == null) throw new IllegalArgumentException("object can not be null!"); 83 | this.object = object; 84 | } 85 | 86 | public Object object(){ 87 | return this.object; 88 | } 89 | 90 | public String toString(){ 91 | return getClass().getSimpleName()+"|[id: "+id+", time: "+time+", spin: "+spin+"\ncurve: "+curve+"\nobject:"+object+"]"; 92 | } 93 | 94 | /** 95 | * Represents a bone in a Spriter SCML file. 96 | * A bone holds a {@link #position}, {@link #scale}, an {@link #angle} and a {@link #pivot}. 97 | * Bones are the only objects which can be used as a parent for other tweenable objects. 98 | * @author Trixt0r 99 | * 100 | */ 101 | public static class Bone{ 102 | public final Point position, scale, pivot; 103 | public float angle; 104 | 105 | public Bone(Point position, Point scale, Point pivot, float angle){ 106 | this.position = new Point(position); 107 | this.scale = new Point(scale); 108 | this.angle = angle; 109 | this.pivot = new Point(pivot); 110 | } 111 | 112 | public Bone(Bone bone){ 113 | this(bone.position, bone.scale, bone.pivot, bone.angle); 114 | } 115 | 116 | public Bone(Point position){ 117 | this(position, new Point(1f,1f), new Point(0f, 1f), 0f); 118 | } 119 | 120 | public Bone(){ 121 | this(new Point()); 122 | } 123 | 124 | /** 125 | * Returns whether this instance is a Spriter object or a bone. 126 | * @return true if this instance is a Spriter bone 127 | */ 128 | public boolean isBone(){ 129 | return !(this instanceof Object); 130 | } 131 | 132 | /** 133 | * Sets the values of this bone to the values of the given bone 134 | * @param bone the bone 135 | */ 136 | public void set(Bone bone){ 137 | this.set(bone.position, bone.angle, bone.scale, bone.pivot); 138 | } 139 | 140 | /** 141 | * Sets the given values for this bone. 142 | * @param x the new position in x direction 143 | * @param y the new position in y direction 144 | * @param angle the new angle 145 | * @param scaleX the new scale in x direction 146 | * @param scaleY the new scale in y direction 147 | * @param pivotX the new pivot in x direction 148 | * @param pivotY the new pivot in y direction 149 | */ 150 | public void set(float x, float y, float angle, float scaleX, float scaleY, float pivotX, float pivotY){ 151 | this.angle = angle; 152 | this.position.set(x, y); 153 | this.scale.set(scaleX, scaleY); 154 | this.pivot.set(pivotX, pivotY); 155 | } 156 | 157 | /** 158 | * Sets the given values for this bone. 159 | * @param position the new position 160 | * @param angle the new angle 161 | * @param scale the new scale 162 | * @param pivot the new pivot 163 | */ 164 | public void set(Point position, float angle, Point scale, Point pivot){ 165 | this.set(position.x, position.y, angle, scale.x, scale.y, pivot.x, pivot.y); 166 | } 167 | 168 | /** 169 | * Maps this bone from it's parent's coordinate system to a global one. 170 | * @param parent the parent bone of this bone 171 | */ 172 | public void unmap(Bone parent){ 173 | this.angle *= Math.signum(parent.scale.x)*Math.signum(parent.scale.y); 174 | this.angle += parent.angle; 175 | this.scale.scale(parent.scale); 176 | this.position.scale(parent.scale); 177 | this.position.rotate(parent.angle); 178 | this.position.translate(parent.position); 179 | } 180 | 181 | /** 182 | * Maps this from it's global coordinate system to the parent's one. 183 | * @param parent the parent bone of this bone 184 | */ 185 | public void map(Bone parent){ 186 | this.position.translate(-parent.position.x, -parent.position.y); 187 | this.position.rotate(-parent.angle); 188 | this.position.scale(1f/parent.scale.x, 1f/parent.scale.y); 189 | this.scale.scale(1f/parent.scale.x, 1f/parent.scale.y); 190 | this.angle -=parent.angle; 191 | this.angle *= Math.signum(parent.scale.x)*Math.signum(parent.scale.y); 192 | } 193 | 194 | public String toString(){ 195 | return getClass().getSimpleName()+"|position: "+position+", scale: "+scale+", angle: "+angle; 196 | } 197 | } 198 | 199 | 200 | /** 201 | * Represents an object in a Spriter SCML file. 202 | * A file has the same properties as a bone with an alpha and file extension. 203 | * @author Trixt0r 204 | * 205 | */ 206 | public static class Object extends Bone{ 207 | 208 | public float alpha; 209 | public final FileReference ref; 210 | 211 | public Object(Point position, Point scale, Point pivot, float angle, float alpha, FileReference ref) { 212 | super(position, scale, pivot, angle); 213 | this.alpha = alpha; 214 | this.ref = ref; 215 | } 216 | 217 | public Object(Point position) { 218 | this(position, new Point(1f,1f), new Point(0f,1f), 0f, 1f, new FileReference(-1,-1)); 219 | } 220 | 221 | public Object(Object object){ 222 | this(object.position.copy(), object.scale.copy(),object.pivot.copy(),object.angle,object.alpha,object.ref); 223 | } 224 | 225 | public Object(){ 226 | this(new Point()); 227 | } 228 | 229 | /** 230 | * Sets the values of this object to the values of the given object. 231 | * @param object the object 232 | */ 233 | public void set(Object object){ 234 | this.set(object.position, object.angle, object.scale, object.pivot, object.alpha, object.ref); 235 | } 236 | 237 | /** 238 | * Sets the given values for this object. 239 | * @param x the new position in x direction 240 | * @param y the new position in y direction 241 | * @param angle the new angle 242 | * @param scaleX the new scale in x direction 243 | * @param scaleY the new scale in y direction 244 | * @param pivotX the new pivot in x direction 245 | * @param pivotY the new pivot in y direction 246 | * @param alpha the new alpha value 247 | * @param folder the new folder index 248 | * @param file the new file index 249 | */ 250 | public void set(float x, float y, float angle, float scaleX, float scaleY, float pivotX, float pivotY, float alpha, int folder, int file){ 251 | super.set(x, y, angle, scaleX, scaleY, pivotX, pivotY); 252 | this.alpha = alpha; 253 | this.ref.folder = folder; 254 | this.ref.file = file; 255 | } 256 | 257 | /** 258 | * Sets the given values for this object. 259 | * @param position the new position 260 | * @param angle the new angle 261 | * @param scale the new scale 262 | * @param pivot the new pivot 263 | * @param alpha the new alpha value 264 | * @param fileRef the new file reference 265 | */ 266 | public void set(Point position, float angle, Point scale, Point pivot, float alpha, FileReference fileRef){ 267 | this.set(position.x, position.y, angle, scale.x, scale.y, pivot.x, pivot.y, alpha , fileRef.folder, fileRef.file); 268 | } 269 | 270 | public String toString(){ 271 | return super.toString()+", pivot: "+pivot+", alpha: "+alpha+", reference: "+ref; 272 | } 273 | 274 | } 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Drawer.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.util.Iterator; 4 | 5 | import com.brashmonkey.spriter.Entity.CharacterMap; 6 | import com.brashmonkey.spriter.Entity.ObjectInfo; 7 | import com.brashmonkey.spriter.Entity.ObjectType; 8 | import com.brashmonkey.spriter.Timeline.Key.Bone; 9 | import com.brashmonkey.spriter.Timeline.Key.Object; 10 | 11 | /** 12 | * A Drawer is responsible for drawing a {@link Player}. 13 | * Since this library is meant to be as generic as possible this class has to be abstract, because it cannot be assumed how to draw a resource. 14 | * Anyone who wants to draw a {@link Player} has to know how to draw a resource. A resource can be e.g. a sprite, a texture or a texture region. 15 | * To draw a {@link Player} call {@link #draw(Player)}. This method relies on {@link #draw(Object)}, which has to be implemented with the chosen backend. 16 | * To debug draw a {@link Player} call {@link #drawBones(Player)}, {@link #drawBoxes(Player)} and {@link #drawPoints(Player)}, 17 | * which rely on {@link #rectangle(float, float, float, float)}, {@link #circle(float, float, float)}, {@link #line(float, float, float, float)} and {@link #setColor(float, float, float, float)}. 18 | * @author Trixt0r 19 | * 20 | * @param The backend specific resource. In general such a resource is called "sprite", "texture" or "image". 21 | */ 22 | public abstract class Drawer { 23 | 24 | /** 25 | * The radius of a point for debug drawing purposes. 26 | */ 27 | public float pointRadius = 5f; 28 | protected Loader loader; 29 | 30 | /** 31 | * Creates a new drawer based on the given loader. 32 | * @param loader the loader containing resources 33 | */ 34 | public Drawer(Loader loader){ 35 | this.loader = loader; 36 | } 37 | 38 | /** 39 | * Sets the loader of this drawer. 40 | * @param loader the loader containing resources 41 | * @throws SpriterException if the loader is null 42 | */ 43 | public void setLoader(Loader loader){ 44 | if(loader == null) throw new SpriterException("The loader instance can not be null!"); 45 | this.loader = loader; 46 | } 47 | 48 | /** 49 | * Draws the bones of the given player composed of lines. 50 | * @param player the player to draw 51 | */ 52 | public void drawBones(Player player){ 53 | this.setColor(1, 0, 0, 1); 54 | Iterator it = player.boneIterator(); 55 | while(it.hasNext()){ 56 | Timeline.Key.Bone bone = it.next(); 57 | Timeline.Key key = player.getKeyFor(bone); 58 | if(!key.active) continue; 59 | ObjectInfo info = player.getObjectInfoFor(bone); 60 | Dimension size = info.size; 61 | drawBone(bone, size); 62 | } 63 | /*for(Mainline.Key.BoneRef ref: player.getCurrentKey().boneRefs){ 64 | Timeline.Key key = player.unmappedTweenedKeys[ref.timeline]; 65 | Timeline.Key.Bone bone = key.object(); 66 | if(player.animation.getTimeline(ref.timeline).objectInfo.type != ObjectType.Bone || !key.active) continue; 67 | ObjectInfo info = player.animation.getTimeline(ref.timeline).objectInfo; 68 | if(info == null) continue; 69 | Dimension size = info.size; 70 | drawBone(bone, size); 71 | }*/ 72 | } 73 | 74 | /** 75 | * Draws the given bone composed of lines with the given size. 76 | * @param bone the bone to draw 77 | * @param size the size of the bone 78 | */ 79 | public void drawBone(Bone bone, Dimension size){ 80 | float halfHeight = size.height/2; 81 | float xx = bone.position.x+(float)Math.cos(Math.toRadians(bone.angle))*size.height; 82 | float yy = bone.position.y+(float)Math.sin(Math.toRadians(bone.angle))*size.height; 83 | float x2 = (float)Math.cos(Math.toRadians(bone.angle+90))*halfHeight*bone.scale.y; 84 | float y2 = (float)Math.sin(Math.toRadians(bone.angle+90))*halfHeight*bone.scale.y; 85 | 86 | float targetX = bone.position.x+(float)Math.cos(Math.toRadians(bone.angle))*size.width*bone.scale.x, 87 | targetY = bone.position.y+(float)Math.sin(Math.toRadians(bone.angle))*size.width*bone.scale.x; 88 | float upperPointX = xx+x2, upperPointY = yy+y2; 89 | this.line(bone.position.x, bone.position.y, upperPointX, upperPointY); 90 | this.line(upperPointX, upperPointY, targetX, targetY); 91 | 92 | float lowerPointX = xx-x2, lowerPointY = yy-y2; 93 | this.line(bone.position.x, bone.position.y, lowerPointX, lowerPointY); 94 | this.line(lowerPointX, lowerPointY, targetX, targetY); 95 | this.line(bone.position.x, bone.position.y, targetX, targetY); 96 | } 97 | 98 | /** 99 | * Draws the boxes of the player. 100 | * @param player the player to draw the boxes from 101 | */ 102 | public void drawBoxes(Player player){ 103 | this.setColor(0f, 1f, 0f, 1f); 104 | this.drawBoneBoxes(player); 105 | this.drawObjectBoxes(player); 106 | this.drawPoints(player); 107 | } 108 | 109 | /** 110 | * Draws the boxes of all bones of the given player. 111 | * @param player the player to draw the bone boxes of 112 | */ 113 | public void drawBoneBoxes(Player player){ 114 | drawBoneBoxes(player, player.boneIterator()); 115 | } 116 | 117 | /** 118 | * Draws the boxes of all bones of the given player based on the given iterator. 119 | * @param player the player to draw the bone boxes of 120 | * @param it the iterator iterating over the bones to draw 121 | */ 122 | public void drawBoneBoxes(Player player, Iterator it){ 123 | while(it.hasNext()){ 124 | Bone bone = it.next(); 125 | this.drawBox(player.getBox(bone)); 126 | } 127 | } 128 | 129 | /** 130 | * Draws the boxes of the player objects, i.e. sprites and objects. 131 | * @param player the player to draw the object boxes of 132 | */ 133 | public void drawObjectBoxes(Player player){ 134 | drawObjectBoxes(player, player.objectIterator()); 135 | } 136 | 137 | /** 138 | * Draws the boxes of sprites and boxes of the given player based on the given iterator. 139 | * @param player player the player to draw the object boxes of 140 | * @param it the iterator iterating over the object to draw 141 | */ 142 | public void drawObjectBoxes(Player player, Iterator it){ 143 | while(it.hasNext()){ 144 | Object bone = it.next(); 145 | this.drawBox(player.getBox(bone)); 146 | } 147 | } 148 | 149 | /** 150 | * Draws all points of the given player. 151 | * @param player the player to draw the points of. 152 | */ 153 | public void drawPoints(Player player){ 154 | drawPoints(player, player.objectIterator()); 155 | } 156 | 157 | /** 158 | * Draws the points of the given player based on the given iterator. 159 | * @param player player the player to draw the points of 160 | * @param it the iterator iterating over the points to draw 161 | */ 162 | public void drawPoints(Player player, Iterator it){ 163 | while(it.hasNext()){ 164 | Object point = it.next(); 165 | if(player.getObjectInfoFor(point).type == ObjectType.Point){ 166 | float x = point.position.x+(float)(Math.cos(Math.toRadians(point.angle))*pointRadius); 167 | float y = point.position.y+(float)(Math.sin(Math.toRadians(point.angle))*pointRadius); 168 | circle(point.position.x, point.position.y, pointRadius); 169 | line(point.position.x, point.position.y, x,y); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Draws the given player with its current character map. 176 | * @param player the player to draw 177 | */ 178 | public void draw(Player player){ 179 | this.draw(player, player.characterMaps); 180 | } 181 | 182 | /** 183 | * Draws the given player with the given character map. 184 | * @param player the player to draw 185 | * @param maps the character map to draw 186 | */ 187 | public void draw(Player player, CharacterMap[] maps){ 188 | this.draw(player.objectIterator(), maps); 189 | } 190 | 191 | /** 192 | * Draws the objects the given iterator is providing with the given character map. 193 | * @param it the iterator iterating over the objects to draw 194 | * @param maps the character map to draw 195 | */ 196 | public void draw(Iterator it, CharacterMap[] maps){ 197 | while(it.hasNext()){ 198 | Timeline.Key.Object object = it.next(); 199 | if(object.ref.hasFile()){ 200 | if(maps != null){ 201 | for(CharacterMap map: maps) 202 | if(map != null) 203 | object.ref.set(map.get(object.ref)); 204 | } 205 | this.draw(object); 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * Draws the given box composed of lines. 212 | * @param box the box to draw 213 | */ 214 | public void drawBox(Box box){ 215 | this.line(box.points[0].x, box.points[0].y, box.points[1].x, box.points[1].y); 216 | this.line(box.points[1].x, box.points[1].y, box.points[3].x, box.points[3].y); 217 | this.line(box.points[3].x, box.points[3].y, box.points[2].x, box.points[2].y); 218 | this.line(box.points[2].x, box.points[2].y, box.points[0].x, box.points[0].y); 219 | } 220 | 221 | public void drawRectangle(Rectangle rect){ 222 | this.rectangle(rect.left, rect.bottom, rect.size.width, rect.size.height); 223 | } 224 | 225 | /** 226 | * Sets the color for drawing lines, rectangles and circles. 227 | * @param r the red value between 0.0 - 1.0 228 | * @param g the green value between 0.0 - 1.0 229 | * @param b the blue value between 0.0 - 1.0 230 | * @param a the alpha value between 0.0 - 1.0 231 | */ 232 | public abstract void setColor(float r, float g, float b, float a); 233 | 234 | /** 235 | * Draws a line from (x1, y1) to (x2, y2). 236 | * @param x1 237 | * @param y1 238 | * @param x2 239 | * @param y2 240 | */ 241 | public abstract void line(float x1, float y1, float x2, float y2); 242 | 243 | /** 244 | * Draws a rectangle with origin at (x, y) and the given size. 245 | * @param x the x coordinate 246 | * @param y the y coordinate 247 | * @param width the width of the size 248 | * @param height the height of the size 249 | */ 250 | public abstract void rectangle(float x, float y, float width, float height); 251 | 252 | /** 253 | * Draws a circle at (x, y) with the given radius. 254 | * @param x the x coordinate 255 | * @param y the y coordinate 256 | * @param radius the radius of the circle 257 | */ 258 | public abstract void circle(float x, float y, float radius); 259 | 260 | /** 261 | * Draws the given object with its current resource. 262 | * @param object the object to draw. 263 | */ 264 | public abstract void draw(Timeline.Key.Object object); 265 | } 266 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/Spriter.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileNotFoundException; 6 | import java.io.InputStream; 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.List; 10 | 11 | /** 12 | * A utility class for managing multiple {@link Loader} and {@link Player} instances. 13 | * @author Trixt0r 14 | * 15 | */ 16 | 17 | @SuppressWarnings("rawtypes") 18 | public class Spriter { 19 | 20 | private static Object[] loaderDependencies = new Object[1], drawerDependencies = new Object[1]; 21 | private static Class[] loaderTypes = new Class[1], drawerTypes = new Class[1]; 22 | static{ 23 | loaderTypes[0] = Data.class; 24 | drawerTypes[0] = Loader.class; 25 | } 26 | private static Class loaderClass; 27 | 28 | private static final HashMap loadedData = new HashMap(); 29 | private static final List players = new ArrayList(); 30 | private static final List loaders = new ArrayList(); 31 | private static Drawer drawer; 32 | private static final HashMap entityToLoader = new HashMap(); 33 | private static boolean initialized = false; 34 | 35 | /** 36 | * Sets the dependencies for implemented {@link Loader}. 37 | * @param loaderDependencies the dependencies a loader has to get 38 | */ 39 | public static void setLoaderDependencies(Object... loaderDependencies){ 40 | if(loaderDependencies == null) return; 41 | Spriter.loaderDependencies = new Object[loaderDependencies.length+1]; 42 | System.arraycopy(loaderDependencies, 0, Spriter.loaderDependencies, 1, loaderDependencies.length); 43 | loaderTypes = new Class[loaderDependencies.length+1]; 44 | loaderTypes[0] = Data.class; 45 | for(int i = 0; i< loaderDependencies.length; i++) 46 | loaderTypes[i+1] = loaderDependencies[i].getClass(); 47 | } 48 | 49 | /** 50 | * Sets the dependencies for implemented {@link Drawer}. 51 | * @param drawerDependencies the dependencies a drawer has to get 52 | */ 53 | public static void setDrawerDependencies(Object... drawerDependencies){ 54 | if(drawerDependencies == null) return; 55 | Spriter.drawerDependencies = new Object[drawerDependencies.length+1]; 56 | Spriter.drawerDependencies[0] = null; 57 | System.arraycopy(drawerDependencies, 0, Spriter.drawerDependencies, 1, drawerDependencies.length); 58 | drawerTypes = new Class[drawerDependencies.length+1]; 59 | drawerTypes[0] = Loader.class; 60 | for(int i = 0; i< drawerDependencies.length; i++) 61 | if(drawerDependencies[i] != null) 62 | drawerTypes[i+1] = drawerDependencies[i].getClass(); 63 | } 64 | 65 | /** 66 | * Initializes this class with the implemented {@link Loader} class and {@link Drawer} class. 67 | * Before calling this method make sure that you have set all necessary dependecies with {@link #setDrawerDependencies(Object...)} and {@link #setLoaderDependencies(Object...)}. 68 | * A drawer is created with this method. 69 | * @param loaderClass the loader class 70 | * @param drawerClass the drawer class 71 | */ 72 | public static void init(Class loaderClass, Class drawerClass){ 73 | Spriter.loaderClass = loaderClass; 74 | try { 75 | drawer = drawerClass.getDeclaredConstructor(drawerTypes).newInstance(drawerDependencies); 76 | } catch (Exception e) { 77 | e.printStackTrace(); 78 | } 79 | initialized = drawer != null; 80 | } 81 | 82 | /** 83 | * Loads a SCML file with the given path. 84 | * @param scmlFile the path to the SCML file 85 | */ 86 | public static void load(String scmlFile){ 87 | load(new File(scmlFile)); 88 | } 89 | 90 | /** 91 | * Loads the given SCML file. 92 | * @param scmlFile the scml file 93 | */ 94 | public static void load(File scmlFile){ 95 | try { 96 | load(new FileInputStream(scmlFile), scmlFile.getPath().replaceAll("\\\\", "/")); 97 | } catch (FileNotFoundException e) { 98 | e.printStackTrace(); 99 | } 100 | } 101 | 102 | /** 103 | * Loads the given SCML stream pointing to a file saved at the given path. 104 | * @param stream the SCML stream 105 | * @param scmlFile the path to the SCML file 106 | */ 107 | public static void load(InputStream stream, String scmlFile){ 108 | SCMLReader reader = new SCMLReader(stream); 109 | Data data = reader.data; 110 | loadedData.put(scmlFile, data); 111 | loaderDependencies[0] = data; 112 | try { 113 | Loader loader = loaderClass.getDeclaredConstructor(loaderTypes).newInstance(loaderDependencies); 114 | loader.load(new File(scmlFile)); 115 | loaders.add(loader); 116 | for(Entity entity: data.entities) 117 | entityToLoader.put(entity, loader); 118 | } catch (Exception e) { 119 | e.printStackTrace(); 120 | } 121 | } 122 | 123 | /** 124 | * Creates a new {@link Player} instance based on the given SCML file with the given entity index 125 | * @param scmlFile name of the SCML file 126 | * @param entityIndex the index of the entity 127 | * @return a {@link Player} instance managed by this class 128 | * @throws SpriterException if the given SCML file was not loaded yet 129 | */ 130 | public static Player newPlayer(String scmlFile, int entityIndex){ 131 | return newPlayer(scmlFile, entityIndex, Player.class); 132 | } 133 | 134 | /** 135 | * Creates a new {@link Player} instance based on the given SCML file with the given entity index and the given class extending a {@link Player} 136 | * @param scmlFile name of the SCML file 137 | * @param entityIndex the index of the entity 138 | * @param playerClass the class extending a {@link Player} class, e.g. {@link PlayerTweener}. 139 | * @return a {@link Player} instance managed by this class 140 | * @throws SpriterException if the given SCML file was not loaded yet 141 | */ 142 | public static Player newPlayer(String scmlFile, int entityIndex, Class playerClass){ 143 | if(!loadedData.containsKey(scmlFile)) throw new SpriterException("You have to load \""+scmlFile+"\" before using it!"); 144 | try { 145 | Player player = playerClass.getDeclaredConstructor(Entity.class).newInstance(loadedData.get(scmlFile).getEntity(entityIndex)); 146 | players.add(player); 147 | return player; 148 | } catch (Exception e) { 149 | e.printStackTrace(); 150 | } 151 | return null; 152 | } 153 | 154 | /** 155 | * Creates a new {@link Player} instance based on the given SCML file with the given entity name 156 | * @param scmlFile name of the SCML file 157 | * @param entityName name of the entity 158 | * @return a {@link Player} instance managed by this class 159 | * @throws SpriterException if the given SCML file was not loaded yet 160 | */ 161 | public static Player newPlayer(String scmlFile, String entityName){ 162 | if(!loadedData.containsKey(scmlFile)) throw new SpriterException("You have to load \""+scmlFile+"\" before using it!"); 163 | return newPlayer(scmlFile, loadedData.get(scmlFile).getEntityIndex(entityName)); 164 | } 165 | 166 | /** 167 | * Returns a loader for the given SCML filename. 168 | * @param scmlFile the name of the SCML file 169 | * @return the loader for the given SCML filename 170 | * @throws NullPointerException if the SCML file was not loaded yet 171 | */ 172 | public static Loader getLoader(String scmlFile){ 173 | return entityToLoader.get(getData(scmlFile).getEntity(0)); 174 | } 175 | 176 | /** 177 | * Updates each created player by this class and immediately draws it. 178 | * This method should only be called if you want to update and render on the same thread. 179 | * @throws SpriterException if {@link #init(Class, Class)} was not called before 180 | */ 181 | @SuppressWarnings("unchecked") 182 | public static void updateAndDraw(){ 183 | if(!initialized) throw new SpriterException("Call init() before updating!"); 184 | for (int i = 0; i < players.size(); i++) { 185 | players.get(i).update(); 186 | drawer.loader = entityToLoader.get(players.get(i).getEntity()); 187 | drawer.draw(players.get(i)); 188 | } 189 | } 190 | 191 | /** 192 | * Updates each created player by this class. 193 | * @throws SpriterException if {@link #init(Class, Class)} was not called before 194 | */ 195 | public static void update(){ 196 | if(!initialized) throw new SpriterException("Call init() before updating!"); 197 | for (int i = 0; i < players.size(); i++) { 198 | players.get(i).update(); 199 | } 200 | } 201 | 202 | /** 203 | * Draws each created player by this class. 204 | * @throws SpriterException if {@link #init(Class, Class)} was not called before 205 | */ 206 | @SuppressWarnings("unchecked") 207 | public static void draw(){ 208 | if(!initialized) throw new SpriterException("Call init() before drawing!"); 209 | for (int i = 0; i < players.size(); i++) { 210 | drawer.loader = entityToLoader.get(players.get(i).getEntity()); 211 | drawer.draw(players.get(i)); 212 | } 213 | } 214 | 215 | /** 216 | * Returns the drawer instance this class is using. 217 | * @return the drawer which draws all players 218 | */ 219 | public static Drawer drawer(){ 220 | return drawer; 221 | } 222 | 223 | /** 224 | * Returns the data for the given SCML filename. 225 | * @param fileName the name of the SCML file 226 | * @return the data for the given SCML filename or null if not loaed yet 227 | */ 228 | public static Data getData(String fileName){ 229 | return loadedData.get(fileName); 230 | } 231 | 232 | /** 233 | * The number of players this class is managing. 234 | * @return number of players 235 | */ 236 | public static int players(){ 237 | return players.size(); 238 | } 239 | 240 | /** 241 | * Clears all previous created players, Spriter datas, disposes all loaders, deletes the drawer and resets all internal lists. 242 | * After this methods was called {@link #init(Class, Class)} has to be called again so that everything works again. 243 | */ 244 | public static void dispose(){ 245 | drawer = null; 246 | drawerDependencies = new Object[1]; 247 | drawerTypes = new Class[1]; 248 | drawerTypes[0] = Loader.class; 249 | 250 | entityToLoader.clear(); 251 | 252 | for (int i = 0; i < loaders.size(); i++) { 253 | loaders.get(i).dispose(); 254 | } 255 | loaders.clear(); 256 | loadedData.clear(); 257 | loaderClass = null; 258 | loaderTypes = new Class[1]; 259 | loaderTypes[0] = Data.class; 260 | loaderDependencies = new Object[1]; 261 | 262 | players.clear(); 263 | 264 | initialized = false; 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/SCMLReader.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | 8 | import com.brashmonkey.spriter.Entity.*; 9 | import com.brashmonkey.spriter.Mainline.Key.*; 10 | import com.brashmonkey.spriter.XmlReader.*; 11 | 12 | /** 13 | * This class parses a SCML file and creates a {@link Data} instance. 14 | * If you want to keep track of what is going on during the build process of the objects parsed from the SCML file, 15 | * you could extend this class and override the load*() methods for pre or post processing. 16 | * This could be e.g. useful for a loading screen which responds to the current building or parsing state. 17 | * @author Trixt0r 18 | */ 19 | public class SCMLReader { 20 | 21 | protected Data data; 22 | 23 | /** 24 | * Creates a new SCML reader and will parse all objects in the given stream. 25 | * @param stream the stream 26 | */ 27 | public SCMLReader(InputStream stream){ 28 | this.data = this.load(stream); 29 | } 30 | 31 | /** 32 | * Creates a new SCML reader and will parse the given xml string. 33 | * @param xml the xml string 34 | */ 35 | public SCMLReader(String xml){ 36 | this.data = this.load(xml); 37 | } 38 | 39 | /** 40 | * Parses the SCML object save in the given xml string and returns the build data object. 41 | * @param xml the xml string 42 | * @return the built data 43 | */ 44 | protected Data load(String xml){ 45 | XmlReader reader = new XmlReader(); 46 | return load(reader.parse(xml)); 47 | } 48 | 49 | /** 50 | * Parses the SCML objects saved in the given stream and returns the built data object. 51 | * @param stream the stream from the SCML file 52 | * @return the built data 53 | */ 54 | protected Data load(InputStream stream){ 55 | try { 56 | XmlReader reader = new XmlReader(); 57 | return load(reader.parse(stream)); 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | return null; 61 | } 62 | } 63 | 64 | /** 65 | * Reads the data from the given root element, i.e. the spriter_data node. 66 | * @param root 67 | * @return 68 | */ 69 | protected Data load(Element root) { 70 | ArrayList folders = root.getChildrenByName("folder"); 71 | ArrayList entities = root.getChildrenByName("entity"); 72 | data = new Data(root.get("scml_version"), root.get("generator"), root.get("generator_version"), 73 | Data.PixelMode.get(root.getInt("pixel_mode", 0)), 74 | folders.size(), entities.size()); 75 | loadFolders(folders); 76 | loadEntities(entities); 77 | return data; 78 | } 79 | 80 | /** 81 | * Iterates through the given folders and adds them to the current {@link Data} object. 82 | * @param folders a list of folders to load 83 | */ 84 | protected void loadFolders(ArrayList folders){ 85 | for(int i = 0; i < folders.size(); i++){ 86 | Element repo = folders.get(i); 87 | ArrayList files = repo.getChildrenByName("file"); 88 | Folder folder = new Folder(repo.getInt("id"), repo.get("name", "no_name_"+i), files.size()); 89 | loadFiles(files, folder); 90 | data.addFolder(folder); 91 | } 92 | } 93 | 94 | /** 95 | * Iterates through the given files and adds them to the given {@link Folder} object. 96 | * @param files a list of files to load 97 | * @param folder the folder containing the files 98 | */ 99 | protected void loadFiles(ArrayList files, Folder folder){ 100 | for(int j = 0; j < files.size(); j++){ 101 | Element f = files.get(j); 102 | File file = new File(f.getInt("id"), f.get("name"), 103 | new Dimension(f.getInt("width", 0), f.getInt("height", 0)), 104 | new Point(f.getFloat("pivot_x", 0f), f.getFloat("pivot_y", 1f))); 105 | 106 | folder.addFile(file); 107 | } 108 | } 109 | 110 | /** 111 | * Iterates through the given entities and adds them to the current {@link Data} object. 112 | * @param entities a list of entities to load 113 | */ 114 | protected void loadEntities(ArrayList entities){ 115 | for(int i = 0; i < entities.size(); i++){ 116 | Element e = entities.get(i); 117 | ArrayList infos = e.getChildrenByName("obj_info"); 118 | ArrayList charMaps = e.getChildrenByName("character_map"); 119 | ArrayList animations = e.getChildrenByName("animation"); 120 | Entity entity = new Entity(e.getInt("id"), e.get("name"), 121 | animations.size(), charMaps.size(), infos.size()); 122 | data.addEntity(entity); 123 | loadObjectInfos(infos, entity); 124 | loadCharacterMaps(charMaps, entity); 125 | loadAnimations(animations, entity); 126 | } 127 | } 128 | 129 | /** 130 | * Iterates through the given object infos and adds them to the given {@link Entity} object. 131 | * @param infos a list of infos to load 132 | * @param entity the entity containing the infos 133 | */ 134 | protected void loadObjectInfos(ArrayList infos, Entity entity){ 135 | for(int i = 0; i< infos.size(); i++){ 136 | Element info = infos.get(i); 137 | ObjectInfo objInfo = new ObjectInfo(info.get("name","info"+i), 138 | ObjectType.getObjectInfoFor(info.get("type","")), 139 | new Dimension(info.getFloat("w", 0), info.getFloat("h", 0))); 140 | entity.addInfo(objInfo); 141 | Element frames = info.getChildByName("frames"); 142 | if(frames == null) continue; 143 | ArrayList frameIndices = frames.getChildrenByName("i"); 144 | for (int i1 = 0; i1 < frameIndices.size(); i1++) { 145 | Element index = frameIndices.get(i1); 146 | int folder = index.getInt("folder", 0); 147 | int file = index.getInt("file", 0); 148 | objInfo.frames.add(new FileReference(folder, file)); 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * Iterates through the given character maps and adds them to the given {@link Entity} object. 155 | * @param maps a list of character maps to load 156 | * @param entity the entity containing the character maps 157 | */ 158 | protected void loadCharacterMaps(ArrayList maps, Entity entity){ 159 | for(int i = 0; i< maps.size(); i++){ 160 | Element map = maps.get(i); 161 | CharacterMap charMap = new CharacterMap(map.getInt("id"), map.getAttribute("name", "charMap"+i)); 162 | entity.addCharacterMap(charMap); 163 | ArrayList mappings = map.getChildrenByName("map"); 164 | for (int i1 = 0; i1 < mappings.size(); i1++) { 165 | Element mapping = mappings.get(i1); 166 | int folder = mapping.getInt("folder"); 167 | int file = mapping.getInt("file"); 168 | charMap.put(new FileReference(folder, file), 169 | new FileReference(mapping.getInt("target_folder", folder), mapping.getInt("target_file", file))); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Iterates through the given animations and adds them to the given {@link Entity} object. 176 | * @param animations a list of animations to load 177 | * @param entity the entity containing the animations maps 178 | */ 179 | protected void loadAnimations(ArrayList animations, Entity entity){ 180 | for(int i = 0; i < animations.size(); i++){ 181 | Element a = animations.get(i); 182 | ArrayList timelines = a.getChildrenByName("timeline"); 183 | Element mainline = a.getChildByName("mainline"); 184 | ArrayList mainlineKeys = mainline.getChildrenByName("key"); 185 | Animation animation = new Animation(new Mainline(mainlineKeys.size()), 186 | a.getInt("id"), a.get("name"), a.getInt("length"), 187 | a.getBoolean("looping", true),timelines.size()); 188 | entity.addAnimation(animation); 189 | loadMainlineKeys(mainlineKeys, animation.mainline); 190 | loadTimelines(timelines, animation, entity); 191 | animation.prepare(); 192 | } 193 | } 194 | 195 | /** 196 | * Iterates through the given mainline keys and adds them to the given {@link Mainline} object. 197 | * @param keys a list of mainline keys 198 | * @param main the mainline 199 | */ 200 | protected void loadMainlineKeys(ArrayList keys, Mainline main){ 201 | for(int i = 0; i < main.keys.length; i++){ 202 | Element k = keys.get(i); 203 | ArrayList objectRefs = k.getChildrenByName("object_ref"); 204 | ArrayList boneRefs = k.getChildrenByName("bone_ref"); 205 | Curve curve = new Curve(); 206 | curve.setType(Curve.getType(k.get("curve_type","linear"))); 207 | curve.constraints.set(k.getFloat("c1", 0f),k.getFloat("c2", 0f),k.getFloat("c3", 0f),k.getFloat("c4", 0f)); 208 | Mainline.Key key = new Mainline.Key(k.getInt("id"), k.getInt("time", 0), curve, 209 | boneRefs.size(), objectRefs.size()); 210 | main.addKey(key); 211 | loadRefs(objectRefs, boneRefs, key); 212 | } 213 | } 214 | 215 | /** 216 | * Iterates through the given bone and object references and adds them to the given {@link Mainline.Key} object. 217 | * @param objectRefs a list of object references 218 | * @param boneRefs a list if bone references 219 | * @param key the mainline key 220 | */ 221 | protected void loadRefs(ArrayList objectRefs, ArrayList boneRefs, Mainline.Key key){ 222 | for (int i = 0; i < boneRefs.size(); i++) { 223 | Element e = boneRefs.get(i); 224 | BoneRef boneRef = new BoneRef(e.getInt("id"), e.getInt("timeline"), 225 | e.getInt("key"), key.getBoneRef(e.getInt("parent", -1))); 226 | key.addBoneRef(boneRef); 227 | } 228 | 229 | for (int i = 0; i < objectRefs.size(); i++) { 230 | Element o = objectRefs.get(i); 231 | ObjectRef objectRef = new ObjectRef(o.getInt("id"), o.getInt("timeline"), 232 | o.getInt("key"), key.getBoneRef(o.getInt("parent", -1)), o.getInt("z_index", 0)); 233 | key.addObjectRef(objectRef); 234 | } 235 | Arrays.sort(key.objectRefs); 236 | } 237 | 238 | /** 239 | * Iterates through the given timelines and adds them to the given {@link Animation} object. 240 | * @param timelines a list of timelines 241 | * @param animation the animation containing the timelines 242 | * @param entity entity for assigning the timeline an object info 243 | */ 244 | protected void loadTimelines(ArrayList timelines, Animation animation, Entity entity){ 245 | for(int i = 0; i< timelines.size(); i++){ 246 | Element t = timelines.get(i); 247 | ArrayList keys = timelines.get(i).getChildrenByName("key"); 248 | String name = t.get("name", "no_name_"+i); 249 | ObjectType type = ObjectType.getObjectInfoFor(t.get("object_type", "sprite")); 250 | ObjectInfo info = entity.getInfo(name); 251 | if(info == null) info = new ObjectInfo(name, type, new Dimension(0,0)); 252 | Timeline timeline = new Timeline(t.getInt("id"), name, info, keys.size()); 253 | animation.addTimeline(timeline); 254 | loadTimelineKeys(keys, timeline); 255 | } 256 | } 257 | 258 | /** 259 | * Iterates through the given timeline keys and adds them to the given {@link Timeline} object. 260 | * @param keys a list if timeline keys 261 | * @param timeline the timeline containing the keys 262 | */ 263 | protected void loadTimelineKeys(ArrayList keys, Timeline timeline){ 264 | for(int i = 0; i< keys.size(); i++){ 265 | Element k = keys.get(i); 266 | Curve curve = new Curve(); 267 | curve.setType(Curve.getType(k.get("curve_type", "linear"))); 268 | curve.constraints.set(k.getFloat("c1", 0f),k.getFloat("c2", 0f),k.getFloat("c3", 0f),k.getFloat("c4", 0f)); 269 | Timeline.Key key = new Timeline.Key(k.getInt("id"), k.getInt("time", 0), k.getInt("spin", 1), curve); 270 | Element obj = k.getChildByName("bone"); 271 | if(obj == null) obj = k.getChildByName("object"); 272 | 273 | Point position = new Point(obj.getFloat("x", 0f), obj.getFloat("y", 0f)); 274 | Point scale = new Point(obj.getFloat("scale_x", 1f), obj.getFloat("scale_y", 1f)); 275 | Point pivot = new Point(obj.getFloat("pivot_x", 0f), obj.getFloat("pivot_y", (timeline.objectInfo.type == ObjectType.Bone)? .5f:1f)); 276 | float angle = obj.getFloat("angle", 0f), alpha = 1f; 277 | int folder = -1, file = -1; 278 | if(obj.getName().equals("object")){ 279 | if(timeline.objectInfo.type == ObjectType.Sprite){ 280 | alpha = obj.getFloat("a", 1f); 281 | folder = obj.getInt("folder", -1); 282 | file = obj.getInt("file", -1); 283 | File f = data.getFolder(folder).getFile(file); 284 | pivot = new Point(obj.getFloat("pivot_x", f.pivot.x), obj.getFloat("pivot_y", f.pivot.y)); 285 | timeline.objectInfo.size.set(f.size); 286 | } 287 | } 288 | Timeline.Key.Object object; 289 | if(obj.getName().equals("bone")) object = new Timeline.Key.Object(position, scale, pivot, angle, alpha, new FileReference(folder, file)); 290 | else object = new Timeline.Key.Object(position, scale, pivot, angle, alpha, new FileReference(folder, file)); 291 | key.setObject(object); 292 | timeline.addKey(key); 293 | } 294 | } 295 | 296 | /** 297 | * Returns the loaded SCML data. 298 | * @return the SCML data. 299 | */ 300 | public Data getData(){ 301 | return data; 302 | } 303 | 304 | } 305 | 306 | -------------------------------------------------------------------------------- /src/main/java/com/brashmonkey/spriter/SCONReader.java: -------------------------------------------------------------------------------- 1 | package com.brashmonkey.spriter; 2 | 3 | import com.brashmonkey.spriter.Entity.CharacterMap; 4 | import com.brashmonkey.spriter.Entity.ObjectInfo; 5 | import com.brashmonkey.spriter.Entity.ObjectType; 6 | import com.brashmonkey.spriter.Mainline.Key.BoneRef; 7 | import com.brashmonkey.spriter.Mainline.Key.ObjectRef; 8 | import org.json.JSONArray; 9 | import org.json.JSONObject; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.util.Arrays; 14 | 15 | /** 16 | * This class parses a SCON file and creates a {@link Data} instance. 17 | * If you want to keep track of what is going on during the build process of the objects parsed from the SCON file, 18 | * you could extend this class and override the load*() methods for pre or post processing. 19 | * This could be e.g. useful for a loading screen which responds to the current building or parsing state. 20 | * @author mrdlink 21 | */ 22 | public class SCONReader { 23 | 24 | protected Data data; 25 | 26 | /** 27 | * Creates a new SCON reader and will parse all objects in the given stream. 28 | * @param stream the stream 29 | */ 30 | public SCONReader(InputStream stream){ 31 | this.data = this.load(stream); 32 | } 33 | 34 | /** 35 | * Creates a new SCON reader and will parse the given json string. 36 | * @param json the json string 37 | */ 38 | public SCONReader(String json){ 39 | this.data = this.load(json); 40 | } 41 | 42 | /** 43 | * Parses the SCON object save in the given json string and returns the build data object. 44 | * @param json the json string 45 | * @return the built data 46 | */ 47 | protected Data load(String json){ 48 | return load(JsonReader.parse(json)); 49 | } 50 | 51 | /** 52 | * Parses the SCON objects saved in the given stream and returns the built data object. 53 | * @param stream the stream from the SCON file 54 | * @return the built data 55 | */ 56 | protected Data load(InputStream stream){ 57 | try { 58 | return load(JsonReader.parse(stream)); 59 | } catch (IOException e) { 60 | e.printStackTrace(); 61 | return null; 62 | } 63 | } 64 | 65 | /** 66 | * Reads the data from the given root element, i.e. the spriter_data node. 67 | * @param root 68 | * @return 69 | */ 70 | protected Data load(JSONObject root) { 71 | JSONArray folders = root.getJSONArray("folder"); 72 | JSONArray entities = root.getJSONArray("entity"); 73 | data = new Data(root.getString("scon_version"), root.getString("generator"), root.getString("generator_version"), 74 | Data.PixelMode.get(root.optInt("pixel_mode", 0)), 75 | folders.length(), entities.length()); 76 | loadFolders(folders); 77 | loadEntities(entities); 78 | return data; 79 | } 80 | 81 | /** 82 | * Iterates through the given folders and adds them to the current {@link Data} object. 83 | * @param folders a list of folders to load 84 | */ 85 | protected void loadFolders(JSONArray folders){ 86 | for(int i = 0; i < folders.length(); i++){ 87 | JSONObject repo = folders.getJSONObject(i); 88 | JSONArray files = repo.getJSONArray("file"); 89 | 90 | Folder folder = new Folder(repo.getInt("id"), repo.optString("name", "no_name_"+i), files.length()); 91 | loadFiles(files, folder); 92 | data.addFolder(folder); 93 | } 94 | } 95 | 96 | /** 97 | * Iterates through the given files and adds them to the given {@link Folder} object. 98 | * @param files a list of files to load 99 | * @param folder the folder containing the files 100 | */ 101 | protected void loadFiles(JSONArray files, Folder folder){ 102 | for(int j = 0; j < files.length(); j++){ 103 | JSONObject f = files.getJSONObject(j); 104 | 105 | File file = new File(f.getInt("id"), f.getString("name"), 106 | new Dimension(f.optInt("width", 0), f.optInt("height", 0)), 107 | new Point(f.optFloat("pivot_x", 0f), f.optFloat("pivot_y", 0f))); 108 | 109 | folder.addFile(file); 110 | } 111 | } 112 | 113 | /** 114 | * Iterates through the given entities and adds them to the current {@link Data} object. 115 | * @param entities a list of entities to load 116 | */ 117 | protected void loadEntities(JSONArray entities){ 118 | for(int i = 0; i < entities.length(); i++){ 119 | JSONObject e = entities.getJSONObject(i); 120 | JSONArray infos = e.getJSONArray("obj_info"); 121 | JSONArray charMaps = e.getJSONArray("character_map"); 122 | JSONArray animations = e.getJSONArray("animation"); 123 | Entity entity = new Entity(e.getInt("id"), e.getString("name"), 124 | animations.length(), charMaps.length(), infos.length()); 125 | data.addEntity(entity); 126 | loadObjectInfos(infos, entity); 127 | loadCharacterMaps(charMaps, entity); 128 | loadAnimations(animations, entity); 129 | } 130 | } 131 | 132 | /** 133 | * Iterates through the given object infos and adds them to the given {@link Entity} object. 134 | * @param infos a list of infos to load 135 | * @param entity the entity containing the infos 136 | */ 137 | protected void loadObjectInfos(JSONArray infos, Entity entity){ 138 | for(int i = 0; i< infos.length(); i++){ 139 | JSONObject info = infos.getJSONObject(i); 140 | ObjectInfo objInfo = new ObjectInfo(info.getString("name"), 141 | ObjectType.getObjectInfoFor(info.optString("type", "")), 142 | new Dimension(info.optFloat("w", 0f), info.optFloat("h", 0f))); 143 | entity.addInfo(objInfo); 144 | JSONObject frames = info.optJSONObject("frames"); 145 | if(frames == null) continue; 146 | JSONArray frameIndices = frames.getJSONArray("i"); 147 | for (int i1 = 0; i1 < frameIndices.length(); i1++) { 148 | JSONObject index = frameIndices.getJSONObject(i1); 149 | int folder = index.optInt("folder", 0); 150 | int file = index.optInt("file", 0); 151 | objInfo.frames.add(new FileReference(folder, file)); 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Iterates through the given character maps and adds them to the given {@link Entity} object. 158 | * @param maps a list of character maps to load 159 | * @param entity the entity containing the character maps 160 | */ 161 | protected void loadCharacterMaps(JSONArray maps, Entity entity){ 162 | for(int i = 0; i< maps.length(); i++){ 163 | JSONObject map = maps.getJSONObject(i); 164 | CharacterMap charMap = new CharacterMap(map.getInt("id"), map.optString("name", "charMap"+i)); 165 | entity.addCharacterMap(charMap); 166 | JSONArray mappings = map.getJSONArray("map"); 167 | for (int i1 = 0; i1 < mappings.length(); i1++) { 168 | JSONObject mapping = mappings.getJSONObject(i1); 169 | int folder = mapping.getInt("folder"); 170 | int file = mapping.getInt("file"); 171 | charMap.put(new FileReference(folder, file), 172 | new FileReference(mapping.optInt("target_folder", folder), mapping.optInt("target_file", file))); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Iterates through the given animations and adds them to the given {@link Entity} object. 179 | * @param animations a list of animations to load 180 | * @param entity the entity containing the animations maps 181 | */ 182 | protected void loadAnimations(JSONArray animations, Entity entity){ 183 | for(int i = 0; i < animations.length(); i++){ 184 | JSONObject a = animations.getJSONObject(i); 185 | JSONArray timelines = a.getJSONArray("timeline"); 186 | JSONObject mainline = a.getJSONObject("mainline"); 187 | JSONArray mainlineKeys = mainline.getJSONArray("key"); 188 | Animation animation = new Animation(new Mainline(mainlineKeys.length()), 189 | a.getInt("id"), a.getString("name"), a.getInt("length"), 190 | a.optBoolean("looping", true),timelines.length()); 191 | entity.addAnimation(animation); 192 | loadMainlineKeys(mainlineKeys, animation.mainline); 193 | loadTimelines(timelines, animation, entity); 194 | animation.prepare(); 195 | } 196 | } 197 | 198 | /** 199 | * Iterates through the given mainline keys and adds them to the given {@link Mainline} object. 200 | * @param keys a list of mainline keys 201 | * @param main the mainline 202 | */ 203 | protected void loadMainlineKeys(JSONArray keys, Mainline main){ 204 | for(int i = 0; i < main.keys.length; i++){ 205 | JSONObject k = keys.getJSONObject(i); 206 | JSONArray objectRefs = k.getJSONArray("object_ref"); 207 | JSONArray boneRefs = k.getJSONArray("bone_ref"); 208 | Curve curve = new Curve(); 209 | curve.setType(Curve.getType(k.optString("curve_type","linear"))); 210 | curve.constraints.set(k.optFloat("c1", 0f),k.optFloat("c2", 0f),k.optFloat("c3", 0f),k.optFloat("c4", 0f)); 211 | Mainline.Key key = new Mainline.Key(k.getInt("id"), k.optInt("time", 0), curve, 212 | boneRefs.length(), objectRefs.length()); 213 | main.addKey(key); 214 | loadRefs(objectRefs, boneRefs, key); 215 | } 216 | } 217 | 218 | /** 219 | * Iterates through the given bone and object references and adds them to the given {@link Mainline.Key} object. 220 | * @param objectRefs a list of object references 221 | * @param boneRefs a list if bone references 222 | * @param key the mainline key 223 | */ 224 | protected void loadRefs(JSONArray objectRefs, JSONArray boneRefs, Mainline.Key key){ 225 | for (int i = 0; i < boneRefs.length(); i++) { 226 | JSONObject e = boneRefs.getJSONObject(i); 227 | BoneRef boneRef = new BoneRef(e.getInt("id"), e.getInt("timeline"), 228 | e.getInt("key"), key.getBoneRef(e.optInt("parent", -1))); 229 | key.addBoneRef(boneRef); 230 | } 231 | 232 | for (int i = 0; i < objectRefs.length(); i++) { 233 | JSONObject o = objectRefs.getJSONObject(i); 234 | ObjectRef objectRef = new ObjectRef(o.getInt("id"), o.getInt("timeline"), 235 | o.getInt("key"), key.getBoneRef(o.optInt("parent", -1)), o.optInt("z_index", 0)); 236 | key.addObjectRef(objectRef); 237 | } 238 | Arrays.sort(key.objectRefs); 239 | } 240 | 241 | /** 242 | * Iterates through the given timelines and adds them to the given {@link Animation} object. 243 | * @param timelines a list of timelines 244 | * @param animation the animation containing the timelines 245 | * @param entity entity for assigning the timeline an object info 246 | */ 247 | protected void loadTimelines(JSONArray timelines, Animation animation, Entity entity){ 248 | for(int i = 0; i< timelines.length(); i++){ 249 | JSONObject t = timelines.getJSONObject(i); 250 | JSONArray keys = timelines.getJSONObject(i).getJSONArray("key"); 251 | String name = t.getString("name"); 252 | ObjectType type = ObjectType.getObjectInfoFor(t.optString("object_type", "sprite")); 253 | ObjectInfo info = entity.getInfo(name); 254 | if(info == null) info = new ObjectInfo(name, type, new Dimension(0,0)); 255 | Timeline timeline = new Timeline(t.getInt("id"), name, info, keys.length()); 256 | animation.addTimeline(timeline); 257 | loadTimelineKeys(keys, timeline); 258 | } 259 | } 260 | 261 | /** 262 | * Iterates through the given timeline keys and adds them to the given {@link Timeline} object. 263 | * @param keys a list if timeline keys 264 | * @param timeline the timeline containing the keys 265 | */ 266 | protected void loadTimelineKeys(JSONArray keys, Timeline timeline){ 267 | for(int i = 0; i< keys.length(); i++){ 268 | JSONObject k = keys.getJSONObject(i); 269 | Curve curve = new Curve(); 270 | curve.setType(Curve.getType(k.optString("curve_type", "linear"))); 271 | curve.constraints.set(k.optFloat("c1", 0f),k.optFloat("c2", 0f),k.optFloat("c3", 0f),k.optFloat("c4", 0f)); 272 | Timeline.Key key = new Timeline.Key(k.getInt("id"), k.optInt("time", 0), k.optInt("spin", 1), curve); 273 | JSONObject obj = k.optJSONObject("bone"); 274 | if(obj == null) obj = k.getJSONObject("object"); 275 | 276 | Point position = new Point(obj.optFloat("x", 0f), obj.optFloat("y", 0f)); 277 | Point scale = new Point(obj.optFloat("scale_x", 1f), obj.optFloat("scale_y", 1f)); 278 | Point pivot = new Point(obj.optFloat("pivot_x", 0f), obj.optFloat("pivot_y", (timeline.objectInfo.type == ObjectType.Bone)? .5f:1f)); 279 | float angle = obj.optFloat("angle", 0f), alpha = 1f; 280 | int folder = -1, file = -1; 281 | if(obj.optString("name", "no_name_"+i).equals("object")){ 282 | if(timeline.objectInfo.type == ObjectType.Sprite){ 283 | alpha = obj.optFloat("a", 1f); 284 | folder = obj.optInt("folder", -1); 285 | file = obj.optInt("file", -1); 286 | File f = data.getFolder(folder).getFile(file); 287 | pivot = new Point(obj.optFloat("pivot_x", f.pivot.x), obj.optFloat("pivot_y", f.pivot.y)); 288 | timeline.objectInfo.size.set(f.size); 289 | } 290 | } 291 | Timeline.Key.Object object; 292 | if(obj.optString("name", "no_name_"+i).equals("bone")) object = new Timeline.Key.Object(position, scale, pivot, angle, alpha, new FileReference(folder, file)); 293 | else object = new Timeline.Key.Object(position, scale, pivot, angle, alpha, new FileReference(folder, file)); 294 | key.setObject(object); 295 | timeline.addKey(key); 296 | } 297 | } 298 | 299 | /** 300 | * Returns the loaded SCON data. 301 | * @return the SCON data. 302 | */ 303 | public Data getData(){ 304 | return data; 305 | } 306 | 307 | } 308 | 309 | -------------------------------------------------------------------------------- /src/test/resources/test.scon: -------------------------------------------------------------------------------- 1 | { 2 | "atlas": [ 3 | { 4 | "name": "lava.json" 5 | }, 6 | { 7 | "name": "lava2.json" 8 | } 9 | ], 10 | "entity": [ 11 | { 12 | "animation": [ 13 | { 14 | "id": 0, 15 | "interval": 100, 16 | "length": 1000, 17 | "mainline": { 18 | "key": [ 19 | { 20 | "bone_ref": [], 21 | "id": 0, 22 | "object_ref": [ 23 | { 24 | "id": 0, 25 | "key": 0, 26 | "timeline": "7", 27 | "z_index": "0" 28 | }, 29 | { 30 | "id": 1, 31 | "key": 0, 32 | "timeline": "6", 33 | "z_index": "1" 34 | }, 35 | { 36 | "id": 2, 37 | "key": 0, 38 | "timeline": "5", 39 | "z_index": "2" 40 | }, 41 | { 42 | "id": 3, 43 | "key": 0, 44 | "timeline": "4", 45 | "z_index": "3" 46 | }, 47 | { 48 | "id": 4, 49 | "key": 0, 50 | "timeline": "3", 51 | "z_index": "4" 52 | }, 53 | { 54 | "id": 5, 55 | "key": 0, 56 | "timeline": "0", 57 | "z_index": "5" 58 | }, 59 | { 60 | "id": 6, 61 | "key": 0, 62 | "timeline": "1", 63 | "z_index": "6" 64 | }, 65 | { 66 | "id": 7, 67 | "key": 0, 68 | "timeline": "2", 69 | "z_index": "7" 70 | }, 71 | { 72 | "id": 8, 73 | "key": 0, 74 | "timeline": "8", 75 | "z_index": "8" 76 | }, 77 | { 78 | "id": 9, 79 | "key": 0, 80 | "timeline": "9", 81 | "z_index": "9" 82 | }, 83 | { 84 | "id": 10, 85 | "key": 0, 86 | "timeline": "10", 87 | "z_index": "10" 88 | } 89 | ] 90 | } 91 | ] 92 | }, 93 | "name": "NewAnimation", 94 | "timeline": [ 95 | { 96 | "id": 0, 97 | "key": [ 98 | { 99 | "id": 0, 100 | "object": { 101 | "angle": 0, 102 | "file": 7, 103 | "folder": 0, 104 | "x": 125, 105 | "y": 243 106 | }, 107 | "spin": 0 108 | } 109 | ], 110 | "name": "2" 111 | }, 112 | { 113 | "id": 1, 114 | "key": [ 115 | { 116 | "id": 0, 117 | "object": { 118 | "angle": 0, 119 | "file": 0, 120 | "folder": 0, 121 | "x": -315, 122 | "y": 249 123 | }, 124 | "spin": 0 125 | } 126 | ], 127 | "name": "1" 128 | }, 129 | { 130 | "id": 2, 131 | "key": [ 132 | { 133 | "id": 0, 134 | "object": { 135 | "angle": 0, 136 | "file": 3, 137 | "folder": 0, 138 | "x": -11, 139 | "y": -57 140 | }, 141 | "spin": 0 142 | } 143 | ], 144 | "name": "0" 145 | }, 146 | { 147 | "id": 3, 148 | "key": [ 149 | { 150 | "id": 0, 151 | "object": { 152 | "angle": 0, 153 | "file": 5, 154 | "folder": 0, 155 | "x": -486, 156 | "y": -76 157 | }, 158 | "spin": 0 159 | } 160 | ], 161 | "name": "sadfasd" 162 | }, 163 | { 164 | "id": 4, 165 | "key": [ 166 | { 167 | "id": 0, 168 | "object": { 169 | "angle": 0, 170 | "file": 6, 171 | "folder": 0, 172 | "x": 401, 173 | "y": -50 174 | }, 175 | "spin": 0 176 | } 177 | ], 178 | "name": "4" 179 | }, 180 | { 181 | "id": 5, 182 | "key": [ 183 | { 184 | "id": 0, 185 | "object": { 186 | "angle": 0, 187 | "file": 2, 188 | "folder": 0, 189 | "x": 442, 190 | "y": 357 191 | }, 192 | "spin": 0 193 | } 194 | ], 195 | "name": "5" 196 | }, 197 | { 198 | "id": 6, 199 | "key": [ 200 | { 201 | "id": 0, 202 | "object": { 203 | "angle": 0, 204 | "file": 4, 205 | "folder": 0, 206 | "x": -604, 207 | "y": 278 208 | }, 209 | "spin": 0 210 | } 211 | ], 212 | "name": "6" 213 | }, 214 | { 215 | "id": 7, 216 | "key": [ 217 | { 218 | "id": 0, 219 | "object": { 220 | "angle": 0, 221 | "file": 1, 222 | "folder": 0, 223 | "x": -78, 224 | "y": 305 225 | }, 226 | "spin": 0 227 | } 228 | ], 229 | "name": "7" 230 | }, 231 | { 232 | "id": 8, 233 | "key": [ 234 | { 235 | "id": 0, 236 | "object": { 237 | "angle": 0, 238 | "file": 0, 239 | "folder": 1, 240 | "x": 415, 241 | "y": 121 242 | }, 243 | "spin": 0 244 | } 245 | ], 246 | "name": "0_000" 247 | }, 248 | { 249 | "id": 9, 250 | "key": [ 251 | { 252 | "id": 0, 253 | "object": { 254 | "angle": 0, 255 | "file": 1, 256 | "folder": 1, 257 | "x": 96, 258 | "y": -18 259 | }, 260 | "spin": 0 261 | } 262 | ], 263 | "name": "2_000" 264 | }, 265 | { 266 | "id": 10, 267 | "key": [ 268 | { 269 | "id": 0, 270 | "object": { 271 | "angle": 0, 272 | "file": 2, 273 | "folder": 1, 274 | "x": -130, 275 | "y": 43 276 | }, 277 | "spin": 0 278 | } 279 | ], 280 | "name": "6_000" 281 | } 282 | ] 283 | } 284 | ], 285 | "character_map": [ 286 | { 287 | "id": 0, 288 | "map": [], 289 | "name": "New Character Map" 290 | }, 291 | { 292 | "id": 1, 293 | "map": [], 294 | "name": "New Character Map_000" 295 | } 296 | ], 297 | "id": 0, 298 | "name": "entity_000", 299 | "obj_info": [] 300 | }, 301 | { 302 | "animation": [ 303 | { 304 | "id": 0, 305 | "interval": 100, 306 | "length": 1000, 307 | "mainline": { 308 | "key": [ 309 | { 310 | "bone_ref": [], 311 | "id": 0, 312 | "object_ref": [ 313 | { 314 | "id": 0, 315 | "key": 0, 316 | "timeline": "7", 317 | "z_index": "0" 318 | }, 319 | { 320 | "id": 1, 321 | "key": 0, 322 | "timeline": "6", 323 | "z_index": "1" 324 | }, 325 | { 326 | "id": 2, 327 | "key": 0, 328 | "timeline": "5", 329 | "z_index": "2" 330 | }, 331 | { 332 | "id": 3, 333 | "key": 0, 334 | "timeline": "4", 335 | "z_index": "3" 336 | }, 337 | { 338 | "id": 4, 339 | "key": 0, 340 | "timeline": "3", 341 | "z_index": "4" 342 | }, 343 | { 344 | "id": 5, 345 | "key": 0, 346 | "timeline": "0", 347 | "z_index": "5" 348 | }, 349 | { 350 | "id": 6, 351 | "key": 0, 352 | "timeline": "1", 353 | "z_index": "6" 354 | }, 355 | { 356 | "id": 7, 357 | "key": 0, 358 | "timeline": "2", 359 | "z_index": "7" 360 | }, 361 | { 362 | "id": 8, 363 | "key": 0, 364 | "timeline": "8", 365 | "z_index": "8" 366 | }, 367 | { 368 | "id": 9, 369 | "key": 0, 370 | "timeline": "9", 371 | "z_index": "9" 372 | }, 373 | { 374 | "id": 10, 375 | "key": 0, 376 | "timeline": "10", 377 | "z_index": "10" 378 | } 379 | ] 380 | } 381 | ] 382 | }, 383 | "name": "NewAnimation", 384 | "timeline": [ 385 | { 386 | "id": 0, 387 | "key": [ 388 | { 389 | "id": 0, 390 | "object": { 391 | "angle": 0, 392 | "file": 7, 393 | "folder": 0, 394 | "x": 125, 395 | "y": 243 396 | }, 397 | "spin": 0 398 | } 399 | ], 400 | "name": "2" 401 | }, 402 | { 403 | "id": 1, 404 | "key": [ 405 | { 406 | "id": 0, 407 | "object": { 408 | "angle": 0, 409 | "file": 0, 410 | "folder": 0, 411 | "x": -315, 412 | "y": 249 413 | }, 414 | "spin": 0 415 | } 416 | ], 417 | "name": "1" 418 | }, 419 | { 420 | "id": 2, 421 | "key": [ 422 | { 423 | "id": 0, 424 | "object": { 425 | "angle": 0, 426 | "file": 3, 427 | "folder": 0, 428 | "x": -11, 429 | "y": -57 430 | }, 431 | "spin": 0 432 | } 433 | ], 434 | "name": "0" 435 | }, 436 | { 437 | "id": 3, 438 | "key": [ 439 | { 440 | "id": 0, 441 | "object": { 442 | "angle": 0, 443 | "file": 5, 444 | "folder": 0, 445 | "x": -486, 446 | "y": -76 447 | }, 448 | "spin": 0 449 | } 450 | ], 451 | "name": "sadfasd" 452 | }, 453 | { 454 | "id": 4, 455 | "key": [ 456 | { 457 | "id": 0, 458 | "object": { 459 | "angle": 0, 460 | "file": 6, 461 | "folder": 0, 462 | "x": 401, 463 | "y": -50 464 | }, 465 | "spin": 0 466 | } 467 | ], 468 | "name": "4" 469 | }, 470 | { 471 | "id": 5, 472 | "key": [ 473 | { 474 | "id": 0, 475 | "object": { 476 | "angle": 0, 477 | "file": 2, 478 | "folder": 0, 479 | "x": 442, 480 | "y": 357 481 | }, 482 | "spin": 0 483 | } 484 | ], 485 | "name": "5" 486 | }, 487 | { 488 | "id": 6, 489 | "key": [ 490 | { 491 | "id": 0, 492 | "object": { 493 | "angle": 0, 494 | "file": 4, 495 | "folder": 0, 496 | "x": -604, 497 | "y": 278 498 | }, 499 | "spin": 0 500 | } 501 | ], 502 | "name": "6" 503 | }, 504 | { 505 | "id": 7, 506 | "key": [ 507 | { 508 | "id": 0, 509 | "object": { 510 | "angle": 0, 511 | "file": 1, 512 | "folder": 0, 513 | "x": -78, 514 | "y": 305 515 | }, 516 | "spin": 0 517 | } 518 | ], 519 | "name": "7" 520 | }, 521 | { 522 | "id": 8, 523 | "key": [ 524 | { 525 | "id": 0, 526 | "object": { 527 | "angle": 0, 528 | "file": 0, 529 | "folder": 1, 530 | "x": 415, 531 | "y": 121 532 | }, 533 | "spin": 0 534 | } 535 | ], 536 | "name": "0_000" 537 | }, 538 | { 539 | "id": 9, 540 | "key": [ 541 | { 542 | "id": 0, 543 | "object": { 544 | "angle": 0, 545 | "file": 1, 546 | "folder": 1, 547 | "x": 96, 548 | "y": -18 549 | }, 550 | "spin": 0 551 | } 552 | ], 553 | "name": "2_000" 554 | }, 555 | { 556 | "id": 10, 557 | "key": [ 558 | { 559 | "id": 0, 560 | "object": { 561 | "angle": 0, 562 | "file": 2, 563 | "folder": 1, 564 | "x": -130, 565 | "y": 43 566 | }, 567 | "spin": 0 568 | } 569 | ], 570 | "name": "6_000" 571 | } 572 | ] 573 | } 574 | ], 575 | "character_map": [], 576 | "id": 1, 577 | "name": "entity_001", 578 | "obj_info": [] 579 | } 580 | ], 581 | "folder": [ 582 | { 583 | "atlas": 0, 584 | "file": [ 585 | { 586 | "height": 224, 587 | "id": 0, 588 | "name": "1.png", 589 | "pivot_x": 0, 590 | "pivot_y": 1, 591 | "width": 256 592 | }, 593 | { 594 | "height": 224, 595 | "id": 1, 596 | "name": "7.png", 597 | "pivot_x": 0, 598 | "pivot_y": 1, 599 | "width": 256 600 | }, 601 | { 602 | "height": 224, 603 | "id": 2, 604 | "name": "5.png", 605 | "pivot_x": 0, 606 | "pivot_y": 1, 607 | "width": 256 608 | }, 609 | { 610 | "height": 224, 611 | "id": 3, 612 | "name": "0.png", 613 | "pivot_x": 0, 614 | "pivot_y": 1, 615 | "width": 256 616 | }, 617 | { 618 | "height": 224, 619 | "id": 4, 620 | "name": "6.png", 621 | "pivot_x": 0, 622 | "pivot_y": 1, 623 | "width": 256 624 | }, 625 | { 626 | "height": 224, 627 | "id": 5, 628 | "name": "3.png", 629 | "pivot_x": 0, 630 | "pivot_y": 1, 631 | "width": 256 632 | }, 633 | { 634 | "height": 224, 635 | "id": 6, 636 | "name": "4.png", 637 | "pivot_x": 0, 638 | "pivot_y": 1, 639 | "width": 256 640 | }, 641 | { 642 | "height": 224, 643 | "id": 7, 644 | "name": "2.png", 645 | "pivot_x": 0, 646 | "pivot_y": 1, 647 | "width": 256 648 | } 649 | ], 650 | "id": 0, 651 | "name": "." 652 | }, 653 | { 654 | "atlas": 1, 655 | "file": [ 656 | { 657 | "height": 224, 658 | "id": 0, 659 | "name": "0.png", 660 | "pivot_x": 0, 661 | "pivot_y": 1, 662 | "width": 256 663 | }, 664 | { 665 | "height": 224, 666 | "id": 1, 667 | "name": "2.png", 668 | "pivot_x": 0, 669 | "pivot_y": 1, 670 | "width": 256 671 | }, 672 | { 673 | "height": 224, 674 | "id": 2, 675 | "name": "6.png", 676 | "pivot_x": 0, 677 | "pivot_y": 1, 678 | "width": 256 679 | } 680 | ], 681 | "id": 1, 682 | "name": "" 683 | } 684 | ], 685 | "generator": "BrashMonkey Spriter", 686 | "generator_version": "r11", 687 | "pixel_mode": 1, 688 | "scon_version": "1.0" 689 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spriter (WIP) 2 | ============= 3 | A Generic Java library for Spriter animation files. 4 | 5 | [Checkout the features video (note: at the moment there are more features than in this video!).](http://www.youtube.com/watch?v=i_OxqopvMH0) 6 | 7 | For people who do not have a single idea how to get a whole project running, [here](https://github.com/blueacorn/libgdx-spriter-demo) is a very detailed tutorial on how to setup a LibGDX project from nothing to a working Spriter showcase. 8 | 9 | Index 10 | ----- 11 | * [Basic usage](#basic-usage) 12 | * [Manipulate a player at runtime](#manipulate-a-player-at-runtime) 13 | * [Listening for player events](#listening-for-player-events) 14 | * [Manipulating bones and objects at runtime](#manipulating-bones-and-objects-at-runtime) 15 | * [Bouding boxes and collision tests](#bounding-boxes-and-collision-tests) 16 | * [Interpolation and animation composition](#interpolation-and-animation-composition) 17 | * [Inverse kinematics](#inverse-kinematics) 18 | * [Attachments](#attachments) 19 | * [Character maps](#character-maps) 20 | * [Spriter managment](#spriter-managment) 21 | 22 | 23 | 24 | Basic usage 25 | ----------- 26 | The library is meant to be generic, so to use it you have to implement some backend specific classes on your own: 27 | * A specific file loader ([Loader](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Loader.java "Loader")) class which loads all needed sprites/images/textures into memory and stores them in a reference map. 28 | * A specific drawer ([Drawer](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Drawer.java "Drawer")) class which is repsonsible for drawing the animation data and also to debug draw an animation. 29 | 30 | As you see, you have to implement all abstract methods. 31 | If you do not know how to implement them, have a look at the examples provided in the [SpriterTests project](https://github.com/Trixt0r/spriter/tree/master/SpriterTests "SpriterTests project"). There are already examples for: LWJGL, Slick2D, Java2D and LibGDX. The examples, except the LibGDX one, are not optimized since I have no need and no time to optimize them. 32 | 33 | After implementing those classes, the next steps should be rather easy. 34 | Create a [SCMLReader](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/SCMLReader.java) instance which takes care of parsing the SCML file and creating a new [Data](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Data.java) containing all necessary data to play back an animation of an entity: 35 | ``` 36 | SCMLReader reader = new SCMLReader("Path to your SCML file"); 37 | Data data = reader.getData(); 38 | ``` 39 | Or create a [SCONReader](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/SCONReader.java) instance which takes care of parsing the SCON file and creating a new [Data](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Data.java) containing all necessary data to play back an animation of an entity: 40 | ``` 41 | SCONReader reader = new SCONReader("Path to your SCON file"); 42 | Data data = reader.getData(); 43 | ``` 44 | At this point you gathered all animation data and you could create a [Player](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Player.java) instance: 45 | ``` 46 | Player yourPlayer = new Player(data.getEntity(entityIndex)); 47 | //or 48 | Player yourPlayer = new Player(data.getEntity("entity name")); 49 | ``` 50 | This will create a player which will play the animations of your current set entity. The animation after instantiation is the first one occuring in the entity. 51 | 52 | Now you also need the sprites which have to be rendered. This means you have to create a loader and a drawer instance: 53 | ``` 54 | Loader loader = new YourLoaderImplementaion(data); 55 | loader.load("Path to the root folder of your SCML file"); //Load all sprites 56 | //or 57 | File scmlFile = new File("Path to your SCML file"); 58 | Loader loader = new YourLoaderImplementaion(data); 59 | loader.load(scmlFile); 60 | 61 | Drawer drawer = new YourDrawerImplementation(loader); 62 | ``` 63 | How you call the constructors depends of course on your implementations. 64 | 65 | After instantiating a Loader and a Drawer, you are able to run and draw the desired animation: 66 | ``` 67 | //update the player 68 | yourPlayer.update(); 69 | //and draw it: 70 | drawer.draw(yourPlayer); 71 | ``` 72 | This will draw the current set animation at the point (0,0). 73 | 74 | That's it! Now read further if you want to know what other abilities the library has. 75 | 76 | [Back to top](#index) 77 | 78 | Manipulate a player at runtime 79 | ------------------------------ 80 | You have the ability to change the animation and speed of your player. 81 | Changing animation: 82 | ``` 83 | player.setAnimation(anAnimationIndex);//Set the animation by an index. 84 | player.setAnimation("animation name");//Set the animation by a name. 85 | ``` 86 | Note: All methods will throw a SpriterException if the given animation index or name does not exist. 87 | 88 | Changing speed and current time: 89 | ``` 90 | player.speed = 15; 91 | //This will set the frame speed to 15. This means that in every update step the frame will jump 15 frames further 92 | 93 | player.setTime(100); 94 | //This will set the current time to 100. 95 | //This method will not allow you to jump below or above the bounds of your current animation, 96 | //i.e. the time will always be between zero and the length of the animation. 97 | ``` 98 | 99 | Changing position, rotation, offset, scale and set flipping: 100 | ``` 101 | player.setPosition(x,y);//Will set the position of the player to (x,y) 102 | player.setAngle(anggle);//Will set the angle of the player to the angle in degrees 103 | player.setScale(2f);//Will set the scale of the player to 200% 104 | player.setPivot(xOffset, yOffset);//Will set the origin of the player 105 | player.flipX();//Will flip the player around the x-axis 106 | player.flipY();//Will flip the player around the y-axis 107 | ``` 108 | That is the basic transformation usage for a player. There are also methods like `player.translatePosition(x,y)` which should be self explanatory. 109 | To see this all in action, checkout out: [AnimationSpeedTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/AnimationSpeedTest.java), [AnimationSwitchTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/AnimationSwitchTest.java) and [TransformationTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/TransformationTest.java). 110 | 111 | Listening for player events 112 | --------------------------- 113 | It is quite common to observe an animation at runtime to e.g. switch the animation to another one if the current ends. 114 | For this purpose I added a [PlayerListener](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Player.java#L998) interface which can be used to listen for such events. 115 | To register listeners on a player one can just call `player.addListener(yourListener)`. To remove a listener just call `player.removeListener(yourListener)`. 116 | 117 | [Back to top](#index) 118 | 119 | Manipulating bones and objects at runtime 120 | ----------------------------------------- 121 | Letting your player look at a specific point in the scene or scaling a specific part of the body is quite useful and does not force your animator to create new animations for every new usecase. 122 | Changing values of a bone or object is rather easy. What you need is the name of the desired bone or object and you will be able to manipulate them: 123 | ``` 124 | player.update();//First update 125 | //Now you are able to manipulate the objects and bones 126 | player.setBone("name of your bone", angle);//Change the angle 127 | player.setBone("name of your bone", x,y);//Change the position 128 | 129 | player.setObject("name of your object", newAlphaValue, folderIndex, fileIndex);//Changes the transparency and the reference sprite of the object 130 | 131 | drawer.draw(player);//Finally draw 132 | ``` 133 | The methods for object and bone manipulation are almost the same, with some extra methods for objects since they have some more attributes. 134 | As you may notice the manipulation takes place between the update and drawing call. This is very important to do the manipulation methods between those two calls, otherwise you will not see any desired results. 135 | See [ObjectManipulationTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/ObjectManipulationTest.java) for more information. 136 | 137 | [Back to top](#index) 138 | 139 | Bounding boxes and collision tests 140 | ---------------------------------- 141 | The library offers you a way to calculate bounding boxes for specific parts of an animation or for the whole animation. 142 | The only thing you have to do is: 143 | ``` 144 | Rectangle bbox = player.getBoundingRectangle(null); 145 | ``` 146 | this will return you the surrounding rectangle of the current animation state. 147 | You could also pass a bone as an argument to calculate the bounding box for a specific part of the sekelton. 148 | The [Rectangle](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Rectangle.java) class has some usefull methods like intersection checking or merging two rectangles to a bigger one. 149 | 150 | Checking if a certain point lies in the bounding box of a bone or object can also be usefull, if you want e.g. pick the head and drag it around or check if the sword during an attack animation is hitting something. All you need to do is: 151 | ``` 152 | boolean hits = player.collidesFor(swordObject, x, y); 153 | if(hits){ 154 | //cut off the balls... 155 | } 156 | ``` 157 | Checking if the object collides with a rectangular are is also possible: 158 | ``` 159 | Rectangle area = new Rectangle(0,0, 500, 500); 160 | //... 161 | boolean hits = player.collidesFor(swordObject, area); 162 | if(hits){ 163 | //cut off the balls... 164 | } 165 | ``` 166 | Have a look at the [CollisionTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/CollisionTest.java) and [CullingTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/CullingTest.java) to see the feature in action. 167 | 168 | [Back to top](#index) 169 | 170 | 171 | Interpolation and animation composition 172 | ---------------------------------------- 173 | It is quite common in many games that animation switches of a character are not instant. In most games there is a smooth transition between the end of e.g. idle animation and the beginning of a walking animation. 174 | This library is able to such stuff. You can create a [PlayerTweener](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/PlayerTweener.java) which relies on two other Player instances. You can either create a PlayerTweener with or without assigning it the Player instances. In any case, you will have access to them. A PlayerTweener will interpolate the bones and objects of two Player instances. Instantiating a PlayerTweener is the same as instantiating a normal Player: 175 | ``` 176 | PlayerTweener tweener = new PlayerTweener(data.getEntity(index)); 177 | ``` 178 | This will create internally two Player instances. You can access them with `tweener.getFirstPlayer()` or `tweener.getSecondPlayer()`. 179 | The tweener will update the players with its `update()` method. You can turn off the auto update by setting `tweener.updatePlayers = false;` but then you have to update them on your own. 180 | As a default value the interplation weight between the two players is at 50%. Setting and getting the weight works like this: 181 | ``` 182 | tweener.setWeight(.75f);//Sets the weight to 75%, i.e. the second player will have more influence than the first 183 | tweener.getWeight();//Returns the weight of the player 184 | ``` 185 | A weight of 0% means that only the first player will be played back, a weight of 100% means that the second player will be played back. 186 | Setting the animation of a player is not possible because it makes no sense and it uses its own interpolated animation internally. 187 | Transforming the tweener works in the same way as for a normal player object. Transforming the internal players of a tweener will have no effect on the interpolation, since only the tweened realtive transformations are taken into consideration. 188 | Note that tweening two animations makes only sense if both animations have a similar structure. I recommend that you create your animations with the same bone naming structure, since this will give you the best tweening result. 189 | 190 | Check out the [InterpolationTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/InterpolationTest.java) for more information. 191 | 192 | You can also force the tweener to only interpolate bones and objects starting from a specific root. This can be useful if you want e.g. play a shooting animation while your character is running. 193 | 194 | Let's say you have an animation called "walk" and one called "shoot". Then we would set the base animation of the tweener to "walk", the animation of the second player would also be "walk" and the animation of the first player would be "shoot". Then we could set the weight of the tweener to zero, indicating that only "shoot" will be played back. As a last step we need to specify what the name of the root bone is (here we assume the bone is called "chest"). The tweener will then tween all bones starting from the given root bone. Then all bones and objects which do not occur in the children list of the root bone will stay at the animation "walk". Here is the code snippet: 195 | ``` 196 | tweener.setBaseAnimation("walk"); 197 | tweener.getSecondPlayer().setAnimation("walk"); 198 | tweener.getFirstPlayer().setAnimation("shoot"); 199 | tweener.baseBoneName = "chest"; 200 | tweener.setWeight(0f); 201 | ``` 202 | If you want that the tweener interpolates all objects and bones again, just set `tweener.baseBoneName = null`. 203 | 204 | Have a look at [CompositionTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/CompositionTest.java). 205 | 206 | [Back to top](#index) 207 | 208 | 209 | Inverse kinematics 210 | ------------------- 211 | Inverse kinematics is also supported by the library. The default inverse kinematics algorithm is [Cyclic Coordinate Descent](http://www.ryanjuckett.com/programming/cyclic-coordinate-descent-in-2d/), but the library is designed to have also other algorithms for ik. If you have a better algorithm, extend [IKResolver](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/IKResolver.java). 212 | The basic usage for inverse kinematics is rather simple. Everything you have to do is mapping specific [IKObjects](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/IKRObject.java) to your resolver and update the resolver after your player got updated. 213 | Here is a snippet for the basic usage: 214 | ``` 215 | //Create a resolver 216 | IKResolver resolver = new CCDResolver(); 217 | 218 | //Create an ik object and map it to the resolver 219 | IKObject ikObject = new IKObject(0, 0, 2, 5); //Creates an ik object at (0,0), with a chain length of 2 and forces the resolver to apply the algorithm at most 5 times 220 | resolver.mapIKObject(ikObject, yourBone); 221 | 222 | 223 | //During main loop 224 | player.update(); 225 | resolver.resolve(); 226 | 227 | drawer.draw(player); 228 | ``` 229 | Since applying inverse kinematics is the same as manipulating objects of a player you have to call the resolve method between update and draw. 230 | See it in action in the [InverseKinematicsTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/InverseKinematicsTest.java). 231 | 232 | [Back to top](#index) 233 | 234 | Attachments 235 | ----------- 236 | The library has also the ability to attach 2D objects to a animated bone or object. For this purpose I implemented the [Attachment](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Player.java#L1064) class. It is an abstract class which extends a bone. 237 | If you want to attach an object to a player object or bone, the only thing you have to do is, to implement the abstact methods. If you want for example to attach a player to a hand of another player, you would create a new attachment and attach it to the hand of the player: 238 | ``` 239 | Attachment attach = new Player.Attachment(handObject) { 240 | @Override 241 | protected void setScale(float xscale, float yscale) { 242 | playerToAttach.setScale(Math.max(xscale, yscale)); 243 | } 244 | 245 | @Override 246 | protected void setPosition(float x, float y) { 247 | playerToAttach.setPosition(x, y); 248 | } 249 | 250 | @Override 251 | protected void setAngle(float angle) { 252 | playerToAttach.setAngle(angle); 253 | } 254 | }; 255 | mainPlayer.attachments.add(attach); 256 | ``` 257 | That is all you need to do. The mainPlayer will now transform the `playerToAttach` instance realtive to the `handObject`. 258 | To see it in action have a look at [AttachmentTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/AttachmentTest.java). 259 | 260 | [Back to top](#index) 261 | 262 | Character maps 263 | -------------- 264 | Applying character maps on the fly is as simple as in Spriter. 265 | First get a character map, then assign it to a player in an array: 266 | ``` 267 | CharacterMap map = player.getEntity().getCharacterMap("name of your char map"); 268 | player.characterMaps = new CharacterMap[]{map}; 269 | ``` 270 | That's it. To remove the character map, just set `player.characterMaps = null` and you will be fine. 271 | 272 | [Back to top](#index) 273 | 274 | Spriter managment 275 | ----------------- 276 | If you have a bunch of players and you do not want to manage all them on your own, the library can do it for you. 277 | The [Spriter](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Spriter.java) class is designed to load SCML files and its sprites, update and draw all created players and to dispose them. 278 | The class will create a drawer and loaders on its own. The only thing you have to do, is to say which class holds the implementation of the loader and drawer and what the dependencies are, to create them. 279 | For example the LibGDX loader depends on no other instances, but the LibGDX drawer depends on a SpriteBatch and a ShapeRenderer. Therefore we have to tell Spriter where those references are: 280 | ``` 281 | Spriter.setDrawerDependencies(batch, renderer); 282 | //Same for loader dependencies: Spriter.setLoaderDependencies(...); 283 | ``` 284 | Assuming the `batch` and `renderer` are already instantiated. 285 | Spriter predicts, that the constructors of your Drawer and Loader implementation look like this: 286 | ``` 287 | Loader loader = new YourLoader(instanceOfData, ...); 288 | Drawer drawer = new YourDrawer(loader, ...); 289 | ``` 290 | So the first argument is always the one from the super class constructor. 291 | 292 | After setting the dependencies, Spriter knows how to create a loader and a drawer. Then we can call `Spriter.init(LibGdxLoader.class, LibGdxDrawer.class)`. This method has to be called before any other. 293 | 294 | Then we can load our desired SCML files with `Spriter.load(scmlStream, "path to scml file")`. 295 | 296 | Now you are able to create Player instances: 297 | ``` 298 | Player player = Spriter.newPlayer("name of scml file", entityIndexOrName); 299 | ``` 300 | You have to call this method, otherwise updating and drawing with Spriter will not work. 301 | 302 | Creating e.g. a PlayerTweener instance with the Spriter class works like this 303 | ``` 304 | Spriter.newPlayer("scmlFile", entityIndex, PlayerTweener.class); 305 | ``` 306 | 307 | In your mainlooop you can therefore call: 308 | ``` 309 | Spriter.updateAndDraw(); 310 | ``` 311 | This will update and immediately draw all player created with Spriter. Node that you are then not able to manipulate objects and bones. For this usecase you should use the [Player.Listener#postProcess()](https://github.com/Trixt0r/spriter/blob/master/Spriter/src/com/brashmonkey/spriter/Player.java#L1046) method. 312 | I only recommend this if you run all your logic and rendering on the same thread. Otherwise you would call update and draw seperately: 313 | ``` 314 | Spriter.update(); 315 | // Do stuff... 316 | Spriter.draw(); 317 | ``` 318 | If you are done with the Spriter class, e.g. if you switch from game screen to menu screen, you can call `Spriter.dispose()`. After this call you have again to set the loader and drawer dependencies and to init Spriter. 319 | 320 | You can take a look at [SpriterStressTest](https://github.com/Trixt0r/spriter/blob/master/SpriterTests/src/com/brashmonkey/spriter/tests/SpriterStressTest.java) to see it in action. 321 | 322 | [Back to top](#index) 323 | --------------------------------------------------------------------------------