From 9227c7dad4c288ad538d6079240182202ec516cc Mon Sep 17 00:00:00 2001 From: Ludovic Bas Date: Sat, 29 Apr 2023 23:31:06 +0200 Subject: [PATCH 1/6] Fix a bug that prevents setting individual time to Spriter's entities --- spriter/engine/Spriter.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spriter/engine/Spriter.hx b/spriter/engine/Spriter.hx index 9c95ebf..5e12ac3 100644 --- a/spriter/engine/Spriter.hx +++ b/spriter/engine/Spriter.hx @@ -132,7 +132,7 @@ class Spriter if (currentAnimation.loopType == LOOPING) { - normalizedTime += Std.int(elapsedMS * playbackSpeed); + normalizedTime = timeMS; if (normalizedTime >= currentAnimation.length)//forward { normalizedTime -= currentAnimation.length; @@ -148,7 +148,7 @@ class Spriter } } //even if paused we need to draw it - currentAnimation.setCurrentTime(normalizedTime, Std.int(MathUtils.fabs(elapsedMS * playbackSpeed)), library, this, currentEntity, info); + currentAnimation.setCurrentTime(normalizedTime, timeMS, library, this, currentEntity, info); //callback if (currentAnimation.loopType == LOOPING) { From 28ed9a7a6e4825ac0c4183c1544386590639b032 Mon Sep 17 00:00:00 2001 From: Ludovic Bas Date: Sat, 29 Apr 2023 23:56:53 +0200 Subject: [PATCH 2/6] handy method to get the next key time --- spriter/engine/Spriter.hx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/spriter/engine/Spriter.hx b/spriter/engine/Spriter.hx index 5e12ac3..7bef5b1 100644 --- a/spriter/engine/Spriter.hx +++ b/spriter/engine/Spriter.hx @@ -10,7 +10,6 @@ import spriter.definitions.SpriterEntity; import spriter.definitions.SpriterFile; import spriter.definitions.SpriterFolder; import spriter.library.AbstractLibrary; -import spriter.util.MathUtils; import spriter.util.SpriterUtil; import spriter.vars.Variable; #if SPRITER_CUSTOM_MAP @@ -387,6 +386,25 @@ class Spriter normalizedTime = timeMS = currentAnimation.length; } } + + public function getNextKeyTime():Int + { + var nextTime = 0; + + var len:Int = currentAnimation.mainlineKeys.length; + for (m in 0...len) + { + if(currentAnimation.mainlineKeys[m].time > timeMS) + { + nextTime = currentAnimation.mainlineKeys[m].time; + break; + } + + } + + return nextTime; + } + public function reverse(value:Bool = true):Spriter { if (value) From 94abd63f477560aca8130401c8c3226e58ee6414 Mon Sep 17 00:00:00 2001 From: Ludovic Bas Date: Mon, 1 May 2023 18:59:14 +0200 Subject: [PATCH 3/6] introducing nextFrame and prevFrame --- spriter/engine/Spriter.hx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spriter/engine/Spriter.hx b/spriter/engine/Spriter.hx index 7bef5b1..b4f41a6 100644 --- a/spriter/engine/Spriter.hx +++ b/spriter/engine/Spriter.hx @@ -405,6 +405,32 @@ class Spriter return nextTime; } + public function getPrevKeyTime():Int + { + var i = currentAnimation.mainlineKeys.length - 1; + + var prevTime = currentAnimation.mainlineKeys[i].time; + while(i >= 0) { + if(currentAnimation.mainlineKeys[i].time < timeMS) + { + prevTime = currentAnimation.mainlineKeys[i].time; + break; + } + i--; + } + return prevTime; + } + + public function nextFrame():Void + { + timeMS = getNextKeyTime(); + } + + public function prevFrame():Void + { + timeMS = getPrevKeyTime(); + } + public function reverse(value:Bool = true):Spriter { if (value) From 57903ef459611e97d694c02a4891fe8045ce46a1 Mon Sep 17 00:00:00 2001 From: Ludovic Bas Date: Mon, 1 May 2023 23:56:23 +0200 Subject: [PATCH 4/6] improvement on event dispatch to support reverse playback --- spriter/definitions/SpriterAnimation.hx | 38 +++++++++++++++++++---- spriter/engine/Spriter.hx | 40 ++++++++++++------------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/spriter/definitions/SpriterAnimation.hx b/spriter/definitions/SpriterAnimation.hx index 3c915a9..29bfa62 100644 --- a/spriter/definitions/SpriterAnimation.hx +++ b/spriter/definitions/SpriterAnimation.hx @@ -46,6 +46,8 @@ class SpriterAnimation public var soundlines:Array>; #end + var _triggerTime:Int; + public function new(xml:Access) @@ -121,10 +123,13 @@ class SpriterAnimation } #end } + + _triggerTime = -1; } /** * - * @param newTime Use a time between [0,length] + * @param newTime Use a time between [0,length[ + * @param elapsedTime elapsed time * @param library library to compute and draw the final graphics * @param root IScml to use some features * @param currentEntity to use some features @@ -231,6 +236,8 @@ class SpriterAnimation objectKeys[k].paint(); }*/ + var triggerResult = false; + //following lines not in scml references yet #if !SPRITER_NO_TAG if (taglines != null) @@ -239,6 +246,7 @@ class SpriterAnimation { if (isTriggered(tag.time, mainKey.time, newTime, elapsedTime)) { + triggerResult = true; spriter.clearTag(); for (i in 0...tag.t.length) { @@ -258,6 +266,7 @@ class SpriterAnimation { if (isTriggered(keyVar.time, mainKey.time, newTime, elapsedTime)) { + triggerResult = true; spriter.updateVar(_var.id, keyVar.value); } } @@ -273,6 +282,7 @@ class SpriterAnimation { if (isTriggered(soundKey.time, mainKey.time, newTime, elapsedTime)) { + triggerResult = true; spriter.dispatchSound(soundKey.folder, soundKey.file); } } @@ -288,24 +298,42 @@ class SpriterAnimation { if (isTriggered(eventKey.time, mainKey.time, newTime, elapsedTime)) { + triggerResult = true; spriter.dispatchEvent(event.name); } } } } #end + + if(triggerResult) + _triggerTime = mainKey.time; + else if (_triggerTime != mainKey.time) + _triggerTime = -1;//we reset if we changed the mainkey because it'll allow to trigger when play back and forth + //clean up spatialInfo = null; } - function isTriggered(triggerTime:Int, keyTime:Int, newTime:Int, elapsedTime:Int):Bool + /** + * Check if an event, variable or sound is triggered. + * By checking if triggerTime and keyTime have same value; + * And then if triggerTime has been reached for the first time (in the current loop) + * @param triggerTime + * @param keyTime + * @param newTime + * @param elapsedTime + * @return Bool + */ + function isTriggered(eventTime:Int, keyTime:Int, newTime:Int, elapsedTime:Int):Bool { - if (triggerTime == keyTime) + if (eventTime == keyTime) { - if (newTime - elapsedTime < keyTime) + //TODO on jumping on a frame on individual spriters, we may update(0) to have better control of times, but this won't trigger anything because elapsed time equals 0 + if (newTime - elapsedTime <= keyTime && _triggerTime != keyTime) { return true; - }else if (triggerTime == 0 && newTime == elapsedTime) { //allow to trigger the first frame + }else if (eventTime == 0 && newTime == elapsedTime) { //allow to trigger the first frame return true; }else { return false; diff --git a/spriter/engine/Spriter.hx b/spriter/engine/Spriter.hx index b4f41a6..1cb1399 100644 --- a/spriter/engine/Spriter.hx +++ b/spriter/engine/Spriter.hx @@ -12,6 +12,7 @@ import spriter.definitions.SpriterFolder; import spriter.library.AbstractLibrary; import spriter.util.SpriterUtil; import spriter.vars.Variable; +import spriter.util.MathUtils; #if SPRITER_CUSTOM_MAP import spriter.definitions.CustomCharMap; #end @@ -27,11 +28,12 @@ class Spriter public var library:AbstractLibrary; public var spriterName:String; /** - * Time elapsed since beginning of current animation + * Time elapsed since beginning of current animation. Value could be negative if you play the animation in reverse. + * You can change the value */ public var timeMS:Int = 0; /** - * Time in the range of [0,currentAnimation.length] + * Time in the range of [0,currentAnimation.length[ in ms */ public var normalizedTime:Int = 0; @@ -125,37 +127,33 @@ class Spriter public function advanceTime(elapsedMS:Int):Void { + var speedElapsed = Std.int(elapsedMS * playbackSpeed); if (!paused) { - timeMS += Std.int(elapsedMS * playbackSpeed); + timeMS += speedElapsed; + //TODO shouldn't be negative in reverse playback (TOFIX trigger event in reverse playback) if (currentAnimation.loopType == LOOPING) { - normalizedTime = timeMS; - if (normalizedTime >= currentAnimation.length)//forward - { - normalizedTime -= currentAnimation.length; - ++loop; - }else if (normalizedTime <= 0)//backward - { - normalizedTime += currentAnimation.length; - ++loop; - } + normalizedTime = timeMS % currentAnimation.length; + loop = Std.int(timeMS / currentAnimation.length); - }else{//no looping - normalizedTime = Std.int(Math.max(0, Math.min(timeMS, currentAnimation.length))); + }else{ + //no looping + normalizedTime = Std.int(Math.max(0, Math.min(timeMS, currentAnimation.length))); } } //even if paused we need to draw it - currentAnimation.setCurrentTime(normalizedTime, timeMS, library, this, currentEntity, info); + currentAnimation.setCurrentTime(normalizedTime, speedElapsed, library, this, currentEntity, info); //callback if (currentAnimation.loopType == LOOPING) { - if (loop > lastLoop) { + if (loop != lastLoop) { lastLoop = loop; dispatchComplete(); } - }else {//no looping + }else { + //no looping var when:Int = playbackSpeed > 0 ? currentAnimation.length : 0; if (normalizedTime == when) { if (!onCompleteOnce && !hasReflect) onCompleteOnce = true;//force to avoid dispatching every frame when it's done @@ -350,7 +348,7 @@ class Spriter /** * Play a stack of animations whatever the entity. - * @param name of the entity + * @param name of the entities * @param anims names of the animations in order * @param endAnimCallback function callback, return (s:Spriter, entity:String, anim:String) * @param removeCallback remove function callback after dispatch @@ -423,12 +421,12 @@ class Spriter public function nextFrame():Void { - timeMS = getNextKeyTime(); + normalizedTime = timeMS = getNextKeyTime(); } public function prevFrame():Void { - timeMS = getPrevKeyTime(); + normalizedTime = timeMS = getPrevKeyTime(); } public function reverse(value:Bool = true):Spriter From 8b28778652b21400b04e9cac9eb0a394c0f9f004 Mon Sep 17 00:00:00 2001 From: Ludovic Bas Date: Tue, 2 May 2023 14:44:53 +0200 Subject: [PATCH 5/6] fix bug that forget a completedCallback when backward playing and reaching time 0; fix bug when negative timeMS; add comments --- spriter/definitions/SpatialInfo.hx | 23 +++++++++++++-- spriter/definitions/SpriterAnimation.hx | 2 +- spriter/engine/Spriter.hx | 39 ++++++++++++++++++++----- spriter/util/MathUtils.hx | 10 +++---- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/spriter/definitions/SpatialInfo.hx b/spriter/definitions/SpatialInfo.hx index 0962737..d67d43b 100644 --- a/spriter/definitions/SpatialInfo.hx +++ b/spriter/definitions/SpatialInfo.hx @@ -61,6 +61,12 @@ class SpatialInfo implements ISpriterPooled return this; } + /** + * Convenient method to set both x and y in one line. + * @param x + * @param y + * @return + */ public function setPos(x:Float = 0, y:Float = 0):SpatialInfo { this.x = x; @@ -68,6 +74,11 @@ class SpatialInfo implements ISpriterPooled return this; } + /** + * Convenient method to set both scaleX and scaleY. + * @param scale + * @return + */ public function setScale(scale:Float):SpatialInfo { this.scaleX = scale; @@ -75,10 +86,10 @@ class SpatialInfo implements ISpriterPooled return this; } /** - * + * Get SpatialInfo modified by parent. * @param parentInfo * @param out if null, this method will override this SpatialInfo - * @return + * @return SpatialInfo modified with parent values. Use out if specified. */ public function unmapFromParent(parentInfo:SpatialInfo, out:SpatialInfo = null):SpatialInfo { @@ -115,10 +126,18 @@ class SpatialInfo implements ISpriterPooled return out; } + /** + * Create a new SpatialInfo with values from this. + * @return new SpatialInfo + */ inline public function copy():SpatialInfo { return new SpatialInfo(x, y, angle, scaleX, scaleY, a, spin); } + /** + * Copy SpatialInfo from other values + * @param out spatialInfo to copy + */ public function clone(out:SpatialInfo):Void { out.init(x, y, angle, scaleX, scaleY, a, spin);//initializing the out object with the values of this object diff --git a/spriter/definitions/SpriterAnimation.hx b/spriter/definitions/SpriterAnimation.hx index 29bfa62..05db37c 100644 --- a/spriter/definitions/SpriterAnimation.hx +++ b/spriter/definitions/SpriterAnimation.hx @@ -306,6 +306,7 @@ class SpriterAnimation } #end + //we store the trigger time only when everything has been tested (if not we'll only trigger first event...) if(triggerResult) _triggerTime = mainKey.time; else if (_triggerTime != mainKey.time) @@ -329,7 +330,6 @@ class SpriterAnimation { if (eventTime == keyTime) { - //TODO on jumping on a frame on individual spriters, we may update(0) to have better control of times, but this won't trigger anything because elapsed time equals 0 if (newTime - elapsedTime <= keyTime && _triggerTime != keyTime) { return true; diff --git a/spriter/engine/Spriter.hx b/spriter/engine/Spriter.hx index 1cb1399..4e89e34 100644 --- a/spriter/engine/Spriter.hx +++ b/spriter/engine/Spriter.hx @@ -28,12 +28,14 @@ class Spriter public var library:AbstractLibrary; public var spriterName:String; /** - * Time elapsed since beginning of current animation. Value could be negative if you play the animation in reverse. - * You can change the value + * Time elapsed since beginning of current animation. Resetted when starting new animation or on reflection. + * Starting at animation length when animating backward and then become negative. + * You can change the value to jump at a specific frame but beware of consequences. */ public var timeMS:Int = 0; /** * Time in the range of [0,currentAnimation.length[ in ms + * Calculated from timeMS and playbackSpeed */ public var normalizedTime:Int = 0; @@ -128,14 +130,22 @@ class Spriter public function advanceTime(elapsedMS:Int):Void { var speedElapsed = Std.int(elapsedMS * playbackSpeed); + var changedSign = false; if (!paused) { + var prevValue = timeMS; timeMS += speedElapsed; - //TODO shouldn't be negative in reverse playback (TOFIX trigger event in reverse playback) + changedSign = !SpriterUtil.sameSign(prevValue, timeMS); if (currentAnimation.loopType == LOOPING) { - normalizedTime = timeMS % currentAnimation.length; + if(timeMS >= 0) + { + normalizedTime = timeMS % currentAnimation.length; + }else{ + normalizedTime = ((currentAnimation.length + (timeMS % currentAnimation.length)) % currentAnimation.length); + } + loop = Std.int(timeMS / currentAnimation.length); }else{ @@ -144,11 +154,11 @@ class Spriter } } //even if paused we need to draw it - currentAnimation.setCurrentTime(normalizedTime, speedElapsed, library, this, currentEntity, info); + currentAnimation.setCurrentTime(normalizedTime, MathUtils.abs(speedElapsed), library, this, currentEntity, info); //callback if (currentAnimation.loopType == LOOPING) { - if (loop != lastLoop) { + if (loop != lastLoop || changedSign) { lastLoop = loop; dispatchComplete(); } @@ -419,16 +429,26 @@ class Spriter return prevTime; } + /** + * Set the current time to the next frame time + */ public function nextFrame():Void { normalizedTime = timeMS = getNextKeyTime(); } - + /** + * Set the current time to the previous frame time + */ public function prevFrame():Void { normalizedTime = timeMS = getPrevKeyTime(); } + /** + * Reverse the playback speed and reset the time to start or end time depending if playing forward or backward + * @param value + * @return + */ public function reverse(value:Bool = true):Spriter { if (value) @@ -442,6 +462,11 @@ class Spriter resetTime(); return this; } + /** + * Set to true to animate forward and then backward + * @param value + * @return + */ public function reflect(value:Bool = true):Spriter { if (value) diff --git a/spriter/util/MathUtils.hx b/spriter/util/MathUtils.hx index 3193063..a18db81 100644 --- a/spriter/util/MathUtils.hx +++ b/spriter/util/MathUtils.hx @@ -6,7 +6,8 @@ package spriter.util; */ class MathUtils { - inline static public function fabs(n:Float):Float + @:generic + inline static public function abs(n:T):T { if(n >= 0) { return n; @@ -118,19 +119,18 @@ class MathUtils var t2:Float = x; var x2:Float; var d2:Float; - var i:Int; // First try a few iterations of Newton's method -- normally very fast. for (i in 0...8) { x2 = sampleCurve(ax, bx, cx, t2) - x; - if(fabs(x2) < epsilon) { + if(abs(x2) < epsilon) { return t2; } d2 = sampleCurveDerivativeX(ax, bx, cx, t2); - if(fabs(d2) < 1e-6) { + if(abs(d2) < 1e-6) { break; } @@ -155,7 +155,7 @@ class MathUtils while(t0 < t1) { x2 = sampleCurve(ax, bx, cx, t2); - if(fabs(x2-x) < epsilon) { + if(abs(x2-x) < epsilon) { return t2; } if(x > x2) { From 27359794200a33c46284bdce6079a8f29b8d7564 Mon Sep 17 00:00:00 2001 From: Ludovic Bas Date: Tue, 2 May 2023 16:35:04 +0200 Subject: [PATCH 6/6] add progression of animation + method to set it; breaking change : change timeMS by time and set normalizedTime as ready only. Warning: there is a regression in this commmit that make calling complete() unstable. --- spriter/engine/Spriter.hx | 80 +++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/spriter/engine/Spriter.hx b/spriter/engine/Spriter.hx index 4e89e34..5d4c567 100644 --- a/spriter/engine/Spriter.hx +++ b/spriter/engine/Spriter.hx @@ -28,16 +28,26 @@ class Spriter public var library:AbstractLibrary; public var spriterName:String; /** - * Time elapsed since beginning of current animation. Resetted when starting new animation or on reflection. - * Starting at animation length when animating backward and then become negative. - * You can change the value to jump at a specific frame but beware of consequences. + * Time in ms that can be used to jump to a specific time. + * Beware of specifities that make it difficult to read: + * - time elapsed since beginning of current animation. + * - Resetted when starting new animation or on reflection. + * - Starting at animation length when animating backward and then become negative. + * @see also progress property that should be more convenient */ - public var timeMS:Int = 0; + public var time:Int = 0; /** * Time in the range of [0,currentAnimation.length[ in ms - * Calculated from timeMS and playbackSpeed + * Calculated from time and playbackSpeed */ - public var normalizedTime:Int = 0; + public var normalizedTime(default, null):Int = 0; + + /** + * Time progression in the range of [0,1] in ms that can be used to jump to a specific progression + */ + public var progress(get, set):Float; + //used to update progress without calling setter that modifies time and normalized Time + var _progress:Float; /** * Manipulate positions (x,y), scale, alpha and rotation through this object. @@ -133,26 +143,28 @@ class Spriter var changedSign = false; if (!paused) { - var prevValue = timeMS; - timeMS += speedElapsed; - changedSign = !SpriterUtil.sameSign(prevValue, timeMS); + var prevValue = time; + time += speedElapsed; + changedSign = !SpriterUtil.sameSign(prevValue, time); if (currentAnimation.loopType == LOOPING) { - if(timeMS >= 0) + if(time >= 0) { - normalizedTime = timeMS % currentAnimation.length; + normalizedTime = time % currentAnimation.length; }else{ - normalizedTime = ((currentAnimation.length + (timeMS % currentAnimation.length)) % currentAnimation.length); + normalizedTime = ((currentAnimation.length + (time % currentAnimation.length)) % currentAnimation.length); } - loop = Std.int(timeMS / currentAnimation.length); + loop = Std.int(time / currentAnimation.length); }else{ //no looping - normalizedTime = Std.int(Math.max(0, Math.min(timeMS, currentAnimation.length))); + normalizedTime = Std.int(Math.max(0, Math.min(time, currentAnimation.length))); } + } + _progress = normalizedTime / currentAnimation.length; //even if paused we need to draw it currentAnimation.setCurrentTime(normalizedTime, MathUtils.abs(speedElapsed), library, this, currentEntity, info); //callback @@ -384,17 +396,41 @@ class Spriter } } + /** + * Reset time to start time or end time depending of playback direction. + */ public function resetTime():Void { if (playbackSpeed > 0) { - loop = lastLoop = normalizedTime = timeMS = 0; - }else{ loop = lastLoop = 0; - normalizedTime = timeMS = currentAnimation.length; + progress = 0; + }else{ + loop = 0; + lastLoop = 0; + progress = 1; } } + /** + * Set time progression + * @param p progress of the animation. Value must be [0,1]. + */ + function set_progress(p:Float):Float + { + if (p < 0) + p = 0; + else if (p > 1) + p = 1; + _progress = p; + time = normalizedTime = Std.int(p * currentAnimation.length); + return _progress; + } + function get_progress():Float + { + return _progress; + } + public function getNextKeyTime():Int { var nextTime = 0; @@ -402,7 +438,7 @@ class Spriter var len:Int = currentAnimation.mainlineKeys.length; for (m in 0...len) { - if(currentAnimation.mainlineKeys[m].time > timeMS) + if(currentAnimation.mainlineKeys[m].time > time) { nextTime = currentAnimation.mainlineKeys[m].time; break; @@ -419,7 +455,7 @@ class Spriter var prevTime = currentAnimation.mainlineKeys[i].time; while(i >= 0) { - if(currentAnimation.mainlineKeys[i].time < timeMS) + if(currentAnimation.mainlineKeys[i].time < time) { prevTime = currentAnimation.mainlineKeys[i].time; break; @@ -434,14 +470,16 @@ class Spriter */ public function nextFrame():Void { - normalizedTime = timeMS = getNextKeyTime(); + var t = getNextKeyTime(); + progress = t / currentAnimation.length; } /** * Set the current time to the previous frame time */ public function prevFrame():Void { - normalizedTime = timeMS = getPrevKeyTime(); + var t = getPrevKeyTime(); + progress = t / currentAnimation.length; } /**