Merge branch 'master' into 3.6-beta

This commit is contained in:
NathanSweet 2016-12-06 16:57:05 +01:00
commit 6fb9a4e991
37 changed files with 348 additions and 130 deletions

View File

@ -366,7 +366,7 @@ public class AnimationState {
from.timelinesRotation.length = 0;
// If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero.
if (from.mixingFrom != null) current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
if (from.mixingFrom != null && from.mixDuration > 0) current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
queue.start(current);
@ -485,7 +485,7 @@ public class AnimationState {
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? int.MAX_VALUE : entry.animationEnd;
entry.trackEnd = int.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;

View File

@ -126,7 +126,7 @@ public class Atlas {
region.splits = new Vector.<int>(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3]));
if (reader.readTuple(tuple) == 4) { // pad is optional, but only present with splits
region.pads = Vector.<int>(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3]));
region.pads = new Vector.<int>(parseInt(tuple[0]), parseInt(tuple[1]), parseInt(tuple[2]), parseInt(tuple[3]));
reader.readTuple(tuple);
}

View File

@ -582,7 +582,7 @@ void _spAnimationState_setCurrent (spAnimationState* self, int index, spTrackEnt
from->timelinesRotationCount = 0;
/* If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero. */
if (from->mixingFrom) current->mixAlpha *= MIN(from->mixTime / from->mixDuration, 1);
if (from->mixingFrom && from->mixDuration > 0) current->mixAlpha *= MIN(from->mixTime / from->mixDuration, 1);
}
_spEventQueue_start(internal->queue, current);
@ -715,7 +715,7 @@ spTrackEntry* _spAnimationState_trackEntry (spAnimationState* self, int trackInd
entry->trackTime = 0;
entry->trackLast = -1;
entry->nextTrackLast = -1;
entry->trackEnd = loop ? (float)INT_MAX : entry->animationEnd;
entry->trackEnd = (float)INT_MAX;
entry->timeScale = 1;
entry->alpha = 1;

View File

@ -66,11 +66,11 @@ typedef struct {
} Str;
static void trim (Str* str) {
while (isspace(*str->begin) && str->begin < str->end)
while (isspace((unsigned char)*str->begin) && str->begin < str->end)
(str->begin)++;
if (str->begin == str->end) return;
str->end--;
while (isspace(*str->end) && str->end >= str->begin)
while (isspace((unsigned char)*str->end) && str->end >= str->begin)
str->end--;
str->end++;
}

View File

@ -36,7 +36,7 @@
#include "kvec.h"
typedef struct {
const unsigned char* cursor;
const unsigned char* cursor;
const unsigned char* end;
} _dataInput;
@ -373,7 +373,7 @@ static spAnimation* _spSkeletonBinary_readAnimation (spSkeletonBinary* self, con
for (frameIndex = 0; frameIndex < frameCount; ++frameIndex) {
float time = readFloat(input);
float mix = readFloat(input);
char bendDirection = readSByte(input);
signed char bendDirection = readSByte(input);
spIkConstraintTimeline_setFrame(timeline, frameIndex, time, mix, bendDirection);
if (frameIndex < frameCount - 1) readCurve(input, SUPER(timeline), frameIndex);
}

View File

@ -30,11 +30,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Spine {
public class AnimationState {
private static Animation EmptyAnimation = new Animation("<empty>", new ExposedList<Timeline>(), 0);
static readonly Animation EmptyAnimation = new Animation("<empty>", new ExposedList<Timeline>(), 0);
private AnimationStateData data;
private readonly ExposedList<TrackEntry> tracks = new ExposedList<TrackEntry>();
@ -44,6 +43,8 @@ namespace Spine {
private bool animationsChanged;
private float timeScale = 1;
Pool<TrackEntry> trackEntryPool = new Pool<TrackEntry>();
public AnimationStateData Data { get { return data; } }
/// <summary>A list of tracks that have animations, which may contain nulls.</summary>
public ExposedList<TrackEntry> Tracks { get { return tracks; } }
@ -58,7 +59,7 @@ namespace Spine {
public AnimationState (AnimationStateData data) {
if (data == null) throw new ArgumentNullException("data", "data cannot be null.");
this.data = data;
this.queue = new EventQueue(this, HandleAnimationsChanged);
this.queue = new EventQueue(this, HandleAnimationsChanged, trackEntryPool);
}
void HandleAnimationsChanged () {
@ -215,7 +216,7 @@ namespace Spine {
float alpha = from.alpha * entry.mixAlpha * (1 - mix);
bool firstFrame = entry.timelinesRotation.Count == 0;
if (firstFrame) entry.timelinesRotation.Capacity = timelineCount << 1;
if (firstFrame) entry.timelinesRotation.EnsureCapacity(timelines.Count << 1);
var timelinesRotation = entry.timelinesRotation.Items;
for (int i = 0; i < timelineCount; i++) {
@ -389,7 +390,7 @@ namespace Spine {
from.timelinesRotation.Clear();
// If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero.
if (from.mixingFrom != null) from.mixAlpha *= Math.Min(from.mixTime / from.mixDuration, 1);
if (from.mixingFrom != null && from.mixDuration > 0) current.mixAlpha *= Math.Min(from.mixTime / from.mixDuration, 1);
}
queue.Start(current);
@ -526,32 +527,32 @@ namespace Spine {
/// <param name="last">May be null.</param>
private TrackEntry NewTrackEntry (int trackIndex, Animation animation, bool loop, TrackEntry last) {
return new TrackEntry {
trackIndex = trackIndex,
animation = animation,
loop = loop,
TrackEntry entry = trackEntryPool.Obtain(); // Pooling
entry.trackIndex = trackIndex;
entry.animation = animation;
entry.loop = loop;
eventThreshold = 0,
attachmentThreshold = 0,
drawOrderThreshold = 0,
entry.eventThreshold = 0;
entry.attachmentThreshold = 0;
entry.drawOrderThreshold = 0;
animationStart = 0,
animationEnd = animation.duration,
animationLast = -1,
nextAnimationLast = -1,
entry.animationStart = 0;
entry.animationEnd = animation.Duration;
entry.animationLast = -1;
entry.nextAnimationLast = -1;
delay = 0,
trackTime = 0,
trackLast = -1,
nextTrackLast = -1,
trackEnd = loop ? int.MaxValue : animation.duration,
timeScale = 1,
entry.delay = 0;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = float.MaxValue; // loop ? float.MaxValue : animation.Duration;
entry.timeScale = 1;
alpha = 1,
mixAlpha = 1,
mixTime = 0,
mixDuration = (last == null) ? 0 : data.GetMix(last.animation, animation),
};
entry.alpha = 1;
entry.mixAlpha = 1;
entry.mixTime = 0;
entry.mixDuration = (last == null) ? 0 : data.GetMix(last.animation, animation);
return entry;
}
private void DisposeNext (TrackEntry entry) {
@ -627,7 +628,7 @@ namespace Spine {
}
override public String ToString () {
var buffer = new StringBuilder();
var buffer = new System.Text.StringBuilder();
for (int i = 0, n = tracks.Count; i < n; i++) {
TrackEntry entry = tracks.Items[i];
if (entry == null) continue;
@ -646,7 +647,7 @@ namespace Spine {
}
/// <summary>State for the playback of an animation.</summary>
public class TrackEntry {
public class TrackEntry : Pool<TrackEntry>.IPoolable {
internal Animation animation;
internal TrackEntry next, mixingFrom;
@ -657,9 +658,25 @@ namespace Spine {
internal float animationStart, animationEnd, animationLast, nextAnimationLast;
internal float delay, trackTime, trackLast, nextTrackLast, trackEnd, timeScale = 1f;
internal float alpha, mixTime, mixDuration, mixAlpha;
internal readonly ExposedList<bool> timelinesFirst = new ExposedList<bool>(), timelinesLast = new ExposedList<bool>();
internal readonly ExposedList<bool> timelinesFirst = new ExposedList<bool>();
internal readonly ExposedList<float> timelinesRotation = new ExposedList<float>();
// IPoolable.Reset()
public void Reset () {
next = null;
mixingFrom = null;
animation = null;
timelinesFirst.Clear();
timelinesRotation.Clear();
Start = null;
Interrupt = null;
End = null;
Dispose = null;
Complete = null;
Event = null;
}
/// <summary>The index of the track where this entry is either current or queued.</summary>
public int TrackIndex { get { return trackIndex; } }
@ -824,11 +841,13 @@ namespace Spine {
public bool drainDisabled;
private readonly AnimationState state;
private readonly Pool<TrackEntry> trackEntryPool;
public event Action AnimationsChanged;
public EventQueue (AnimationState state, Action HandleAnimationsChanged) {
public EventQueue (AnimationState state, Action HandleAnimationsChanged, Pool<TrackEntry> trackEntryPool) {
this.state = state;
this.AnimationsChanged += HandleAnimationsChanged;
this.trackEntryPool = trackEntryPool;
}
struct EventQueueEntry {
@ -901,6 +920,7 @@ namespace Spine {
case EventType.Dispose:
trackEntry.OnDispose();
state.OnDispose(trackEntry);
trackEntryPool.Free(trackEntry); // Pooling
break;
case EventType.Complete:
trackEntry.OnComplete();
@ -921,4 +941,57 @@ namespace Spine {
eventQueueEntries.Clear();
}
}
public class Pool<T> where T : class, new() {
public readonly int max;
readonly Stack<T> freeObjects;
public int Count { get { return freeObjects.Count; } }
public int Peak { get; private set; }
public Pool (int initialCapacity = 16, int max = int.MaxValue) {
freeObjects = new Stack<T>(initialCapacity);
this.max = max;
}
public T Obtain () {
return freeObjects.Count == 0 ? new T() : freeObjects.Pop();
}
public void Free (T obj) {
if (obj == null) throw new ArgumentNullException("obj", "obj cannot be null");
if (freeObjects.Count < max) {
freeObjects.Push(obj);
Peak = Math.Max(Peak, freeObjects.Count);
}
Reset(obj);
}
// protected void FreeAll (List<T> objects) {
// if (objects == null) throw new ArgumentNullException("objects", "objects cannot be null.");
// var freeObjects = this.freeObjects;
// int max = this.max;
// for (int i = 0; i < objects.Count; i++) {
// T obj = objects[i];
// if (obj == null) continue;
// if (freeObjects.Count < max) freeObjects.Push(obj);
// Reset(obj);
// }
// Peak = Math.Max(Peak, freeObjects.Count);
// }
public void Clear () {
freeObjects.Clear();
}
protected void Reset (T obj) {
var poolable = obj as IPoolable;
if (poolable != null) poolable.Reset();
}
public interface IPoolable {
void Reset ();
}
}
}

View File

@ -134,7 +134,7 @@ public class AnimationStateTests {
expect(0, "end", 1, 1.1f), //
expect(0, "dispose", 1, 1.1f) //
);
state.setAnimation(0, "events0", false);
state.setAnimation(0, "events0", false).setTrackEnd(1);
run(0.1f, 1000, null);
setup("1/60 time step, dispose queued", // 2
@ -160,7 +160,7 @@ public class AnimationStateTests {
state.addAnimation(0, "events1", false, 0);
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events1", false, 0);
state.setAnimation(0, "events0", false);
state.setAnimation(0, "events0", false).setTrackEnd(1);
run(1 / 60f, 1000, null);
setup("30 time step", // 3
@ -172,7 +172,7 @@ public class AnimationStateTests {
expect(0, "end", 30, 60), //
expect(0, "dispose", 30, 60) //
);
state.setAnimation(0, "events0", false);
state.setAnimation(0, "events0", false).setTrackEnd(1);
run(30, 1000, null);
setup("1 time step", // 4
@ -184,7 +184,7 @@ public class AnimationStateTests {
expect(0, "end", 1, 2), //
expect(0, "dispose", 1, 2) //
);
state.setAnimation(0, "events0", false);
state.setAnimation(0, "events0", false).setTrackEnd(1);
run(1, 1.01f, null);
setup("interrupt", // 5
@ -220,7 +220,7 @@ public class AnimationStateTests {
);
state.setAnimation(0, "events0", false);
state.addAnimation(0, "events1", false, 0);
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
run(0.1f, 4f, null);
setup("interrupt with delay", // 6
@ -242,7 +242,7 @@ public class AnimationStateTests {
expect(1, "dispose", 1, 1.6f) //
);
state.setAnimation(0, "events0", false);
state.addAnimation(0, "events1", false, 0.5f);
state.addAnimation(0, "events1", false, 0.5f).setTrackEnd(1);
run(0.1f, 1000, null);
setup("interrupt with delay and mix time", // 7
@ -268,7 +268,7 @@ public class AnimationStateTests {
);
stateData.setMix("events0", "events1", 0.7f);
state.setAnimation(0, "events0", true);
state.addAnimation(0, "events1", false, 0.9f);
state.addAnimation(0, "events1", false, 0.9f).setTrackEnd(1);
run(0.1f, 1000, null);
setup("animation 0 events do not fire during mix", // 8
@ -291,7 +291,7 @@ public class AnimationStateTests {
);
stateData.setDefaultMix(0.7f);
state.setAnimation(0, "events0", false);
state.addAnimation(0, "events1", false, 0.4f);
state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1);
run(0.1f, 1000, null);
setup("event threshold, some animation 0 events fire during mix", // 9
@ -317,7 +317,7 @@ public class AnimationStateTests {
);
stateData.setMix("events0", "events1", 0.7f);
state.setAnimation(0, "events0", false).setEventThreshold(0.5f);
state.addAnimation(0, "events1", false, 0.4f);
state.addAnimation(0, "events1", false, 0.4f).setTrackEnd(1);
run(0.1f, 1000, null);
setup("event threshold, all animation 0 events fire during mix", // 10
@ -344,7 +344,9 @@ public class AnimationStateTests {
expect(1, "dispose", 1, 1.9f) //
);
state.setAnimation(0, "events0", true).setEventThreshold(1);
state.addAnimation(0, "events1", false, 0.8f).setMixDuration(0.7f);
entry = state.addAnimation(0, "events1", false, 0.8f);
entry.setMixDuration(0.7f);
entry.setTrackEnd(1);
run(0.1f, 1000, null);
setup("looping", // 11
@ -393,7 +395,7 @@ public class AnimationStateTests {
expect(1, "dispose", 1, 3.1f) //
);
state.setAnimation(0, "events0", false);
state.addAnimation(0, "events1", false, 2);
state.addAnimation(0, "events1", false, 2).setTrackEnd(1);
run(0.1f, 4f, null);
setup("interrupt animation after first loop complete", // 13
@ -424,7 +426,7 @@ public class AnimationStateTests {
state.setAnimation(0, "events0", true);
run(0.1f, 6, new TestListener() {
public void frame (float time) {
if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events1", false, 0);
if (MathUtils.isEqual(time, 1.4f)) state.addAnimation(0, "events1", false, 0).setTrackEnd(1);
}
});
@ -437,7 +439,7 @@ public class AnimationStateTests {
expect(0, "end", 1, 1.1f), //
expect(0, "dispose", 1, 1.1f) //
);
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
run(0.1f, 1.9f, null);
setup("end time beyond non-looping animation duration", // 15
@ -495,6 +497,7 @@ public class AnimationStateTests {
entry.setAnimationStart(0.2f);
entry.setAnimationLast(0.2f);
entry.setAnimationEnd(0.8f);
entry.setTrackEnd(1);
run(0.1f, 1.8f, null);
setup("mix out looping with animation start and end", // 19
@ -524,7 +527,9 @@ public class AnimationStateTests {
entry.setAnimationLast(0.2f);
entry.setAnimationEnd(0.8f);
entry.setEventThreshold(1);
state.addAnimation(0, "events1", false, 0.7f).setMixDuration(0.7f);
entry = state.addAnimation(0, "events1", false, 0.7f);
entry.setMixDuration(0.7f);
entry.setTrackEnd(1);
run(0.1f, 20, null);
setup("setAnimation with track entry mix", // 20
@ -552,7 +557,11 @@ public class AnimationStateTests {
state.setAnimation(0, "events0", true);
run(0.1f, 1000, new TestListener() {
public void frame (float time) {
if (MathUtils.isEqual(time, 1f)) state.setAnimation(0, "events1", false).setMixDuration(0.7f);
if (MathUtils.isEqual(time, 1f)) {
TrackEntry entry = state.setAnimation(0, "events1", false);
entry.setMixDuration(0.7f);
entry.setTrackEnd(1);
}
}
});
@ -593,7 +602,7 @@ public class AnimationStateTests {
public void frame (float time) {
if (MathUtils.isEqual(time, 0.8f)) {
state.setAnimation(0, "events0", false); // First should be ignored.
state.setAnimation(0, "events2", false);
state.setAnimation(0, "events2", false).setTrackEnd(1);
}
}
});
@ -655,7 +664,7 @@ public class AnimationStateTests {
}
if (MathUtils.isEqual(time, 0.4f)) {
state.setAnimation(0, "events1", false); // First should be ignored.
state.setAnimation(0, "events0", false);
state.setAnimation(0, "events0", false).setTrackEnd(1);
}
}
});
@ -669,7 +678,7 @@ public class AnimationStateTests {
expect(0, "end", 1, 6.1f), //
expect(0, "dispose", 1, 6.1f) //
);
state.addAnimation(0, "events0", false, 5);
state.addAnimation(0, "events0", false, 5).setTrackEnd(1);
run(0.1f, 10, null);
setup("setAnimation during AnimationStateListener"); // 24
@ -700,7 +709,7 @@ public class AnimationStateTests {
});
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events1", false, 0);
state.setAnimation(1, "events1", false);
state.setAnimation(1, "events1", false).setTrackEnd(1);
run(0.1f, 10, null);
setup("clearTrack", // 25
@ -710,7 +719,7 @@ public class AnimationStateTests {
expect(0, "end", 0.7f, 0.7f), //
expect(0, "dispose", 0.7f, 0.7f) //
);
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
run(0.1f, 10, new TestListener() {
public void frame (float time) {
if (MathUtils.isEqual(time, 0.7f)) state.clearTrack(0);
@ -732,7 +741,7 @@ public class AnimationStateTests {
expect(-1, "end", 0.2f, 1), //
expect(-1, "dispose", 0.2f, 1) //
);
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events0", false, 0).setTrackEnd(1);
run(0.1f, 10, new TestListener() {
public void frame (float time) {
if (MathUtils.isEqual(time, 0.7f)) state.setEmptyAnimation(0, 0);
@ -768,7 +777,7 @@ public class AnimationStateTests {
});
state.addAnimation(0, "events0", false, 0);
state.addAnimation(0, "events1", false, 0);
state.setAnimation(1, "events1", false);
state.setAnimation(1, "events1", false).setTrackEnd(1);
run(0.1f, 10, null);
if (counter.get() != 15082016) {
log("TEST 26 FAILED! " + counter);

View File

@ -185,6 +185,7 @@ public class AnimationState {
}
}
queueEvents(current, animationTime);
events.clear();
current.nextAnimationLast = animationTime;
current.nextTrackLast = current.trackTime;
}
@ -230,7 +231,8 @@ public class AnimationState {
}
}
queueEvents(from, animationTime);
if (entry.mixDuration > 0) queueEvents(from, animationTime);
this.events.clear();
from.nextAnimationLast = animationTime;
from.nextTrackLast = from.trackTime;
@ -330,7 +332,6 @@ public class AnimationState {
if (event.time < animationStart) continue; // Discard events outside animation start/end.
queue.event(entry, events.get(i));
}
events.clear();
}
/** Removes all animations from all tracks, leaving skeletons in their previous pose.
@ -385,7 +386,7 @@ public class AnimationState {
from.timelinesRotation.clear(); // Reset rotation for mixing out, in case entry was mixed in.
// If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero.
if (from.mixingFrom != null) current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
if (from.mixingFrom != null && from.mixDuration > 0) current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
queue.start(current);
@ -539,7 +540,7 @@ public class AnimationState {
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Integer.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Float.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
@ -752,10 +753,10 @@ public class AnimationState {
this.trackTime = trackTime;
}
/** The track time in seconds when this animation will be removed from the track. Defaults to the animation
* {@link Animation#duration} for non-looping animations and the highest float possible for looping animations. If the track
* end time is reached, no other animations are queued for playback, and mixing from any previous animations is complete,
* then the properties keyed by the animation are set to the setup pose and the track is cleared.
/** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float
* value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time
* is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the
* properties keyed by the animation are set to the setup pose and the track is cleared.
* <p>
* It may be desired to use {@link AnimationState#addEmptyAnimation(int, float, float)} to mix the properties back to the
* setup pose over time, rather than have it happen instantly. */

View File

@ -541,7 +541,7 @@ function AnimationState:setCurrent (index, current, interrupt)
from.timelinesRotation = {};
-- If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero.
if from.mixingFrom then current.mixAlpha = current.mixAlpha * math_min(from.mixTime / from.mixDuration, 1) end
if from.mixingFrom and from.mixDuration > 0 then current.mixAlpha = current.mixAlpha * math_min(from.mixTime / from.mixDuration, 1) end
end
queue:start(current)
@ -665,11 +665,7 @@ function AnimationState:trackEntry (trackIndex, animation, loop, last)
entry.trackTime = 0
entry.trackLast = -1
entry.nextTrackLast = -1
if loop then
entry.trackEnd = 999999999
else
entry.trackEnd = entry.animationEnd
end
entry.trackEnd = 999999999
entry.timeScale = 1
entry.alpha = 1

View File

@ -75,8 +75,8 @@ setup a development environment, follow these steps.
* **WebGL**: `tsc -w -p tsconfig.webgl.json`, builds `core/src` and `webgl/src`, outputs `build/spine-webgl.js|d.ts|js.map`
* **Canvas**: `tsc -w -p tsconfig.canvas.json`, builds `core/src` and `canvas/src`, outputs `build/spine-canvas.js|d.ts|js.map`
* **THREE.JS**: `tsc -w -p tsconfig.threejs.json`, builds `core/src` and `threejs/src`, outputs `build/spine-threejs.js|d.ts|js.map`
* **Widget**: `tsc -w -p tsconfig.widget.json`, builds `core/src` and `widget/src`, outputs `build/spine-widget.js|d.ts|js.map`
6. Open the `spine-ts` folder in Visual Studio Code. VS Code will use the `tsconfig.json` file all source files from core and all
* **Widget**: `tsc -w -p tsconfig.widget.json`, builds `core/src` and `widget/src`, outputs `build/spine-widget.js|d.ts|js.map`
6. Open the `spine-ts` folder in Visual Studio Code. VS Code will use the `tsconfig.json` file all source files from core and all
backends for your development pleasure. The actual JavaScript output is still created by the command line TypeScript compiler process from the previous step.
Each backend contains an `example/` folder with an `index.html` file that demonstrates the respective backend. For development, we
@ -90,7 +90,7 @@ python -m SimpleHTTPServer
Then navigate to `http://localhost:8000/webgl/example`, `http://localhost:8000/canvas/example`, `http://localhost:8000/threejs/example` or `http://localhost:8000/widget/example`
### Using the Widget
To easily display Spine animations on your website, you can use the spine-ts Widget backend.
To easily display Spine animations on your website, you can use the spine-ts Widget backend.
1. Export your Spine animation with a texture atlas and put the resulting `.json`, `.atlas` and `.png` files on your server.
2. Copy the `build/spine-widget.js` file to your server and include it on your website `<script src="spine-widget.js"></script>`, adjusting the src to match the location of the file on your server.
@ -114,7 +114,7 @@ To specify the configuration of a Spine Widget via HTML, you can use these HTML
* `data-fit-to-canvas`: optional, whether to fit the animation to the canvas size or not. Defaults to `true` if omitted, in which case `data-scale`, `data-x` and `data-y` are irrelevant. This setting calculates the setup pose bounding box using the specified skin to center and scale the animation on the canvas.
* `data-background-color`: optional, the background color to use. Defaults to `#000000` if omitted.
* `data-premultiplied-alpha`: optional, whether the atlas pages use premultiplied alpha or not. Defaults to `false` if omitted.
* `data-debug`: optional, whether to show debug information such as bones, attachments, etc. Defaults to `false` if omitted.
* `data-debug`: optional, whether to show debug information such as bones, attachments, etc. Defaults to `false` if omitted.
You can specify these as attribuets on the HTML element like this:
@ -139,12 +139,12 @@ Then create a new `spine.SpineWidget`, providing a [`SpineWidgetConfiguration`](
```JavaScript
new spine.SpineWidget("my-widget", {
json: "assets/spineboy.json",
atlas: "assets/spineboy.atlas",
animation: "run",
atlas: "assets/spineboy.atlas",
animation: "run",
backgroundColor: "#000000",
success: function (widget) {
var animIndex = 0;
widget.canvas.onclick = function () {
widget.canvas.onclick = function () {
animIndex++;
let animations = widget.skeleton.data.animations;
if (animIndex >= animations.length) animIndex = 0;
@ -160,6 +160,7 @@ The configuration object has the following fields:
* `atlas`: required, path to the `.atlas` file, absolute or relative, e.g. "assets/animation.atlas"
* `animation`: required, the name of the animation to play back
* `imagesPath`: optional, the location of images on the server to load atlas pages from. If omitted, atlas `.png` page files are loaded relative to the `.atlas` file.
* `atlasPages`: optional, the list of atlas page images, e.g. `atlasPages: ["assets/page1.png", "assets/page2.png"]` when using code, or `data-atlas-pages="assets/page1.png,assets/page2.png"` on case of HTML instantiation. Use this if you have a multi-page atlas. If ommited, only one atlas page image is loaded based on the atlas file name, replacing `.atlas` with `.png`.
* `skin`: optional, the name of the skin to use. Defaults to `default` if omitted.
* `loop`: optional, whether to loop the animation or not. Defaults to `true` if omitted.
* `scale`: optional, the scaling factor to apply when loading the `.json` file. Defaults to `1` if omitted. Irrelevant if `data-fit-to-canavs` is `true`.

View File

@ -1543,6 +1543,7 @@ declare module spine {
atlas: string;
animation: string;
imagesPath: string;
atlasPages: string[];
skin: string;
loop: boolean;
scale: number;

View File

@ -1568,7 +1568,7 @@ var spine;
current.mixingFrom = from;
current.mixTime = 0;
from.timelinesRotation.length = 0;
if (from.mixingFrom != null)
if (from.mixingFrom != null && from.mixDuration > 0)
current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -1680,7 +1680,7 @@ var spine;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.mixAlpha = 1;
@ -7899,7 +7899,14 @@ var spine;
var assets = this.assetManager = new spine.webgl.AssetManager(gl);
assets.loadText(config.atlas);
assets.loadText(config.json);
assets.loadTexture(config.atlas.replace(".atlas", ".png"));
if (config.atlasPages == null) {
assets.loadTexture(config.atlas.replace(".atlas", ".png"));
}
else {
for (var i = 0; i < config.atlasPages.length; i++) {
assets.loadTexture(config.atlasPages[i]);
}
}
requestAnimationFrame(function () { _this.load(); });
}
SpineWidget.prototype.validateConfig = function (config) {
@ -8082,6 +8089,8 @@ var spine;
config.animation = widget.getAttribute("data-animation");
if (widget.getAttribute("data-images-path"))
config.imagesPath = widget.getAttribute("data-images-path");
if (widget.getAttribute("data-atlas-pages"))
config.atlasPages = widget.getAttribute("data-atlas-pages").split(",");
if (widget.getAttribute("data-skin"))
config.skin = widget.getAttribute("data-skin");
if (widget.getAttribute("data-loop"))

File diff suppressed because one or more lines are too long

View File

@ -1568,7 +1568,7 @@ var spine;
current.mixingFrom = from;
current.mixTime = 0;
from.timelinesRotation.length = 0;
if (from.mixingFrom != null)
if (from.mixingFrom != null && from.mixDuration > 0)
current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -1680,7 +1680,7 @@ var spine;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.mixAlpha = 1;

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@ var spine;
current.mixingFrom = from;
current.mixTime = 0;
from.timelinesRotation.length = 0;
if (from.mixingFrom != null)
if (from.mixingFrom != null && from.mixDuration > 0)
current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -1333,7 +1333,7 @@ var spine;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.mixAlpha = 1;

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@ var spine;
current.mixingFrom = from;
current.mixTime = 0;
from.timelinesRotation.length = 0;
if (from.mixingFrom != null)
if (from.mixingFrom != null && from.mixDuration > 0)
current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -1333,7 +1333,7 @@ var spine;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.mixAlpha = 1;

File diff suppressed because one or more lines are too long

View File

@ -1221,7 +1221,7 @@ var spine;
current.mixingFrom = from;
current.mixTime = 0;
from.timelinesRotation.length = 0;
if (from.mixingFrom != null)
if (from.mixingFrom != null && from.mixDuration > 0)
current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -1333,7 +1333,7 @@ var spine;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.mixAlpha = 1;

File diff suppressed because one or more lines are too long

View File

@ -1473,6 +1473,7 @@ declare module spine {
atlas: string;
animation: string;
imagesPath: string;
atlasPages: string[];
skin: string;
loop: boolean;
scale: number;

View File

@ -1221,7 +1221,7 @@ var spine;
current.mixingFrom = from;
current.mixTime = 0;
from.timelinesRotation.length = 0;
if (from.mixingFrom != null)
if (from.mixingFrom != null && from.mixDuration > 0)
current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -1333,7 +1333,7 @@ var spine;
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.mixAlpha = 1;
@ -7478,7 +7478,14 @@ var spine;
var assets = this.assetManager = new spine.webgl.AssetManager(gl);
assets.loadText(config.atlas);
assets.loadText(config.json);
assets.loadTexture(config.atlas.replace(".atlas", ".png"));
if (config.atlasPages == null) {
assets.loadTexture(config.atlas.replace(".atlas", ".png"));
}
else {
for (var i = 0; i < config.atlasPages.length; i++) {
assets.loadTexture(config.atlasPages[i]);
}
}
requestAnimationFrame(function () { _this.load(); });
}
SpineWidget.prototype.validateConfig = function (config) {
@ -7661,6 +7668,8 @@ var spine;
config.animation = widget.getAttribute("data-animation");
if (widget.getAttribute("data-images-path"))
config.imagesPath = widget.getAttribute("data-images-path");
if (widget.getAttribute("data-atlas-pages"))
config.atlasPages = widget.getAttribute("data-atlas-pages").split(",");
if (widget.getAttribute("data-skin"))
config.skin = widget.getAttribute("data-skin");
if (widget.getAttribute("data-loop"))

File diff suppressed because one or more lines are too long

View File

@ -349,7 +349,7 @@ module spine {
from.timelinesRotation.length = 0;
// If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero.
if (from.mixingFrom != null) current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
if (from.mixingFrom != null && from.mixDuration > 0) current.mixAlpha *= Math.min(from.mixTime / from.mixDuration, 1);
}
this.queue.start(current);
@ -469,7 +469,7 @@ module spine {
entry.trackTime = 0;
entry.trackLast = -1;
entry.nextTrackLast = -1;
entry.trackEnd = loop ? Number.MAX_VALUE : entry.animationEnd;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;

View File

@ -80,7 +80,13 @@ module spine {
let assets = this.assetManager = new spine.webgl.AssetManager(gl);
assets.loadText(config.atlas);
assets.loadText(config.json);
assets.loadTexture(config.atlas.replace(".atlas", ".png"));
if (config.atlasPages == null) {
assets.loadTexture(config.atlas.replace(".atlas", ".png"));
} else {
for (let i = 0; i < config.atlasPages.length; i++) {
assets.loadTexture(config.atlasPages[i]);
}
}
requestAnimationFrame(() => { this.load(); });
}
@ -264,6 +270,7 @@ module spine {
config.json = widget.getAttribute("data-json");
config.animation = widget.getAttribute("data-animation");
if (widget.getAttribute("data-images-path")) config.imagesPath = widget.getAttribute("data-images-path");
if (widget.getAttribute("data-atlas-pages")) config.atlasPages = widget.getAttribute("data-atlas-pages").split(",");
if (widget.getAttribute("data-skin")) config.skin = widget.getAttribute("data-skin");
if (widget.getAttribute("data-loop")) config.loop = widget.getAttribute("data-loop") === "true";
if (widget.getAttribute("data-scale")) config.scale = parseFloat(widget.getAttribute("data-scale"));
@ -303,6 +310,7 @@ module spine {
atlas: string;
animation: string;
imagesPath: string;
atlasPages: string[];
skin = "default";
loop = true;
scale = 1.0;

View File

@ -69,9 +69,8 @@ namespace Spine.Unity.Examples {
SetXPosition(endX);
separator.enabled = true; // Enable Separator when hit
var poleTrack = state.SetAnimation(0, pole, false);
float duration = poleTrack.TrackEnd;
poleTrack.TrackEnd = float.PositiveInfinity;
yield return new WaitForSeconds(duration + 1f);
yield return new WaitForSpineAnimationComplete(poleTrack);
yield return new WaitForSeconds(1f);
}
}

View File

@ -1079,7 +1079,7 @@ namespace Spine.Unity.Editor {
AssetDatabase.CreateAsset(skeletonDataAsset, filePath);
AssetDatabase.SaveAssets();
} else {
skeletonDataAsset.Reset();
skeletonDataAsset.Clear();
skeletonDataAsset.GetSkeletonData(true);
}
@ -1312,16 +1312,16 @@ namespace Spine.Unity.Editor {
anim.skeletonDataAsset = skeletonDataAsset;
// Detect "Lit" shader and automatically enable calculateNormals.
bool requiresNormals = false;
foreach (AtlasAsset atlasAsset in anim.skeletonDataAsset.atlasAssets) {
foreach (Material m in atlasAsset.materials) {
if (m.shader.name.Contains("Lit")) {
requiresNormals = true;
break;
}
}
}
anim.calculateNormals = requiresNormals;
// bool requiresNormals = false;
// foreach (AtlasAsset atlasAsset in anim.skeletonDataAsset.atlasAssets) {
// foreach (Material m in atlasAsset.materials) {
// if (m.shader.name.Contains("Lit")) {
// requiresNormals = true;
// break;
// }
// }
// }
// anim.calculateNormals = requiresNormals;
SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
if (data == null) {
@ -1352,8 +1352,9 @@ namespace Spine.Unity.Editor {
static void EnableTK2D () {
bool added = false;
foreach (BuildTargetGroup group in System.Enum.GetValues(typeof(BuildTargetGroup))) {
if (group == BuildTargetGroup.Unknown)
foreach (BuildTargetGroup group in System.Enum.GetValues(typeof(BuildTargetGroup))) {
int gi = (int)group;
if (gi == 15 || gi == 16 || group == BuildTargetGroup.Unknown)
continue;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
@ -1379,6 +1380,10 @@ namespace Spine.Unity.Editor {
static void DisableTK2D () {
bool removed = false;
foreach (BuildTargetGroup group in System.Enum.GetValues(typeof(BuildTargetGroup))) {
int gi = (int)group;
if (gi == 15 || gi == 16 || group == BuildTargetGroup.Unknown)
continue;
string defines = PlayerSettings.GetScriptingDefineSymbolsForGroup(group);
if (defines.Contains(SPINE_TK2D_DEFINE)) {
removed = true;

View File

@ -62,14 +62,13 @@ namespace Spine.Unity {
Debug.LogWarning("TrackEntry was null. Coroutine will continue immediately.");
m_WasFired = true;
} else {
// Function normally.
trackEntry.Complete += HandleComplete;
}
}
#region Reuse
/// <summary>
/// One optimization high-frequency YieldInstruction returns is to cache instances to minimize pressure.
/// One optimization high-frequency YieldInstruction returns is to cache instances to minimize GC pressure.
/// Use NowWaitFor to reuse the same instance of WaitForSpineAnimationComplete.</summary>
public WaitForSpineAnimationComplete NowWaitFor (Spine.TrackEntry trackEntry) {
SafeSubscribe(trackEntry);

View File

@ -0,0 +1,94 @@
/******************************************************************************
* Spine Runtimes Software License v2.5
*
* Copyright (c) 2013-2016, Esoteric Software
* All rights reserved.
*
* You are granted a perpetual, non-exclusive, non-sublicensable, and
* non-transferable license to use, install, execute, and perform the Spine
* Runtimes software and derivative works solely for personal or internal
* use. Without the written permission of Esoteric Software (see Section 2 of
* the Spine Software License Agreement), you may not (a) modify, translate,
* adapt, or develop new applications using the Spine Runtimes or otherwise
* create derivative works or improvements of the Spine Runtimes or (b) remove,
* delete, alter, or obscure any trademarks or any copyright, trademark, patent,
* or other intellectual property or proprietary rights notices on or in the
* Software, including any copy thereof. Redistributions in binary or source
* form must include this license and terms.
*
* THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL ESOTERIC SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, OR LOSS OF
* USE, DATA, OR PROFITS) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#if (UNITY_5_0 || UNITY_5_1 || UNITY_5_2 || UNITY_4_0 || UNITY_4_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_4 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7)
#define PREUNITY_5_3
#endif
using UnityEngine;
using System.Collections;
using Spine;
namespace Spine.Unity {
/// <summary>
/// Use this as a condition-blocking yield instruction for Unity Coroutines.
/// The routine will pause until the AnimationState.TrackEntry fires its End event.</summary>
public class WaitForSpineTrackEntryEnd : IEnumerator {
bool m_WasFired = false;
public WaitForSpineTrackEntryEnd (Spine.TrackEntry trackEntry) {
#if PREUNITY_5_3
Debug.LogWarning("Unity 5.3 or later is required for Spine Unity custom yield instructions to function correctly.");
#endif
SafeSubscribe(trackEntry);
}
void HandleEnd (TrackEntry trackEntry) {
m_WasFired = true;
}
void SafeSubscribe (Spine.TrackEntry trackEntry) {
if (trackEntry == null) {
// Break immediately if trackEntry is null.
Debug.LogWarning("TrackEntry was null. Coroutine will continue immediately.");
m_WasFired = true;
} else {
trackEntry.End += HandleEnd;
}
}
#region Reuse
/// <summary>
/// One optimization high-frequency YieldInstruction returns is to cache instances to minimize GC pressure.
/// Use NowWaitFor to reuse the same instance of WaitForSpineAnimationEnd.</summary>
public WaitForSpineTrackEntryEnd NowWaitFor (Spine.TrackEntry trackEntry) {
SafeSubscribe(trackEntry);
return this;
}
#endregion
#region IEnumerator
bool IEnumerator.MoveNext () {
if (m_WasFired) {
((IEnumerator)this).Reset(); // auto-reset for YieldInstruction reuse
return false;
}
return true;
}
void IEnumerator.Reset () { m_WasFired = false; }
object IEnumerator.Current { get { return null; } }
#endregion
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8036c6c2897d2764db92f632d2aef568
timeCreated: 1480672707
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -132,8 +132,8 @@ namespace Spine.Unity.Editor {
skeleton.FlipY = EditorGUILayout.ToggleLeft("skeleton.FlipY", skeleton.FlipY);
requireRepaint |= EditorGUI.EndChangeCheck();
foreach (var t in skeleton.IkConstraints)
EditorGUILayout.LabelField(t.Data.Name + " " + t.Mix + " " + t.Target.Data.Name);
// foreach (var t in skeleton.IkConstraints)
// EditorGUILayout.LabelField(t.Data.Name + " " + t.Mix + " " + t.Target.Data.Name);
showSlots.target = EditorGUILayout.Foldout(showSlots.target, SlotsRootLabel);
if (showSlots.faded > 0) {

View File

@ -110,12 +110,13 @@ namespace Spine.Unity {
public Transform boneRoot;
void Update () {
if (boneRoot != null && skeletonRenderer.skeleton != null) {
var skeleton = skeletonRenderer.skeleton;
if (boneRoot != null && skeleton != null) {
Vector3 flipScale = Vector3.one;
if (skeletonRenderer.skeleton.FlipX)
if (skeleton.FlipX)
flipScale.x = -1;
if (skeletonRenderer.skeleton.FlipY)
if (skeleton.FlipY)
flipScale.y = -1;
boneRoot.localScale = flipScale;

View File

@ -121,7 +121,7 @@ namespace Spine.Unity {
cachedTransform.localPosition = new Vector3(bone.x, bone.y, 0);
if (rotation) {
if (!bone.data.transformMode.InheritsRotation()) {
if (bone.data.transformMode.InheritsRotation()) {
cachedTransform.localRotation = Quaternion.Euler(0, 0, bone.AppliedRotation);
} else {
Vector3 euler = skeletonTransform.rotation.eulerAngles;