diff --git a/README.md b/README.md
index 03e0e6a..be69fac 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,22 @@ There are three primary code paths within the library:
This library can be called automatically by the OpenFL/Lime command-line tools to process `` tags, or it can be used on the command-line to process SWF files into Animate ZIP files.
+Compact Animate timelines
+=========================
+
+Projects with large Animate libraries may define
+`swf_compact_animate_timelines` to keep serialized timeline events in compact
+numeric buffers. The default parser creates one `AnimateFrame` and
+`AnimateFrameObject` graph for every frame and event, including separate
+matrix, color-transform and filter objects. The compact mode decodes these
+values directly during playback, reducing retained managed memory and the
+amount of graph traversal performed by full garbage collections.
+
+The exported Animate asset format does not change, and standard timeline
+playback remains compatible. This mode is opt-in because code that accesses
+`AnimateSpriteSymbol.frames` directly will not receive the eagerly expanded
+legacy metadata.
+
Usage
=====
diff --git a/src/swf/exporters/animate/AnimateLibrary.hx b/src/swf/exporters/animate/AnimateLibrary.hx
index 9aed6ed..3d8ac40 100644
--- a/src/swf/exporters/animate/AnimateLibrary.hx
+++ b/src/swf/exporters/animate/AnimateLibrary.hx
@@ -38,6 +38,7 @@ import openfl.filters.GlowFilter;
@:noDebug
#end
@:access(swf.exporters.animate.AnimateSpriteSymbol)
+@:access(swf.exporters.animate.AnimateTimelineData)
@:access(swf.exporters.animate)
@SuppressWarnings("checkstyle:FieldDocComment")
@:keep class AnimateLibrary extends AssetLibrary
@@ -743,6 +744,9 @@ import openfl.filters.GlowFilter;
symbol.scale9Grid = data.scale9Grid != null ? new Rectangle(__pixel(data.scale9Grid[0]), __pixel(data.scale9Grid[1]), __pixel(data.scale9Grid[2]),
__pixel(data.scale9Grid[3])) : null;
var frames:Array = data.frames;
+ #if swf_compact_animate_timelines
+ symbol.__setCompactTimeline(frames);
+ #else
var frame:AnimateFrame,
objects:Array,
object:AnimateFrameObject;
@@ -790,6 +794,7 @@ import openfl.filters.GlowFilter;
}
symbol.frames.push(frame);
}
+ #end
return symbol;
}
diff --git a/src/swf/exporters/animate/AnimateSpriteSymbol.hx b/src/swf/exporters/animate/AnimateSpriteSymbol.hx
index 1a6bf2f..0ddbcea 100644
--- a/src/swf/exporters/animate/AnimateSpriteSymbol.hx
+++ b/src/swf/exporters/animate/AnimateSpriteSymbol.hx
@@ -17,6 +17,7 @@ class AnimateSpriteSymbol extends AnimateSymbol
public var frames:Array;
public var scale9Grid:Rectangle;
+ private var compactTimeline:AnimateTimelineData;
private var library:AnimateLibrary;
public function new()
@@ -128,4 +129,9 @@ class AnimateSpriteSymbol extends AnimateSymbol
this.library = library;
__constructor(cast instance);
}
+
+ private function __setCompactTimeline(frames:Array):Void
+ {
+ compactTimeline = new AnimateTimelineData(frames);
+ }
}
diff --git a/src/swf/exporters/animate/AnimateTimeline.hx b/src/swf/exporters/animate/AnimateTimeline.hx
index dbe2cca..b7fbfe7 100644
--- a/src/swf/exporters/animate/AnimateTimeline.hx
+++ b/src/swf/exporters/animate/AnimateTimeline.hx
@@ -18,6 +18,7 @@ import openfl.filters.DisplacementMapFilter;
import openfl.filters.DropShadowFilter;
import openfl.filters.GlowFilter;
import openfl.geom.ColorTransform;
+import openfl.geom.Matrix;
#if hscript
import hscript.Interp;
import hscript.Parser;
@@ -28,10 +29,13 @@ import hscript.Parser;
@:noDebug
#end
@:access(swf.exporters.animate.AnimateLibrary)
+@:access(swf.exporters.animate.AnimateSpriteSymbol)
@:access(swf.exporters.animate.AnimateSymbol)
+@:access(swf.exporters.animate.AnimateTimelineData)
@:access(openfl.display.DisplayObject)
@:access(openfl.display.MovieClip)
@:access(openfl.geom.ColorTransform)
+@:access(openfl.geom.Transform)
class AnimateTimeline extends Timeline
{
#if 0
@@ -59,6 +63,7 @@ class AnimateTimeline extends Timeline
@:noCompletion private var __currentInstancesByFrameObjectID:Map;
@:noCompletion private var __instanceFields:Array;
@:noCompletion private var __lastUpdated:Map;
+ @:noCompletion private var __lastUpdatedCompact:Map;
@:noCompletion private var __library:AnimateLibrary;
@:noCompletion private var __previousFrame:Int;
@:noCompletion private var __sprite:Sprite;
@@ -77,29 +82,34 @@ class AnimateTimeline extends Timeline
var frame:Int;
var frameData:AnimateFrame;
+ var frameCount = __symbol.compactTimeline != null ? __symbol.compactTimeline.frameCount : __symbol.frames.length;
+ var frameLabels:Array;
+ var scriptSource:String;
#if hscript
var parser:Parser = null;
#end
- for (i in 0...__symbol.frames.length)
+ for (i in 0...frameCount)
{
frame = i + 1;
- frameData = __symbol.frames[i];
+ frameData = __symbol.compactTimeline == null ? __symbol.frames[i] : null;
+ frameLabels = frameData != null ? frameData.labels : __symbol.compactTimeline.getLabels(i);
+ scriptSource = frameData != null ? frameData.scriptSource : __symbol.compactTimeline.getScriptSource(i);
- if (frameData.labels != null)
+ if (frameLabels != null)
{
- for (label in frameData.labels)
+ for (label in frameLabels)
{
labels.push(new FrameLabel(label, frame));
}
}
- if (frameData.script != null)
+ if (frameData != null && frameData.script != null)
{
scripts.push(new FrameScript(frameData.script, frame));
}
- else if (frameData.scriptSource != null)
+ else if (scriptSource != null)
{
try
{
@@ -110,10 +120,10 @@ class AnimateTimeline extends Timeline
parser.allowTypes = true;
}
- var script = __createScriptCallback(parser, frameData.scriptSource);
+ var script = __createScriptCallback(parser, scriptSource);
scripts.push(new FrameScript(script, frame));
#elseif js
- var script = __createScriptCallback(frameData.scriptSource);
+ var script = __createScriptCallback(scriptSource);
scripts.push(new FrameScript(script, frame));
#end
}
@@ -122,17 +132,17 @@ class AnimateTimeline extends Timeline
if (__symbol.className != null)
{
Log.warn("Unable to evaluate frame script source for symbol \"" + __symbol.className + "\" frame " + frame + "\n"
- + frameData.scriptSource);
+ + scriptSource);
}
else
{
- Log.warn("Unable to evaluate frame script source:\n" + frameData.scriptSource);
+ Log.warn("Unable to evaluate frame script source:\n" + scriptSource);
}
}
}
}
- scenes = [new Scene("", labels, __symbol.frames.length)];
+ scenes = [new Scene("", labels, frameCount)];
}
public override function attachMovieClip(movieClip:MovieClip):Void
@@ -142,6 +152,12 @@ class AnimateTimeline extends Timeline
public override function enterFrame(currentFrame:Int):Void
{
+ if (__symbol != null && __symbol.compactTimeline != null)
+ {
+ __enterCompactFrame(currentFrame);
+ return;
+ }
+
if (__symbol != null && currentFrame != __previousFrame)
{
var frame:Int;
@@ -222,107 +238,186 @@ class AnimateTimeline extends Timeline
}
currentInstances.sort(__sortDepths);
+ __applyCurrentInstances(currentInstances, currentMasks);
+ }
+
+ __previousFrame = currentFrame;
+ }
+ }
+
+ private function __enterCompactFrame(currentFrame:Int):Void
+ {
+ if (currentFrame == __previousFrame) return;
- var existingChild:DisplayObject;
- var targetDepth:Int;
- var targetChild:DisplayObject;
- var child:DisplayObject;
- var maskApplied:Bool;
+ var timeline = __symbol.compactTimeline;
+ var data = timeline.objects;
+ var updateFrameStart = __previousFrame < currentFrame ? (__previousFrame == -1 ? 0 : __previousFrame) : 0;
+ var skipFrame = false;
- for (i in 0...currentInstances.length)
+ if (currentFrame == 1 && __previousFrame > currentFrame && timeline.getFrameStart(0) != timeline.getFrameEnd(0))
+ {
+ skipFrame = true;
+ for (i in 1...timeline.frameCount)
+ {
+ if (timeline.getFrameStart(i) != timeline.getFrameEnd(i))
{
- existingChild = (i < __sprite.numChildren) ? __sprite.getChildAt(i) : null;
- instance = currentInstances[i];
+ skipFrame = false;
+ break;
+ }
+ }
+ }
- targetDepth = instance.depth;
- targetChild = instance.displayObject;
+ if (!skipFrame)
+ {
+ if (updateFrameStart <= 0)
+ {
+ __currentInstancesByFrameObjectID = new Map();
+ }
- if (existingChild != targetChild)
- {
- __sprite.addChildAt(targetChild, i);
- }
+ for (i in updateFrameStart...currentFrame)
+ {
+ var position = timeline.getFrameStart(i);
+ var end = timeline.getFrameEnd(i);
- child = targetChild;
- maskApplied = false;
+ while (position < end)
+ {
+ var objectPosition = position;
+ var type:AnimateFrameObjectType = cast Std.int(data[position]);
+ var id = Std.int(data[position + 1]);
+ var instance = __activeInstancesByFrameObjectID.get(id);
+ position = timeline.getNextObjectPosition(position);
- for (mask in currentMasks)
+ if (instance != null)
{
- if (targetDepth > mask.depth && targetDepth <= mask.clipDepth)
+ switch (type)
{
- #if (openfl >= "9.5.0" && !flash)
- child.clippingLayer = mask.displayObject;
- #else
- child.mask = mask.displayObject;
- #end
- maskApplied = true;
- break;
- }
- }
+ case CREATE:
+ __currentInstancesByFrameObjectID.set(id, instance);
+ __updateDisplayObjectCompact(instance.displayObject, objectPosition, true);
- if (currentMasks.length > 0 && !maskApplied && child.mask != null)
- {
- child.mask = null;
+ case UPDATE:
+ __updateDisplayObjectCompact(instance.displayObject, objectPosition);
+
+ case DESTROY:
+ __currentInstancesByFrameObjectID.remove(id);
+ }
}
}
+ }
+
+ var currentInstances = new Array();
+ var currentMasks = new Array();
- // TODO: How to tell if shapes are for a scale9Grid clip?
- if (__sprite.scale9Grid != null)
+ for (instance in __currentInstancesByFrameObjectID)
+ {
+ if (currentInstances.indexOf(instance) == -1)
{
- __sprite.graphics.clear();
- if (currentInstances.length > 0)
+ currentInstances.push(instance);
+ if (instance.clipDepth > 0)
{
- var displayObject = currentInstances[0].displayObject;
- if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (displayObject, Shape))
- {
- var shape:Shape = cast displayObject;
- // for scale9Grid to work with the shape's graphics,
- // we need to move them to the __sprite instead
- // because scale9Grid does not apply to children
- __sprite.graphics.copyFrom(shape.graphics);
- __sprite.removeChild(displayObject);
- }
+ currentMasks.push(instance);
}
}
- else
+ }
+
+ currentInstances.sort(__sortDepths);
+ __applyCurrentInstances(currentInstances, currentMasks);
+ }
+
+ __previousFrame = currentFrame;
+ }
+
+ private function __applyCurrentInstances(currentInstances:Array, currentMasks:Array):Void
+ {
+ var child:DisplayObject;
+ var instance:FrameSymbolInstance;
+
+ for (i in 0...currentInstances.length)
+ {
+ var existingChild = (i < __sprite.numChildren) ? __sprite.getChildAt(i) : null;
+ instance = currentInstances[i];
+ var targetDepth = instance.depth;
+ var targetChild = instance.displayObject;
+
+ if (existingChild != targetChild)
+ {
+ __sprite.addChildAt(targetChild, i);
+ }
+
+ child = targetChild;
+ var maskApplied = false;
+
+ for (mask in currentMasks)
+ {
+ if (targetDepth > mask.depth && targetDepth <= mask.clipDepth)
{
- var child:DisplayObject;
- var i = currentInstances.length;
- var length = __sprite.numChildren;
+ #if (openfl >= "9.5.0" && !flash)
+ child.clippingLayer = mask.displayObject;
+ #else
+ child.mask = mask.displayObject;
+ #end
+ maskApplied = true;
+ break;
+ }
+ }
- while (i < length)
- {
- child = __sprite.getChildAt(i);
+ if (currentMasks.length > 0 && !maskApplied && child.mask != null)
+ {
+ child.mask = null;
+ }
+ }
- // TODO: Faster method of determining if this was automatically added?
+ // TODO: How to tell if shapes are for a scale9Grid clip?
+ if (__sprite.scale9Grid != null)
+ {
+ __sprite.graphics.clear();
+ if (currentInstances.length > 0)
+ {
+ var displayObject = currentInstances[0].displayObject;
+ if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (displayObject, Shape))
+ {
+ var shape:Shape = cast displayObject;
+ // for scale9Grid to work with the shape's graphics,
+ // we need to move them to the __sprite instead
+ // because scale9Grid does not apply to children
+ __sprite.graphics.copyFrom(shape.graphics);
+ __sprite.removeChild(displayObject);
+ }
+ }
+ }
+ else
+ {
+ var i = currentInstances.length;
+ var length = __sprite.numChildren;
- for (instance in __activeInstances)
- {
- if (instance.displayObject == child)
- {
- // set MovieClips back to initial state (autoplay)
- if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (child, MovieClip))
- {
- var movie:MovieClip = cast child;
- movie.gotoAndPlay(1);
- }
+ while (i < length)
+ {
+ child = __sprite.getChildAt(i);
- __sprite.removeChild(child);
- i--;
- length--;
- }
+ // TODO: Faster method of determining if this was automatically added?
+ for (activeInstance in __activeInstances)
+ {
+ if (activeInstance.displayObject == child)
+ {
+ if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (child, MovieClip))
+ {
+ var movie:MovieClip = cast child;
+ movie.gotoAndPlay(1);
}
- i++;
+ __sprite.removeChild(child);
+ i--;
+ length--;
}
}
- #if !openfljs
- __updateInstanceFields();
- #end
+ i++;
}
-
- __previousFrame = currentFrame;
}
+
+ #if !openfljs
+ __updateInstanceFields();
+ #end
}
private function init(sprite:Sprite):Void
@@ -330,89 +425,95 @@ class AnimateTimeline extends Timeline
if (__activeInstances != null) return;
__sprite = sprite;
-
__instanceFields = [];
__previousFrame = -1;
-
__activeInstances = [];
__activeInstancesByFrameObjectID = new Map();
__currentInstancesByFrameObjectID = new Map();
- __lastUpdated = new Map();
-
- var frame:Int;
- var frameData:AnimateFrame;
- var instance:FrameSymbolInstance;
- var duplicate:Bool;
- var symbol:AnimateSymbol;
- var displayObject:DisplayObject;
-
- // TODO: Create later?
- for (i in 0...scenes[0].numFrames)
+ if (__symbol.compactTimeline != null)
{
- frame = i + 1;
- frameData = __symbol.frames[i];
+ __lastUpdatedCompact = new Map();
+ __initCompactInstances();
+ }
+ else
+ {
+ __lastUpdated = new Map();
- if (frameData.objects == null) continue;
+ var frame:Int;
+ var frameData:AnimateFrame;
+ var instance:FrameSymbolInstance;
+ var duplicate:Bool;
+ var symbol:AnimateSymbol;
+ var displayObject:DisplayObject;
- for (frameObject in frameData.objects)
+ // TODO: Create later?
+ for (i in 0...scenes[0].numFrames)
{
- if (frameObject.type == AnimateFrameObjectType.CREATE)
+ frame = i + 1;
+ frameData = __symbol.frames[i];
+
+ if (frameData.objects == null) continue;
+
+ for (frameObject in frameData.objects)
{
- if (__activeInstancesByFrameObjectID.exists(frameObject.id))
+ if (frameObject.type == AnimateFrameObjectType.CREATE)
{
- continue;
- }
- else
- {
- instance = null;
- duplicate = false;
-
- for (activeInstance in __activeInstances)
+ if (__activeInstancesByFrameObjectID.exists(frameObject.id))
{
- if (activeInstance.displayObject != null
- && activeInstance.characterID == frameObject.symbol
- && activeInstance.depth == frameObject.depth)
+ continue;
+ }
+ else
+ {
+ instance = null;
+ duplicate = false;
+
+ for (activeInstance in __activeInstances)
{
- // TODO: Fix duplicates in exporter
- instance = activeInstance;
- duplicate = true;
- break;
+ if (activeInstance.displayObject != null
+ && activeInstance.characterID == frameObject.symbol
+ && activeInstance.depth == frameObject.depth)
+ {
+ // TODO: Fix duplicates in exporter
+ instance = activeInstance;
+ duplicate = true;
+ break;
+ }
}
}
- }
- if (instance == null)
- {
- symbol = __library.symbols.get(frameObject.symbol);
-
- if (symbol != null)
+ if (instance == null)
{
- displayObject = symbol.__createObject(__library);
+ symbol = __library.symbols.get(frameObject.symbol);
- if (displayObject != null)
+ if (symbol != null)
{
- #if !flash
- // displayObject.parent = __sprite;
- // displayObject.stage = __sprite.stage;
+ displayObject = symbol.__createObject(__library);
+
+ if (displayObject != null)
+ {
+ #if !flash
+ // displayObject.parent = __sprite;
+ // displayObject.stage = __sprite.stage;
- // if (__sprite.stage != null) displayObject.dispatchEvent(new Event(Event.ADDED_TO_STAGE, false, false));
- #end
+ // if (__sprite.stage != null) displayObject.dispatchEvent(new Event(Event.ADDED_TO_STAGE, false, false));
+ #end
- instance = new FrameSymbolInstance(frame, frameObject.id, frameObject.symbol, frameObject.depth, displayObject,
- frameObject.clipDepth);
+ instance = new FrameSymbolInstance(frame, frameObject.id, frameObject.symbol, frameObject.depth, displayObject,
+ frameObject.clipDepth);
+ }
}
}
- }
-
- if (instance != null)
- {
- __activeInstancesByFrameObjectID.set(frameObject.id, instance);
- if (!duplicate)
+ if (instance != null)
{
- __activeInstances.push(instance);
- __updateDisplayObject(instance.displayObject, frameObject);
+ __activeInstancesByFrameObjectID.set(frameObject.id, instance);
+
+ if (!duplicate)
+ {
+ __activeInstances.push(instance);
+ __updateDisplayObject(instance.displayObject, frameObject);
+ }
}
}
}
@@ -426,6 +527,75 @@ class AnimateTimeline extends Timeline
enterFrame(1);
}
+ private function __initCompactInstances():Void
+ {
+ var timeline = __symbol.compactTimeline;
+ var data = timeline.objects;
+
+ for (i in 0...timeline.frameCount)
+ {
+ var frame = i + 1;
+ var position = timeline.getFrameStart(i);
+ var end = timeline.getFrameEnd(i);
+
+ while (position < end)
+ {
+ var objectPosition = position;
+ var type:AnimateFrameObjectType = cast Std.int(data[position]);
+ var id = Std.int(data[position + 1]);
+ var symbolID = Std.int(data[position + 2]);
+ var depth = Std.int(data[position + 3]);
+ var clipDepth = Std.int(data[position + 4]);
+ position = timeline.getNextObjectPosition(position);
+
+ if (type != AnimateFrameObjectType.CREATE || __activeInstancesByFrameObjectID.exists(id))
+ {
+ continue;
+ }
+
+ var instance:FrameSymbolInstance = null;
+ var duplicate = false;
+
+ for (activeInstance in __activeInstances)
+ {
+ if (activeInstance.displayObject != null
+ && activeInstance.characterID == symbolID
+ && activeInstance.depth == depth)
+ {
+ // TODO: Fix duplicates in exporter
+ instance = activeInstance;
+ duplicate = true;
+ break;
+ }
+ }
+
+ if (instance == null)
+ {
+ var symbol = __library.symbols.get(symbolID);
+ if (symbol != null)
+ {
+ var displayObject = symbol.__createObject(__library);
+ if (displayObject != null)
+ {
+ instance = new FrameSymbolInstance(frame, id, symbolID, depth, displayObject, clipDepth);
+ }
+ }
+ }
+
+ if (instance != null)
+ {
+ __activeInstancesByFrameObjectID.set(id, instance);
+
+ if (!duplicate)
+ {
+ __activeInstances.push(instance);
+ __updateDisplayObjectCompact(instance.displayObject, objectPosition);
+ }
+ }
+ }
+ }
+ }
+
public override function initializeSprite(sprite:Sprite):Void
{
if (__activeInstances != null) return;
@@ -437,6 +607,7 @@ class AnimateTimeline extends Timeline
__currentInstancesByFrameObjectID = null;
__instanceFields = null;
__lastUpdated = null;
+ __lastUpdatedCompact = null;
__sprite = null;
__previousFrame = -1;
}
@@ -530,6 +701,142 @@ class AnimateTimeline extends Timeline
__lastUpdated.set(displayObject, frameObject);
}
+ @:noCompletion private function __updateDisplayObjectCompact(displayObject:DisplayObject, objectPosition:Int, reset:Bool = false):Void
+ {
+ if (displayObject == null) return;
+ if (__lastUpdatedCompact.get(displayObject) == objectPosition) return;
+
+ var timeline = __symbol.compactTimeline;
+ var data = timeline.objects;
+ var references = timeline.references;
+ var flags = Std.int(data[objectPosition + 5]);
+ var position = objectPosition + 6;
+
+ if ((flags & AnimateTimelineData.HAS_MATRIX) != 0)
+ {
+ #if flash
+ displayObject.transform.matrix = new Matrix(data[position], data[position + 1], data[position + 2], data[position + 3], data[position + 4] / 20,
+ data[position + 5] / 20);
+ #else
+ displayObject.transform.__setTransform(data[position], data[position + 1], data[position + 2], data[position + 3], data[position + 4] / 20,
+ data[position + 5] / 20);
+ #end
+ position += 6;
+ }
+
+ if ((flags & AnimateTimelineData.HAS_COLOR_TRANSFORM) != 0)
+ {
+ #if flash
+ displayObject.transform.colorTransform = new ColorTransform(data[position] / 20, data[position + 1] / 20, data[position + 2] / 20,
+ data[position + 3] / 20, data[position + 4] / 20, data[position + 5] / 20, data[position + 6] / 20, data[position + 7] / 20);
+ #else
+ var colorTransform = ColorTransform.__pool.get();
+ colorTransform.redMultiplier = data[position] / 20;
+ colorTransform.greenMultiplier = data[position + 1] / 20;
+ colorTransform.blueMultiplier = data[position + 2] / 20;
+ colorTransform.alphaMultiplier = data[position + 3] / 20;
+ colorTransform.redOffset = data[position + 4] / 20;
+ colorTransform.greenOffset = data[position + 5] / 20;
+ colorTransform.blueOffset = data[position + 6] / 20;
+ colorTransform.alphaOffset = data[position + 7] / 20;
+ displayObject.transform.colorTransform = colorTransform;
+ ColorTransform.__pool.release(colorTransform);
+ #end
+ position += 8;
+ }
+ else if (reset #if !flash && !displayObject.transform.colorTransform.__isDefault(false) #end)
+ {
+ #if flash
+ displayObject.transform.colorTransform = new ColorTransform();
+ #else
+ var colorTransform = ColorTransform.__pool.get();
+ displayObject.transform.colorTransform = colorTransform;
+ ColorTransform.__pool.release(colorTransform);
+ #end
+ }
+
+ #if flash
+ displayObject.transform = displayObject.transform;
+ #end
+
+ if ((flags & AnimateTimelineData.HAS_FILTERS) != 0)
+ {
+ var filters:Array = [];
+ var filtersEnd = position + 1 + Std.int(data[position]);
+ position++;
+
+ while (position < filtersEnd)
+ {
+ var filterType = Std.int(data[position++]);
+ switch (filterType)
+ {
+ case 0:
+ filters.push(new BlurFilter(data[position++], data[position++], Std.int(data[position++])));
+
+ case 1:
+ var matrixLength = Std.int(data[position++]);
+ var matrix:Array = [];
+ for (i in 0...matrixLength)
+ {
+ matrix.push(data[position++]);
+ }
+ filters.push(new ColorMatrixFilter(matrix));
+
+ case 2:
+ filters.push(new DropShadowFilter(data[position++], data[position++], Std.int(data[position++]), data[position++], data[position++],
+ data[position++], data[position++], Std.int(data[position++]), data[position++] != 0, data[position++] != 0,
+ data[position++] != 0));
+
+ case 3:
+ filters.push(new GlowFilter(Std.int(data[position++]), data[position++], data[position++], data[position++], data[position++],
+ Std.int(data[position++]), data[position++] != 0, data[position++] != 0));
+
+ default:
+ position = filtersEnd;
+ }
+ }
+
+ displayObject.filters = filters;
+ }
+ else
+ {
+ displayObject.filters = null;
+ }
+
+ if ((flags & AnimateTimelineData.HAS_NAME) != 0)
+ {
+ displayObject.name = cast references[Std.int(data[position++])];
+ }
+
+ if ((flags & AnimateTimelineData.HAS_BLEND_MODE) != 0)
+ {
+ displayObject.blendMode = cast references[Std.int(data[position++])];
+ }
+
+ if ((flags & AnimateTimelineData.HAS_CACHE_AS_BITMAP) != 0)
+ {
+ displayObject.cacheAsBitmap = (flags & AnimateTimelineData.CACHE_AS_BITMAP) != 0;
+ }
+
+ if ((flags & AnimateTimelineData.HAS_VISIBLE) != 0)
+ {
+ displayObject.visible = (flags & AnimateTimelineData.VISIBLE) != 0;
+ }
+
+ #if (openfl >= "9.5.0")
+ if ((flags & AnimateTimelineData.HAS_META_DATA) != 0)
+ {
+ displayObject.metaData = references[Std.int(data[position++])];
+ }
+ #end
+
+ #if openfljs
+ Reflect.setField(__sprite, displayObject.name, displayObject);
+ #end
+
+ __lastUpdatedCompact.set(displayObject, objectPosition);
+ }
+
@:noCompletion private function __updateInstanceFields():Void
{
for (field in __instanceFields)
diff --git a/src/swf/exporters/animate/AnimateTimelineData.hx b/src/swf/exporters/animate/AnimateTimelineData.hx
new file mode 100644
index 0000000..f5ea94a
--- /dev/null
+++ b/src/swf/exporters/animate/AnimateTimelineData.hx
@@ -0,0 +1,208 @@
+package swf.exporters.animate;
+
+import haxe.ds.Vector;
+
+#if !openfl_debug
+@:fileXml('tags="haxe,release"')
+@:noDebug
+#end
+@:noCompletion
+class AnimateTimelineData
+{
+ public static inline var HAS_MATRIX = 1;
+ public static inline var HAS_COLOR_TRANSFORM = 2;
+ public static inline var HAS_FILTERS = 4;
+ public static inline var HAS_NAME = 8;
+ public static inline var HAS_BLEND_MODE = 16;
+ public static inline var HAS_CACHE_AS_BITMAP = 32;
+ public static inline var CACHE_AS_BITMAP = 64;
+ public static inline var HAS_VISIBLE = 128;
+ public static inline var VISIBLE = 256;
+ public static inline var HAS_META_DATA = 512;
+
+ public var frameCount(default, null):Int;
+ public var frameOffsets(default, null):Array;
+ public var objects(default, null):Vector;
+ public var references(default, null):Array;
+
+ private var encodedObjects:Array;
+ private var labelsByFrame:Map>;
+ private var scriptSourcesByFrame:Map;
+
+ public function new(frames:Array)
+ {
+ frameCount = frames != null ? frames.length : 0;
+ frameOffsets = [];
+ encodedObjects = [];
+
+ if (frames == null)
+ {
+ frameOffsets.push(0);
+ objects = new Vector(0);
+ encodedObjects = null;
+ return;
+ }
+
+ for (frameIndex in 0...frames.length)
+ {
+ var frameData:Dynamic = frames[frameIndex];
+ frameOffsets.push(encodedObjects.length);
+
+ if (Reflect.hasField(frameData, "label"))
+ {
+ if (labelsByFrame == null) labelsByFrame = new Map();
+ labelsByFrame.set(frameIndex, [Reflect.field(frameData, "label")]);
+ }
+ else if (Reflect.hasField(frameData, "labels"))
+ {
+ if (labelsByFrame == null) labelsByFrame = new Map();
+ labelsByFrame.set(frameIndex, Reflect.field(frameData, "labels"));
+ }
+
+ if (Reflect.hasField(frameData, "scriptSource"))
+ {
+ if (scriptSourcesByFrame == null) scriptSourcesByFrame = new Map();
+ scriptSourcesByFrame.set(frameIndex, Reflect.field(frameData, "scriptSource"));
+ }
+
+ var frameObjects:Array = Reflect.field(frameData, "objects");
+ if (frameObjects != null)
+ {
+ for (frameObject in frameObjects)
+ {
+ __encodeFrameObject(frameObject);
+ }
+ }
+ }
+
+ frameOffsets.push(encodedObjects.length);
+ objects = new Vector(encodedObjects.length);
+ for (i in 0...encodedObjects.length)
+ {
+ objects[i] = encodedObjects[i];
+ }
+ encodedObjects = null;
+ }
+
+ public inline function getLabels(frameIndex:Int):Array
+ {
+ return labelsByFrame != null ? labelsByFrame.get(frameIndex) : null;
+ }
+
+ public inline function getScriptSource(frameIndex:Int):String
+ {
+ return scriptSourcesByFrame != null ? scriptSourcesByFrame.get(frameIndex) : null;
+ }
+
+ public inline function getFrameStart(frameIndex:Int):Int
+ {
+ return frameOffsets[frameIndex];
+ }
+
+ public inline function getFrameEnd(frameIndex:Int):Int
+ {
+ return frameOffsets[frameIndex + 1];
+ }
+
+ public inline function getNextObjectPosition(position:Int):Int
+ {
+ var flags = Std.int(objects[position + 5]);
+ position += 6;
+
+ if ((flags & HAS_MATRIX) != 0) position += 6;
+ if ((flags & HAS_COLOR_TRANSFORM) != 0) position += 8;
+ if ((flags & HAS_FILTERS) != 0) position += 1 + Std.int(objects[position]);
+ if ((flags & HAS_NAME) != 0) position++;
+ if ((flags & HAS_BLEND_MODE) != 0) position++;
+ if ((flags & HAS_META_DATA) != 0) position++;
+
+ return position;
+ }
+
+ private function __addReference(value:Dynamic):Int
+ {
+ if (references == null) references = [];
+ references.push(value);
+ return references.length - 1;
+ }
+
+ private function __encodeFrameObject(frameObject:Dynamic):Void
+ {
+ var matrix:Array = Reflect.field(frameObject, "matrix");
+ var colorTransform:Array = Reflect.field(frameObject, "colorTransform");
+ var filters:Array> = Reflect.field(frameObject, "filters");
+ var name:Dynamic = Reflect.field(frameObject, "name");
+ var blendMode:Dynamic = Reflect.field(frameObject, "blendMode");
+ var cacheAsBitmap:Dynamic = Reflect.field(frameObject, "cacheAsBitmap");
+ var visible:Dynamic = Reflect.field(frameObject, "visible");
+ var metaData:Dynamic = Reflect.field(frameObject, "metaData");
+ var flags = 0;
+
+ if (matrix != null) flags |= HAS_MATRIX;
+ if (colorTransform != null) flags |= HAS_COLOR_TRANSFORM;
+ if (filters != null) flags |= HAS_FILTERS;
+ if (name != null) flags |= HAS_NAME;
+ if (blendMode != null) flags |= HAS_BLEND_MODE;
+ if (cacheAsBitmap != null)
+ {
+ flags |= HAS_CACHE_AS_BITMAP;
+ if (cacheAsBitmap) flags |= CACHE_AS_BITMAP;
+ }
+ if (visible != null)
+ {
+ flags |= HAS_VISIBLE;
+ if (visible) flags |= VISIBLE;
+ }
+ if (metaData != null) flags |= HAS_META_DATA;
+
+ encodedObjects.push(Reflect.field(frameObject, "type"));
+ encodedObjects.push(Reflect.field(frameObject, "id"));
+ encodedObjects.push(Reflect.field(frameObject, "symbol"));
+ encodedObjects.push(Reflect.field(frameObject, "depth"));
+ encodedObjects.push(Reflect.field(frameObject, "clipDepth"));
+ encodedObjects.push(flags);
+
+ if (matrix != null)
+ {
+ for (value in matrix) encodedObjects.push(value);
+ }
+
+ if (colorTransform != null)
+ {
+ for (value in colorTransform) encodedObjects.push(value);
+ }
+
+ if (filters != null)
+ {
+ var lengthPosition = encodedObjects.length;
+ encodedObjects.push(0);
+ for (filter in filters)
+ {
+ if (filter == null || filter.length == 0) continue;
+
+ encodedObjects.push(filter[0]);
+ if (filter[0] == 1)
+ {
+ var matrixValues:Array = filter[1];
+ encodedObjects.push(matrixValues != null ? matrixValues.length : 0);
+ if (matrixValues != null)
+ {
+ for (value in matrixValues) encodedObjects.push(value);
+ }
+ }
+ else
+ {
+ for (i in 1...filter.length)
+ {
+ encodedObjects.push(filter[i] == true ? 1 : filter[i] == false ? 0 : filter[i]);
+ }
+ }
+ }
+ encodedObjects[lengthPosition] = encodedObjects.length - lengthPosition - 1;
+ }
+
+ if (name != null) encodedObjects.push(__addReference(name));
+ if (blendMode != null) encodedObjects.push(__addReference(Std.string(blendMode)));
+ if (metaData != null) encodedObjects.push(__addReference(metaData));
+ }
+}
diff --git a/tests/src/TestMain.hx b/tests/src/TestMain.hx
index 99caed6..06b4dc8 100644
--- a/tests/src/TestMain.hx
+++ b/tests/src/TestMain.hx
@@ -9,6 +9,7 @@ class TestMain extends Sprite
super();
var runner = new Runner();
+ runner.addCase(new tests.AnimateTimelineTest());
runner.addCase(new tests.ShapesTest());
Report.create(runner);
diff --git a/tests/src/tests/AnimateTimelineTest.hx b/tests/src/tests/AnimateTimelineTest.hx
new file mode 100644
index 0000000..ecbd243
--- /dev/null
+++ b/tests/src/tests/AnimateTimelineTest.hx
@@ -0,0 +1,102 @@
+package tests;
+
+import openfl.display.MovieClip;
+import swf.exporters.animate.AnimateLibrary;
+import swf.exporters.animate.AnimateShapeSymbol;
+import swf.exporters.animate.AnimateSpriteSymbol;
+import swf.exporters.animate.AnimateTimeline;
+import utest.Assert;
+import utest.Test;
+
+@:access(swf.exporters.animate.AnimateLibrary)
+@:access(swf.exporters.animate.AnimateSpriteSymbol)
+class AnimateTimelineTest extends Test
+{
+ public function testCompactTimelinePlayback():Void
+ {
+ var library = new AnimateLibrary("timeline-test", "timeline-test");
+ library.frameRate = 30;
+ library.symbols = new Map();
+
+ var childSymbol = new AnimateShapeSymbol();
+ childSymbol.id = 2;
+ childSymbol.commands = [];
+ library.symbols.set(childSymbol.id, childSymbol);
+
+ var symbol = new AnimateSpriteSymbol();
+ symbol.id = 1;
+ var frames:Array = [
+ {
+ label: "start",
+ objects: [
+ {
+ type: 0,
+ id: 10,
+ symbol: 2,
+ depth: 1,
+ clipDepth: 0,
+ matrix: [1, 0, 0, 1, 200, 400],
+ colorTransform: [20, 20, 20, 10, 0, 0, 0, 0],
+ filters: [[0, 4, 6, 2]],
+ name: "compactChild",
+ visible: false
+ }
+ ]
+ },
+ {
+ objects: [
+ {
+ type: 1,
+ id: 10,
+ symbol: 2,
+ depth: 1,
+ clipDepth: 0,
+ matrix: [2, 0, 0, 3, 600, 800],
+ visible: true
+ }
+ ]
+ },
+ {
+ objects: [
+ {
+ type: 2,
+ id: 10,
+ symbol: 2,
+ depth: 1,
+ clipDepth: 0
+ }
+ ]
+ }
+ ];
+ symbol.__setCompactTimeline(frames);
+
+ var timeline = new AnimateTimeline(library, symbol);
+ var movieClip = new MovieClip();
+ timeline.attachMovieClip(movieClip);
+
+ Assert.equals(3, timeline.scenes[0].numFrames);
+ Assert.equals("start", timeline.scenes[0].labels[0].name);
+ Assert.equals(1, movieClip.numChildren);
+
+ var child = movieClip.getChildAt(0);
+ Assert.equals("compactChild", child.name);
+ Assert.equals(10.0, child.x);
+ Assert.equals(20.0, child.y);
+ Assert.equals(0.5, child.alpha);
+ Assert.isFalse(child.visible);
+ Assert.equals(1, child.filters.length);
+
+ timeline.enterFrame(2);
+ Assert.equals(child, movieClip.getChildAt(0));
+ Assert.equals(30.0, child.x);
+ Assert.equals(40.0, child.y);
+ Assert.equals(2.0, child.scaleX);
+ Assert.equals(3.0, child.scaleY);
+ Assert.equals(0.5, child.alpha);
+ Assert.isTrue(child.visible);
+ Assert.equals(0, child.filters.length);
+
+ timeline.enterFrame(3);
+ Assert.equals(0, movieClip.numChildren);
+ }
+}