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); + } +}