diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5e2e92862..19a72f770 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -42,7 +42,7 @@ jobs:
needs: package-haxelib
strategy:
matrix:
- haxe-version: [4.0.5, 4.1.5, 4.2.5, 4.3.6]
+ haxe-version: [4.0.5, 4.1.5, 4.2.5, 4.3.7]
runs-on: ubuntu-latest
steps:
@@ -61,6 +61,7 @@ jobs:
- name: Install Haxe dependencies
run: |
+ haxelib install format --quiet
haxelib install lime --quiet
haxelib install openfl --quiet
haxelib install layout --quiet
@@ -97,7 +98,7 @@ jobs:
unit-test-neko:
strategy:
matrix:
- haxe-version: [3.4.7, 4.0.5, 4.1.5, 4.2.5, 4.3.6]
+ haxe-version: [3.4.7, 4.0.5, 4.1.5, 4.2.5, 4.3.7]
runs-on: ubuntu-latest
env:
SDL_VIDEODRIVER: "dummy"
@@ -126,11 +127,139 @@ jobs:
run: |
haxelib run openfl test neko
+ unit-test-neko-defines:
+ strategy:
+ matrix:
+ define: [swf_compact_animate_timelines, swf_hardware_bitmap_cache, "swf_compact_animate_timelines -Dswf_hardware_bitmap_cache"]
+ runs-on: ubuntu-latest
+ env:
+ SDL_VIDEODRIVER: "dummy"
+ SDL_AUDIODRIVER: "disk"
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: krdlab/setup-haxe@v2
+ with:
+ haxe-version: 4.3.7
+
+ - name: Set HAXEPATH
+ run: |
+ echo "HAXEPATH=$HAXE_STD_PATH/.." >> $GITHUB_ENV
+
+ - name: Install Haxe dependencies
+ run: |
+ haxelib install format --quiet
+ haxelib install lime --quiet
+ haxelib install openfl --quiet
+ haxelib install utest --quiet
+ haxelib dev swf ${{ github.workspace }}
+
+ - name: Run tests on Neko with ${{ matrix.define }}
+ working-directory: tests
+ run: |
+ haxelib run openfl test neko -D${{ matrix.define }}
+
+ unit-test-tools:
+ strategy:
+ matrix:
+ haxe-version: [4.2.5, 4.3.7]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: krdlab/setup-haxe@v2
+ with:
+ haxe-version: ${{ matrix.haxe-version }}
+
+ - name: Set HAXEPATH
+ run: |
+ echo "HAXEPATH=$HAXE_STD_PATH/.." >> $GITHUB_ENV
+
+ - name: Install Haxe dependencies
+ run: |
+ haxelib install format --quiet
+ haxelib install lime --quiet
+ haxelib install openfl --quiet
+ haxelib install hxp --quiet
+ haxelib install utest --quiet
+ haxelib dev swf ${{ github.workspace }}
+
+ - name: Check haxelib dependencies
+ run: |
+ haxelib path swf | grep -q -- "-D format="
+
+ - name: Rebuild swf run.n
+ run: |
+ haxe rebuild.hxml
+
+ - name: Run tools tests
+ working-directory: tests/tools
+ run: |
+ haxe tools.hxml
+ neko bin/tools.n
+
+ - name: Run tools tests with compact timelines
+ working-directory: tests/tools
+ run: |
+ haxe tools.hxml -D swf_compact_animate_timelines
+ neko bin/tools.n
+
+ unit-test-html5:
+ strategy:
+ matrix:
+ flags: ["", "-Dswf_compact_animate_timelines -Dswf_hardware_bitmap_cache"]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: krdlab/setup-haxe@v2
+ with:
+ haxe-version: 4.3.7
+
+ - name: Set HAXEPATH
+ run: |
+ echo "HAXEPATH=$HAXE_STD_PATH/.." >> $GITHUB_ENV
+
+ - name: Install Haxe dependencies
+ run: |
+ haxelib install format --quiet
+ haxelib install lime --quiet
+ haxelib install openfl --quiet
+ haxelib install utest --quiet
+ haxelib dev swf ${{ github.workspace }}
+
+ - name: Build tests for HTML5
+ working-directory: tests
+ run: |
+ haxelib run openfl build html5 ${{ matrix.flags }}
+
+ - name: Run tests in headless Chrome
+ working-directory: tests
+ timeout-minutes: 8
+ run: |
+ python3 -m http.server 8000 --bind 127.0.0.1 --directory bin/html5/bin &
+ sleep 2
+ # headless Chrome sometimes hangs, so each attempt has a time limit
+ for attempt in 1 2 3; do
+ if timeout 120 google-chrome --headless=new --no-sandbox --disable-gpu --virtual-time-budget=10000 --dump-dom http://127.0.0.1:8000/index.html > report.html; then
+ break
+ fi
+ done
+ sed -e 's/<[^>]*>/ /g' report.html
+ grep -q "TEST OK" report.html
+
unit-test-hashlink:
strategy:
matrix:
# HashLink is not supported by Haxe 3
- haxe-version: [4.0.5, 4.1.5, 4.2.5, 4.3.6]
+ haxe-version: [4.0.5, 4.1.5, 4.2.5, 4.3.7]
+ # Before Haxe 4.2, two HashLink threads reading their message queues for
+ # the first time can crash, so Lime loads assets without threads there
+ include:
+ - haxe-version: 4.0.5
+ lime-flags: -Dforce-synchronous
+ - haxe-version: 4.1.5
+ lime-flags: -Dforce-synchronous
# AL init fails on both windows and ubuntu
runs-on: macos-latest
steps:
@@ -155,12 +284,12 @@ jobs:
- name: Run tests on HashLink
working-directory: tests
run: |
- haxelib run openfl test hl
+ haxelib run openfl test hl ${{ matrix.lime-flags }}
unit-test-air:
strategy:
matrix:
- haxe-version: [3.4.7, 4.0.5, 4.1.5, 4.2.5, 4.3.6]
+ haxe-version: [3.4.7, 4.0.5, 4.1.5, 4.2.5, 4.3.7]
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
@@ -200,7 +329,7 @@ jobs:
- uses: krdlab/setup-haxe@v2
with:
- haxe-version: "4.3.6"
+ haxe-version: "4.3.7"
- uses: joshtynjala/setup-adobe-air-action@v2
with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5c954b8e8..7312bb36b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,42 @@
+3.5.0 (10/27/2026)
+------------------
+
+* Added `DisplayObject.metaData` support for Animate libraries
+* Added `lineGradientStyle` support for SWF shapes in Animate, SWFLite and runtime shapes
+* Added shape tween (`DefineMorphShape` and `DefineMorphShape2`) support for Animate libraries
+* Added caching of generated classes for Flash and AIR SWF libraries
+* Added export of linked MP3 and uncompressed sounds in Animate libraries (MP3 on native targets requires Lime 8.4 or newer)
+* Added the `swf_compact_animate_timelines` define, to keep Animate timelines in compact buffers instead of one object per frame and event
+* Added the `swf_hardware_bitmap_cache` define, to share Animate bitmaps and release their CPU images once uploaded
+* Improved `generate="true"` on Animate libraries to compile the generated classes, so symbols are created as their linkage class
+* Improved Animate shape memory usage by keeping commands compact until a shape is first drawn
+* Improved SWF exporter performance for shapes with many drawing commands
+* Improved timeline performance by caching instance field lists per class
+* Improved dynamic text performance by caching the embedded font found for each symbol
+* Changed `AnimateShapeSymbol.commands` to be `null` for shapes loaded from an Animate library
+* Changed `ShapeCommand` and `AnimateShapeCommand` to include `LineGradientStyle`, so exhaustive `switch` statements over them need another case
+* Changed haxelib.json to declare the `format` dependency
+* Fixed Animate and SWFLite exports of SWF files without ABC or `SymbolClass` tags, such as AS2 SWF files using `ExportAssets`
+* Fixed `SWFLibrary` failing to load non-embedded SWF files
+* Fixed processing SWF files with linked Speex sounds
+* Fixed generated Animate classes using another library's symbol when several libraries export the same class
+* Fixed generated Animate classes not being used, or extending the wrong base class, when the library has a `prefix`
+* Fixed the document class not being applied to the root timeline when it is not named `*_fla.MainTimeline`
+* Fixed clipping characters being displayed when nothing is in their mask range
+* Fixed reused timeline instances keeping an outdated `clipDepth`
+* Fixed Animate `blendMode` being set to the string `"null"`
+* Fixed `clippingLayer` being set on the Flash target
+* Fixed SWFLite timelines failing to compile on the Flash target
+* Fixed `DefineFont2` and `DefineFont3` parsing reading past the tag for fonts without layout
+* Fixed `SWFRoot.publish()` sizing bit fields from the decimal digits of each value, which wrote broken matrices, rectangles, color transforms and shapes
+* Fixed morph shapes moving to the origin at style changes, leaving paths open, and losing focal gradients, gradient modes and the width of filled lines
+* Fixed generated Flash/AIR classes being written to the wrong package directory
+* Fixed C++ compile error in `AnimateLibraryExporter` class generation
+* Fixed Animate libraries created without a UUID being registered under a `null` key
+* Fixed processed Animate ZIP files not being closed after writing
+* Fixed the command-line tools showing the version of Lime instead of swf
+
+
3.4.0 (11/11/2025)
------------------
diff --git a/README.md b/README.md
index 03e0e6a2e..fe5df290b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[](LICENSE.md) [](http://lib.haxe.org/p/swf)
+[](LICENSE.md) [](https://lib.haxe.org/p/swf)
SWF
===
@@ -23,6 +23,37 @@ 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. Filters are
+rare, so they are still kept parsed.
+
+Hardware-only Animate bitmap cache
+==================================
+
+Native hardware-rendered projects may define `swf_hardware_bitmap_cache` to
+share each Animate bitmap asset between bitmap instances. After its texture is
+uploaded, the decoded CPU image is released. OpenFL draws shape paths in
+software, which reads the pixels of bitmap fills, so bitmaps used by shapes
+keep one shared readable `BitmapData` instead.
+
+This mode is opt-in because cached bitmap assets used by direct `Bitmap`
+instances become non-readable. Pixel read/write APIs and software-renderer
+fallback are not supported for those bitmaps, and their textures cannot be
+restored after a graphics context is lost.
+
+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/haxelib.json b/haxelib.json
index 89b74306e..822a502f5 100644
--- a/haxelib.json
+++ b/haxelib.json
@@ -1,15 +1,18 @@
{
"name": "swf",
- "url": "http://github.com/openfl/swf",
+ "url": "https://github.com/openfl/swf",
"license": "MIT",
"tags": [ "openfl", "swf", "animation", "flash", "assets", "swc" ],
"description": "Provides support for parsing and displaying SWF content",
- "version": "3.4.0",
- "releasenote": "Fix duplicate scale9Grid graphics, improved clipping layers, new filters",
+ "version": "3.5.0",
+ "releasenote": "Shape tweens, linked sounds, line gradients, generated class fixes, opt-in compact timelines and bitmap cache, many timeline and exporter fixes",
"contributors": [
"singmajesty",
"bowlerhat",
"Dimensionscape"
],
- "classPath": "src"
+ "classPath": "src",
+ "dependencies": {
+ "format": ""
+ }
}
diff --git a/scripts/Tools.hx b/scripts/Tools.hx
index 2ab0dc76d..ea686b9f9 100644
--- a/scripts/Tools.hx
+++ b/scripts/Tools.hx
@@ -337,7 +337,7 @@ class Tools
// }
- var templateFile = new Asset("", Path.combine(targetPath, Path.directory(className.split(".").join("/"))) + "/" + prefix + name + ".hx",
+ var templateFile = new Asset("", Path.combine(Path.combine(targetPath, packageName.split(".").join("/")), name + ".hx"),
AssetType.TEMPLATE);
templateFile.data = template.execute(context);
output.assets.push(templateFile);
@@ -737,8 +737,8 @@ class Tools
// }
// else
{
- var version = Haxelib.getVersion();
- if (version == null) version = "0.0.0";
+ // Haxelib.getVersion() without an argument is the version of Lime
+ var version = getVersion(Path.directory(Sys.programPath()));
Log.println("\x1b[36;1mSWF Command-Line Tools (" + version + ")\x1b[0m");
}
@@ -749,6 +749,46 @@ class Tools
}
}
+ private static function getGeneratedPath(project:HXProject):String
+ {
+ if (project.target == IOS)
+ {
+ return Path.tryFullPath(targetDirectory) + "/" + project.app.file + "/" + "/haxe/_generated";
+ }
+ else
+ {
+ return Path.tryFullPath(targetDirectory) + "/haxe/_generated";
+ }
+ }
+
+ private static function getVersion(directory:String):String
+ {
+ var path = Path.combine(directory, "haxelib.json");
+
+ if (FileSystem.exists(path))
+ {
+ try
+ {
+ var version:String = Json.parse(File.getContent(path)).version;
+ if (version != null) return version;
+ }
+ catch (e:Dynamic) {}
+ }
+
+ return "0.0.0";
+ }
+
+ private static function isCacheCurrent(cacheFile:String, sourcePath:String):Bool
+ {
+ if (!FileSystem.exists(cacheFile)) return false;
+
+ var cacheDate = FileSystem.stat(cacheFile).mtime;
+ var toolDate = FileSystem.stat(Haxelib.getPath(new Haxelib("swf"), true) + "/run.n").mtime;
+ var sourceDate = FileSystem.stat(sourcePath).mtime;
+
+ return sourceDate.getTime() < cacheDate.getTime() && toolDate.getTime() < cacheDate.getTime();
+ }
+
private static function processFile(sourcePath:String, targetPath:String, prefix:String = null, generate:Bool = false):Bool
{
if (targetPath == null)
@@ -992,7 +1032,30 @@ class Tools
if (library.generate != false)
{
- var generatedClasses = generateSWFClasses(project, output, swf, library.prefix);
+ var generatedClasses:Array = null;
+ var cacheFile = null;
+
+ if (targetDirectory != null)
+ {
+ cacheFile = targetDirectory + "/obj/libraries/" + library.name + ".classes.json";
+ generatedClasses = readGeneratedClassesCache(cacheFile, library.sourcePath, library.prefix, output);
+ }
+
+ if (generatedClasses == null)
+ {
+ if (!Log.verbose)
+ {
+ Log.info("\x1b[1mProcessing library:\x1b[0m " + library.sourcePath + " [SWF]");
+ }
+
+ var firstTemplate = output.assets.length;
+ generatedClasses = generateSWFClasses(project, output, swf, library.prefix);
+
+ if (cacheFile != null)
+ {
+ writeGeneratedClassesCache(cacheFile, library.prefix, generatedClasses, output.assets.slice(firstTemplate));
+ }
+ }
for (className in generatedClasses)
{
@@ -1024,15 +1087,18 @@ class Tools
cacheDirectory = targetDirectory + "/obj/libraries";
cacheFile = cacheDirectory + "/" + library.name + ".zip";
- if (FileSystem.exists(cacheFile))
+ var classesCacheFile = cacheDirectory + "/" + library.name + ".classes.json";
+ var includedClasses:Array = null;
+
+ cacheAvailable = isCacheCurrent(cacheFile, library.sourcePath);
+
+ if (cacheAvailable && library.generate != false)
{
- var cacheDate = FileSystem.stat(cacheFile).mtime;
- var toolDate = FileSystem.stat(Haxelib.getPath(new Haxelib("swf"), true) + "/run.n").mtime;
- var sourceDate = FileSystem.stat(library.sourcePath).mtime;
+ includedClasses = readGeneratedClassesCache(classesCacheFile, library.sourcePath, library.prefix, output);
- if (sourceDate.getTime() < cacheDate.getTime() && toolDate.getTime() < cacheDate.getTime())
+ if (includedClasses == null)
{
- cacheAvailable = true;
+ cacheAvailable = false;
}
}
@@ -1055,38 +1121,21 @@ class Tools
if (library.generate != false)
{
- var targetPath:String;
-
- if (project.target == IOS)
- {
- targetPath = Path.tryFullPath(targetDirectory) + "/" + project.app.file + "/" + "/haxe/_generated";
- }
- else
- {
- targetPath = Path.tryFullPath(targetDirectory) + "/haxe/_generated";
- }
-
- var generatedClasses = exporter.generateClasses(targetPath, output.assets, library.prefix);
-
- // for (className in generatedClasses)
- // {
- // output.haxeflags.push(className);
- // }
-
- // if (cacheDirectory != null)
- // {
- // File.saveContent(cacheDirectory + "/classNames.txt", generatedClasses.join("\n"));
- // }
+ var firstTemplate = output.assets.length;
+ includedClasses = [];
+ exporter.generateClasses(getGeneratedPath(project), output.assets, library.prefix, includedClasses);
+ writeGeneratedClassesCache(classesCacheFile, library.prefix, includedClasses, output.assets.slice(firstTemplate));
}
}
- else
- {
- // var generatedClasses = File.getContent(cacheDirectory + "/classNames.txt").split("\n");
- // for (className in generatedClasses)
- // {
- // output.haxeflags.push(className);
- // }
+ // generate="true" also compiles the generated classes, so that symbols are
+ // created as their linkage class even when nothing else references it
+ if (library.generate == true)
+ {
+ for (className in includedClasses)
+ {
+ output.haxeflags.push(className);
+ }
}
var asset = new Asset(cacheFile, "lib/" + library.name + ".zip", AssetType.BUNDLE);
@@ -1480,6 +1529,37 @@ class Tools
return null;
}
+ private static function readGeneratedClassesCache(cacheFile:String, sourcePath:String, prefix:String, output:HXProject):Array
+ {
+ if (!isCacheCurrent(cacheFile, sourcePath)) return null;
+
+ var cache:Dynamic = null;
+
+ try
+ {
+ cache = Json.parse(File.getContent(cacheFile));
+ }
+ catch (e:Dynamic)
+ {
+ return null;
+ }
+
+ if (cache == null || cache.prefix != (prefix != null ? prefix : "") || cache.classes == null || cache.templates == null) return null;
+
+ // the generated class templates are added again, otherwise Lime deletes them as stale files
+ var templates:Array = cache.templates;
+
+ for (template in templates)
+ {
+ var asset = new Asset("", template.targetPath, AssetType.TEMPLATE);
+ asset.data = template.data;
+ asset.embed = template.embed;
+ output.assets.push(asset);
+ }
+
+ return cache.classes;
+ }
+
private static function readSWC(bytes:ByteArray):ByteArray
{
var input = new BytesInput(bytes);
@@ -1519,4 +1599,17 @@ class Tools
return result;
}
+
+ private static function writeGeneratedClassesCache(cacheFile:String, prefix:String, classes:Array, templates:Array):Void
+ {
+ var templateData = [];
+
+ for (asset in templates)
+ {
+ templateData.push({targetPath: asset.targetPath, data: asset.data, embed: asset.embed});
+ }
+
+ System.mkdir(Path.directory(cacheFile));
+ File.saveContent(cacheFile, Json.stringify({prefix: prefix != null ? prefix : "", classes: classes, templates: templateData}));
+ }
}
diff --git a/src/swf/data/SWFMorphFillStyle.hx b/src/swf/data/SWFMorphFillStyle.hx
index 56412e867..b94a16a36 100644
--- a/src/swf/data/SWFMorphFillStyle.hx
+++ b/src/swf/data/SWFMorphFillStyle.hx
@@ -82,7 +82,7 @@ class SWFMorphFillStyle
{
case 0x00:
fillStyle.rgb = ColorUtils.interpolate(startColor, endColor, ratio);
- case 0x10, 0x12:
+ case 0x10, 0x12, 0x13:
fillStyle.gradientMatrix = MatrixUtils.interpolate(startGradientMatrix, endGradientMatrix, ratio);
fillStyle.gradient = gradient.getMorphedGradient(ratio);
case 0x40, 0x41, 0x42, 0x43:
diff --git a/src/swf/data/SWFMorphFocalGradient.hx b/src/swf/data/SWFMorphFocalGradient.hx
index 2bc319c02..fcbe7be50 100644
--- a/src/swf/data/SWFMorphFocalGradient.hx
+++ b/src/swf/data/SWFMorphFocalGradient.hx
@@ -25,12 +25,8 @@ class SWFMorphFocalGradient extends SWFMorphGradient
override public function getMorphedGradient(ratio:Float = 0):SWFGradient
{
- var gradient:SWFGradient = new SWFGradient();
- // TODO: focalPoint
- for (i in 0...records.length)
- {
- gradient.records.push(records[i].getMorphedGradientRecord(ratio));
- }
+ var gradient:SWFGradient = super.getMorphedGradient(ratio);
+ gradient.focalPoint = startFocalPoint + (endFocalPoint - startFocalPoint) * ratio;
return gradient;
}
diff --git a/src/swf/data/SWFMorphGradient.hx b/src/swf/data/SWFMorphGradient.hx
index c4afc693d..ed9a6d62a 100644
--- a/src/swf/data/SWFMorphGradient.hx
+++ b/src/swf/data/SWFMorphGradient.hx
@@ -48,6 +48,9 @@ class SWFMorphGradient
public function getMorphedGradient(ratio:Float = 0):SWFGradient
{
var gradient:SWFGradient = new SWFGradient();
+ gradient.spreadMode = spreadMode;
+ gradient.interpolationMode = interpolationMode;
+ gradient.focalPoint = 0;
for (i in 0...records.length)
{
gradient.records.push(records[i].getMorphedGradientRecord(ratio));
diff --git a/src/swf/data/SWFMorphLineStyle.hx b/src/swf/data/SWFMorphLineStyle.hx
index d4a361356..0455678a8 100644
--- a/src/swf/data/SWFMorphLineStyle.hx
+++ b/src/swf/data/SWFMorphLineStyle.hx
@@ -28,6 +28,11 @@ class SWFMorphLineStyle
startCapsStyle = LineCapsStyle.ROUND;
endCapsStyle = LineCapsStyle.ROUND;
jointStyle = LineJointStyle.ROUND;
+ hasFillFlag = false;
+ noHScaleFlag = false;
+ noVScaleFlag = false;
+ pixelHintingFlag = false;
+ noClose = false;
miterLimitFactor = 3;
if (data != null)
{
@@ -54,6 +59,7 @@ class SWFMorphLineStyle
public function getMorphedLineStyle(ratio:Float = 0):SWFLineStyle
{
var lineStyle:SWFLineStyle = new SWFLineStyle();
+ lineStyle.width = Math.round(startWidth + (endWidth - startWidth) * ratio);
if (hasFillFlag)
{
lineStyle.fillType = fillType.getMorphedFillStyle(ratio);
@@ -61,7 +67,6 @@ class SWFMorphLineStyle
else
{
lineStyle.color = ColorUtils.interpolate(startColor, endColor, ratio);
- lineStyle.width = Std.int(startWidth + (endWidth - startWidth) * ratio);
}
lineStyle.startCapsStyle = startCapsStyle;
lineStyle.endCapsStyle = endCapsStyle;
diff --git a/src/swf/data/SWFShape.hx b/src/swf/data/SWFShape.hx
index fc93214a3..8be5c9e63 100644
--- a/src/swf/data/SWFShape.hx
+++ b/src/swf/data/SWFShape.hx
@@ -18,6 +18,7 @@ import openfl.display.LineScaleMode;
import openfl.geom.Matrix;
import openfl.geom.Point;
import openfl.errors.Error;
+import haxe.ds.ObjectMap;
class SWFShape
{
@@ -655,37 +656,56 @@ class SWFShape
var subPath:Array = edgeMap.get(styleIdx);
if (subPath != null && subPath.length > 0)
{
- var idx:Int;
- var prevEdge:IEdge = null;
+ var n = subPath.length;
+ var nextFree = new Array();
+ var prevFree = new Array();
+ var edgeIndexMap = new ObjectMap();
+
+ for (i in 0...n)
+ {
+ nextFree.push(i + 1);
+ prevFree.push(i - 1);
+ edgeIndexMap.set(subPath[i], i);
+ }
+
var tmpPath:Array = new Array();
createCoordMap(subPath);
- while (subPath.length > 0)
+
+ var head = 0;
+ var idx = 0;
+ var remaining = n;
+ var prevEdge:IEdge = null;
+
+ while (remaining > 0)
{
- idx = 0;
- while (idx < subPath.length)
+ var cand = subPath[idx];
+ if (prevEdge == null || prevEdge.to.equals(cand.from))
{
- if (prevEdge == null || prevEdge.to.equals(subPath[idx].from))
+ remaining--;
+ tmpPath.push(cand);
+ removeEdgeFromCoordMap(cand);
+ prevEdge = cand;
+
+ var next = nextFree[idx];
+ var prev = prevFree[idx];
+
+ if (prev >= 0) nextFree[prev] = next;
+ else head = next;
+
+ if (next < n) prevFree[next] = prev;
+ idx = next < n ? next : head;
+ }
+ else
+ {
+ var edge = findNextEdgeInCoordMap(prevEdge);
+ if (edge != null)
{
- var edge:IEdge = subPath.splice(idx, 1)[0];
- tmpPath.push(edge);
- removeEdgeFromCoordMap(edge);
- prevEdge = edge;
+ idx = edgeIndexMap.get(edge);
}
else
{
- var edge = findNextEdgeInCoordMap(prevEdge);
- if (edge != null)
- {
- for (i in 0...subPath.length)
- {
- if (subPath[i] == edge) idx = i;
- }
- }
- else
- {
- idx = 0;
- prevEdge = null;
- }
+ idx = head;
+ prevEdge = null;
}
}
}
diff --git a/src/swf/exporters/AnimateLibraryExporter.hx b/src/swf/exporters/AnimateLibraryExporter.hx
index 1a5e2ee99..89af2ae95 100644
--- a/src/swf/exporters/AnimateLibraryExporter.hx
+++ b/src/swf/exporters/AnimateLibraryExporter.hx
@@ -4,6 +4,9 @@ import format.png.Data;
import format.png.Writer as PNGWriter;
import swf.data.consts.BitmapFormat;
import swf.data.consts.BlendMode;
+import swf.data.consts.SoundCompression;
+import swf.data.consts.SoundRate;
+import swf.data.consts.SoundSize;
import swf.data.filters.IFilter;
import swf.data.SWFButtonRecord;
import swf.data.SWFSymbol;
@@ -21,6 +24,7 @@ import swf.tags.TagDefineEditText;
import swf.tags.TagDefineFont;
import swf.tags.TagDefineFont2;
import swf.tags.TagDefineFont4;
+import swf.tags.TagDefineMorphShape;
import swf.tags.TagDefineScalingGrid;
import swf.tags.TagDefineShape;
import swf.tags.TagDefineSound;
@@ -44,6 +48,7 @@ import swf.exporters.core.FilterType;
import swf.exporters.core.ShapeCommand;
import openfl.display.BitmapData;
import openfl.display.PNGEncoderOptions;
+import openfl.errors.Error;
import openfl.geom.ColorTransform;
import openfl.geom.Matrix;
import openfl.geom.Rectangle;
@@ -51,6 +56,7 @@ import openfl.text.TextFormatAlign;
import openfl.utils.AssetManifest;
import openfl.utils.AssetType;
import openfl.utils.ByteArray;
+import openfl.utils.Endian;
import haxe.crypto.Crc32;
import haxe.io.Bytes;
import haxe.io.BytesOutput;
@@ -587,84 +593,136 @@ class AnimateLibraryExporter
symbol.type = SWFSymbolType.SHAPE;
symbol.id = tag.characterId;
- var commands:Array = [];
+ symbol.commands = serializeShapeCommands(handler.commands);
+ libraryData.symbols.set(symbol.id, symbol);
+ return symbol;
+ }
+ }
- for (command in handler.commands)
- {
- switch (command)
- {
- case LineStyle(thickness, color, alpha, pixelHinting, scaleMode, startCaps, joints, miterLimit):
- if (thickness == null && color == null && alpha == null)
- {
- commands.push(SWFShapeCommandType.CLEAR_LINE_STYLE);
- }
- else
- {
- commands = commands.concat([
- SWFShapeCommandType.LINE_STYLE,
- thickness,
- color,
- alpha,
- pixelHinting,
- scaleMode,
- startCaps,
- joints,
- miterLimit
- ]);
- }
+ private function addMorphShape(tag:TagDefineMorphShape):Dynamic
+ {
+ var symbol:Dynamic = {};
+ symbol.type = SWFSymbolType.MORPH_SHAPE;
+ symbol.id = tag.characterId;
+ symbol.ratios = [];
+ symbol.shapes = [];
- case BeginFill(color, alpha):
- commands = commands.concat([SWFShapeCommandType.BEGIN_FILL, color, alpha]);
-
- case BeginGradientFill(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
- commands = commands.concat([
- SWFShapeCommandType.BEGIN_GRADIENT_FILL,
- type,
- colors,
- alphas,
- ratios,
- serializeMatrix(matrix),
- spreadMethod,
- interpolationMethod,
- focalPointRatio
- ]);
+ // The start of the shape is always drawn, and timelines add the other ratios they use
+ try
+ {
+ addMorphShapeRatio(symbol, 0);
+ }
+ catch (e:Error)
+ {
+ Log.warn("Skipping morph shape " + tag.characterId + ", its start and end edges do not match");
+ return null;
+ }
- case BeginBitmapFill(bitmapID, matrix, repeat, smooth):
- commands = commands.concat([
- SWFShapeCommandType.BEGIN_BITMAP_FILL,
- bitmapID,
- serializeMatrix(matrix),
- repeat,
- smooth
- ]);
- processTag(cast swfData.getCharacter(bitmapID));
+ libraryData.symbols.set(symbol.id, symbol);
+ return symbol;
+ }
- case EndFill:
- commands.push(SWFShapeCommandType.END_FILL);
+ private function addMorphShapeRatio(symbol:Dynamic, ratio:Int):Void
+ {
+ var ratios:Array = symbol.ratios;
+ if (ratios.indexOf(ratio) > -1) return;
- case MoveTo(x, y):
- commands = commands.concat([SWFShapeCommandType.MOVE_TO, twip(x), twip(y)]);
+ var tag:TagDefineMorphShape = cast swfData.getCharacter(symbol.id);
+ var handler = new ShapeCommandExporter(swfData);
+ tag.exportHandler = handler;
+ tag.export(ratio / 65535);
- case LineTo(x, y):
- commands = commands.concat([SWFShapeCommandType.LINE_TO, twip(x), twip(y)]);
+ var shapes:Array = symbol.shapes;
+ ratios.push(ratio);
+ shapes.push(serializeShapeCommands(handler.commands));
+ }
- case CurveTo(controlX, controlY, anchorX, anchorY):
- commands = commands.concat([
- SWFShapeCommandType.CURVE_TO,
- twip(controlX),
- twip(controlY),
- twip(anchorX),
- twip(anchorY)
- ]);
+ private function serializeShapeCommands(shapeCommands:Array):Array
+ {
+ var commands:Array = [];
- default:
- }
- }
+ for (command in shapeCommands)
+ {
+ switch (command)
+ {
+ case LineStyle(thickness, color, alpha, pixelHinting, scaleMode, startCaps, joints, miterLimit):
+ if (thickness == null && color == null && alpha == null)
+ {
+ commands.push(SWFShapeCommandType.CLEAR_LINE_STYLE);
+ }
+ else
+ {
+ commands.push(SWFShapeCommandType.LINE_STYLE);
+ commands.push(thickness);
+ commands.push(color);
+ commands.push(alpha);
+ commands.push(pixelHinting);
+ commands.push(scaleMode);
+ commands.push(startCaps);
+ commands.push(joints);
+ commands.push(miterLimit);
+ }
- symbol.commands = commands;
- libraryData.symbols.set(symbol.id, symbol);
- return symbol;
+ case LineGradientStyle(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ commands.push(SWFShapeCommandType.LINE_GRADIENT_STYLE);
+ commands.push(type);
+ commands.push(colors);
+ commands.push(alphas);
+ commands.push(ratios);
+ commands.push(serializeMatrix(matrix));
+ commands.push(spreadMethod);
+ commands.push(interpolationMethod);
+ commands.push(focalPointRatio);
+
+ case BeginFill(color, alpha):
+ commands.push(SWFShapeCommandType.BEGIN_FILL);
+ commands.push(color);
+ commands.push(alpha);
+
+ case BeginGradientFill(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ commands.push(SWFShapeCommandType.BEGIN_GRADIENT_FILL);
+ commands.push(type);
+ commands.push(colors);
+ commands.push(alphas);
+ commands.push(ratios);
+ commands.push(serializeMatrix(matrix));
+ commands.push(spreadMethod);
+ commands.push(interpolationMethod);
+ commands.push(focalPointRatio);
+
+ case BeginBitmapFill(bitmapID, matrix, repeat, smooth):
+ commands.push(SWFShapeCommandType.BEGIN_BITMAP_FILL);
+ commands.push(bitmapID);
+ commands.push(serializeMatrix(matrix));
+ commands.push(repeat);
+ commands.push(smooth);
+ processTag(cast swfData.getCharacter(bitmapID));
+
+ case EndFill:
+ commands.push(SWFShapeCommandType.END_FILL);
+
+ case MoveTo(x, y):
+ commands.push(SWFShapeCommandType.MOVE_TO);
+ commands.push(twip(x));
+ commands.push(twip(y));
+
+ case LineTo(x, y):
+ commands.push(SWFShapeCommandType.LINE_TO);
+ commands.push(twip(x));
+ commands.push(twip(y));
+
+ case CurveTo(controlX, controlY, anchorX, anchorY):
+ commands.push(SWFShapeCommandType.CURVE_TO);
+ commands.push(twip(controlX));
+ commands.push(twip(controlY));
+ commands.push(twip(anchorX));
+ commands.push(twip(anchorY));
+
+ default:
+ }
}
+
+ return commands;
}
private function addSprite(tag:SWFTimelineContainer, root:Bool = false):Dynamic
@@ -734,6 +792,20 @@ class AnimateLibraryExporter
frameObject.type = SWFFrameObjectType.UPDATE;
}
+ var characterSymbol:Dynamic = libraryData.symbols.get(object.characterId);
+ if (characterSymbol != null && characterSymbol.type == SWFSymbolType.MORPH_SHAPE)
+ {
+ if (placeTag.hasRatio)
+ {
+ frameObject.ratio = placeTag.ratio;
+ addMorphShapeRatio(characterSymbol, placeTag.ratio);
+ }
+ else if (frameObject.type == SWFFrameObjectType.CREATE)
+ {
+ frameObject.ratio = 0;
+ }
+ }
+
if (placeTag.matrix != null)
{
var matrix = placeTag.matrix.matrix.clone();
@@ -979,62 +1051,71 @@ class AnimateLibraryExporter
}
else
{
- commands = commands.concat([
- SWFShapeCommandType.LINE_STYLE,
- thickness,
- color,
- alpha,
- pixelHinting,
- scaleMode,
- startCaps,
- joints,
- miterLimit
- ]);
+ commands.push(SWFShapeCommandType.LINE_STYLE);
+ commands.push(thickness);
+ commands.push(color);
+ commands.push(alpha);
+ commands.push(pixelHinting);
+ commands.push(scaleMode);
+ commands.push(startCaps);
+ commands.push(joints);
+ commands.push(miterLimit);
}
+ case LineGradientStyle(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ commands.push(SWFShapeCommandType.LINE_GRADIENT_STYLE);
+ commands.push(type);
+ commands.push(colors);
+ commands.push(alphas);
+ commands.push(ratios);
+ commands.push(serializeMatrix(matrix));
+ commands.push(spreadMethod);
+ commands.push(interpolationMethod);
+ commands.push(focalPointRatio);
+
case BeginFill(color, alpha):
- commands = commands.concat([SWFShapeCommandType.BEGIN_FILL, color, alpha]);
+ commands.push(SWFShapeCommandType.BEGIN_FILL);
+ commands.push(color);
+ commands.push(alpha);
case BeginGradientFill(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
- commands = commands.concat([
- SWFShapeCommandType.BEGIN_GRADIENT_FILL,
- type,
- colors,
- alphas,
- ratios,
- serializeMatrix(matrix),
- spreadMethod,
- interpolationMethod,
- focalPointRatio
- ]);
+ commands.push(SWFShapeCommandType.BEGIN_GRADIENT_FILL);
+ commands.push(type);
+ commands.push(colors);
+ commands.push(alphas);
+ commands.push(ratios);
+ commands.push(serializeMatrix(matrix));
+ commands.push(spreadMethod);
+ commands.push(interpolationMethod);
+ commands.push(focalPointRatio);
case BeginBitmapFill(bitmapID, matrix, repeat, smooth):
- commands = commands.concat([
- SWFShapeCommandType.BEGIN_BITMAP_FILL,
- bitmapID,
- serializeMatrix(matrix),
- repeat,
- smooth
- ]);
+ commands.push(SWFShapeCommandType.BEGIN_BITMAP_FILL);
+ commands.push(bitmapID);
+ commands.push(serializeMatrix(matrix));
+ commands.push(repeat);
+ commands.push(smooth);
processTag(cast swfData.getCharacter(bitmapID));
case EndFill:
commands.push(SWFShapeCommandType.END_FILL);
case MoveTo(x, y):
- commands = commands.concat([SWFShapeCommandType.MOVE_TO, twip(x), twip(y)]);
+ commands.push(SWFShapeCommandType.MOVE_TO);
+ commands.push(twip(x));
+ commands.push(twip(y));
case LineTo(x, y):
- commands = commands.concat([SWFShapeCommandType.LINE_TO, twip(x), twip(y)]);
+ commands.push(SWFShapeCommandType.LINE_TO);
+ commands.push(twip(x));
+ commands.push(twip(y));
case CurveTo(controlX, controlY, anchorX, anchorY):
- commands = commands.concat([
- SWFShapeCommandType.CURVE_TO,
- twip(controlX),
- twip(controlY),
- twip(anchorX),
- twip(anchorY)
- ]);
+ commands.push(SWFShapeCommandType.CURVE_TO);
+ commands.push(twip(controlX));
+ commands.push(twip(controlY));
+ commands.push(twip(anchorX));
+ commands.push(twip(anchorY));
default:
}
@@ -1063,13 +1144,12 @@ class AnimateLibraryExporter
return symbol;
}
- private function addSound(tag:IDefinitionTag):Void
+ private function addSound(tag:IDefinitionTag):Dynamic
{
if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (tag, TagDefineSound))
{
var defineSound:TagDefineSound = cast tag;
- var byteArray = defineSound.soundData;
var type:SoundType = switch (defineSound.soundFormat)
{
case 0: SoundType.UNCOMPRESSED_NATIVE_ENDIAN;
@@ -1079,61 +1159,106 @@ class AnimateLibraryExporter
case 4: SoundType.NELLYMOSER_16_KHZ;
case 5: SoundType.NELLYMOSER_8_KHZ;
case 6: SoundType.NELLYMOSER;
- case 7: SoundType.SPEEX;
+ case 11: SoundType.SPEEX;
case _: throw("invalid sound type!");
}
- // TODO
-
- // var entry:Entry = {
- // fileName: symbol.path,
- // fileSize: byteArray.length,
- // fileTime: Date.now(),
- // compressed: false,
- // dataSize: 0,
- // data: byteArray,
- // crc32: Crc32.make(byteArray)
- // };
- // outputList.add(entry);
-
- // createdDirectory = false;
- // for (id in exporter.sounds.keys())
- // {
- // if (!createdDirectory)
- // {
- // System.mkdir(Path.combine(targetPath, "sounds"));
- // createdDirectory = true;
- // }
-
- // var symbolClassName = exporter.soundSymbolClassNames.get(id);
- // var typeId = exporter.soundTypes.get(id);
-
- // Log.info("", " - \x1b[1mExporting sound:\x1b[0m [id=" + id + ", type=" + typeId + ", symbolClassName=" + symbolClassName + "]");
-
- // var type;
- // switch (typeId)
- // {
- // case SoundType.MP3:
- // type = "mp3";
- // case SoundType.ADPCM:
- // type = "adpcm";
- // case _:
- // throw "unsupported sound type " + id + ", type " + typeId + ", symbol class name " + symbolClassName;
- // };
- // var path = "sounds/" + symbolClassName + "." + type;
- // var assetData = exporter.sounds.get(id);
-
- // File.saveBytes(Path.combine(targetPath, path), assetData);
-
- // // NOTICE: everything must be .mp3 in its final form, even though we write out various formats to disk
- // var soundAsset = new Asset("", "sounds/" + symbolClassName + ".mp3", AssetType.SOUND);
- // project.assets.push(soundAsset);
+ var byteArray:ByteArray = null;
+ var extension:String = null;
+
+ switch (type)
+ {
+ case MP3:
+ // skip SeekSamples, the rest is MP3 frames
+ byteArray = new ByteArray();
+ if (defineSound.soundData.length > 2)
+ {
+ byteArray.writeBytes(defineSound.soundData, 2, defineSound.soundData.length - 2);
+ }
+ extension = "mp3";
+
+ case UNCOMPRESSED_NATIVE_ENDIAN, UNCOMPRESSED_LITTLE_ENDIAN:
+ byteArray = createWAV(defineSound);
+ extension = "wav";
+
+ default:
+ Log.warn("Skipping sound " + tag.characterId + ", " + SoundCompression.toString(defineSound.soundFormat)
+ + " sounds cannot be exported");
+ return null;
+ }
+
+ var symbol:Dynamic = {};
+ symbol.type = SWFSymbolType.SOUND;
+ symbol.id = tag.characterId;
+ symbol.path = "sounds/" + symbol.id + "." + extension;
+
+ var entry:Entry = {
+ fileName: symbol.path,
+ fileSize: byteArray.length,
+ fileTime: Date.now(),
+ compressed: false,
+ dataSize: 0,
+ data: byteArray,
+ crc32: Crc32.make(byteArray)
+ };
+ outputList.add(entry);
+
+ manifestData.assets.push({
+ path: symbol.path,
+ type: AssetType.SOUND
+ });
+
+ libraryData.symbols.set(symbol.id, symbol);
+
+ return symbol;
}
- return;
+ return null;
+ }
+
+ private function createWAV(defineSound:TagDefineSound):ByteArray
+ {
+ var pcm = defineSound.soundData;
+ var channels = defineSound.soundType == swf.data.consts.SoundType.STEREO ? 2 : 1;
+ var bitsPerSample = defineSound.soundSize == SoundSize.BIT_16 ? 16 : 8;
+ var sampleRate = switch (defineSound.soundRate)
+ {
+ case SoundRate.KHZ_5: 5512;
+ case SoundRate.KHZ_11: 11025;
+ case SoundRate.KHZ_22: 22050;
+ default: 44100;
+ }
+ var blockAlign = channels * Std.int(bitsPerSample / 8);
+ var padding = pcm.length % 2;
+
+ var wav = new ByteArray();
+ wav.endian = Endian.LITTLE_ENDIAN;
+ wav.writeUTFBytes("RIFF");
+ wav.writeInt(36 + pcm.length + padding);
+ wav.writeUTFBytes("WAVE");
+ wav.writeUTFBytes("fmt ");
+ wav.writeInt(16);
+ wav.writeShort(1); // PCM
+ wav.writeShort(channels);
+ wav.writeInt(sampleRate);
+ wav.writeInt(sampleRate * blockAlign);
+ wav.writeShort(blockAlign);
+ wav.writeShort(bitsPerSample);
+ wav.writeUTFBytes("data");
+ wav.writeInt(pcm.length);
+ wav.writeBytes(pcm, 0, pcm.length);
+ if (padding > 0) wav.writeByte(0);
+ return wav;
}
- public function generateClasses(targetPath:String, output:Array, prefix:String = ""):Array
+ /**
+ Generates linkage classes as template assets, and returns their class names.
+
+ If `includedClasses` is provided, it is filled with the generated classes that are
+ safe to compile without being referenced: classes that only depend on OpenFL and
+ other included classes, and not on ActionScript classes outside of this library.
+ **/
+ public function generateClasses(targetPath:String, output:Array, prefix:String = "", includedClasses:Array = null):Array
{
#if commonjs
var bitmapDataTemplate = File.getContent(Path.combine(js.Node.__dirname, "../templates/animate/BitmapData.mtt"));
@@ -1146,6 +1271,16 @@ class AnimateLibraryExporter
#end
var generatedClasses = [];
+ var classDependencies = new Map>();
+
+ var libraryClassNames = new Map();
+ for (symbol in libraryData.symbols)
+ {
+ if (Reflect.hasField(symbol, "className") && symbol.className != null)
+ {
+ libraryClassNames.set(symbol.className, true);
+ }
+ }
for (symbolID in libraryData.symbols.keys())
{
@@ -1165,6 +1300,11 @@ class AnimateLibraryExporter
if (Reflect.hasField(symbol, "baseClassName"))
{
baseClassName = symbol.baseClassName;
+ // a base class from this library is generated too, with the same prefix
+ if (baseClassName != null && libraryClassNames.exists(baseClassName))
+ {
+ baseClassName = SymbolUtils.formatClassName(baseClassName, prefix);
+ }
}
}
else if (type == BUTTON)
@@ -1301,12 +1441,65 @@ class AnimateLibraryExporter
var template = new Template(templateData);
var templateFile = new Asset("", Path.combine(Path.combine(targetPath, Path.directory(className.split(".").join("/"))), name + ".hx"),
- cast AssetType.TEMPLATE);
+ lime.tools.AssetType.TEMPLATE);
templateFile.embed = false;
templateFile.data = template.execute(context);
output.push(templateFile);
- generatedClasses.push((packageName.length > 0 ? packageName + "." : "") + name);
+ var generatedClassName = (packageName.length > 0 ? packageName + "." : "") + name;
+ generatedClasses.push(generatedClassName);
+
+ var dependencies = [];
+ if (baseClassName != null)
+ {
+ dependencies.push(baseClassName);
+ }
+ for (property in classProperties)
+ {
+ var propertyType:String = property.type;
+ if (propertyType != "Dynamic" && !StringTools.startsWith(propertyType, "openfl."))
+ {
+ dependencies.push(propertyType);
+ }
+ }
+ classDependencies.set(generatedClassName, dependencies);
+ }
+ }
+
+ if (includedClasses != null)
+ {
+ var included = new Map();
+ for (className in generatedClasses)
+ {
+ included.set(className, true);
+ }
+
+ var changed = true;
+ while (changed)
+ {
+ changed = false;
+ for (className in generatedClasses)
+ {
+ if (!included.exists(className)) continue;
+
+ for (dependency in classDependencies.get(className))
+ {
+ if (!included.exists(dependency))
+ {
+ included.remove(className);
+ changed = true;
+ break;
+ }
+ }
+ }
+ }
+
+ for (className in generatedClasses)
+ {
+ if (included.exists(className))
+ {
+ includedClasses.push(className);
+ }
}
}
@@ -1317,7 +1510,7 @@ class AnimateLibraryExporter
{
var data2 = processTag(cast swfData.getCharacter(symbol.tagId));
- if (data2 == null && ~/_fla\.MainTimeline$/.match(symbol.name))
+ if (data2 == null && (symbol.tagId == 0 || ~/_fla\.MainTimeline$/.match(symbol.name)))
{
data2 = libraryData.root;
}
@@ -1362,6 +1555,10 @@ class AnimateLibraryExporter
{
return addShape(cast tag);
}
+ else if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (tag, TagDefineMorphShape))
+ {
+ return addMorphShape(cast tag);
+ }
else if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (tag, TagDefineFont)
|| #if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (tag, TagDefineFont4))
{
@@ -1369,7 +1566,7 @@ class AnimateLibraryExporter
}
else if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (tag, TagDefineSound))
{
- addSound(tag);
+ return addSound(tag);
}
return null;
@@ -1541,6 +1738,7 @@ private #if (haxe_ver >= 4.0) enum #end abstract SWFShapeCommandType(Int) from I
public var LINE_STYLE = 6;
public var LINE_TO = 7;
public var MOVE_TO = 8;
+ public var LINE_GRADIENT_STYLE = 9;
}
#if (haxe_ver < 4.0) @:enum #end
@@ -1553,6 +1751,8 @@ private #if (haxe_ver >= 4.0) enum #end abstract SWFSymbolType(Int) from Int to
public var SHAPE = 4;
public var SPRITE = 5;
public var STATIC_TEXT = 6;
+ public var SOUND = 7;
+ public var MORPH_SHAPE = 8;
}
#if (haxe_ver < 4.0) @:enum #end
diff --git a/src/swf/exporters/SWFLiteExporter.hx b/src/swf/exporters/SWFLiteExporter.hx
index a92f5a2ae..f4e1eb947 100644
--- a/src/swf/exporters/SWFLiteExporter.hx
+++ b/src/swf/exporters/SWFLiteExporter.hx
@@ -844,7 +844,7 @@ class SWFLiteExporter
case 4: SoundType.NELLYMOSER_16_KHZ;
case 5: SoundType.NELLYMOSER_8_KHZ;
case 6: SoundType.NELLYMOSER;
- case 7: SoundType.SPEEX;
+ case 11: SoundType.SPEEX;
case _: throw("invalid sound type!");
}
sounds.set(tag.characterId, byteArray);
@@ -860,7 +860,7 @@ class SWFLiteExporter
var data2 = processTag(cast data.getCharacter(symbol.tagId));
- if (data2 == null && ~/_fla\.MainTimeline$/.match(symbol.name))
+ if (data2 == null && (symbol.tagId == 0 || ~/_fla\.MainTimeline$/.match(symbol.name)))
{
data2 = swfLite.root;
}
@@ -870,6 +870,10 @@ class SWFLiteExporter
data2.className = symbol.name;
data2.baseClassName = FrameScriptParser.getBaseClassName(data, symbol.name);
}
+ else if (symbol.name != null && sounds.exists(symbol.tagId))
+ {
+ soundSymbolClassNames.set(symbol.tagId, symbol.name);
+ }
}
private function processTag(tag:IDefinitionTag):Dynamic
diff --git a/src/swf/exporters/ShapeCommandExporter.hx b/src/swf/exporters/ShapeCommandExporter.hx
index ff5129de4..5dbe15f25 100644
--- a/src/swf/exporters/ShapeCommandExporter.hx
+++ b/src/swf/exporters/ShapeCommandExporter.hx
@@ -69,6 +69,12 @@ class ShapeCommandExporter extends DefaultShapeExporter
commands.push(LineStyle(thickness, color, alpha, pixelHinting, scaleMode, startCaps, /*endCaps,*/ joints, miterLimit));
}
+ override public function lineGradientStyle(type:GradientType, colors:Array, alphas:Array, ratios:Array, matrix:Matrix = null,
+ spreadMethod:SpreadMethod = null, interpolationMethod:InterpolationMethod = null, focalPointRatio:Float = 0):Void
+ {
+ commands.push(LineGradientStyle(type, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio));
+ }
+
override public function moveTo(x:Float, y:Float):Void
{
commands.push(MoveTo(x, y));
diff --git a/src/swf/exporters/animate/AnimateBitmapSymbol.hx b/src/swf/exporters/animate/AnimateBitmapSymbol.hx
index 3b5e06ea6..424eec78d 100644
--- a/src/swf/exporters/animate/AnimateBitmapSymbol.hx
+++ b/src/swf/exporters/animate/AnimateBitmapSymbol.hx
@@ -22,9 +22,25 @@ class AnimateBitmapSymbol extends AnimateSymbol
private override function __createObject(library:AnimateLibrary):Bitmap
{
#if lime
- return new Bitmap(BitmapData.fromImage(library.getImage(path)), PixelSnapping.AUTO, smooth != false);
+ return new Bitmap(__createBitmapData(library), PixelSnapping.AUTO, smooth != false);
#else
return null;
#end
}
+
+ private function __createBitmapData(library:AnimateLibrary):BitmapData
+ {
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ return library.__getHardwareBitmapData(this);
+ #else
+ return __createBitmapDataUncached(library);
+ #end
+ }
+
+ @:allow(swf.exporters.animate.AnimateLibrary)
+ private function __createBitmapDataUncached(library:AnimateLibrary):BitmapData
+ {
+ var image = library.getImage(path);
+ return image != null ? BitmapData.fromImage(image) : null;
+ }
}
diff --git a/src/swf/exporters/animate/AnimateButtonSymbol.hx b/src/swf/exporters/animate/AnimateButtonSymbol.hx
index 602939eb1..7f6947fce 100644
--- a/src/swf/exporters/animate/AnimateButtonSymbol.hx
+++ b/src/swf/exporters/animate/AnimateButtonSymbol.hx
@@ -9,6 +9,7 @@ import openfl.display.SimpleButton;
@:noDebug
#end
@:access(openfl.display.SimpleButton)
+@:access(swf.exporters.animate.AnimateLibrary)
class AnimateButtonSymbol extends AnimateSymbol
{
public var downState:AnimateSpriteSymbol;
@@ -59,18 +60,16 @@ class AnimateButtonSymbol extends AnimateSymbol
}
#end
- if (className != null)
+ var symbolType = library.__getGeneratedClass(id);
+
+ if (symbolType == null && className != null)
+ {
+ symbolType = Type.resolveClass(SymbolUtils.formatClassName(className));
+ }
+
+ if (symbolType != null)
{
- var symbolType = Type.resolveClass(SymbolUtils.formatClassName(className));
-
- if (symbolType != null)
- {
- simpleButton = Type.createInstance(symbolType, []);
- }
- else
- {
- // Log.warn ("Could not resolve class \"" + symbol.className + "\"");
- }
+ simpleButton = __createInstance(library, symbolType);
}
if (simpleButton == null)
diff --git a/src/swf/exporters/animate/AnimateDynamicTextSymbol.hx b/src/swf/exporters/animate/AnimateDynamicTextSymbol.hx
index 3bda14751..1c99c7054 100644
--- a/src/swf/exporters/animate/AnimateDynamicTextSymbol.hx
+++ b/src/swf/exporters/animate/AnimateDynamicTextSymbol.hx
@@ -37,6 +37,10 @@ class AnimateDynamicTextSymbol extends AnimateSymbol
public var x:Float;
public var y:Float;
+ private var resolvedFontCount:Int = -1;
+ private var resolvedFontFound:Bool;
+ private var resolvedFontName:String;
+
public function new()
{
super();
@@ -99,40 +103,52 @@ class AnimateDynamicTextSymbol extends AnimateSymbol
format.font = fontName;
- var found = false;
-
- switch (format.font)
+ // fonts are only ever added, so the font to use only changes when another one is registered
+ var fontCount = Font.enumerateFonts().length;
+ if (resolvedFontCount != fontCount)
{
- case "_sans", "_serif", "_typewriter", "", null:
- found = true;
+ var found = false;
+
+ switch (format.font)
+ {
+ case "_sans", "_serif", "_typewriter", "", null:
+ found = true;
+
+ default:
+ for (font in Font.enumerateFonts())
+ {
+ if (font.fontName == format.font)
+ {
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found)
+ {
+ var alpha = ~/[^a-zA-Z]+/g;
+ var spaces = ~/\s/g;
- default:
for (font in Font.enumerateFonts())
{
- if (font.fontName == format.font)
+ if (alpha.replace(font.fontName, "").substr(0, fontName.length) == spaces.replace(fontName, ""))
{
+ format.font = font.fontName;
found = true;
break;
}
}
- }
-
- if (!found)
- {
- var alpha = ~/[^a-zA-Z]+/g;
- var spaces = ~/\s/g;
-
- for (font in Font.enumerateFonts())
- {
- if (alpha.replace(font.fontName, "").substr(0, fontName.length) == spaces.replace(fontName, ""))
- {
- format.font = font.fontName;
- found = true;
- break;
- }
}
+
+ resolvedFontCount = fontCount;
+ resolvedFontFound = found;
+ resolvedFontName = format.font;
}
+ format.font = resolvedFontName;
+ var found = resolvedFontFound;
+
if (found)
{
textField.embedFonts = true;
diff --git a/src/swf/exporters/animate/AnimateFrameObject.hx b/src/swf/exporters/animate/AnimateFrameObject.hx
index b7431ee36..3fecc43f6 100644
--- a/src/swf/exporters/animate/AnimateFrameObject.hx
+++ b/src/swf/exporters/animate/AnimateFrameObject.hx
@@ -20,6 +20,7 @@ class AnimateFrameObject
public var id:Int;
public var matrix:Matrix;
public var name:String;
+ public var ratio:Null;
public var symbol:Int;
public var type:AnimateFrameObjectType;
public var visible:Null;
diff --git a/src/swf/exporters/animate/AnimateLibrary.hx b/src/swf/exporters/animate/AnimateLibrary.hx
index 9aed6edf7..78481da30 100644
--- a/src/swf/exporters/animate/AnimateLibrary.hx
+++ b/src/swf/exporters/animate/AnimateLibrary.hx
@@ -5,6 +5,7 @@ import lime.graphics.Image;
import lime.graphics.ImageChannel;
import lime.math.Vector2;
import swf.exporters.core.FilterType;
+import openfl.display.BitmapData;
import openfl.display.MovieClip;
import openfl.events.Event;
import openfl.events.IOErrorEvent;
@@ -19,6 +20,7 @@ import openfl.utils.AssetType;
import openfl.utils.Future;
import openfl.utils.Promise;
#if lime
+import lime.media.AudioBuffer;
import lime.utils.AssetBundle;
import lime.utils.AssetManifest;
import lime.utils.AssetLibrary as LimeAssetLibrary;
@@ -38,6 +40,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
@@ -53,32 +56,44 @@ import openfl.filters.GlowFilter;
];
#end
+ // Generated classes register here from __init__, which can run before static initializers
+ @:noCompletion private static var __generatedClasses:Map>;
private static var instances:Map = new Map();
private var alphaCheck:Map;
private var bitmapClassNames:Map;
private var bitmapSymbols:Array;
private var frameRate:Float;
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ private var hardwareBitmapData:Map;
+ private var readableBitmapDataKeys:Map;
+ private var readableShapeBitmapData:Map;
+ #end
private var id:String;
private var instanceID:String;
private var preloading:Bool;
private var root:AnimateSpriteSymbol;
private var rootPath:String;
+ private var soundClassNames:Map;
private var symbols:Map;
private var symbolsByClassName:Map;
- private var uuid:String;
public function new(id:String, uuid:String = null)
{
super();
this.id = id;
- this.uuid = uuid;
instanceID = uuid != null ? uuid : id;
alphaCheck = new Map();
bitmapClassNames = new Map();
+ soundClassNames = new Map();
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ hardwareBitmapData = new Map();
+ readableBitmapDataKeys = new Map();
+ readableShapeBitmapData = new Map();
+ #end
#if (ios || tvos)
rootPath = "assets/";
@@ -86,7 +101,7 @@ import openfl.filters.GlowFilter;
rootPath = "";
#end
- instances.set(uuid, this);
+ instances.set(instanceID, this);
// Hack to include filter classes, macro.include is not working properly
@@ -129,7 +144,12 @@ import openfl.filters.GlowFilter;
if (type == null || type == (cast AssetType.IMAGE) || type == (cast AssetType.MOVIE_CLIP))
{
- return (symbolsByClassName != null && symbolsByClassName.exists(id));
+ return (symbolsByClassName != null && symbolsByClassName.exists(id)) || (type == null && soundClassNames.exists(id));
+ }
+
+ if ((type == (cast AssetType.SOUND) || type == (cast AssetType.MUSIC)) && soundClassNames.exists(id))
+ {
+ return true;
}
}
@@ -142,6 +162,114 @@ import openfl.filters.GlowFilter;
return instances.get(uuid);
}
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ @:allow(swf.exporters.animate.AnimateBitmapSymbol)
+ private function __getHardwareBitmapData(symbol:AnimateBitmapSymbol):BitmapData
+ {
+ var key = __getBitmapDataKey(symbol);
+ if (readableBitmapDataKeys.exists(key))
+ {
+ return __getReadableShapeBitmapData(symbol);
+ }
+
+ var bitmapData = hardwareBitmapData.get(key);
+
+ if (bitmapData == null)
+ {
+ bitmapData = symbol.__createBitmapDataUncached(this);
+
+ if (bitmapData != null)
+ {
+ bitmapData.disposeImage();
+ hardwareBitmapData.set(key, bitmapData);
+
+ if (symbol.path != null)
+ {
+ cachedImages.remove(symbol.path);
+ alphaCheck.remove(symbol.path);
+ }
+
+ if (symbol.alpha != null)
+ {
+ cachedImages.remove(symbol.alpha);
+ }
+ }
+ }
+
+ return bitmapData;
+ }
+
+ @:allow(swf.exporters.animate.AnimateShapeSymbol)
+ private function __markBitmapDataReadable(symbol:AnimateBitmapSymbol):Void
+ {
+ if (symbol != null)
+ {
+ readableBitmapDataKeys.set(__getBitmapDataKey(symbol), true);
+ }
+ }
+
+ @:allow(swf.exporters.animate.AnimateShapeSymbol)
+ private function __getReadableShapeBitmapData(symbol:AnimateBitmapSymbol):BitmapData
+ {
+ var key = __getBitmapDataKey(symbol);
+ readableBitmapDataKeys.set(key, true);
+ var bitmapData = readableShapeBitmapData.get(key);
+
+ if (bitmapData == null)
+ {
+ bitmapData = symbol.__createBitmapDataUncached(this);
+
+ if (bitmapData != null)
+ {
+ readableShapeBitmapData.set(key, bitmapData);
+ }
+ }
+
+ return bitmapData;
+ }
+
+ private function __getBitmapDataKey(symbol:AnimateBitmapSymbol):String
+ {
+ return symbol.path != null
+ ? "path:" + symbol.path + "|alpha:" + (symbol.alpha != null ? symbol.alpha : "")
+ : "symbol:" + symbol.id;
+ }
+
+ // Shapes are drawn in software, so bitmaps used by shape fills keep their pixels
+ private function __markShapeBitmapsReadable():Void
+ {
+ for (symbol in symbols)
+ {
+ if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (symbol, AnimateShapeSymbol))
+ {
+ __markFillBitmapsReadable(cast symbol);
+ }
+ else if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (symbol, AnimateMorphShapeSymbol))
+ {
+ for (shape in cast(symbol, AnimateMorphShapeSymbol).shapes)
+ {
+ __markFillBitmapsReadable(shape);
+ }
+ }
+ }
+ }
+
+ private function __markFillBitmapsReadable(shape:AnimateShapeSymbol):Void
+ {
+ for (bitmapID in shape.__getBitmapFillIDs())
+ {
+ __markBitmapDataReadable(cast symbols.get(bitmapID));
+ }
+ }
+ #end
+
+ #if lime
+ public override function getAudioBuffer(id:String):AudioBuffer
+ {
+ return super.getAudioBuffer(__getSoundID(id));
+ }
+ #end
+
#if lime
public override function getImage(id:String):Image
{
@@ -259,6 +387,7 @@ import openfl.filters.GlowFilter;
var data:Dynamic, type:SWFSymbolType, symbol:AnimateSymbol = null;
var bitmapSymbol:AnimateBitmapSymbol;
+ var soundSymbol:AnimateSoundSymbol;
var spriteSymbol:AnimateSpriteSymbol;
symbols = new Map();
@@ -270,6 +399,7 @@ import openfl.filters.GlowFilter;
data = symbolData[i];
if (data == null) continue;
type = data.type;
+ symbol = null;
switch (type)
{
@@ -284,8 +414,14 @@ import openfl.filters.GlowFilter;
symbol = __parseDynamicText(data);
case FONT:
symbol = __parseFont(data);
+ case MORPH_SHAPE:
+ symbol = __parseMorphShape(data);
case SHAPE:
symbol = __parseShape(data);
+ case SOUND:
+ soundSymbol = __parseSound(data);
+ if (soundSymbol.className != null) soundClassNames.set(soundSymbol.className, soundSymbol.path);
+ symbol = soundSymbol;
case SPRITE:
spriteSymbol = __parseSprite(data);
if (i == rootIndex) root = spriteSymbol;
@@ -297,7 +433,8 @@ import openfl.filters.GlowFilter;
if (symbol == null) continue;
symbols.set(symbol.id, symbol);
- if (symbol.className != null)
+ // sounds are not display objects, so they are only found through soundClassNames
+ if (symbol.className != null && type != SOUND)
{
symbolsByClassName.set(symbol.className, symbol);
#if (openfl > "9.1.0")
@@ -306,6 +443,10 @@ import openfl.filters.GlowFilter;
}
}
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ __markShapeBitmapsReadable();
+ #end
+
// SWFLite.instances.set(instanceID, swf);
__load().onProgress(promise.progress).onError(promise.error).onComplete(function(_)
@@ -362,6 +503,13 @@ import openfl.filters.GlowFilter;
}
#end
+ #if lime
+ public override function loadAudioBuffer(id:String):Future
+ {
+ return super.loadAudioBuffer(__getSoundID(id));
+ }
+ #end
+
public static function loadFromBundle(bundle:AssetBundle):#if (java && lime) Future #else Future #end
{
var library = AssetLibrary.fromBundle(bundle);
@@ -447,7 +595,12 @@ import openfl.filters.GlowFilter;
#if lime
public override function unload():Void
{
- instances.remove(uuid);
+ instances.remove(instanceID);
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ hardwareBitmapData = new Map();
+ readableBitmapDataKeys = new Map();
+ readableShapeBitmapData = new Map();
+ #end
if (symbols == null) return;
// if (swf == null) return;
@@ -512,6 +665,44 @@ import openfl.filters.GlowFilter;
}
#end
+ // Generated class names can have a prefix, so symbols find their class by library and symbol ID
+ @:noCompletion private function __getGeneratedClass(symbolID:Int):Class
+ {
+ return __generatedClasses != null ? __generatedClasses.get(instanceID + ":" + symbolID) : null;
+ }
+
+ #if lime
+ private function __getSoundID(id:String):String
+ {
+ if (soundClassNames.exists(id))
+ {
+ id = soundClassNames.get(id);
+ }
+
+ // Lime keeps bundled sounds as bytes on HTML5, and does not decode them later
+ if (!cachedAudioBuffers.exists(id) && cachedBytes.exists(id))
+ {
+ var bytes = cachedBytes.get(id);
+ #if (js && html5 && lime_howlerjs)
+ // AudioBuffer.fromBytes() cannot detect MP3 data without an ID3 tag, which
+ // SWF files do not include, so use the file extension from the exporter
+ var audioBuffer = new AudioBuffer();
+ audioBuffer.src = new lime.media.howlerjs.Howl({
+ src: ["data:audio/" + haxe.io.Path.extension(id) + ";base64," + haxe.crypto.Base64.encode(bytes)],
+ html5: true,
+ preload: false
+ });
+ cachedAudioBuffers.set(id, audioBuffer);
+ #else
+ cachedAudioBuffers.set(id, AudioBuffer.fromBytes(bytes));
+ #end
+ cachedBytes.remove(id);
+ }
+
+ return id;
+ }
+ #end
+
#if lime
private function __load():Future
{
@@ -662,6 +853,10 @@ import openfl.filters.GlowFilter;
commands.push(LineStyle(data[i + 1], data[i + 2], data[i + 3], data[i + 4], data[i + 5], data[i + 6], data[i + 7],
data[i + 8]));
i += 9;
+ case LINE_GRADIENT_STYLE:
+ commands.push(LineGradientStyle(data[i + 1], data[i + 2], data[i + 3], data[i + 4], __parseMatrix(data[i + 5]), data[i + 6],
+ data[i + 7], data[i + 8]));
+ i += 9;
case LINE_TO:
commands.push(LineTo(__pixel(data[i + 1]), __pixel(data[i + 2])));
i += 3;
@@ -678,71 +873,8 @@ import openfl.filters.GlowFilter;
return symbol;
}
- private function __parseMatrix(values:Array):Matrix
+ private function __parseFrames(symbol:AnimateSpriteSymbol, frames:Array):Void
{
- return values != null ? new Matrix(values[0], values[1], values[2], values[3], __pixel(cast values[4]), __pixel(cast values[5])) : null;
- }
-
- private function __parseShape(data:Dynamic):AnimateShapeSymbol
- {
- var symbol = new AnimateShapeSymbol();
- symbol.id = data.id;
- symbol.commands = [];
-
- var data:Array = data.commands;
- var commands = symbol.commands;
- var i = 0;
-
- while (i < data.length)
- {
- switch (data[i])
- {
- case BEGIN_BITMAP_FILL:
- commands.push(BeginBitmapFill(data[i + 1], __parseMatrix(data[i + 2]), data[i + 3], data[i + 4]));
- i += 5;
- case BEGIN_FILL:
- commands.push(BeginFill(data[i + 1], data[i + 2]));
- i += 3;
- case BEGIN_GRADIENT_FILL:
- commands.push(BeginGradientFill(data[i + 1], data[i + 2], data[i + 3], data[i + 4], __parseMatrix(data[i + 5]), data[i + 6], data[i + 7],
- data[i + 8]));
- i += 9;
- case CLEAR_LINE_STYLE:
- commands.push(LineStyle(null, null, null, null, null, null, null, null));
- i++;
- case CURVE_TO:
- commands.push(CurveTo(__pixel(data[i + 1]), __pixel(data[i + 2]), __pixel(data[i + 3]), __pixel(data[i + 4])));
- i += 5;
- case END_FILL:
- commands.push(EndFill);
- i++;
- case LINE_STYLE:
- commands.push(LineStyle(data[i + 1], data[i + 2], data[i + 3], data[i + 4], data[i + 5], data[i + 6], data[i + 7], data[i + 8]));
- i += 9;
- case LINE_TO:
- commands.push(LineTo(__pixel(data[i + 1]), __pixel(data[i + 2])));
- i += 3;
- case MOVE_TO:
- commands.push(MoveTo(__pixel(data[i + 1]), __pixel(data[i + 2])));
- i += 3;
- default:
- i++;
- }
- }
- return symbol;
- }
-
- private function __parseSprite(data:Dynamic):AnimateSpriteSymbol
- {
- if (data == null) return null;
-
- var symbol = new AnimateSpriteSymbol();
- symbol.id = data.id;
- symbol.className = data.className;
- symbol.baseClassName = data.baseClassName;
- 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;
var frame:AnimateFrame,
objects:Array,
object:AnimateFrameObject;
@@ -781,6 +913,7 @@ import openfl.filters.GlowFilter;
object.id = objectData.id;
object.matrix = __parseMatrix(objectData.matrix);
object.name = objectData.name;
+ object.ratio = objectData.ratio;
object.symbol = objectData.symbol;
object.type = objectData.type;
object.visible = objectData.visible;
@@ -790,6 +923,65 @@ import openfl.filters.GlowFilter;
}
symbol.frames.push(frame);
}
+ }
+
+ private function __parseMatrix(values:Array):Matrix
+ {
+ return values != null ? new Matrix(values[0], values[1], values[2], values[3], __pixel(cast values[4]), __pixel(cast values[5])) : null;
+ }
+
+ private function __parseMorphShape(data:Dynamic):AnimateMorphShapeSymbol
+ {
+ var symbol = new AnimateMorphShapeSymbol();
+ symbol.id = data.id;
+ symbol.ratios = [];
+ symbol.shapes = [];
+ var ratios:Array = data.ratios;
+ var shapes:Array = data.shapes;
+ for (i in 0...ratios.length)
+ {
+ var shape = new AnimateShapeSymbol();
+ shape.id = symbol.id;
+ shape.__setCompactCommands(shapes[i]);
+ symbol.ratios.push(Std.int(ratios[i]));
+ symbol.shapes.push(shape);
+ }
+ return symbol;
+ }
+
+ private function __parseShape(data:Dynamic):AnimateShapeSymbol
+ {
+ var symbol = new AnimateShapeSymbol();
+ symbol.id = data.id;
+ symbol.__setCompactCommands(data.commands);
+ return symbol;
+ }
+
+ private function __parseSound(data:Dynamic):AnimateSoundSymbol
+ {
+ var symbol = new AnimateSoundSymbol();
+ symbol.id = data.id;
+ symbol.className = data.className;
+ symbol.path = data.path;
+ return symbol;
+ }
+
+ private function __parseSprite(data:Dynamic):AnimateSpriteSymbol
+ {
+ if (data == null) return null;
+
+ var symbol = new AnimateSpriteSymbol();
+ symbol.id = data.id;
+ symbol.className = data.className;
+ symbol.baseClassName = data.baseClassName;
+ 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, this);
+ #else
+ __parseFrames(symbol, frames);
+ #end
return symbol;
}
@@ -802,6 +994,13 @@ import openfl.filters.GlowFilter;
return symbol;
}
+ // Called from __init__ by generated classes that are compiled
+ @:noCompletion private static function __registerGeneratedClass(uuid:String, symbolID:Int, type:Class):Void
+ {
+ if (__generatedClasses == null) __generatedClasses = new Map();
+ __generatedClasses.set(uuid + ":" + symbolID, type);
+ }
+
private inline function __pixel(value:Int):Float
{
return value / 20;
@@ -819,6 +1018,7 @@ import openfl.filters.GlowFilter;
public var LINE_STYLE = 6;
public var LINE_TO = 7;
public var MOVE_TO = 8;
+ public var LINE_GRADIENT_STYLE = 9;
}
#if (haxe_ver >= 4.0) enum #else @:enum #end abstract SWFSymbolType(Int) from Int to Int
@@ -830,4 +1030,6 @@ import openfl.filters.GlowFilter;
public var SHAPE = 4;
public var SPRITE = 5;
public var STATIC_TEXT = 6;
+ public var SOUND = 7;
+ public var MORPH_SHAPE = 8;
}
diff --git a/src/swf/exporters/animate/AnimateMorphShapeSymbol.hx b/src/swf/exporters/animate/AnimateMorphShapeSymbol.hx
new file mode 100644
index 000000000..e487eb69b
--- /dev/null
+++ b/src/swf/exporters/animate/AnimateMorphShapeSymbol.hx
@@ -0,0 +1,44 @@
+package swf.exporters.animate;
+
+import openfl.display.Shape;
+
+#if !openfl_debug
+@:fileXml('tags="haxe,release"')
+@:noDebug
+#end
+@:access(swf.exporters.animate.AnimateShapeSymbol)
+class AnimateMorphShapeSymbol extends AnimateSymbol
+{
+ // The exporter draws the shape at each ratio the timelines use, from 0 to 65535
+ public var ratios:Array;
+ public var shapes:Array;
+
+ public function new()
+ {
+ super();
+ }
+
+ private override function __createObject(library:AnimateLibrary):Shape
+ {
+ var shape = new Shape();
+ __setRatio(shape, 0, library);
+ return shape;
+ }
+
+ private function __setRatio(shape:Shape, ratio:Int, library:AnimateLibrary):Void
+ {
+ if (ratios == null || ratios.length == 0) return;
+
+ var index = 0;
+ for (i in 1...ratios.length)
+ {
+ if (Math.abs(ratios[i] - ratio) < Math.abs(ratios[index] - ratio))
+ {
+ index = i;
+ }
+ }
+
+ shape.graphics.clear();
+ shape.graphics.copyFrom(shapes[index].__getRendered(library).graphics);
+ }
+}
diff --git a/src/swf/exporters/animate/AnimateShapeCommand.hx b/src/swf/exporters/animate/AnimateShapeCommand.hx
index 7a708b0c3..d768c6bcc 100644
--- a/src/swf/exporters/animate/AnimateShapeCommand.hx
+++ b/src/swf/exporters/animate/AnimateShapeCommand.hx
@@ -19,6 +19,8 @@ enum AnimateShapeCommand
EndFill;
LineStyle(thickness:Null, color:Null, alpha:Null, pixelHinting:Null, scaleMode:Null /*LineScaleMode*/,
caps:Null /*CapsStyle*/, joints:Null /*JointStyle*/, miterLimit:Null);
+ LineGradientStyle(fillType:Null /*GradientType*/, colors:Array, alphas:Array, ratios:Array, matrix:Matrix,
+ spreadMethod:Null /*SpreadMethod*/, interpolationMethod:Null /*InterpolationMethod*/, focalPointRatio:Float);
LineTo(x:Float, y:Float);
MoveTo(x:Float, y:Float);
}
diff --git a/src/swf/exporters/animate/AnimateShapeSymbol.hx b/src/swf/exporters/animate/AnimateShapeSymbol.hx
index 7f2d1c959..8cab22a5c 100644
--- a/src/swf/exporters/animate/AnimateShapeSymbol.hx
+++ b/src/swf/exporters/animate/AnimateShapeSymbol.hx
@@ -8,6 +8,7 @@ import openfl.display.JointStyle;
import openfl.display.LineScaleMode;
import openfl.display.Shape;
import openfl.display.SpreadMethod;
+import swf.exporters.animate.AnimateLibrary.SWFShapeCommandType;
#if !openfl_debug
@:fileXml('tags="haxe,release"')
@@ -25,6 +26,11 @@ class AnimateShapeSymbol extends AnimateSymbol
public var commands:Array;
public var rendered:Shape;
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ private var compactBitmapFillIDs:Array;
+ #end
+ private var compactCommands:Array;
+
public function new()
{
super();
@@ -33,46 +39,319 @@ class AnimateShapeSymbol extends AnimateSymbol
private override function __createObject(library:AnimateLibrary):Shape
{
var shape = new Shape();
- var graphics = shape.graphics;
+ shape.graphics.copyFrom(__getRendered(library).graphics);
+ return shape;
+ }
- if (rendered != null)
+ private function __getRendered(library:AnimateLibrary):Shape
+ {
+ if (rendered == null)
{
- graphics.copyFrom(rendered.graphics);
- return shape;
+ rendered = new Shape();
+ var graphics = rendered.graphics;
+
+ if (compactCommands != null)
+ {
+ __renderCompactCommands(graphics, library);
+ }
+ else if (commands != null)
+ {
+ for (command in commands)
+ {
+ switch (command)
+ {
+ case BeginFill(color, alpha):
+ graphics.beginFill(color, alpha);
+
+ case BeginBitmapFill(bitmapID, matrix, repeat, smooth):
+ __beginBitmapFill(graphics, library, bitmapID, matrix, repeat, smooth);
+
+ case BeginGradientFill(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ #if flash
+ var colors:Array = cast colors;
+ #end
+ graphics.beginGradientFill(GradientType.fromInt(fillType), colors, alphas, ratios, matrix, SpreadMethod.fromInt(spreadMethod),
+ InterpolationMethod.fromInt(interpolationMethod), focalPointRatio);
+
+ case CurveTo(controlX, controlY, anchorX, anchorY):
+ graphics.curveTo(controlX, controlY, anchorX, anchorY);
+
+ case EndFill:
+ graphics.endFill();
+
+ case LineStyle(thickness, color, alpha, pixelHinting, scaleMode, caps, joints, miterLimit):
+ if (thickness != null)
+ {
+ graphics.lineStyle(thickness, color, alpha, pixelHinting, LineScaleMode.fromInt(scaleMode), CapsStyle.fromInt(caps),
+ JointStyle.fromInt(joints), miterLimit);
+ }
+ else
+ {
+ graphics.lineStyle();
+ }
+
+ case LineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ #if flash
+ var colors:Array = cast colors;
+ #end
+ graphics.lineGradientStyle(GradientType.fromInt(fillType), colors, alphas, ratios, matrix, SpreadMethod.fromInt(spreadMethod),
+ InterpolationMethod.fromInt(interpolationMethod), focalPointRatio);
+
+ case LineTo(x, y):
+ graphics.lineTo(x, y);
+
+ case MoveTo(x, y):
+ graphics.moveTo(x, y);
+ }
+ }
+ }
+
+ commands = null;
+ compactCommands = null;
}
- for (command in commands)
+ return rendered;
+ }
+
+ private static function __beginBitmapFill(graphics:openfl.display.Graphics, library:AnimateLibrary, bitmapID:Int, matrix:openfl.geom.Matrix, repeat:Bool,
+ smooth:Bool):Void
+ {
+ #if lime
+ var bitmapSymbol:AnimateBitmapSymbol = cast library.symbols.get(bitmapID);
+ #if (!flash && swf_hardware_bitmap_cache)
+ // OpenFL draws shape paths in software, which reads the pixels of bitmap fills
+ var bitmapData = library.__getReadableShapeBitmapData(bitmapSymbol);
+ #else
+ var bitmap = library.getImage(bitmapSymbol.path);
+ var bitmapData = bitmap != null ? BitmapData.fromImage(bitmap) : null;
+ #end
+
+ if (bitmapData != null)
{
- switch (command)
+ graphics.beginBitmapFill(bitmapData, matrix, repeat, smooth);
+ }
+ #end
+ }
+
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ @:allow(swf.exporters.animate.AnimateLibrary)
+ private function __getBitmapFillIDs():Array
+ {
+ if (compactCommands != null) return compactBitmapFillIDs != null ? compactBitmapFillIDs : [];
+
+ var ids = [];
+ if (commands != null)
+ {
+ for (command in commands)
{
- case BeginFill(color, alpha):
- graphics.beginFill(color, alpha);
+ switch (command)
+ {
+ case BeginBitmapFill(bitmapID, _, _, _):
+ ids.push(bitmapID);
- case BeginBitmapFill(bitmapID, matrix, repeat, smooth):
- #if lime
- var bitmapSymbol:AnimateBitmapSymbol = cast library.symbols.get(bitmapID);
- var bitmap = library.getImage(bitmapSymbol.path);
+ default:
+ }
+ }
+ }
+ return ids;
+ }
+ #end
- if (bitmap != null)
- {
- graphics.beginBitmapFill(BitmapData.fromImage(bitmap), matrix, repeat, smooth);
- }
+ @:allow(swf.exporters.animate.AnimateLibrary)
+ private function __setCompactCommands(data:Array):Void
+ {
+ compactCommands = [];
+ var i = 0;
+
+ while (i < data.length)
+ {
+ var type:SWFShapeCommandType = data[i];
+ compactCommands.push(cast type);
+
+ switch (type)
+ {
+ case BEGIN_BITMAP_FILL:
+ #if (lime && !flash && swf_hardware_bitmap_cache)
+ if (compactBitmapFillIDs == null) compactBitmapFillIDs = [];
+ compactBitmapFillIDs.push(data[i + 1]);
#end
+ compactCommands.push(data[i + 1]);
+ __encodeMatrix(compactCommands, data[i + 2]);
+ compactCommands.push(data[i + 3] ? 1 : 0);
+ compactCommands.push(data[i + 4] ? 1 : 0);
+ i += 5;
+
+ case BEGIN_FILL:
+ compactCommands.push(data[i + 1]);
+ compactCommands.push(data[i + 2]);
+ i += 3;
+
+ case BEGIN_GRADIENT_FILL, LINE_GRADIENT_STYLE:
+ __encodeNullableNumber(compactCommands, data[i + 1]);
+ __encodeArray(compactCommands, data[i + 2]);
+ __encodeArray(compactCommands, data[i + 3]);
+ __encodeArray(compactCommands, data[i + 4]);
+ __encodeMatrix(compactCommands, data[i + 5]);
+ __encodeNullableNumber(compactCommands, data[i + 6]);
+ __encodeNullableNumber(compactCommands, data[i + 7]);
+ compactCommands.push(data[i + 8]);
+ i += 9;
+
+ case CLEAR_LINE_STYLE:
+ i++;
+
+ case CURVE_TO:
+ compactCommands.push(data[i + 1]);
+ compactCommands.push(data[i + 2]);
+ compactCommands.push(data[i + 3]);
+ compactCommands.push(data[i + 4]);
+ i += 5;
+
+ case END_FILL:
+ i++;
+
+ case LINE_STYLE:
+ __encodeNullableNumber(compactCommands, data[i + 1]);
+ __encodeNullableNumber(compactCommands, data[i + 2]);
+ __encodeNullableNumber(compactCommands, data[i + 3]);
+ __encodeNullableBool(compactCommands, data[i + 4]);
+ __encodeNullableNumber(compactCommands, data[i + 5]);
+ __encodeNullableNumber(compactCommands, data[i + 6]);
+ __encodeNullableNumber(compactCommands, data[i + 7]);
+ __encodeNullableNumber(compactCommands, data[i + 8]);
+ i += 9;
+
+ case LINE_TO, MOVE_TO:
+ compactCommands.push(data[i + 1]);
+ compactCommands.push(data[i + 2]);
+ i += 3;
+
+ default:
+ i++;
+ }
+ }
+ }
+
+ private static function __encodeArray(output:Array, values:Array):Void
+ {
+ if (values == null)
+ {
+ output.push(-1);
+ return;
+ }
+
+ output.push(values.length);
+ for (value in values)
+ {
+ output.push(value);
+ }
+ }
+
+ private static function __encodeMatrix(output:Array, values:Array):Void
+ {
+ if (values == null)
+ {
+ output.push(0);
+ return;
+ }
+
+ output.push(1);
+ for (i in 0...6)
+ {
+ output.push(values[i]);
+ }
+ }
+
+ private static function __encodeNullableBool(output:Array, value:Dynamic):Void
+ {
+ output.push(value == null ? Math.NaN : (value ? 1 : 0));
+ }
+
+ private static function __encodeNullableNumber(output:Array, value:Dynamic):Void
+ {
+ output.push(value == null ? Math.NaN : value);
+ }
+
+ private function __renderCompactCommands(graphics:openfl.display.Graphics, library:AnimateLibrary):Void
+ {
+ var data = compactCommands;
+ var position = 0;
+
+ while (position < data.length)
+ {
+ var type:SWFShapeCommandType = cast Std.int(data[position++]);
+
+ switch (type)
+ {
+ case BEGIN_BITMAP_FILL:
+ var bitmapID = Std.int(data[position++]);
+ var matrix = __decodeMatrix(data, position);
+ position += data[position] == 0 ? 1 : 7;
+ var repeat = data[position++] != 0;
+ var smooth = data[position++] != 0;
+ __beginBitmapFill(graphics, library, bitmapID, matrix, repeat, smooth);
+
+ case BEGIN_FILL:
+ graphics.beginFill(Std.int(data[position++]), data[position++]);
+
+ case BEGIN_GRADIENT_FILL, LINE_GRADIENT_STYLE:
+ var value = data[position++];
+ var fillType:Null = Math.isNaN(value) ? null : Std.int(value);
+ var colors = __decodeIntArray(data, position);
+ position += 1 + (colors == null ? 0 : colors.length);
+ var alphas = __decodeFloatArray(data, position);
+ position += 1 + (alphas == null ? 0 : alphas.length);
+ var ratios = __decodeIntArray(data, position);
+ position += 1 + (ratios == null ? 0 : ratios.length);
+ var matrix = __decodeMatrix(data, position);
+ position += data[position] == 0 ? 1 : 7;
+ value = data[position++];
+ var spreadMethod:Null = Math.isNaN(value) ? null : Std.int(value);
+ value = data[position++];
+ var interpolationMethod:Null = Math.isNaN(value) ? null : Std.int(value);
+ var focalPointRatio = data[position++];
- case BeginGradientFill(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
#if flash
- var colors:Array = cast colors;
+ var flashColors:Array = cast colors;
#end
- graphics.beginGradientFill(GradientType.fromInt(fillType), colors, alphas, ratios, matrix, SpreadMethod.fromInt(spreadMethod),
- InterpolationMethod.fromInt(interpolationMethod), focalPointRatio);
+ if (type == BEGIN_GRADIENT_FILL)
+ {
+ graphics.beginGradientFill(GradientType.fromInt(fillType), #if flash flashColors #else colors #end, alphas, ratios, matrix,
+ SpreadMethod.fromInt(spreadMethod), InterpolationMethod.fromInt(interpolationMethod), focalPointRatio);
+ }
+ else
+ {
+ graphics.lineGradientStyle(GradientType.fromInt(fillType), #if flash flashColors #else colors #end, alphas, ratios, matrix,
+ SpreadMethod.fromInt(spreadMethod), InterpolationMethod.fromInt(interpolationMethod), focalPointRatio);
+ }
+
+ case CLEAR_LINE_STYLE:
+ graphics.lineStyle();
- case CurveTo(controlX, controlY, anchorX, anchorY):
- graphics.curveTo(controlX, controlY, anchorX, anchorY);
+ case CURVE_TO:
+ graphics.curveTo(data[position++] / 20, data[position++] / 20, data[position++] / 20, data[position++] / 20);
- case EndFill:
+ case END_FILL:
graphics.endFill();
- case LineStyle(thickness, color, alpha, pixelHinting, scaleMode, caps, joints, miterLimit):
+ case LINE_STYLE:
+ var value = data[position++];
+ var thickness:Null = Math.isNaN(value) ? null : value;
+ value = data[position++];
+ var color:Null = Math.isNaN(value) ? null : Std.int(value);
+ value = data[position++];
+ var alpha:Null = Math.isNaN(value) ? null : value;
+ value = data[position++];
+ var pixelHinting:Null = Math.isNaN(value) ? null : value != 0;
+ value = data[position++];
+ var scaleMode:Null = Math.isNaN(value) ? null : Std.int(value);
+ value = data[position++];
+ var caps:Null = Math.isNaN(value) ? null : Std.int(value);
+ value = data[position++];
+ var joints:Null = Math.isNaN(value) ? null : Std.int(value);
+ value = data[position++];
+ var miterLimit:Null = Math.isNaN(value) ? null : value;
+
if (thickness != null)
{
graphics.lineStyle(thickness, color, alpha, pixelHinting, LineScaleMode.fromInt(scaleMode), CapsStyle.fromInt(caps),
@@ -83,18 +362,45 @@ class AnimateShapeSymbol extends AnimateSymbol
graphics.lineStyle();
}
- case LineTo(x, y):
- graphics.lineTo(x, y);
+ case LINE_TO:
+ graphics.lineTo(data[position++] / 20, data[position++] / 20);
- case MoveTo(x, y):
- graphics.moveTo(x, y);
+ case MOVE_TO:
+ graphics.moveTo(data[position++] / 20, data[position++] / 20);
}
}
+ }
- commands = null;
- rendered = new Shape();
- rendered.graphics.copyFrom(shape.graphics);
+ private static function __decodeFloatArray(data:Array, position:Int):Array
+ {
+ var length = Std.int(data[position++]);
+ if (length < 0) return null;
- return shape;
+ var result = [];
+ for (i in 0...length)
+ {
+ result.push(data[position + i]);
+ }
+ return result;
+ }
+
+ private static function __decodeIntArray(data:Array, position:Int):Array
+ {
+ var length = Std.int(data[position++]);
+ if (length < 0) return null;
+
+ var result = [];
+ for (i in 0...length)
+ {
+ result.push(Std.int(data[position + i]));
+ }
+ return result;
+ }
+
+ private static function __decodeMatrix(data:Array, position:Int):openfl.geom.Matrix
+ {
+ if (data[position++] == 0) return null;
+ return new openfl.geom.Matrix(data[position], data[position + 1], data[position + 2], data[position + 3], data[position + 4] / 20,
+ data[position + 5] / 20);
}
}
diff --git a/src/swf/exporters/animate/AnimateSoundSymbol.hx b/src/swf/exporters/animate/AnimateSoundSymbol.hx
new file mode 100644
index 000000000..7f5bc21b6
--- /dev/null
+++ b/src/swf/exporters/animate/AnimateSoundSymbol.hx
@@ -0,0 +1,15 @@
+package swf.exporters.animate;
+
+#if !openfl_debug
+@:fileXml('tags="haxe,release"')
+@:noDebug
+#end
+class AnimateSoundSymbol extends AnimateSymbol
+{
+ public var path:String;
+
+ public function new()
+ {
+ super();
+ }
+}
diff --git a/src/swf/exporters/animate/AnimateSpriteSymbol.hx b/src/swf/exporters/animate/AnimateSpriteSymbol.hx
index 1a6bf2f1b..6ba4f86fd 100644
--- a/src/swf/exporters/animate/AnimateSpriteSymbol.hx
+++ b/src/swf/exporters/animate/AnimateSpriteSymbol.hx
@@ -10,6 +10,7 @@ import openfl.geom.Rectangle;
@:fileXml('tags="haxe,release"')
@:noDebug
#end
+@:access(swf.exporters.animate.AnimateLibrary)
@:access(openfl.display.Sprite)
class AnimateSpriteSymbol extends AnimateSymbol
{
@@ -17,6 +18,7 @@ class AnimateSpriteSymbol extends AnimateSymbol
public var frames:Array;
public var scale9Grid:Rectangle;
+ private var compactTimeline:AnimateTimelineData;
private var library:AnimateLibrary;
public function new()
@@ -57,9 +59,9 @@ class AnimateSpriteSymbol extends AnimateSymbol
}
#end
- var symbolType = null;
+ var symbolType = library.__getGeneratedClass(id);
- if (className != null)
+ if (symbolType == null && className != null)
{
symbolType = Type.resolveClass(SymbolUtils.formatClassName(className));
@@ -90,7 +92,7 @@ class AnimateSpriteSymbol extends AnimateSymbol
if (symbolType != null)
{
- sprite = Type.createInstance(symbolType, []);
+ sprite = __createInstance(library, symbolType);
}
else
{
@@ -128,4 +130,9 @@ class AnimateSpriteSymbol extends AnimateSymbol
this.library = library;
__constructor(cast instance);
}
+
+ private function __setCompactTimeline(frames:Array, library:AnimateLibrary):Void
+ {
+ compactTimeline = new AnimateTimelineData(frames, library);
+ }
}
diff --git a/src/swf/exporters/animate/AnimateSymbol.hx b/src/swf/exporters/animate/AnimateSymbol.hx
index 5ed699339..d1ed387c8 100644
--- a/src/swf/exporters/animate/AnimateSymbol.hx
+++ b/src/swf/exporters/animate/AnimateSymbol.hx
@@ -8,11 +8,49 @@ import openfl.display.DisplayObject;
#end
@:keepSub class AnimateSymbol
{
+ // The symbol whose linkage class is being instantiated, so that a generated class
+ // initializes from the library creating it, even when another library exports a
+ // class with the same name
+ @:noCompletion private static var __pendingLibrary:AnimateLibrary;
+ @:noCompletion private static var __pendingSymbol:AnimateSymbol;
+ @:noCompletion private static var __pendingType:Class;
+
public var className:String;
public var id:Int;
public function new() {}
+ private function __createInstance(library:AnimateLibrary, type:Class):Dynamic
+ {
+ var previousLibrary = __pendingLibrary;
+ var previousSymbol = __pendingSymbol;
+ var previousType = __pendingType;
+
+ __pendingLibrary = library;
+ __pendingSymbol = this;
+ __pendingType = type;
+
+ var instance:Dynamic = null;
+
+ try
+ {
+ instance = Type.createInstance(type, []);
+ }
+ catch (e:Dynamic)
+ {
+ __pendingLibrary = previousLibrary;
+ __pendingSymbol = previousSymbol;
+ __pendingType = previousType;
+ throw e;
+ }
+
+ __pendingLibrary = previousLibrary;
+ __pendingSymbol = previousSymbol;
+ __pendingType = previousType;
+
+ return instance;
+ }
+
private function __createObject(library:AnimateLibrary):DisplayObject
{
return null;
@@ -20,5 +58,29 @@ import openfl.display.DisplayObject;
private function __init(library:AnimateLibrary):Void {}
+ // Called by generated classes before super(). Returns false if the class is not
+ // being instantiated by a library, such as when it is created with "new".
+ private static function __initPending(type:Class):Bool
+ {
+ var pendingType = __pendingType;
+
+ while (pendingType != null && pendingType != type)
+ {
+ pendingType = Type.getSuperClass(pendingType);
+ }
+
+ if (pendingType == null) return false;
+
+ var library = __pendingLibrary;
+ var symbol = __pendingSymbol;
+
+ __pendingLibrary = null;
+ __pendingSymbol = null;
+ __pendingType = null;
+
+ symbol.__init(library);
+ return true;
+ }
+
private function __initObject(library:AnimateLibrary, instance:DisplayObject):Void {}
}
diff --git a/src/swf/exporters/animate/AnimateTimeline.hx b/src/swf/exporters/animate/AnimateTimeline.hx
index dbe2cca18..96060355a 100644
--- a/src/swf/exporters/animate/AnimateTimeline.hx
+++ b/src/swf/exporters/animate/AnimateTimeline.hx
@@ -1,6 +1,7 @@
package swf.exporters.animate;
import lime.utils.Log;
+import swf.exporters.core.FilterType;
import openfl.display.DisplayObject;
import openfl.display.FrameLabel;
import openfl.display.FrameScript;
@@ -18,6 +19,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,12 +30,18 @@ import hscript.Parser;
@:noDebug
#end
@:access(swf.exporters.animate.AnimateLibrary)
+@:access(swf.exporters.animate.AnimateMorphShapeSymbol)
+@: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
{
+ @:noCompletion private static var __instanceFieldsCacheByClassName:Map> = new Map();
+
#if 0
// Suppress checkstyle warning
private static var __unusedImport:Array> = [
@@ -59,6 +67,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 +86,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 +124,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 +136,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 +156,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 +242,206 @@ 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);
+
+ case UPDATE:
+ __updateDisplayObjectCompact(instance.displayObject, objectPosition);
+
+ case DESTROY:
+ __currentInstancesByFrameObjectID.remove(id);
}
}
+ }
+ }
+
+ var currentInstances = new Array();
+ var currentMasks = new Array();
- if (currentMasks.length > 0 && !maskApplied && child.mask != null)
+ for (instance in __currentInstancesByFrameObjectID)
+ {
+ if (currentInstances.indexOf(instance) == -1)
+ {
+ currentInstances.push(instance);
+ if (instance.clipDepth > 0)
{
- child.mask = null;
+ currentMasks.push(instance);
}
}
+ }
+
+ currentInstances.sort(__sortDepths);
+ __applyCurrentInstances(currentInstances, currentMasks);
+ }
+
+ __previousFrame = currentFrame;
+ }
+
+ private function __applyCurrentInstances(currentInstances:Array, currentMasks:Array):Void
+ {
+ var instance:FrameSymbolInstance;
+
+ var existingChild:DisplayObject;
+ var targetDepth:Int;
+ var targetChild:DisplayObject;
+ var child:DisplayObject;
+ var maskApplied:Bool;
+
+ for (i in 0...currentInstances.length)
+ {
+ existingChild = (i < __sprite.numChildren) ? __sprite.getChildAt(i) : null;
+ instance = currentInstances[i];
+
+ targetDepth = instance.depth;
+ targetChild = instance.displayObject;
+
+ if (existingChild != targetChild)
+ {
+ __sprite.addChildAt(targetChild, i);
+ }
+
+ child = targetChild;
+ maskApplied = false;
- // TODO: How to tell if shapes are for a scale9Grid clip?
- if (__sprite.scale9Grid != null)
+ for (mask in currentMasks)
+ {
+ if (targetDepth > mask.depth && targetDepth <= mask.clipDepth)
{
- __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);
- }
- }
+ #if (openfl >= "9.5.0")
+ #if flash
+ child.mask = mask.displayObject;
+ #else
+ child.clippingLayer = mask.displayObject;
+ #end
+ #else
+ child.mask = mask.displayObject;
+ #end
+ maskApplied = true;
+ break;
}
- else
+ }
+
+ if (currentMasks.length > 0 && !maskApplied && child.mask != null)
+ {
+ child.mask = null;
+ }
+ }
+
+ #if !flash
+ for (mask in currentMasks)
+ {
+ __markTimelineMask(mask.displayObject);
+ }
+ #end
+
+ // 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 child:DisplayObject;
- var i = currentInstances.length;
- var length = __sprite.numChildren;
+ 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 child:DisplayObject;
+ var i = currentInstances.length;
+ var length = __sprite.numChildren;
- while (i < length)
- {
- child = __sprite.getChildAt(i);
+ while (i < length)
+ {
+ child = __sprite.getChildAt(i);
- // TODO: Faster method of determining if this was automatically added?
+ // TODO: Faster method of determining if this was automatically added?
- for (instance in __activeInstances)
+ 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))
{
- 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);
- }
-
- __sprite.removeChild(child);
- i--;
- length--;
- }
+ 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 +449,96 @@ 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
+ && activeInstance.clipDepth == frameObject.clipDepth)
+ {
+ // 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 (__sprite.stage != null) displayObject.dispatchEvent(new Event(Event.ADDED_TO_STAGE, false, false));
- #end
+ 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
- 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);
+ }
}
}
}
@@ -420,12 +546,82 @@ class AnimateTimeline extends Timeline
}
#if !openfljs
- __instanceFields = Type.getInstanceFields(Type.getClass(__sprite));
+ __instanceFields = __getCachedInstanceFields(__sprite);
#end
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
+ && activeInstance.clipDepth == clipDepth)
+ {
+ // 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 +633,7 @@ class AnimateTimeline extends Timeline
__currentInstancesByFrameObjectID = null;
__instanceFields = null;
__lastUpdated = null;
+ __lastUpdatedCompact = null;
__sprite = null;
__previousFrame = -1;
}
@@ -446,6 +643,18 @@ class AnimateTimeline extends Timeline
return a.depth - b.depth;
}
+ #if !flash
+ @:noCompletion private inline function __markTimelineMask(displayObject:DisplayObject):Void
+ {
+ // SWF clipDepth>0 placements are masks even before a child exists in their depth range.
+ if (!displayObject.__isMask)
+ {
+ displayObject.__isMask = true;
+ displayObject.__setRenderDirty();
+ }
+ }
+ #end
+
@:noCompletion private function __updateDisplayObject(displayObject:DisplayObject, frameObject:AnimateFrameObject, reset:Bool = false):Void
{
if (displayObject == null) return;
@@ -474,27 +683,7 @@ class AnimateTimeline extends Timeline
if (frameObject.filters != null)
{
- var filters:Array = [];
-
- for (filter in frameObject.filters)
- {
- switch (filter)
- {
- case BlurFilter(blurX, blurY, quality):
- filters.push(new BlurFilter(blurX, blurY, quality));
-
- case ColorMatrixFilter(matrix):
- filters.push(new ColorMatrixFilter(matrix));
-
- case DropShadowFilter(distance, angle, color, alpha, blurX, blurY, strength, quality, inner, knockout, hideObject):
- filters.push(new DropShadowFilter(distance, angle, color, alpha, blurX, blurY, strength, quality, inner, knockout, hideObject));
-
- case GlowFilter(color, alpha, blurX, blurY, strength, quality, inner, knockout):
- filters.push(new GlowFilter(color, alpha, blurX, blurY, strength, quality, inner, knockout));
- }
- }
-
- displayObject.filters = filters;
+ displayObject.filters = __createFilters(frameObject.filters);
}
else
{
@@ -516,6 +705,11 @@ class AnimateTimeline extends Timeline
displayObject.cacheAsBitmap = frameObject.cacheAsBitmap;
}
+ if (frameObject.ratio != null)
+ {
+ __setRatio(displayObject, frameObject.symbol, frameObject.ratio);
+ }
+
#if (openfl >= "9.5.0")
if (frameObject.metaData != null)
{
@@ -530,6 +724,123 @@ 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)
+ {
+ displayObject.filters = __createFilters(cast references[Std.int(data[position++])]);
+ }
+ 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_RATIO) != 0)
+ {
+ __setRatio(displayObject, Std.int(data[objectPosition + 2]), 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 __setRatio(displayObject:DisplayObject, symbolID:Int, ratio:Int):Void
+ {
+ var symbol = __library.symbols.get(symbolID);
+
+ if (#if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (symbol, AnimateMorphShapeSymbol)
+ && #if (haxe_ver >= 4.2) Std.isOfType #else Std.is #end (displayObject, Shape))
+ {
+ cast(symbol, AnimateMorphShapeSymbol).__setRatio(cast displayObject, ratio, __library);
+ }
+ }
+
@:noCompletion private function __updateInstanceFields():Void
{
for (field in __instanceFields)
@@ -547,6 +858,55 @@ class AnimateTimeline extends Timeline
}
}
+ @:noCompletion private static function __getCachedInstanceFields(displayObject:DisplayObject):Array
+ {
+ var clazz = Type.getClass(displayObject);
+ if (clazz == null)
+ {
+ return [];
+ }
+
+ var className = Type.getClassName(clazz);
+ if (className == null)
+ {
+ return Type.getInstanceFields(clazz);
+ }
+
+ var cached = __instanceFieldsCacheByClassName.get(className);
+ if (cached == null)
+ {
+ cached = Type.getInstanceFields(clazz);
+ __instanceFieldsCacheByClassName.set(className, cached);
+ }
+
+ return cached;
+ }
+
+ @:noCompletion private static function __createFilters(filters:Array):Array
+ {
+ var result:Array = [];
+
+ for (filter in filters)
+ {
+ switch (filter)
+ {
+ case BlurFilter(blurX, blurY, quality):
+ result.push(new BlurFilter(blurX, blurY, quality));
+
+ case ColorMatrixFilter(matrix):
+ result.push(new ColorMatrixFilter(matrix));
+
+ case DropShadowFilter(distance, angle, color, alpha, blurX, blurY, strength, quality, inner, knockout, hideObject):
+ result.push(new DropShadowFilter(distance, angle, color, alpha, blurX, blurY, strength, quality, inner, knockout, hideObject));
+
+ case GlowFilter(color, alpha, blurX, blurY, strength, quality, inner, knockout):
+ result.push(new GlowFilter(color, alpha, blurX, blurY, strength, quality, inner, knockout));
+ }
+ }
+
+ return result;
+ }
+
#if hscript
@:noCompletion private function __createScriptCallback(parser:Parser, scriptSource:String):MovieClip->Void
{
diff --git a/src/swf/exporters/animate/AnimateTimelineData.hx b/src/swf/exporters/animate/AnimateTimelineData.hx
new file mode 100644
index 000000000..78908a3a5
--- /dev/null
+++ b/src/swf/exporters/animate/AnimateTimelineData.hx
@@ -0,0 +1,196 @@
+package swf.exporters.animate;
+
+import haxe.ds.Vector;
+import openfl.display.BlendMode;
+
+#if !openfl_debug
+@:fileXml('tags="haxe,release"')
+@:noDebug
+#end
+@:access(swf.exporters.animate.AnimateLibrary)
+@: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 static inline var HAS_RATIO = 1024;
+
+ 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, library:AnimateLibrary)
+ {
+ 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, library);
+ }
+ }
+ }
+
+ 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++;
+ if ((flags & HAS_NAME) != 0) position++;
+ if ((flags & HAS_BLEND_MODE) != 0) position++;
+ if ((flags & HAS_RATIO) != 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 static inline function __number(value:Dynamic):Float
+ {
+ return value != null ? value : 0;
+ }
+
+ private function __encodeFrameObject(frameObject:Dynamic, library:AnimateLibrary):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 ratio:Dynamic = Reflect.field(frameObject, "ratio");
+ 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;
+ if (ratio != null) flags |= HAS_RATIO;
+
+ // DESTROY records only have a type and an ID
+ encodedObjects.push(__number(Reflect.field(frameObject, "type")));
+ encodedObjects.push(__number(Reflect.field(frameObject, "id")));
+ encodedObjects.push(__number(Reflect.field(frameObject, "symbol")));
+ encodedObjects.push(__number(Reflect.field(frameObject, "depth")));
+ encodedObjects.push(__number(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);
+ }
+
+ // filters are rare, so they are kept parsed, the same as without compact timelines
+ if (filters != null) encodedObjects.push(__addReference(library.__parseFilters(filters)));
+ if (name != null) encodedObjects.push(__addReference(name));
+ // stored as a BlendMode, which is not a string on every target
+ if (blendMode != null) encodedObjects.push(__addReference((Std.string(blendMode) : BlendMode)));
+ // before metaData, which is only read with OpenFL 9.5 and later
+ if (ratio != null) encodedObjects.push(ratio);
+ if (metaData != null) encodedObjects.push(__addReference(metaData));
+ }
+}
diff --git a/src/swf/exporters/core/ShapeCommand.hx b/src/swf/exporters/core/ShapeCommand.hx
index 923e3f81f..9c8c4d8f7 100644
--- a/src/swf/exporters/core/ShapeCommand.hx
+++ b/src/swf/exporters/core/ShapeCommand.hx
@@ -25,6 +25,8 @@ enum ShapeCommand
EndFill;
LineStyle(thickness:Null, color:Null, alpha:Null, pixelHinting:Null, scaleMode:LineScaleMode, caps:CapsStyle, joints:JointStyle,
miterLimit:Null);
+ LineGradientStyle(fillType:GradientType, colors:Array, alphas:Array, ratios:Array, matrix:Matrix, spreadMethod:SpreadMethod,
+ interpolationMethod:InterpolationMethod, focalPointRatio:Float);
LineTo(x:Float, y:Float);
MoveTo(x:Float, y:Float);
}
diff --git a/src/swf/exporters/swflite/DynamicTextSymbol.hx b/src/swf/exporters/swflite/DynamicTextSymbol.hx
index 971f0f126..6e3042cdf 100644
--- a/src/swf/exporters/swflite/DynamicTextSymbol.hx
+++ b/src/swf/exporters/swflite/DynamicTextSymbol.hx
@@ -37,6 +37,10 @@ class DynamicTextSymbol extends SWFSymbol
public var x:Float;
public var y:Float;
+ private var resolvedFontCount:Int = -1;
+ private var resolvedFontFound:Bool;
+ private var resolvedFontName:String;
+
public function new()
{
super();
@@ -99,40 +103,52 @@ class DynamicTextSymbol extends SWFSymbol
format.font = fontName;
- var found = false;
-
- switch (format.font)
+ // fonts are only ever added, so the font to use only changes when another one is registered
+ var fontCount = Font.enumerateFonts().length;
+ if (resolvedFontCount != fontCount)
{
- case "_sans", "_serif", "_typewriter", "", null:
- found = true;
+ var found = false;
+
+ switch (format.font)
+ {
+ case "_sans", "_serif", "_typewriter", "", null:
+ found = true;
+
+ default:
+ for (font in Font.enumerateFonts())
+ {
+ if (font.fontName == format.font)
+ {
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found)
+ {
+ var alpha = ~/[^a-zA-Z]+/g;
+ var spaces = ~/\s/g;
- default:
for (font in Font.enumerateFonts())
{
- if (font.fontName == format.font)
+ if (alpha.replace(font.fontName, "").substr(0, fontName.length) == spaces.replace(fontName, ""))
{
+ format.font = font.fontName;
found = true;
break;
}
}
- }
-
- if (!found)
- {
- var alpha = ~/[^a-zA-Z]+/g;
- var spaces = ~/\s/g;
-
- for (font in Font.enumerateFonts())
- {
- if (alpha.replace(font.fontName, "").substr(0, fontName.length) == spaces.replace(fontName, ""))
- {
- format.font = font.fontName;
- found = true;
- break;
- }
}
+
+ resolvedFontCount = fontCount;
+ resolvedFontFound = found;
+ resolvedFontName = format.font;
}
+ format.font = resolvedFontName;
+ var found = resolvedFontFound;
+
if (found)
{
textField.embedFonts = true;
diff --git a/src/swf/exporters/swflite/ShapeSymbol.hx b/src/swf/exporters/swflite/ShapeSymbol.hx
index 706423294..1cedcdb8b 100644
--- a/src/swf/exporters/swflite/ShapeSymbol.hx
+++ b/src/swf/exporters/swflite/ShapeSymbol.hx
@@ -83,6 +83,12 @@ class ShapeSymbol extends SWFSymbol
graphics.lineStyle();
}
+ case LineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ #if flash
+ var colors:Array = cast colors;
+ #end
+ graphics.lineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio);
+
case LineTo(x, y):
graphics.lineTo(x, y);
diff --git a/src/swf/exporters/swflite/timeline/SymbolTimeline.hx b/src/swf/exporters/swflite/timeline/SymbolTimeline.hx
index ed1e8d294..aab235259 100644
--- a/src/swf/exporters/swflite/timeline/SymbolTimeline.hx
+++ b/src/swf/exporters/swflite/timeline/SymbolTimeline.hx
@@ -39,6 +39,8 @@ import hscript.Parser;
@:access(openfl.geom.ColorTransform)
class SymbolTimeline extends Timeline
{
+ @:noCompletion private static var __instanceFieldsCacheByClassName:Map> = new Map();
+
#if openfljs
@:noCompletion private static var __useParentFPS:Bool;
#else
@@ -210,7 +212,8 @@ class SymbolTimeline extends Timeline
{
if (activeInstance.displayObject != null
&& activeInstance.characterID == frameObject.symbol
- && activeInstance.depth == frameObject.depth)
+ && activeInstance.depth == frameObject.depth
+ && activeInstance.clipDepth == frameObject.clipDepth)
{
// TODO: Fix duplicates in exporter
instance = activeInstance;
@@ -256,7 +259,7 @@ class SymbolTimeline extends Timeline
}
#if !openfljs
- __instanceFields = Type.getInstanceFields(Type.getClass(movieClip));
+ __instanceFields = __getCachedInstanceFields(movieClip);
#end
enterFrame(1);
@@ -360,8 +363,12 @@ class SymbolTimeline extends Timeline
{
if (targetDepth > mask.depth && targetDepth <= mask.clipDepth)
{
- #if (openfl >= "9.5.0" && !flash)
+ #if (openfl >= "9.5.0")
+ #if flash
+ child.mask = mask.displayObject;
+ #else
child.clippingLayer = mask.displayObject;
+ #end
#else
child.mask = mask.displayObject;
#end
@@ -376,6 +383,13 @@ class SymbolTimeline extends Timeline
}
}
+ #if !flash
+ for (mask in currentMasks)
+ {
+ __markTimelineMask(mask.displayObject);
+ }
+ #end
+
var child:DisplayObject;
var i = currentInstances.length;
var length = __movieClip.numChildren;
@@ -419,6 +433,18 @@ class SymbolTimeline extends Timeline
return a.depth - b.depth;
}
+ #if !flash
+ @:noCompletion private inline function __markTimelineMask(displayObject:DisplayObject):Void
+ {
+ // SWF clipDepth>0 placements are masks even before a child exists in their depth range.
+ if (!displayObject.__isMask)
+ {
+ displayObject.__isMask = true;
+ displayObject.__setRenderDirty();
+ }
+ }
+ #end
+
@:noCompletion private function __updateDisplayObject(displayObject:DisplayObject, frameObject:FrameObject, reset:Bool = false):Void
{
if (displayObject == null) return;
@@ -437,7 +463,7 @@ class SymbolTimeline extends Timeline
{
displayObject.transform.colorTransform = frameObject.colorTransform;
}
- else if (reset && !displayObject.transform.colorTransform.__isDefault(true))
+ else if (reset #if !flash && !displayObject.transform.colorTransform.__isDefault(true) #end)
{
displayObject.transform.colorTransform = new ColorTransform();
}
@@ -511,6 +537,30 @@ class SymbolTimeline extends Timeline
}
}
}
+
+ @:noCompletion private static function __getCachedInstanceFields(displayObject:DisplayObject):Array
+ {
+ var clazz = Type.getClass(displayObject);
+ if (clazz == null)
+ {
+ return [];
+ }
+
+ var className = Type.getClassName(clazz);
+ if (className == null)
+ {
+ return Type.getInstanceFields(clazz);
+ }
+
+ var cached = __instanceFieldsCacheByClassName.get(className);
+ if (cached == null)
+ {
+ cached = Type.getInstanceFields(clazz);
+ __instanceFieldsCacheByClassName.set(className, cached);
+ }
+
+ return cached;
+ }
}
#if !openfl_debug
diff --git a/src/swf/runtime/MorphShape.hx b/src/swf/runtime/MorphShape.hx
index aab8d50d8..8cd4c0eec 100644
--- a/src/swf/runtime/MorphShape.hx
+++ b/src/swf/runtime/MorphShape.hx
@@ -69,6 +69,12 @@ class MorphShape extends openfl.display.Shape
graphics.lineStyle();
}
+ case LineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ #if flash
+ var colors:Array = cast colors;
+ #end
+ graphics.lineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio);
+
case LineTo(x, y):
graphics.lineTo(x, y);
diff --git a/src/swf/runtime/Shape.hx b/src/swf/runtime/Shape.hx
index a8afa806e..680f9a5a1 100644
--- a/src/swf/runtime/Shape.hx
+++ b/src/swf/runtime/Shape.hx
@@ -49,6 +49,12 @@ class Shape extends openfl.display.Shape
graphics.lineStyle();
}
+ case LineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio):
+ #if flash
+ var colors:Array = cast colors;
+ #end
+ graphics.lineGradientStyle(fillType, colors, alphas, ratios, matrix, spreadMethod, interpolationMethod, focalPointRatio);
+
case LineTo(x, y):
graphics.lineTo(x, y);
diff --git a/src/swf/tags/TagDefineFont2.hx b/src/swf/tags/TagDefineFont2.hx
index ec0a45e85..e190fda1e 100644
--- a/src/swf/tags/TagDefineFont2.hx
+++ b/src/swf/tags/TagDefineFont2.hx
@@ -43,6 +43,7 @@ class TagDefineFont2 extends TagDefineFont implements IDefinitionTag
override public function parse(data:SWFData, length:Int, version:Int, async:Bool = false):Void
{
+ var start:Int = data.position;
characterId = data.readUI16();
var flags:Int = data.readUI8();
hasLayout = ((flags & 0x80) != 0);
@@ -82,6 +83,16 @@ class TagDefineFont2 extends TagDefineFont implements IDefinitionTag
codeTable.push(wideCodes ? data.readUI16() : data.readUI8());
}
}
+ else
+ {
+ // Flash may write a CodeTableOffset even when there are no glyphs.
+ // Without glyphs to kern, the layout that follows is 8 bytes.
+ var remaining:Int = length - (data.position - start);
+ if (remaining > (hasLayout ? 8 : 0))
+ {
+ data.skipBytes(wideOffsets ? 4 : 2);
+ }
+ }
if (hasLayout)
{
ascent = data.readUI16();
@@ -103,7 +114,6 @@ class TagDefineFont2 extends TagDefineFont implements IDefinitionTag
}
else
{
- data.skipBytes(2);
ascent = 0;
descent = 0;
leading = 0;
diff --git a/src/swf/tags/TagDefineMorphShape.hx b/src/swf/tags/TagDefineMorphShape.hx
index eaba3d8ad..b9fa38c23 100644
--- a/src/swf/tags/TagDefineMorphShape.hx
+++ b/src/swf/tags/TagDefineMorphShape.hx
@@ -9,6 +9,7 @@ import swf.data.SWFRectangle;
import swf.data.SWFShape;
import swf.data.SWFShapeRecord;
import swf.data.SWFShapeRecordCurvedEdge;
+import swf.data.SWFShapeRecordEnd;
import swf.data.SWFShapeRecordStraightEdge;
import swf.data.SWFShapeRecordStyleChange;
import swf.exporters.core.IShapeExporter;
@@ -32,8 +33,6 @@ class TagDefineMorphShape implements IDefinitionTag
public var morphLineStyles(default, null):Array;
public var exportHandler:IShapeExporter;
- private var exportShape:SWFShape;
-
public function new()
{
type = TYPE;
@@ -43,8 +42,6 @@ class TagDefineMorphShape implements IDefinitionTag
morphFillStyles = new Array();
morphLineStyles = new Array();
-
- exportShape = new SWFShape();
}
public function parse(data:SWFData, length:Int, version:Int, async:Bool = false):Void
@@ -133,114 +130,166 @@ class TagDefineMorphShape implements IDefinitionTag
public function export(ratio:Float = 0):Void
{
- var i:Int;
- var j:Int = 0;
- // var exportShape:SWFShape = new SWFShape();
-
- exportShape.records.splice(0, exportShape.records.length);
- exportShape.fillStyles.splice(0, exportShape.fillStyles.length);
- exportShape.lineStyles.splice(0, exportShape.lineStyles.length);
+ getMorphedShape(ratio).export(exportHandler);
+ }
- var numEdges:Int = startEdges.records.length;
- for (i in 0...numEdges)
+ // Interpolates the start and end edges, where a ratio of 0 is the start and 1 is the end
+ public function getMorphedShape(ratio:Float = 0):SWFShape
+ {
+ var shape:SWFShape = new SWFShape();
+ for (fillStyle in morphFillStyles)
+ {
+ shape.fillStyles.push(fillStyle.getMorphedFillStyle(ratio));
+ }
+ for (lineStyle in morphLineStyles)
{
- var startRecord:SWFShapeRecord = startEdges.records[i];
- // Ignore start records that are style change records and don't have moveTo
- // The end record index is not incremented, because end records do not have
- // style change records without moveTo's.
- // if(startRecord.type == SWFShapeRecord.TYPE_STYLECHANGE && !cast(startRecord,SWFShapeRecordStyleChange).stateMoveTo) {
- // exportShape.records.push(startRecord.clone());
- /* //Also increment the endEdges (Prevent errors)
- j++; */
- // continue;
- // }
+ shape.lineStyles.push(lineStyle.getMorphedLineStyle(ratio));
+ }
- var endRecord:SWFShapeRecord = endEdges.records[j++];
+ // End edges only have style change records for moves, and either side can move without the
+ // other, so both pen positions are tracked. Positions are rounded instead of deltas, so paths
+ // still close at any ratio.
+ var startRecords:Array = startEdges.records;
+ var endRecords:Array = endEdges.records;
+ var startIndex:Int = 0;
+ var endIndex:Int = 0;
+ var startX:Int = 0;
+ var startY:Int = 0;
+ var endX:Int = 0;
+ var endY:Int = 0;
+ var x:Int = 0;
+ var y:Int = 0;
- var exportRecord:SWFShapeRecord = null;
- // It is possible for an edge to change type over the course of a morph sequence.
- // A straight edge can become a curved edge and vice versa
- // Convert straight edge to curved edge, if needed:
- if (startRecord.type == SWFShapeRecord.TYPE_CURVEDEDGE && endRecord.type == SWFShapeRecord.TYPE_STRAIGHTEDGE)
+ while (startIndex < startRecords.length && startRecords[startIndex].type != SWFShapeRecord.TYPE_END)
+ {
+ var startRecord:SWFShapeRecord = startRecords[startIndex];
+ var endRecord:SWFShapeRecord = endIndex < endRecords.length ? endRecords[endIndex] : null;
+
+ if (startRecord.type == SWFShapeRecord.TYPE_STYLECHANGE)
{
- endRecord = convertToCurvedEdge(cast(endRecord, SWFShapeRecordStraightEdge));
+ var startStyleChange:SWFShapeRecordStyleChange = cast startRecord;
+ var styleChange:SWFShapeRecordStyleChange = cast startStyleChange.clone();
+ var moved:Bool = startStyleChange.stateMoveTo;
+ if (startStyleChange.stateMoveTo)
+ {
+ startX = startStyleChange.moveDeltaX;
+ startY = startStyleChange.moveDeltaY;
+ }
+ if (endRecord != null && endRecord.type == SWFShapeRecord.TYPE_STYLECHANGE)
+ {
+ var endStyleChange:SWFShapeRecordStyleChange = cast endRecord;
+ if (endStyleChange.stateMoveTo)
+ {
+ endX = endStyleChange.moveDeltaX;
+ endY = endStyleChange.moveDeltaY;
+ moved = true;
+ }
+ endIndex++;
+ }
+ if (moved)
+ {
+ x = interpolate(startX, endX, ratio);
+ y = interpolate(startY, endY, ratio);
+ styleChange.stateMoveTo = true;
+ styleChange.moveDeltaX = x;
+ styleChange.moveDeltaY = y;
+ }
+ shape.records.push(styleChange);
+ startIndex++;
}
- else if (startRecord.type == SWFShapeRecord.TYPE_STRAIGHTEDGE && endRecord.type == SWFShapeRecord.TYPE_CURVEDEDGE)
+ else if (endRecord != null && endRecord.type == SWFShapeRecord.TYPE_STYLECHANGE)
{
- startRecord = convertToCurvedEdge(cast(startRecord, SWFShapeRecordStraightEdge));
+ var endStyleChange:SWFShapeRecordStyleChange = cast endRecord;
+ if (endStyleChange.stateMoveTo)
+ {
+ endX = endStyleChange.moveDeltaX;
+ endY = endStyleChange.moveDeltaY;
+ var moveTo:SWFShapeRecordStyleChange = new SWFShapeRecordStyleChange();
+ x = interpolate(startX, endX, ratio);
+ y = interpolate(startY, endY, ratio);
+ moveTo.stateMoveTo = true;
+ moveTo.moveDeltaX = x;
+ moveTo.moveDeltaY = y;
+ shape.records.push(moveTo);
+ }
+ endIndex++;
}
-
- switch (startRecord.type)
+ else if (endRecord != null && endRecord.isEdgeRecord)
{
- case SWFShapeRecord.TYPE_STYLECHANGE:
- var startStyleChange:SWFShapeRecordStyleChange = cast startRecord.clone();
- startStyleChange.stateMoveTo = true;
- if (endRecord.type == SWFShapeRecord.TYPE_STYLECHANGE)
- {
- var endStyleChange:SWFShapeRecordStyleChange = cast endRecord;
- startStyleChange.moveDeltaX += Std.int((endStyleChange.moveDeltaX - startStyleChange.moveDeltaX) * ratio);
- startStyleChange.moveDeltaY += Std.int((endStyleChange.moveDeltaY - startStyleChange.moveDeltaY) * ratio);
- }
- else
- {
- startStyleChange.moveDeltaX += Std.int((-startStyleChange.moveDeltaX) * ratio);
- startStyleChange.moveDeltaY += Std.int((-startStyleChange.moveDeltaY) * ratio);
- j--;
- }
- exportRecord = startStyleChange;
- case SWFShapeRecord.TYPE_STRAIGHTEDGE:
- var startStraightEdge:SWFShapeRecordStraightEdge = cast startRecord.clone();
- var endStraightEdge:SWFShapeRecordStraightEdge = cast endRecord;
-
- startStraightEdge.deltaX += Std.int((endStraightEdge.deltaX - startStraightEdge.deltaX) * ratio);
- startStraightEdge.deltaY += Std.int((endStraightEdge.deltaY - startStraightEdge.deltaY) * ratio);
-
- if (startStraightEdge.deltaX != 0 && startStraightEdge.deltaY != 0)
- {
- startStraightEdge.generalLineFlag = true;
- startStraightEdge.vertLineFlag = false;
- }
- else
- {
- startStraightEdge.generalLineFlag = false;
- startStraightEdge.vertLineFlag = (startStraightEdge.deltaX == 0);
- }
-
- exportRecord = startStraightEdge;
- case SWFShapeRecord.TYPE_CURVEDEDGE:
- var startCurvedEdge:SWFShapeRecordCurvedEdge = cast startRecord.clone();
- var endCurvedEdge:SWFShapeRecordCurvedEdge = cast endRecord;
- startCurvedEdge.controlDeltaX += Std.int((endCurvedEdge.controlDeltaX - startCurvedEdge.controlDeltaX) * ratio);
- startCurvedEdge.controlDeltaY += Std.int((endCurvedEdge.controlDeltaY - startCurvedEdge.controlDeltaY) * ratio);
- startCurvedEdge.anchorDeltaX += Std.int((endCurvedEdge.anchorDeltaX - startCurvedEdge.anchorDeltaX) * ratio);
- startCurvedEdge.anchorDeltaY += Std.int((endCurvedEdge.anchorDeltaY - startCurvedEdge.anchorDeltaY) * ratio);
- exportRecord = startCurvedEdge;
- case SWFShapeRecord.TYPE_END:
- exportRecord = startRecord.clone();
+ var startAnchor:Array = getEdgeAnchor(startRecord, startX, startY);
+ var endAnchor:Array = getEdgeAnchor(endRecord, endX, endY);
+ var anchorX:Int = interpolate(startAnchor[0], endAnchor[0], ratio);
+ var anchorY:Int = interpolate(startAnchor[1], endAnchor[1], ratio);
+ if (startRecord.type == SWFShapeRecord.TYPE_STRAIGHTEDGE && endRecord.type == SWFShapeRecord.TYPE_STRAIGHTEDGE)
+ {
+ var straightEdge:SWFShapeRecordStraightEdge = new SWFShapeRecordStraightEdge();
+ straightEdge.deltaX = anchorX - x;
+ straightEdge.deltaY = anchorY - y;
+ straightEdge.generalLineFlag = (straightEdge.deltaX != 0 && straightEdge.deltaY != 0);
+ straightEdge.vertLineFlag = (straightEdge.deltaX == 0);
+ shape.records.push(straightEdge);
+ }
+ else
+ {
+ // An edge can change between straight and curved, so a straight edge becomes a curve
+ // with its control point in the middle
+ var startControl:Array = getEdgeControl(startRecord, startX, startY);
+ var endControl:Array = getEdgeControl(endRecord, endX, endY);
+ var controlX:Int = interpolate(startControl[0], endControl[0], ratio);
+ var controlY:Int = interpolate(startControl[1], endControl[1], ratio);
+ var curvedEdge:SWFShapeRecordCurvedEdge = new SWFShapeRecordCurvedEdge();
+ curvedEdge.controlDeltaX = controlX - x;
+ curvedEdge.controlDeltaY = controlY - y;
+ curvedEdge.anchorDeltaX = anchorX - controlX;
+ curvedEdge.anchorDeltaY = anchorY - controlY;
+ shape.records.push(curvedEdge);
+ }
+ startX = Std.int(startAnchor[0]);
+ startY = Std.int(startAnchor[1]);
+ endX = Std.int(endAnchor[0]);
+ endY = Std.int(endAnchor[1]);
+ x = anchorX;
+ y = anchorY;
+ startIndex++;
+ endIndex++;
+ }
+ else
+ {
+ throw new Error("The start and end edges of morph shape " + characterId + " do not match");
}
- exportShape.records.push(exportRecord);
- }
- for (i in 0...morphFillStyles.length)
- {
- exportShape.fillStyles.push(morphFillStyles[i].getMorphedFillStyle(ratio));
}
- for (i in 0...morphLineStyles.length)
+ shape.records.push(new SWFShapeRecordEnd());
+ return shape;
+ }
+
+ private static inline function interpolate(start:Float, end:Float, ratio:Float):Int
+ {
+ return Math.round(start + (end - start) * ratio);
+ }
+
+ private static function getEdgeAnchor(record:SWFShapeRecord, x:Int, y:Int):Array
+ {
+ if (record.type == SWFShapeRecord.TYPE_CURVEDEDGE)
{
- exportShape.lineStyles.push(morphLineStyles[i].getMorphedLineStyle(ratio));
+ var curvedEdge:SWFShapeRecordCurvedEdge = cast record;
+ return [
+ x + curvedEdge.controlDeltaX + curvedEdge.anchorDeltaX,
+ y + curvedEdge.controlDeltaY + curvedEdge.anchorDeltaY
+ ];
}
- exportShape.export(exportHandler);
+ var straightEdge:SWFShapeRecordStraightEdge = cast record;
+ return [x + straightEdge.deltaX, y + straightEdge.deltaY];
}
- private function convertToCurvedEdge(straightEdge:SWFShapeRecordStraightEdge):SWFShapeRecordCurvedEdge
+ private static function getEdgeControl(record:SWFShapeRecord, x:Int, y:Int):Array
{
- var curvedEdge:SWFShapeRecordCurvedEdge = new SWFShapeRecordCurvedEdge();
- curvedEdge.controlDeltaX = Std.int(straightEdge.deltaX / 2);
- curvedEdge.controlDeltaY = Std.int(straightEdge.deltaY / 2);
- // curvedEdge.anchorDeltaX = straightEdge.deltaX;
- // curvedEdge.anchorDeltaY = straightEdge.deltaY;
- curvedEdge.anchorDeltaX = Std.int(straightEdge.deltaX / 2);
- curvedEdge.anchorDeltaY = Std.int(straightEdge.deltaY / 2);
- return curvedEdge;
+ if (record.type == SWFShapeRecord.TYPE_CURVEDEDGE)
+ {
+ var curvedEdge:SWFShapeRecordCurvedEdge = cast record;
+ return [x + curvedEdge.controlDeltaX, y + curvedEdge.controlDeltaY];
+ }
+ var straightEdge:SWFShapeRecordStraightEdge = cast record;
+ return [x + straightEdge.deltaX / 2, y + straightEdge.deltaY / 2];
}
public function toString(indent:Int = 0):String
diff --git a/src/swf/utils/BitArray.hx b/src/swf/utils/BitArray.hx
index 269da30f2..0789024d9 100644
--- a/src/swf/utils/BitArray.hx
+++ b/src/swf/utils/BitArray.hx
@@ -1,7 +1,6 @@
package swf.utils;
import openfl.utils.ByteArray;
-import haxe.crypto.BaseCode;
class BitArray extends ByteArrayData
{
@@ -115,40 +114,24 @@ class BitArray extends ByteArrayData
public function calculateMaxBits(signed:Bool, values:Array):Int
{
- var b:Int = 0;
- var vmax:Int = SWFData.MIN_INT_VALUE;
- if (!signed)
+ var bits:Int = 0;
+ for (value in values)
{
- for (usvalue in values)
+ if (value == 0)
{
- b |= usvalue;
+ continue;
}
- }
- else
- {
- for (svalue in values)
+ // A signed value needs the bits of its magnitude plus a sign bit
+ var magnitude:Int = (signed && value < 0) ? ~value : value;
+ var valueBits:Int = signed ? 1 : 0;
+ while (magnitude != 0)
{
- if (svalue >= 0)
- {
- b |= svalue;
- }
- else
- {
- b |= ~svalue << 1;
- }
- if (vmax < svalue)
- {
- vmax = svalue;
- }
+ magnitude >>>= 1;
+ valueBits++;
}
- }
- var bits:Int = 0;
- if (b > 0)
- {
- bits = BaseCode.encode(Std.string(b), "01").length;
- if (signed && vmax > 0 && BaseCode.encode(Std.string(vmax), "01").length >= bits)
+ if (valueBits > bits)
{
- bits++;
+ bits = valueBits;
}
}
return bits;
diff --git a/templates/animate/MovieClip.mtt b/templates/animate/MovieClip.mtt
index 5aeb90c31..b1cf6bef8 100644
--- a/templates/animate/MovieClip.mtt
+++ b/templates/animate/MovieClip.mtt
@@ -7,11 +7,19 @@ class ::CLASS_NAME:: extends ::if BASE_CLASS_NAME::::BASE_CLASS_NAME::::else::#i
::foreach CLASS_PROPERTIES::@:keep ::if hidden::@:noCompletion @:dox(hide) ::end::public var ::name::(default, null):::type::;
::end::
+ @:noCompletion private static function __init__():Void
+ {
+ swf.exporters.animate.AnimateLibrary.__registerGeneratedClass("::UUID::", ::SYMBOL_ID::, ::CLASS_NAME::);
+ }
+
public function new()
{
- var library = swf.exporters.animate.AnimateLibrary.get("::UUID::");
- var symbol = library.symbols.get(::SYMBOL_ID::);
- symbol.__init(library);
+ if (!swf.exporters.animate.AnimateSymbol.__initPending(::CLASS_NAME::))
+ {
+ var library = swf.exporters.animate.AnimateLibrary.get("::UUID::");
+ var symbol = library.symbols.get(::SYMBOL_ID::);
+ symbol.__init(library);
+ }
super();
}
diff --git a/templates/animate/SimpleButton.mtt b/templates/animate/SimpleButton.mtt
index 95fec87dc..d8002a6e0 100644
--- a/templates/animate/SimpleButton.mtt
+++ b/templates/animate/SimpleButton.mtt
@@ -4,11 +4,19 @@ package ::PACKAGE_NAME::;
class ::CLASS_NAME:: extends ::if BASE_CLASS_NAME::::BASE_CLASS_NAME::::else::#if flash flash.display.SimpleButton.SimpleButton2 #else openfl.display.SimpleButton #end::end::
{
+ @:noCompletion private static function __init__():Void
+ {
+ swf.exporters.animate.AnimateLibrary.__registerGeneratedClass("::UUID::", ::SYMBOL_ID::, ::CLASS_NAME::);
+ }
+
public function new()
{
- var library = swf.exporters.animate.AnimateLibrary.get("::UUID::");
- var symbol = library.symbols.get(::SYMBOL_ID::);
- symbol.__init(library);
+ if (!swf.exporters.animate.AnimateSymbol.__initPending(::CLASS_NAME::))
+ {
+ var library = swf.exporters.animate.AnimateLibrary.get("::UUID::");
+ var symbol = library.symbols.get(::SYMBOL_ID::);
+ symbol.__init(library);
+ }
super();
}
diff --git a/tests/project.xml b/tests/project.xml
index e7b6bd3ac..28f1b1e1f 100644
--- a/tests/project.xml
+++ b/tests/project.xml
@@ -13,5 +13,7 @@
+
+
\ No newline at end of file
diff --git a/tests/src/TestMain.hx b/tests/src/TestMain.hx
index 99caed6cf..1a154fe19 100644
--- a/tests/src/TestMain.hx
+++ b/tests/src/TestMain.hx
@@ -9,7 +9,25 @@ class TestMain extends Sprite
super();
var runner = new Runner();
+ #if !flash
+ runner.addCase(new tests.AnimateBitmapSymbolTest());
+ #end
+ runner.addCase(new tests.AnimateGeneratedClassTest());
+ runner.addCase(new tests.AnimateLibraryTest());
+ runner.addCase(new tests.AnimateShapeSymbolTest());
+ runner.addCase(new tests.AnimateTimelineTest());
+ runner.addCase(new tests.MorphShapeTest());
+ #if !flash
+ runner.addCase(new tests.DynamicTextFontTest());
+ #end
+ runner.addCase(new tests.ShapeExportTest());
runner.addCase(new tests.ShapesTest());
+ runner.addCase(new tests.SWFDataTest());
+ #if !flash
+ runner.addCase(new tests.SWFLiteExporterTest());
+ #end
+ runner.addCase(new tests.TagDefineFont2Test());
+ runner.addCase(new tests.TimelineTest());
Report.create(runner);
diff --git a/tests/src/tests/AnimateBitmapSymbolTest.hx b/tests/src/tests/AnimateBitmapSymbolTest.hx
new file mode 100644
index 000000000..020fec4f3
--- /dev/null
+++ b/tests/src/tests/AnimateBitmapSymbolTest.hx
@@ -0,0 +1,162 @@
+package tests;
+
+import lime.graphics.Image;
+import openfl.display.BitmapData;
+import swf.exporters.animate.AnimateBitmapSymbol;
+import swf.exporters.animate.AnimateLibrary;
+import swf.exporters.animate.AnimateShapeCommand;
+import swf.exporters.animate.AnimateShapeSymbol;
+import utest.Assert;
+import utest.Test;
+
+@:access(lime.utils.AssetLibrary)
+@:access(openfl.display.BitmapData)
+@:access(openfl.display.Graphics)
+@:access(openfl.display._internal.DrawCommandBuffer)
+@:access(swf.exporters.animate.AnimateBitmapSymbol)
+@:access(swf.exporters.animate.AnimateLibrary)
+@:access(swf.exporters.animate.AnimateShapeSymbol)
+class AnimateBitmapSymbolTest extends Test
+{
+ // these tests read OpenFL's draw commands and BitmapData.readable, which Flash does not have
+ #if !flash
+ public function testHardwareBitmapCacheIsOptIn():Void
+ {
+ var library = createLibrary("bitmap-test");
+ var symbol = addBitmap(library, 1, "bitmap.png");
+ var image = library.cachedImages.get(symbol.path);
+
+ var first = symbol.__createBitmapData(library);
+ var second = symbol.__createBitmapData(library);
+ Assert.notNull(first);
+ Assert.notNull(second);
+
+ #if (lime && swf_hardware_bitmap_cache)
+ // a bitmap only used directly shares one BitmapData, without its CPU image
+ Assert.isTrue(first == second);
+ Assert.isFalse(first.readable);
+ Assert.isFalse(library.cachedImages.exists(symbol.path));
+ Assert.isTrue(first.image == image);
+ #else
+ Assert.isFalse(first == second);
+ Assert.isTrue(first.readable);
+ Assert.isTrue(second.readable);
+ Assert.isTrue(library.cachedImages.exists(symbol.path));
+ #end
+ }
+
+ public function testShapeBitmapFillsStayReadable():Void
+ {
+ var library = createLibrary("shape-bitmap-test");
+ var bitmapSymbol = addBitmap(library, 1, "shape-bitmap.png");
+ var image = library.cachedImages.get(bitmapSymbol.path);
+ var shapeSymbol = addShape(library, 2, bitmapSymbol.id);
+
+ var bitmapData:BitmapData = cast shapeSymbol.__createObject(library).graphics.__commands.o[0];
+ Assert.notNull(bitmapData);
+ Assert.isTrue(bitmapData.readable);
+
+ #if (lime && swf_hardware_bitmap_cache)
+ // OpenFL draws shape paths in software, so the fill keeps its pixels and is shared
+ Assert.isTrue(bitmapData.image == image);
+ var directBitmapData = bitmapSymbol.__createBitmapData(library);
+ Assert.isTrue(directBitmapData.readable);
+ Assert.isTrue(bitmapData == directBitmapData);
+ Assert.isTrue(library.cachedImages.exists(bitmapSymbol.path));
+ #else
+ Assert.isFalse(bitmapData == bitmapSymbol.__createBitmapData(library));
+ #end
+ }
+
+ public function testBitmapUsedByShapeStaysReadableWhenUsedDirectlyFirst():Void
+ {
+ #if (lime && swf_hardware_bitmap_cache)
+ var library = createLibrary("shared-bitmap-test");
+ var bitmapSymbol = addBitmap(library, 1, "shared-bitmap.png");
+ var shapeSymbol = addShape(library, 2, bitmapSymbol.id);
+
+ // done when the library loads, so the result does not depend on what is created first
+ library.__markShapeBitmapsReadable();
+
+ var directBitmapData = bitmapSymbol.__createBitmapData(library);
+ Assert.isTrue(directBitmapData.readable);
+
+ var shapeBitmapData:BitmapData = cast shapeSymbol.__createObject(library).graphics.__commands.o[0];
+ Assert.isTrue(shapeBitmapData == directBitmapData);
+ #else
+ Assert.isTrue(true);
+ #end
+ }
+
+ public function testCompactShapeBitmapsStayReadable():Void
+ {
+ #if (lime && swf_hardware_bitmap_cache)
+ var library = createLibrary("compact-shape-bitmap-test");
+ var bitmapSymbol = addBitmap(library, 1, "compact-shape-bitmap.png");
+
+ // shapes loaded from a library keep compact commands
+ var shapeSymbol = new AnimateShapeSymbol();
+ shapeSymbol.id = 2;
+ var data:Array = [0, bitmapSymbol.id, null, true, true, 8, 0, 0, 7, 640, 0, 7, 640, 640, 7, 0, 640, 5];
+ shapeSymbol.__setCompactCommands(data);
+ library.symbols.set(shapeSymbol.id, shapeSymbol);
+
+ library.__markShapeBitmapsReadable();
+ Assert.isTrue(bitmapSymbol.__createBitmapData(library).readable);
+ #else
+ Assert.isTrue(true);
+ #end
+ }
+
+ public function testMorphShapeBitmapsStayReadable():Void
+ {
+ #if (lime && swf_hardware_bitmap_cache)
+ var library = createLibrary("morph-shape-bitmap-test");
+ var bitmapSymbol = addBitmap(library, 1, "morph-shape-bitmap.png");
+
+ var data:Array = [0, bitmapSymbol.id, null, true, true, 8, 0, 0, 7, 640, 0, 7, 640, 640, 7, 0, 640, 5];
+ var morphShape = library.__parseMorphShape({id: 2, ratios: [0], shapes: [data]});
+ library.symbols.set(morphShape.id, morphShape);
+
+ library.__markShapeBitmapsReadable();
+ Assert.isTrue(bitmapSymbol.__createBitmapData(library).readable);
+ #else
+ Assert.isTrue(true);
+ #end
+ }
+
+ private static function createLibrary(id:String):AnimateLibrary
+ {
+ var library = new AnimateLibrary(id, id);
+ library.bitmapSymbols = [];
+ library.symbols = new Map();
+ return library;
+ }
+
+ private static function addBitmap(library:AnimateLibrary, id:Int, path:String):AnimateBitmapSymbol
+ {
+ var symbol = new AnimateBitmapSymbol();
+ symbol.id = id;
+ symbol.path = path;
+ library.cachedImages.set(path, new Image(null, 0, 0, 32, 32, 0xFF336699));
+ library.symbols.set(id, symbol);
+ return symbol;
+ }
+
+ private static function addShape(library:AnimateLibrary, id:Int, bitmapID:Int):AnimateShapeSymbol
+ {
+ var symbol = new AnimateShapeSymbol();
+ symbol.id = id;
+ symbol.commands = [
+ BeginBitmapFill(bitmapID, null, true, true),
+ MoveTo(0, 0),
+ LineTo(32, 0),
+ LineTo(32, 32),
+ LineTo(0, 32),
+ EndFill
+ ];
+ library.symbols.set(id, symbol);
+ return symbol;
+ }
+ #end
+}
diff --git a/tests/src/tests/AnimateGeneratedClassTest.hx b/tests/src/tests/AnimateGeneratedClassTest.hx
new file mode 100644
index 000000000..2d77c77bf
--- /dev/null
+++ b/tests/src/tests/AnimateGeneratedClassTest.hx
@@ -0,0 +1,65 @@
+package tests;
+
+import openfl.display.MovieClip;
+import openfl.geom.Rectangle;
+import swf.exporters.animate.AnimateFrame;
+import swf.exporters.animate.AnimateLibrary;
+import swf.exporters.animate.AnimateSpriteSymbol;
+import swf.exporters.animate.AnimateSymbol;
+import utest.Assert;
+import utest.Test;
+
+@:access(swf.exporters.animate)
+class AnimateGeneratedClassTest extends Test
+{
+ #if !flash
+ public function testGeneratedClassInitializesFromInstantiatingLibrary():Void
+ {
+ var libraryA = createLibrary(GeneratedTestClip.UUID, new Rectangle(1, 2, 3, 4));
+ var libraryB = createLibrary("generated-class-test-b", new Rectangle(5, 6, 7, 8));
+
+ // both libraries export the same class, which was generated for library A
+ var clipB:MovieClip = cast libraryB.symbols.get(1).__createObject(libraryB);
+ Assert.equals(Type.getClassName(GeneratedTestClip), Type.getClassName(Type.getClass(clipB)));
+ Assert.same(new Rectangle(5, 6, 7, 8), clipB.scale9Grid);
+
+ var clipA:MovieClip = cast libraryA.symbols.get(1).__createObject(libraryA);
+ Assert.same(new Rectangle(1, 2, 3, 4), clipA.scale9Grid);
+
+ // created without a library, the generated class uses its own library
+ var clip = new GeneratedTestClip();
+ Assert.same(new Rectangle(1, 2, 3, 4), clip.scale9Grid);
+ Assert.isNull(AnimateSymbol.__pendingSymbol);
+ }
+
+ public function testPrefixedGeneratedClass():Void
+ {
+ // with a prefix, the generated class is not named after the linkage, so it is found by library and symbol
+ var library = createLibrary(LibPrefixedTestClip.UUID, new Rectangle(1, 2, 3, 4), "tests.PrefixedTestClip");
+ var clip:MovieClip = cast library.symbols.get(1).__createObject(library);
+ Assert.equals(Type.getClassName(LibPrefixedTestClip), Type.getClassName(Type.getClass(clip)));
+ Assert.same(new Rectangle(1, 2, 3, 4), clip.scale9Grid);
+
+ // another library with the same linkage did not generate this class
+ var other = createLibrary("generated-prefix-test-other", new Rectangle(5, 6, 7, 8), "tests.PrefixedTestClip");
+ var otherClip:MovieClip = cast other.symbols.get(1).__createObject(other);
+ Assert.equals(Type.getClassName(MovieClip), Type.getClassName(Type.getClass(otherClip)));
+ Assert.same(new Rectangle(5, 6, 7, 8), otherClip.scale9Grid);
+ }
+ #end
+
+ private static function createLibrary(uuid:String, scale9Grid:Rectangle, className:String = null):AnimateLibrary
+ {
+ var symbol = new AnimateSpriteSymbol();
+ symbol.id = 1;
+ symbol.className = className != null ? className : Type.getClassName(GeneratedTestClip);
+ symbol.frames.push(new AnimateFrame());
+ symbol.scale9Grid = scale9Grid;
+
+ var library = new AnimateLibrary(uuid, uuid);
+ library.frameRate = 24;
+ library.symbols = new Map();
+ library.symbols.set(symbol.id, symbol);
+ return library;
+ }
+}
diff --git a/tests/src/tests/AnimateLibraryTest.hx b/tests/src/tests/AnimateLibraryTest.hx
new file mode 100644
index 000000000..d736607cb
--- /dev/null
+++ b/tests/src/tests/AnimateLibraryTest.hx
@@ -0,0 +1,170 @@
+package tests;
+
+import haxe.Json;
+import haxe.crypto.Crc32;
+import haxe.io.Bytes;
+import haxe.io.BytesOutput;
+import haxe.zip.Entry;
+import haxe.zip.Writer;
+import lime.media.AudioBuffer;
+import openfl.utils.AssetType;
+import swf.exporters.animate.AnimateLibrary;
+import utest.Assert;
+import utest.Async;
+import utest.Test;
+
+@:access(lime.utils.AssetLibrary)
+@:access(swf.exporters.animate.AnimateLibrary)
+class AnimateLibraryTest extends Test
+{
+ public function testLibraryWithoutUUID():Void
+ {
+ var library = new AnimateLibrary("animate-library-test");
+ Assert.equals(library, AnimateLibrary.get("animate-library-test"));
+
+ library.unload();
+ Assert.isNull(AnimateLibrary.get("animate-library-test"));
+ }
+
+ public function testLinkedSoundLookup():Void
+ {
+ var library = new AnimateLibrary("animate-library-sound-test");
+ var buffer = new AudioBuffer();
+ library.soundClassNames.set("TestSound", "sounds/1.wav");
+ library.cachedAudioBuffers.set("sounds/1.wav", buffer);
+ library.symbols = new Map();
+ library.symbolsByClassName = new Map();
+ library.bitmapSymbols = [];
+
+ Assert.equals(buffer, library.getAudioBuffer("TestSound"));
+ Assert.isTrue(library.exists("TestSound", cast AssetType.SOUND));
+ Assert.isTrue(library.exists("TestSound", cast AssetType.MUSIC));
+ Assert.isTrue(library.exists("TestSound", null));
+ Assert.isFalse(library.exists("TestSound", cast AssetType.MOVIE_CLIP));
+ Assert.isFalse(library.exists("TestSound", cast AssetType.IMAGE));
+
+ var loaded:AudioBuffer = null;
+ library.loadAudioBuffer("TestSound").onComplete(function(value) loaded = value);
+ Assert.equals(buffer, loaded);
+
+ library.unload();
+ }
+
+ #if !flash
+ public function testBundledSoundBytesAreDecoded():Void
+ {
+ // on HTML5, Lime leaves bundled sounds as bytes
+ var library = new AnimateLibrary("animate-library-bytes-test");
+ library.soundClassNames.set("TestSound", "sounds/1.wav");
+ library.cachedBytes.set("sounds/1.wav", createWAV());
+
+ assertDecodedWAV(library.getAudioBuffer("TestSound"));
+ Assert.isFalse(library.cachedBytes.exists("sounds/1.wav"));
+
+ library.unload();
+ }
+
+ public function testLoadLinkedSound(async:Async):Void
+ {
+ var data = {
+ version: 0.1,
+ frameRate: 24,
+ root: -1,
+ uuid: "animate-library-load-test",
+ symbols: ([
+ {type: 7, id: 1, className: "TestSound", path: "sounds/1.wav"},
+ // unknown symbol types are skipped, without registering the previous symbol again
+ {type: 99, id: 2, className: "Unknown"}
+ ] : Array)
+ };
+ var manifest = {
+ name: null,
+ rootPath: null,
+ version: 3,
+ libraryType: "swf.exporters.animate.AnimateLibrary",
+ libraryArgs: ["data.json", "animate-library-load-test"],
+ assets: [{path: "data.json", type: "TEXT"}, {path: "sounds/1.wav", type: "SOUND"}]
+ };
+
+ var zip = createZip([
+ "library.json" => Bytes.ofString(Json.stringify(manifest)),
+ "data.json" => Bytes.ofString(Json.stringify(data)),
+ "sounds/1.wav" => createWAV()
+ ]);
+
+ AnimateLibrary.loadFromBytes(zip).onComplete(function(value)
+ {
+ var library:AnimateLibrary = cast value;
+ assertDecodedWAV(library.getAudioBuffer("TestSound"));
+ Assert.isTrue(library.exists("TestSound", cast AssetType.SOUND));
+ Assert.isFalse(library.symbolsByClassName.exists("TestSound"));
+ Assert.isFalse(library.symbolsByClassName.exists("Unknown"));
+ Assert.isFalse(library.symbols.exists(2));
+ library.unload();
+ async.done();
+ }).onError(function(error)
+ {
+ Assert.fail("Could not load library: " + error);
+ async.done();
+ });
+ }
+
+ private static function assertDecodedWAV(buffer:AudioBuffer):Void
+ {
+ Assert.notNull(buffer);
+ #if (js && html5)
+ // Howler plays the sound on HTML5, and does not expose its samples
+ Assert.notNull(buffer.src);
+ #else
+ Assert.equals(11025, buffer.sampleRate);
+ Assert.equals(1, buffer.channels);
+ #end
+ }
+
+ private static function createWAV():Bytes
+ {
+ var samples = 1102;
+ var output = new BytesOutput();
+ output.writeString("RIFF");
+ output.writeInt32(36 + samples * 2);
+ output.writeString("WAVE");
+ output.writeString("fmt ");
+ output.writeInt32(16);
+ output.writeUInt16(1);
+ output.writeUInt16(1);
+ output.writeInt32(11025);
+ output.writeInt32(11025 * 2);
+ output.writeUInt16(2);
+ output.writeUInt16(16);
+ output.writeString("data");
+ output.writeInt32(samples * 2);
+ for (i in 0...samples)
+ {
+ output.writeInt16(Std.int(Math.sin(i / 4) * 8000));
+ }
+ return output.getBytes();
+ }
+
+ private static function createZip(files:Map):Bytes
+ {
+ var entries = new List();
+ for (path in files.keys())
+ {
+ var bytes = files.get(path);
+ entries.add({
+ fileName: path,
+ fileSize: bytes.length,
+ fileTime: Date.now(),
+ compressed: false,
+ dataSize: 0,
+ data: bytes,
+ crc32: Crc32.make(bytes)
+ });
+ }
+
+ var output = new BytesOutput();
+ new Writer(output).write(entries);
+ return output.getBytes();
+ }
+ #end
+}
diff --git a/tests/src/tests/AnimateShapeSymbolTest.hx b/tests/src/tests/AnimateShapeSymbolTest.hx
new file mode 100644
index 000000000..0a3a94e9c
--- /dev/null
+++ b/tests/src/tests/AnimateShapeSymbolTest.hx
@@ -0,0 +1,75 @@
+package tests;
+
+import openfl.geom.Matrix;
+import swf.exporters.animate.AnimateLibrary;
+import swf.exporters.animate.AnimateShapeCommand;
+import swf.exporters.animate.AnimateShapeSymbol;
+import utest.Assert;
+import utest.Test;
+
+@:access(swf.exporters.animate.AnimateShapeSymbol)
+#if !flash
+@:access(openfl.display.Graphics)
+@:access(openfl.display._internal.DrawCommandBuffer)
+#end
+class AnimateShapeSymbolTest extends Test
+{
+ public function testCompactCommandsRenderLikeLegacyCommands():Void
+ {
+ var colors = [0x123456, 0xABCDEF];
+ var alphas = [1.0, 0.25];
+ var ratios = [0, 255];
+ var matrix = new Matrix(1, 0.25, -0.5, 2, 10, 20);
+
+ var legacy = new AnimateShapeSymbol();
+ legacy.commands = [
+ BeginFill(0x102030, 0.75),
+ MoveTo(1, 2),
+ LineStyle(2.5, 0x405060, 0.5, true, 2, 1, 2, 3),
+ LineGradientStyle(1, colors, alphas, ratios, matrix, 2, 1, -0.5),
+ LineTo(3, 4),
+ CurveTo(5, 6, 7, 8),
+ LineStyle(null, null, null, null, null, null, null, null),
+ BeginGradientFill(0, colors, alphas, ratios, matrix, 1, 0, 0.25),
+ EndFill
+ ];
+
+ var compact = new AnimateShapeSymbol();
+ var data:Array = [
+ 1, 0x102030, 0.75,
+ 8, 20, 40,
+ 6, 2.5, 0x405060, 0.5, true, 2, 1, 2, 3,
+ 9, 1, colors, alphas, ratios, [1, 0.25, -0.5, 2, 200, 400], 2, 1, -0.5,
+ 7, 60, 80,
+ 4, 100, 120, 140, 160,
+ 3,
+ 2, 0, colors, alphas, ratios, [1, 0.25, -0.5, 2, 200, 400], 1, 0, 0.25,
+ 5
+ ];
+ compact.__setCompactCommands(data);
+
+ var library = new AnimateLibrary("test", "test");
+ var legacyShape = legacy.__createObject(library);
+ var compactShape = compact.__createObject(library);
+ var cachedCompactShape = compact.__createObject(library);
+
+ Assert.same(legacyShape.graphics.readGraphicsData(false), compactShape.graphics.readGraphicsData(false));
+ Assert.same(legacyShape.graphics.readGraphicsData(false), cachedCompactShape.graphics.readGraphicsData(false));
+ Assert.same(legacyShape.getBounds(legacyShape), compactShape.getBounds(compactShape));
+
+ #if !flash
+ // readGraphicsData() does not report line gradients, so compare the raw draw commands too
+ var legacyCommands = legacyShape.graphics.__commands;
+ var compactCommands = compactShape.graphics.__commands;
+ Assert.same(legacyCommands.types, compactCommands.types);
+ Assert.same(legacyCommands.b, compactCommands.b);
+ Assert.same(legacyCommands.f, compactCommands.f);
+ Assert.same(legacyCommands.i, compactCommands.i);
+ Assert.same(legacyCommands.ii, compactCommands.ii);
+ Assert.same(legacyCommands.ff, compactCommands.ff);
+ Assert.same(legacyCommands.o, compactCommands.o);
+ #end
+ Assert.isNull(compact.commands);
+ Assert.isNull(compact.compactCommands);
+ }
+}
diff --git a/tests/src/tests/AnimateTimelineTest.hx b/tests/src/tests/AnimateTimelineTest.hx
new file mode 100644
index 000000000..885a70163
--- /dev/null
+++ b/tests/src/tests/AnimateTimelineTest.hx
@@ -0,0 +1,286 @@
+package tests;
+
+import openfl.display.BlendMode;
+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(openfl.display.DisplayObject)
+@: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, library);
+
+ 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);
+ }
+
+ public function testParseSpriteStorage():Void
+ {
+ var library = createLibrary();
+ var symbol = library.__parseSprite({id: 1, frames: [{objects: [createObject(0, 10, 1, 1, 0)]}]});
+ #if swf_compact_animate_timelines
+ Assert.notNull(symbol.compactTimeline);
+ Assert.equals(0, symbol.frames.length);
+ #else
+ Assert.isNull(symbol.compactTimeline);
+ Assert.equals(1, symbol.frames.length);
+ #end
+ }
+
+ #if !flash
+ public function testCompactTimelineMatchesDefault():Void
+ {
+ var frames:Array = [
+ {label: "start", objects: [createObject(0, 10, 1, 1, 0, "first", [1, 0, 0, 1, 200, 400]), createObject(0, 11, 1, 2, 3)]},
+ {objects: [createObject(1, 10, 1, 1, 0, null, [2, 0, 0, 3, 600, 800], [20, 20, 20, 10, 0, 0, 0, 0], "screen")]},
+ {objects: [{type: 2, id: 11}, createObject(0, 12, 1, 3, 0, "second")]},
+ {objects: [{type: 2, id: 10}, {type: 2, id: 12}]}
+ ];
+
+ var defaultClip = createClip(frames, false);
+ var compactClip = createClip(frames, true);
+
+ for (frame in 1...frames.length + 1)
+ {
+ defaultClip.gotoAndStop(frame);
+ compactClip.gotoAndStop(frame);
+ Assert.equals(describe(defaultClip), describe(compactClip), "frame " + frame);
+ }
+ }
+
+ public function testCompactClippingCharacterWithoutTargetIsMask():Void
+ {
+ var clip = createClip([{objects: [createObject(0, 10, 1, 1, 5)]}], true);
+ Assert.isTrue(clip.getChildAt(0).__isMask);
+ }
+
+ public function testCompactClipDepthChangeCreatesNewInstance():Void
+ {
+ var clip = createClip([
+ {objects: [createObject(0, 10, 1, 1, 0)]},
+ {objects: [{type: 2, id: 10}, createObject(0, 11, 1, 1, 3)]}
+ ], true);
+
+ var first = clip.getChildAt(0);
+ clip.gotoAndStop(2);
+ var second = clip.getChildAt(0);
+ Assert.notEquals(first, second);
+ Assert.isTrue(second.__isMask);
+ }
+
+ public function testUnknownFiltersAreSkipped():Void
+ {
+ // an unknown filter type only drops that filter
+ var object = createObject(0, 10, 1, 1, 0);
+ var filters:Array> = [[0, 4, 6, 2], [99, 1, 2, 3], [3, 0xFF0000, 1, 6, 6, 2, 1, false, false]];
+ object.filters = filters;
+
+ for (compact in [false, true])
+ {
+ var filters = createClip([{objects: [object]}], compact).getChildAt(0).filters;
+ Assert.equals(2, filters.length);
+ Assert.isOfType(filters[0], openfl.filters.BlurFilter);
+ Assert.isOfType(filters[1], openfl.filters.GlowFilter);
+ }
+ }
+
+ private static function describe(clip:MovieClip):String
+ {
+ var children = [];
+ for (i in 0...clip.numChildren)
+ {
+ var child = clip.getChildAt(i);
+ // unnamed objects get an automatic instance name from OpenFL, which depends on creation order
+ var name = StringTools.startsWith(child.name, "instance") ? "" : child.name;
+ var values:Array = [
+ name, child.x, child.y, child.scaleX, child.scaleY, child.alpha, child.visible, child.__isMask == true, child.blendMode == BlendMode.SCREEN
+ ];
+ children.push(values.join(","));
+ }
+ return children.join(" | ");
+ }
+ #end
+
+ public function testMorphShapeRatio():Void
+ {
+ var frames:Array = [
+ {objects: [createMorphObject(0, 0, "morph")]},
+ {objects: [createMorphObject(1, 32768, null, "multiply")]},
+ {objects: [createMorphObject(1, 65535)]},
+ // the ratio stays when an update does not set it
+ {objects: [createObject(1, 10, 2, 1, 0, null, [1, 0, 0, 1, 200, 0])]}
+ ];
+
+ for (compact in [false, true])
+ {
+ var clip = createClip(frames, compact);
+ var shape = clip.getChildAt(0);
+ var states = [];
+ for (frame in [1, 2, 3, 4, 2, 1])
+ {
+ clip.gotoAndStop(frame);
+ Assert.equals(shape, clip.getChildAt(0));
+ states.push(Math.round(shape.width) + " at " + Math.round(shape.x));
+ }
+ var message = compact ? "compact" : "default";
+ Assert.equals("20 at 0, 30 at 0, 40 at 0, 40 at 10, 30 at 0, 20 at 0", states.join(", "), message);
+ Assert.equals("morph", shape.name, message);
+ Assert.equals(BlendMode.MULTIPLY, shape.blendMode, message);
+ }
+ }
+
+ private static function createClip(frames:Array, compact:Bool):MovieClip
+ {
+ var library = createLibrary();
+ var symbol:AnimateSpriteSymbol;
+ if (compact)
+ {
+ symbol = new AnimateSpriteSymbol();
+ symbol.id = 1;
+ symbol.__setCompactTimeline(frames, library);
+ }
+ else
+ {
+ symbol = new AnimateSpriteSymbol();
+ symbol.id = 1;
+ library.__parseFrames(symbol, frames);
+ }
+ library.symbols.set(100, symbol);
+ return cast symbol.__createObject(library);
+ }
+
+ private static function createLibrary():AnimateLibrary
+ {
+ var library = new AnimateLibrary("compact-timeline-test", "compact-timeline-test");
+ library.frameRate = 24;
+ library.symbols = new Map();
+ library.symbolsByClassName = new Map();
+ library.bitmapSymbols = [];
+
+ var shape = new AnimateShapeSymbol();
+ shape.id = 1;
+ shape.commands = [];
+ library.symbols.set(shape.id, shape);
+
+ // a red square, 20 pixels wide at the start and 40 at the end
+ var morphShape = library.__parseMorphShape({id: 2, ratios: [0, 32768, 65535], shapes: [createSquare(400), createSquare(600), createSquare(800)]});
+ library.symbols.set(morphShape.id, morphShape);
+ return library;
+ }
+
+ private static function createSquare(size:Int):Array
+ {
+ return [1, 0xFF0000, 1, 8, 0, 0, 7, size, 0, 7, size, size, 7, 0, size, 7, 0, 0, 5];
+ }
+
+ private static function createMorphObject(type:Int, ratio:Int, name:String = null, blendMode:String = null):Dynamic
+ {
+ var object = createObject(type, 10, 2, 1, 0, name, [1, 0, 0, 1, 0, 0], null, blendMode);
+ object.ratio = ratio;
+ return object;
+ }
+
+ private static function createObject(type:Int, id:Int, symbol:Int, depth:Int, clipDepth:Int, name:String = null, matrix:Array = null,
+ colorTransform:Array = null, blendMode:String = null):Dynamic
+ {
+ return {
+ type: type,
+ id: id,
+ symbol: symbol,
+ depth: depth,
+ clipDepth: clipDepth,
+ name: name,
+ matrix: matrix,
+ colorTransform: colorTransform,
+ blendMode: blendMode
+ };
+ }
+}
diff --git a/tests/src/tests/DynamicTextFontTest.hx b/tests/src/tests/DynamicTextFontTest.hx
new file mode 100644
index 000000000..8975a4910
--- /dev/null
+++ b/tests/src/tests/DynamicTextFontTest.hx
@@ -0,0 +1,94 @@
+package tests;
+
+import openfl.text.Font;
+import openfl.text.TextField;
+import swf.exporters.animate.AnimateDynamicTextSymbol;
+import swf.exporters.animate.AnimateLibrary;
+import swf.exporters.swflite.DynamicTextSymbol;
+import swf.exporters.swflite.SWFLite;
+import utest.Assert;
+import utest.Test;
+
+@:access(swf.exporters.animate.AnimateDynamicTextSymbol)
+@:access(swf.exporters.animate.AnimateLibrary)
+@:access(swf.exporters.swflite.DynamicTextSymbol)
+class DynamicTextFontTest extends Test
+{
+ // Flash fonts cannot be created and named at runtime
+ #if !flash
+ public function testAnimateFontRegisteredLater():Void
+ {
+ checkFontRegisteredLater("SwfTestAnimateLate", createAnimateTextField);
+ }
+
+ public function testSWFLiteFontRegisteredLater():Void
+ {
+ checkFontRegisteredLater("SwfTestSWFLiteLate", createSWFLiteTextField);
+ }
+
+ public function testAnimateFontMatching():Void
+ {
+ checkFontMatching("SwfTestAnimateFuzzy", createAnimateTextField);
+ }
+
+ public function testSWFLiteFontMatching():Void
+ {
+ checkFontMatching("SwfTestSWFLiteFuzzy", createSWFLiteTextField);
+ }
+
+ private static function checkFontRegisteredLater(fontName:String, createTextField:String->(Void->TextField)):Void
+ {
+ var create = createTextField(fontName);
+ Assert.isFalse(create().embedFonts);
+
+ // the font found for a symbol is cached, until another font is registered
+ Font.registerFont(new Font(fontName));
+ var textField = create();
+ Assert.isTrue(textField.embedFonts);
+ Assert.equals(fontName, textField.defaultTextFormat.font);
+ }
+
+ private static function checkFontMatching(fontName:String, createTextField:String->(Void->TextField)):Void
+ {
+ // a font name can match the start of an embedded font name, ignoring other characters
+ Font.registerFont(new Font(fontName + "-Bold"));
+ var create = createTextField(fontName);
+
+ for (i in 0...2)
+ {
+ var textField = create();
+ Assert.isTrue(textField.embedFonts);
+ Assert.equals(fontName + "-Bold", textField.defaultTextFormat.font);
+ }
+ }
+
+ private static function createAnimateTextField(fontName:String):Void->TextField
+ {
+ var library = new AnimateLibrary("dynamic-text-font-test", "dynamic-text-font-test");
+ library.symbols = new Map();
+
+ var symbol = new AnimateDynamicTextSymbol();
+ symbol.fontName = fontName;
+ symbol.fontHeight = 240;
+ symbol.fontID = 0;
+ symbol.width = symbol.height = symbol.x = symbol.y = 0;
+ symbol.border = symbol.html = symbol.input = symbol.multiline = symbol.password = symbol.wordWrap = false;
+ symbol.selectable = true;
+ return function() return symbol.__createObject(library);
+ }
+
+ private static function createSWFLiteTextField(fontName:String):Void->TextField
+ {
+ var swf = new SWFLite();
+
+ var symbol = new DynamicTextSymbol();
+ symbol.fontName = fontName;
+ symbol.fontHeight = 240;
+ symbol.fontID = 0;
+ symbol.width = symbol.height = symbol.x = symbol.y = 0;
+ symbol.border = symbol.html = symbol.input = symbol.multiline = symbol.password = symbol.wordWrap = false;
+ symbol.selectable = true;
+ return function() return symbol.__createObject(swf);
+ }
+ #end
+}
diff --git a/tests/src/tests/GeneratedTestClip.hx b/tests/src/tests/GeneratedTestClip.hx
new file mode 100644
index 000000000..4422c733f
--- /dev/null
+++ b/tests/src/tests/GeneratedTestClip.hx
@@ -0,0 +1,25 @@
+package tests;
+
+// Matches the class generated from templates/animate/MovieClip.mtt
+@:access(swf.exporters.animate)
+class GeneratedTestClip extends openfl.display.MovieClip
+{
+ public static inline var UUID = "generated-class-test-a";
+
+ @:noCompletion private static function __init__():Void
+ {
+ swf.exporters.animate.AnimateLibrary.__registerGeneratedClass(UUID, 1, GeneratedTestClip);
+ }
+
+ public function new()
+ {
+ if (!swf.exporters.animate.AnimateSymbol.__initPending(GeneratedTestClip))
+ {
+ var library = swf.exporters.animate.AnimateLibrary.get(UUID);
+ var symbol = library.symbols.get(1);
+ symbol.__init(library);
+ }
+
+ super();
+ }
+}
diff --git a/tests/src/tests/LibPrefixedTestClip.hx b/tests/src/tests/LibPrefixedTestClip.hx
new file mode 100644
index 000000000..d8ac81cbc
--- /dev/null
+++ b/tests/src/tests/LibPrefixedTestClip.hx
@@ -0,0 +1,26 @@
+package tests;
+
+// Matches a class generated from templates/animate/MovieClip.mtt with the prefix "Lib",
+// for a symbol linked as tests.PrefixedTestClip
+@:access(swf.exporters.animate)
+class LibPrefixedTestClip extends openfl.display.MovieClip
+{
+ public static inline var UUID = "generated-prefix-test";
+
+ @:noCompletion private static function __init__():Void
+ {
+ swf.exporters.animate.AnimateLibrary.__registerGeneratedClass(UUID, 1, LibPrefixedTestClip);
+ }
+
+ public function new()
+ {
+ if (!swf.exporters.animate.AnimateSymbol.__initPending(LibPrefixedTestClip))
+ {
+ var library = swf.exporters.animate.AnimateLibrary.get(UUID);
+ var symbol = library.symbols.get(1);
+ symbol.__init(library);
+ }
+
+ super();
+ }
+}
diff --git a/tests/src/tests/MorphShapeTest.hx b/tests/src/tests/MorphShapeTest.hx
new file mode 100644
index 000000000..633545b16
--- /dev/null
+++ b/tests/src/tests/MorphShapeTest.hx
@@ -0,0 +1,202 @@
+package tests;
+
+import openfl.errors.Error;
+import swf.data.SWFMatrix;
+import swf.data.SWFMorphFillStyle;
+import swf.data.SWFMorphFocalGradient;
+import swf.data.SWFMorphGradientRecord;
+import swf.data.SWFMorphLineStyle;
+import swf.data.SWFMorphLineStyle2;
+import swf.data.SWFRectangle;
+import swf.data.SWFShape;
+import swf.data.SWFShapeRecord;
+import swf.data.SWFShapeRecordCurvedEdge;
+import swf.data.SWFShapeRecordStraightEdge;
+import swf.data.SWFShapeRecordStyleChange;
+import swf.exporters.ShapeCommandExporter;
+import swf.tags.TagDefineMorphShape;
+import utest.Assert;
+import utest.Test;
+
+class MorphShapeTest extends Test
+{
+ public function testMorphedEdges():Void
+ {
+ var tag = createMorphShape(1, 400, 800);
+ Assert.equals("move 0 0, line 400 0, line 0 400, line -400 0, line 0 -400, end", describeRecords(tag.getMorphedShape(0)));
+ Assert.equals("move 0 0, line 600 0, line 0 600, line -600 0, line 0 -600, end", describeRecords(tag.getMorphedShape(0.5)));
+ Assert.equals("move 0 0, line 800 0, line 0 800, line -800 0, line 0 -800, end", describeRecords(tag.getMorphedShape(1)));
+
+ var exporter = new ShapeCommandExporter(null);
+ tag.exportHandler = exporter;
+ tag.export(0.5);
+ Assert.equals("noLine\nfill FF0000 1\nmoveTo 0 0\nlineTo 30 0\nlineTo 30 30\nlineTo 0 30\nlineTo 0 0\nendFill", ShapeBuilder.describe(exporter.commands));
+ }
+
+ public function testStyleChangeWithoutMoveKeepsPosition():Void
+ {
+ var tag = createMorphShape(1, 400, 800);
+ tag.startEdges = new ShapeBuilder().move(0, 0, 0, 1, 0).lineTo(400, 0).style(-1, 1).lineTo(0, 400).lineTo(-400, 0).lineTo(0, -400).edges();
+ Assert.equals("move 0 0, line 600 0, style, line 0 600, line -600 0, line 0 -600, end", describeRecords(tag.getMorphedShape(0.5)));
+ }
+
+ public function testEndMoveWithoutStartMove():Void
+ {
+ var tag = createMorphShape(1, 400, 800);
+ tag.endEdges = new ShapeBuilder().move(0, 0).lineTo(800, 0).move(800, 200).lineTo(0, 600).lineTo(-800, 0).lineTo(0, -800).edges();
+ Assert.equals("move 0 0, line 600 0, move 600 100, line 0 500, line -600 0, line 0 -600, end", describeRecords(tag.getMorphedShape(0.5)));
+ }
+
+ public function testStraightEdgeBecomesCurve():Void
+ {
+ var tag = createMorphShape(1, 400, 400);
+ tag.startEdges = new ShapeBuilder().move(0, 0, 0, 1, 0).lineTo(400, 0).lineTo(0, 400).lineTo(-400, -400).edges();
+ tag.endEdges = new ShapeBuilder().move(0, 0).curveTo(200, -200, 200, 200).lineTo(0, 400).lineTo(-400, -400).edges();
+ Assert.equals("move 0 0, curve 200 -100 200 100, line 0 400, line -400 -400, end", describeRecords(tag.getMorphedShape(0.5)));
+ }
+
+ public function testPathsCloseAtAnyRatio():Void
+ {
+ // rounding each edge on its own leaves a gap where the path should close
+ var tag = createMorphShape(1, 400, 400);
+ tag.startEdges = new ShapeBuilder().move(0, 0, 0, 1, 0).lineTo(10, 0).lineTo(10, 20).lineTo(-20, -20).edges();
+ tag.endEdges = new ShapeBuilder().move(0, 0).lineTo(11, 0).lineTo(11, 22).lineTo(-22, -22).edges();
+
+ for (ratio in [0.25, 0.5, 0.75])
+ {
+ var x = 0;
+ var y = 0;
+ for (record in tag.getMorphedShape(ratio).records)
+ {
+ if (record.type == SWFShapeRecord.TYPE_STRAIGHTEDGE)
+ {
+ var edge:SWFShapeRecordStraightEdge = cast record;
+ x += edge.deltaX;
+ y += edge.deltaY;
+ }
+ }
+ Assert.equals("0,0", x + "," + y, "ratio " + ratio);
+ }
+ }
+
+ public function testMismatchedEdgesThrow():Void
+ {
+ var tag = createMorphShape(1, 400, 800);
+ tag.endEdges = new ShapeBuilder().move(0, 0).lineTo(800, 0).lineTo(0, 800).lineTo(-800, -800).edges();
+ Assert.raises(function() tag.getMorphedShape(0.5), Error);
+ }
+
+ public function testMorphedFocalGradient():Void
+ {
+ var morphedFill = createFocalGradientFill().getMorphedFillStyle(0.75);
+ Assert.notNull(morphedFill.gradientMatrix);
+ Assert.notNull(morphedFill.gradient);
+ if (morphedFill.gradient == null) return;
+ Assert.equals(1, morphedFill.gradient.spreadMode);
+ Assert.equals(1, morphedFill.gradient.interpolationMode);
+ Assert.floatEquals(0.25, morphedFill.gradient.focalPoint);
+ Assert.equals(1, morphedFill.gradient.records.length);
+ }
+
+ public function testMorphedLineWithFillChangesWidth():Void
+ {
+ var lineStyle = new SWFMorphLineStyle2();
+ lineStyle.startWidth = 20;
+ lineStyle.endWidth = 60;
+ lineStyle.hasFillFlag = true;
+ lineStyle.fillType = new SWFMorphFillStyle();
+ lineStyle.fillType.type = 0x00;
+ lineStyle.fillType.startColor = 0xFF000000;
+ lineStyle.fillType.endColor = 0xFFFFFFFF;
+ Assert.equals(40, lineStyle.getMorphedLineStyle(0.5).width);
+ }
+
+ public function testMorphedLineWithoutFlags():Void
+ {
+ // DefineMorphShape line styles have no flags
+ var lineStyle = new SWFMorphLineStyle();
+ lineStyle.startWidth = 20;
+ lineStyle.endWidth = 20;
+ lineStyle.startColor = 0xFF000000;
+ lineStyle.endColor = 0xFF000000;
+ var morphedLine = lineStyle.getMorphedLineStyle(0.5);
+ Assert.isFalse(morphedLine.hasFillFlag);
+ Assert.isFalse(morphedLine.pixelHintingFlag);
+ Assert.isFalse(morphedLine.noHScaleFlag);
+ }
+
+ private static function createFocalGradientFill():SWFMorphFillStyle
+ {
+ var record = new SWFMorphGradientRecord();
+ record.startRatio = 0;
+ record.endRatio = 255;
+ record.startColor = 0xFFFF0000;
+ record.endColor = 0xFF0000FF;
+
+ var gradient = new SWFMorphFocalGradient();
+ gradient.spreadMode = 1;
+ gradient.interpolationMode = 1;
+ gradient.startFocalPoint = -0.5;
+ gradient.endFocalPoint = 0.5;
+ gradient.records.push(record);
+
+ var fillStyle = new SWFMorphFillStyle();
+ fillStyle.type = 0x13;
+ fillStyle.startGradientMatrix = new SWFMatrix();
+ fillStyle.endGradientMatrix = new SWFMatrix();
+ fillStyle.gradient = gradient;
+ return fillStyle;
+ }
+
+ // A red square that grows from its top left corner, in twips
+ public static function createMorphShape(characterId:Int, startSize:Int, endSize:Int):TagDefineMorphShape
+ {
+ var tag = new TagDefineMorphShape();
+ tag.characterId = characterId;
+ tag.startBounds = createRectangle(startSize);
+ tag.endBounds = createRectangle(endSize);
+
+ var fillStyle = new SWFMorphFillStyle();
+ fillStyle.type = 0x00;
+ fillStyle.startColor = 0xFFFF0000;
+ fillStyle.endColor = 0xFFFF0000;
+ tag.morphFillStyles.push(fillStyle);
+
+ tag.startEdges = new ShapeBuilder().move(0, 0, 0, 1, 0).lineTo(startSize, 0).lineTo(0, startSize).lineTo(-startSize, 0).lineTo(0, -startSize).edges();
+ tag.endEdges = new ShapeBuilder().move(0, 0).lineTo(endSize, 0).lineTo(0, endSize).lineTo(-endSize, 0).lineTo(0, -endSize).edges();
+ return tag;
+ }
+
+ private static function createRectangle(size:Int):SWFRectangle
+ {
+ var rectangle = new SWFRectangle();
+ rectangle.xmin = 0;
+ rectangle.ymin = 0;
+ rectangle.xmax = size;
+ rectangle.ymax = size;
+ return rectangle;
+ }
+
+ private static function describeRecords(shape:SWFShape):String
+ {
+ var records = [];
+ for (record in shape.records)
+ {
+ switch (record.type)
+ {
+ case SWFShapeRecord.TYPE_STYLECHANGE:
+ var styleChange:SWFShapeRecordStyleChange = cast record;
+ records.push(styleChange.stateMoveTo ? "move " + styleChange.moveDeltaX + " " + styleChange.moveDeltaY : "style");
+ case SWFShapeRecord.TYPE_STRAIGHTEDGE:
+ var edge:SWFShapeRecordStraightEdge = cast record;
+ records.push("line " + edge.deltaX + " " + edge.deltaY);
+ case SWFShapeRecord.TYPE_CURVEDEDGE:
+ var edge:SWFShapeRecordCurvedEdge = cast record;
+ records.push("curve " + edge.controlDeltaX + " " + edge.controlDeltaY + " " + edge.anchorDeltaX + " " + edge.anchorDeltaY);
+ default:
+ records.push("end");
+ }
+ }
+ return records.join(", ");
+ }
+}
diff --git a/tests/src/tests/SWFBuilder.hx b/tests/src/tests/SWFBuilder.hx
new file mode 100644
index 000000000..ee85f0883
--- /dev/null
+++ b/tests/src/tests/SWFBuilder.hx
@@ -0,0 +1,87 @@
+package tests;
+
+import openfl.utils.ByteArray;
+import swf.SWF;
+import swf.SWFData;
+import swf.SWFRoot;
+import swf.data.SWFSymbol;
+import swf.tags.ITag;
+import swf.tags.TagDefineSprite;
+import swf.tags.TagEnd;
+import swf.tags.TagShowFrame;
+import swf.tags.TagSymbolClass;
+
+// Writes a minimal uncompressed SWF file from tags created in code
+class SWFBuilder
+{
+ private var symbolClass:TagSymbolClass;
+ private var tags:Array;
+
+ public function new()
+ {
+ tags = [];
+ symbolClass = new TagSymbolClass();
+ }
+
+ public function add(tag:ITag):SWFBuilder
+ {
+ tags.push(tag);
+ return this;
+ }
+
+ public function link(characterId:Int, className:String):SWFBuilder
+ {
+ symbolClass.symbols.push(SWFSymbol.create(characterId, className));
+ return this;
+ }
+
+ public function build():ByteArray
+ {
+ var output = new SWFData();
+ output.writeUI8(0x46);
+ output.writeUI8(0x57);
+ output.writeUI8(0x53);
+ output.writeUI8(10);
+ output.writeUI32(0);
+ // RECT with 12 bit values, for a 100x100 stage
+ for (value in [0x60, 0x00, 0x3E, 0x80, 0x00, 0x3E, 0x80])
+ {
+ output.writeUI8(value);
+ }
+ output.writeUI8(0x00);
+ output.writeUI8(0x18);
+ output.writeUI16(1);
+
+ for (tag in tags)
+ {
+ tag.publish(output, 10);
+ }
+ if (symbolClass.symbols.length > 0)
+ {
+ symbolClass.publish(output, 10);
+ }
+ new TagShowFrame().publish(output, 10);
+ new TagEnd().publish(output, 10);
+
+ var length = output.length;
+ output.position = 4;
+ output.writeUI32(length);
+ output.position = 0;
+ return output;
+ }
+
+ public function parse():SWFRoot
+ {
+ return new SWF(build()).data;
+ }
+
+ public static function createSprite(characterId:Int):TagDefineSprite
+ {
+ var sprite = new TagDefineSprite();
+ sprite.characterId = characterId;
+ sprite.frameCount = 1;
+ sprite.tags.push(new TagShowFrame());
+ sprite.tags.push(new TagEnd());
+ return sprite;
+ }
+}
diff --git a/tests/src/tests/SWFDataTest.hx b/tests/src/tests/SWFDataTest.hx
new file mode 100644
index 000000000..d4d04c49b
--- /dev/null
+++ b/tests/src/tests/SWFDataTest.hx
@@ -0,0 +1,96 @@
+package tests;
+
+import swf.SWFData;
+import swf.data.SWFColorTransformWithAlpha;
+import swf.data.SWFMatrix;
+import swf.data.SWFRectangle;
+import utest.Assert;
+import utest.Test;
+
+class SWFDataTest extends Test
+{
+ public function testCalculateMaxBits():Void
+ {
+ var data = new SWFData();
+ Assert.equals(0, data.calculateMaxBits(false, [0]));
+ Assert.equals(1, data.calculateMaxBits(false, [1]));
+ Assert.equals(8, data.calculateMaxBits(false, [255]));
+ Assert.equals(10, data.calculateMaxBits(false, [1000]));
+ Assert.equals(0, data.calculateMaxBits(true, [0, 0]));
+ Assert.equals(2, data.calculateMaxBits(true, [1]));
+ Assert.equals(1, data.calculateMaxBits(true, [-1, 0]));
+ Assert.equals(2, data.calculateMaxBits(true, [-2]));
+ Assert.equals(12, data.calculateMaxBits(true, [2047, -2048]));
+ Assert.equals(13, data.calculateMaxBits(true, [2048, 0]));
+ Assert.equals(13, data.calculateMaxBits(true, [0, -2049]));
+ }
+
+ public function testMatrixRoundTrip():Void
+ {
+ var matrix = new SWFMatrix();
+ matrix.scaleX = 1.5;
+ matrix.scaleY = 0.5;
+ matrix.rotateSkew0 = 0.25;
+ matrix.rotateSkew1 = -0.25;
+ matrix.translateX = 2000;
+ matrix.translateY = -3000;
+
+ var data = new SWFData();
+ data.writeMATRIX(matrix);
+ data.position = 0;
+ Assert.equals(matrix.toString(), data.readMATRIX().toString());
+
+ var translation = new SWFMatrix();
+ translation.translateX = -1;
+ data = new SWFData();
+ data.writeMATRIX(translation);
+ data.position = 0;
+ Assert.equals(translation.toString(), data.readMATRIX().toString());
+ }
+
+ public function testRectangleRoundTrip():Void
+ {
+ var rect = new SWFRectangle();
+ rect.xmin = -20;
+ rect.xmax = 11000;
+ rect.ymin = 0;
+ rect.ymax = 8000;
+
+ var data = new SWFData();
+ data.writeRECT(rect);
+ data.position = 0;
+ var result = data.readRECT();
+ Assert.equals(-20, result.xmin);
+ Assert.equals(11000, result.xmax);
+ Assert.equals(0, result.ymin);
+ Assert.equals(8000, result.ymax);
+ }
+
+ public function testColorTransformRoundTrip():Void
+ {
+ var colorTransform = new SWFColorTransformWithAlpha();
+ colorTransform.hasMultTerms = true;
+ colorTransform.hasAddTerms = true;
+ colorTransform.rMult = 256;
+ colorTransform.gMult = 128;
+ colorTransform.bMult = 0;
+ colorTransform.aMult = 256;
+ colorTransform.rAdd = -255;
+ colorTransform.gAdd = 255;
+ colorTransform.bAdd = 0;
+ colorTransform.aAdd = 0;
+
+ var data = new SWFData();
+ data.writeCXFORMWITHALPHA(colorTransform);
+ data.position = 0;
+ var result = data.readCXFORMWITHALPHA();
+ Assert.equals(256, result.rMult);
+ Assert.equals(128, result.gMult);
+ Assert.equals(0, result.bMult);
+ Assert.equals(256, result.aMult);
+ Assert.equals(-255, result.rAdd);
+ Assert.equals(255, result.gAdd);
+ Assert.equals(0, result.bAdd);
+ Assert.equals(0, result.aAdd);
+ }
+}
diff --git a/tests/src/tests/SWFLiteExporterTest.hx b/tests/src/tests/SWFLiteExporterTest.hx
new file mode 100644
index 000000000..420f415da
--- /dev/null
+++ b/tests/src/tests/SWFLiteExporterTest.hx
@@ -0,0 +1,44 @@
+package tests;
+
+import openfl.utils.ByteArray;
+import swf.data.consts.SoundCompression;
+import swf.data.consts.SoundRate;
+import swf.data.consts.SoundSize;
+import swf.exporters.SWFLiteExporter;
+import swf.tags.TagDefineSound;
+import utest.Assert;
+import utest.Test;
+
+class SWFLiteExporterTest extends Test
+{
+ #if !flash
+ public function testSymbolClassZeroIsRoot():Void
+ {
+ // the document class is linked to character 0, and is not always named *_fla.MainTimeline
+ var exporter = new SWFLiteExporter(new SWFBuilder().link(0, "MainDocument").parse());
+ Assert.equals("MainDocument", exporter.swfLite.root.className);
+ }
+
+ public function testLinkedSoundClassNames():Void
+ {
+ var data = new ByteArray();
+ for (i in 0...16)
+ {
+ data.writeByte(i);
+ }
+
+ var swf = new SWFBuilder()
+ .add(TagDefineSound.create(1, SoundCompression.UNCOMPRESSED_LITTLE_ENDIAN, SoundRate.KHZ_11, SoundSize.BIT_16, swf.data.consts.SoundType.MONO, 8, data))
+ .add(TagDefineSound.create(2, SoundCompression.SPEEX, SoundRate.KHZ_5, SoundSize.BIT_16, swf.data.consts.SoundType.MONO, 8, data))
+ .link(1, "TestSound")
+ .link(2, "TestSpeex")
+ .parse();
+
+ var exporter = new SWFLiteExporter(swf);
+ Assert.equals("TestSound", exporter.soundSymbolClassNames.get(1));
+ Assert.equals("TestSpeex", exporter.soundSymbolClassNames.get(2));
+ Assert.equals("UNCOMPRESSED_LITTLE_ENDIAN", Std.string(exporter.soundTypes.get(1)));
+ Assert.equals("SPEEX", Std.string(exporter.soundTypes.get(2)));
+ }
+ #end
+}
diff --git a/tests/src/tests/ShapeBuilder.hx b/tests/src/tests/ShapeBuilder.hx
new file mode 100644
index 000000000..0931cf8c4
--- /dev/null
+++ b/tests/src/tests/ShapeBuilder.hx
@@ -0,0 +1,173 @@
+package tests;
+
+import swf.data.SWFFillStyle;
+import swf.data.SWFGradient;
+import swf.data.SWFGradientRecord;
+import swf.data.SWFLineStyle2;
+import swf.data.SWFMatrix;
+import swf.data.SWFShape;
+import swf.data.SWFShapeRecordCurvedEdge;
+import swf.data.SWFShapeRecordEnd;
+import swf.data.SWFShapeRecordStraightEdge;
+import swf.data.SWFShapeRecordStyleChange;
+import swf.data.SWFShapeWithStyle;
+import swf.exporters.core.ShapeCommand;
+
+// Builds DefineShape4 records in code, in twips like a SWF file
+class ShapeBuilder
+{
+ public var shape(default, null):SWFShapeWithStyle;
+
+ public function new()
+ {
+ shape = new SWFShapeWithStyle(null, 4);
+ }
+
+ public function addSolidFill(argb:Int):ShapeBuilder
+ {
+ var fillStyle = new SWFFillStyle();
+ fillStyle.type = 0x00;
+ fillStyle.rgb = argb;
+ shape.initialFillStyles.push(fillStyle);
+ return this;
+ }
+
+ public function addLine(width:Int, argb:Int):ShapeBuilder
+ {
+ var lineStyle = new SWFLineStyle2(null, 4);
+ lineStyle.width = width;
+ lineStyle.color = argb;
+ shape.initialLineStyles.push(lineStyle);
+ return this;
+ }
+
+ public function addGradientLine(width:Int, colors:Array, ratios:Array):ShapeBuilder
+ {
+ var gradient = new SWFGradient();
+ for (i in 0...colors.length)
+ {
+ var record = new SWFGradientRecord();
+ record.color = colors[i];
+ record.ratio = ratios[i];
+ gradient.records.push(record);
+ }
+
+ var fillStyle = new SWFFillStyle();
+ fillStyle.type = 0x10;
+ fillStyle.gradient = gradient;
+ fillStyle.gradientMatrix = new SWFMatrix();
+
+ var lineStyle = new SWFLineStyle2(null, 4);
+ lineStyle.width = width;
+ lineStyle.hasFillFlag = true;
+ lineStyle.fillType = fillStyle;
+ shape.initialLineStyles.push(lineStyle);
+ return this;
+ }
+
+ // fill and line style indexes start at 1, 0 clears a style and -1 leaves it unchanged
+ public function move(x:Int, y:Int, fill0:Int = -1, fill1:Int = -1, line:Int = -1):ShapeBuilder
+ {
+ addStyleChange(true, x, y, fill0, fill1, line);
+ return this;
+ }
+
+ public function style(fill0:Int = -1, fill1:Int = -1, line:Int = -1):ShapeBuilder
+ {
+ addStyleChange(false, 0, 0, fill0, fill1, line);
+ return this;
+ }
+
+ private function addStyleChange(moveTo:Bool, x:Int, y:Int, fill0:Int, fill1:Int, line:Int):Void
+ {
+ var record = new SWFShapeRecordStyleChange();
+ if (moveTo)
+ {
+ record.stateMoveTo = true;
+ record.moveDeltaX = x;
+ record.moveDeltaY = y;
+ }
+ if (fill0 >= 0)
+ {
+ record.stateFillStyle0 = true;
+ record.fillStyle0 = fill0;
+ }
+ if (fill1 >= 0)
+ {
+ record.stateFillStyle1 = true;
+ record.fillStyle1 = fill1;
+ }
+ if (line >= 0)
+ {
+ record.stateLineStyle = true;
+ record.lineStyle = line;
+ }
+ shape.records.push(record);
+ }
+
+ public function lineTo(deltaX:Int, deltaY:Int):ShapeBuilder
+ {
+ var record = new SWFShapeRecordStraightEdge();
+ record.generalLineFlag = true;
+ record.deltaX = deltaX;
+ record.deltaY = deltaY;
+ shape.records.push(record);
+ return this;
+ }
+
+ public function curveTo(controlDeltaX:Int, controlDeltaY:Int, anchorDeltaX:Int, anchorDeltaY:Int):ShapeBuilder
+ {
+ var record = new SWFShapeRecordCurvedEdge();
+ record.controlDeltaX = controlDeltaX;
+ record.controlDeltaY = controlDeltaY;
+ record.anchorDeltaX = anchorDeltaX;
+ record.anchorDeltaY = anchorDeltaY;
+ shape.records.push(record);
+ return this;
+ }
+
+ public function end():SWFShapeWithStyle
+ {
+ shape.records.push(new SWFShapeRecordEnd());
+ return shape;
+ }
+
+ // Only the records, like the start and end edges of a morph shape
+ public function edges():SWFShape
+ {
+ var edges = new SWFShape();
+ for (record in end().records)
+ {
+ edges.records.push(record);
+ }
+ return edges;
+ }
+
+ // A readable description of shape commands, with numbers rounded so it is the same on every target
+ public static function describe(commands:Array):String
+ {
+ var lines = [];
+ for (command in commands)
+ {
+ lines.push(switch (command)
+ {
+ case BeginBitmapFill(bitmap, _, repeat, smooth): "bitmapFill " + bitmap + " " + repeat + " " + smooth;
+ case BeginFill(color, alpha): "fill " + StringTools.hex(color, 6) + " " + number(alpha);
+ case BeginGradientFill(_, colors, _, ratios, _, _, _, _): "gradientFill " + colors.length + " " + ratios.join(",");
+ case CurveTo(controlX, controlY, anchorX, anchorY): "curve " + number(controlX) + " " + number(controlY) + " " + number(anchorX) + " "
+ + number(anchorY);
+ case EndFill: "endFill";
+ case LineStyle(thickness, color, _, _, _, _, _, _): thickness == null ? "noLine" : "line " + number(thickness) + " " + StringTools.hex(color, 6);
+ case LineGradientStyle(_, colors, _, ratios, _, _, _, _): "lineGradient " + colors.length + " " + ratios.join(",");
+ case LineTo(x, y): "lineTo " + number(x) + " " + number(y);
+ case MoveTo(x, y): "moveTo " + number(x) + " " + number(y);
+ });
+ }
+ return lines.join("\n");
+ }
+
+ private static function number(value:Float):String
+ {
+ return Std.string(Math.round(value * 100) / 100);
+ }
+}
diff --git a/tests/src/tests/ShapeExportTest.hx b/tests/src/tests/ShapeExportTest.hx
new file mode 100644
index 000000000..7bb070d36
--- /dev/null
+++ b/tests/src/tests/ShapeExportTest.hx
@@ -0,0 +1,175 @@
+package tests;
+
+import openfl.display.Shape;
+import swf.exporters.ShapeCommandExporter;
+import swf.exporters.core.ShapeCommand;
+import swf.exporters.swflite.SWFLite;
+import swf.exporters.swflite.ShapeSymbol;
+import swf.tags.TagDefineMorphShape;
+import swf.tags.TagDefineShape;
+import utest.Assert;
+import utest.Test;
+
+#if !flash
+@:access(openfl.display.Graphics)
+@:access(openfl.display._internal.DrawCommandBuffer)
+#end
+@:access(swf.exporters.swflite.ShapeSymbol)
+class ShapeExportTest extends Test
+{
+ // Two squares that share an edge, a closed curve, and a triangle whose edges are given out of order
+ public static function createComplexShape():ShapeBuilder
+ {
+ return new ShapeBuilder()
+ .addSolidFill(0xFFFF0000)
+ .addSolidFill(0xFF0000FF)
+ .addLine(20, 0xFF000000)
+ .move(0, 0, 0, 1, 1)
+ .lineTo(1000, 0)
+ .lineTo(0, 1000)
+ .style(2, 1)
+ .lineTo(-1000, 0)
+ .style(0, 1)
+ .lineTo(0, -1000)
+ .move(1000, 1000, 0, 2)
+ .lineTo(0, 1000)
+ .lineTo(-1000, 0)
+ .lineTo(0, -1000)
+ .move(3000, 0, 0, 1, 0)
+ .curveTo(500, 500, 500, -500)
+ .lineTo(-1000, 0)
+ .move(5000, 1000, 0, 2)
+ .lineTo(1000, 0)
+ .move(5500, 0)
+ .lineTo(-500, 1000)
+ .move(6000, 1000)
+ .lineTo(-500, -1000);
+ }
+
+ public function testComplexShapeExport():Void
+ {
+ var exporter = new ShapeCommandExporter(null);
+ createComplexShape().end().export(exporter);
+
+ // recorded from the exporter before it was optimized, which must not change the output
+ Assert.equals(EXPECTED_COMPLEX_SHAPE, ShapeBuilder.describe(exporter.commands));
+ }
+
+ public function testLineGradientStyleExport():Void
+ {
+ var exporter = new ShapeCommandExporter(null);
+ createGradientLineShape().export(exporter);
+
+ Assert.equals("line 1 000000\nlineGradient 2 0,255\nmoveTo 0 0\nlineTo 50 0", ShapeBuilder.describe(exporter.commands));
+ }
+
+ #if !flash
+ public function testRuntimeShapeDrawsLineGradient():Void
+ {
+ var tag = new TagDefineShape();
+ tag.shapes = createGradientLineShape();
+
+ var shape = new swf.runtime.Shape(null, tag);
+ Assert.isTrue(hasLineGradient(shape));
+ }
+
+ public function testRuntimeMorphShapeDrawsLineGradient():Void
+ {
+ var morphShape = new swf.runtime.MorphShape(null, new GradientLineMorphShape());
+ morphShape.render();
+ Assert.isTrue(hasLineGradient(morphShape));
+ }
+
+ public function testSWFLiteShapeDrawsLineGradient():Void
+ {
+ var symbol = new ShapeSymbol();
+ symbol.commands = [
+ LineStyle(1, 0, 1, false, null, null, null, null),
+ LineGradientStyle(LINEAR, [0xFF0000, 0x0000FF], [1, 1], [0, 255], null, PAD, RGB, 0),
+ MoveTo(0, 0),
+ LineTo(50, 0)
+ ];
+
+ var shape = symbol.__createObject(new SWFLite());
+ Assert.isTrue(hasLineGradient(shape));
+ }
+
+ private static function hasLineGradient(shape:Shape):Bool
+ {
+ for (type in shape.graphics.__commands.types)
+ {
+ switch (type)
+ {
+ case LINE_GRADIENT_STYLE:
+ return true;
+ default:
+ }
+ }
+ return false;
+ }
+ #end
+
+ private static function createGradientLineShape():swf.data.SWFShapeWithStyle
+ {
+ return new ShapeBuilder()
+ .addGradientLine(20, [0xFFFF0000, 0xFF0000FF], [0, 255])
+ .move(0, 0, -1, -1, 1)
+ .lineTo(1000, 0)
+ .end();
+ }
+
+ private static var EXPECTED_COMPLEX_SHAPE = [
+ "noLine",
+ "fill FF0000 1",
+ "moveTo 0 0",
+ "lineTo 50 0",
+ "lineTo 50 50",
+ "lineTo 0 50",
+ "lineTo 0 0",
+ "moveTo 150 0",
+ "curve 175 25 200 0",
+ "lineTo 150 0",
+ "endFill",
+ "fill 0000FF 1",
+ "moveTo 0 50",
+ "lineTo 50 50",
+ "lineTo 50 100",
+ "lineTo 0 100",
+ "lineTo 0 50",
+ "moveTo 250 50",
+ "lineTo 300 50",
+ "lineTo 275 0",
+ "lineTo 250 50",
+ "endFill",
+ "line 1 000000",
+ "moveTo 0 0",
+ "lineTo 50 0",
+ "lineTo 50 50",
+ "lineTo 0 50",
+ "lineTo 0 0",
+ "moveTo 50 50",
+ "lineTo 50 100",
+ "lineTo 0 100",
+ "lineTo 0 50"
+ ].join("\n");
+}
+
+private class GradientLineMorphShape extends TagDefineMorphShape
+{
+ public function new()
+ {
+ super();
+ }
+
+ override public function export(ratio:Float = 0):Void
+ {
+ exportHandler.beginShape();
+ exportHandler.beginLines();
+ exportHandler.lineStyle(1, 0, 1);
+ exportHandler.lineGradientStyle(LINEAR, [0xFF0000, 0x0000FF], [1, 1], [0, 255]);
+ exportHandler.moveTo(0, 0);
+ exportHandler.lineTo(50, 0);
+ exportHandler.endLines();
+ exportHandler.endShape();
+ }
+}
diff --git a/tests/src/tests/TagDefineFont2Test.hx b/tests/src/tests/TagDefineFont2Test.hx
new file mode 100644
index 000000000..f3e42cb50
--- /dev/null
+++ b/tests/src/tests/TagDefineFont2Test.hx
@@ -0,0 +1,105 @@
+package tests;
+
+import swf.SWFData;
+import swf.data.SWFShape;
+import swf.data.SWFShapeRecordEnd;
+import swf.tags.TagDefineFont2;
+import swf.tags.TagDefineFont3;
+import utest.Assert;
+import utest.Test;
+
+class TagDefineFont2Test extends Test
+{
+ public function testFontWithGlyphsWithoutLayout():Void
+ {
+ // fonts used only by static text have glyphs, but no layout
+ var font = new TagDefineFont3();
+ font.characterId = 1;
+ font.fontName = "Test";
+ font.languageCode = 0;
+ font.hasLayout = false;
+ font.shiftJIS = false;
+ font.smallText = false;
+ font.ansi = false;
+ font.wideOffsets = false;
+ font.wideCodes = true;
+ font.italic = false;
+ font.bold = false;
+ font.glyphShapeTable.push(createGlyph());
+ font.codeTable.push(65);
+
+ var data = new SWFData();
+ font.publish(data, 10);
+
+ var parsed = new TagDefineFont3();
+ var end = parse(data, parsed);
+ Assert.equals(end, data.position);
+ Assert.equals(1, parsed.glyphShapeTable.length);
+ Assert.equals(65, parsed.codeTable[0]);
+ }
+
+ public function testFontWithoutGlyphsWithLayout():Void
+ {
+ // Flash writes a CodeTableOffset even when there are no glyphs
+ var data = createFontWithoutGlyphs(true);
+
+ var parsed = new TagDefineFont3();
+ var end = parse(data, parsed);
+ Assert.equals(end, data.position);
+ Assert.equals(0, parsed.glyphShapeTable.length);
+ Assert.equals(1000, parsed.ascent);
+ Assert.equals(200, parsed.descent);
+ Assert.equals(50, parsed.leading);
+ }
+
+ public function testFontWithoutGlyphsWithoutLayout():Void
+ {
+ var data = createFontWithoutGlyphs(false);
+
+ var parsed = new TagDefineFont2();
+ var end = parse(data, parsed);
+ Assert.equals(end, data.position);
+ Assert.equals("Test", parsed.fontName);
+ }
+
+ private static function parse(data:SWFData, font:TagDefineFont2):Int
+ {
+ data.position = 0;
+ var header = data.readTagHeader();
+ var end = data.position + header.contentLength;
+ font.parse(data, header.contentLength, 10);
+ return end;
+ }
+
+ private static function createFontWithoutGlyphs(hasLayout:Bool):SWFData
+ {
+ var body = new SWFData();
+ body.writeUI16(1);
+ body.writeUI8(hasLayout ? 0x84 : 0x04);
+ body.writeLANGCODE(0);
+ body.writeUI8(4);
+ body.writeUTFBytes("Test");
+ body.writeUI16(0);
+ body.writeUI16(4); // CodeTableOffset
+ if (hasLayout)
+ {
+ body.writeUI16(1000);
+ body.writeUI16(200);
+ body.writeSI16(50);
+ body.writeUI16(0);
+ }
+
+ var data = new SWFData();
+ data.writeTagHeader(hasLayout ? TagDefineFont3.TYPE : TagDefineFont2.TYPE, body.length);
+ data.writeBytes(body);
+ return data;
+ }
+
+ private static function createGlyph():SWFShape
+ {
+ // an empty outline, the glyph contents do not matter here
+ var shape = new SWFShape();
+ shape.records.push(new SWFShapeRecordEnd());
+ return shape;
+ }
+}
diff --git a/tests/src/tests/TimelineTest.hx b/tests/src/tests/TimelineTest.hx
new file mode 100644
index 000000000..6c61509a9
--- /dev/null
+++ b/tests/src/tests/TimelineTest.hx
@@ -0,0 +1,202 @@
+package tests;
+
+import openfl.display.DisplayObject;
+import openfl.display.MovieClip;
+import openfl.display.Shape;
+import swf.exporters.animate.AnimateFrame;
+import swf.exporters.animate.AnimateFrameObject;
+import swf.exporters.animate.AnimateFrameObjectType;
+import swf.exporters.animate.AnimateLibrary;
+import swf.exporters.animate.AnimateShapeSymbol;
+import swf.exporters.animate.AnimateSpriteSymbol;
+import swf.exporters.animate.AnimateTimeline;
+import swf.exporters.swflite.SWFLite;
+import swf.exporters.swflite.ShapeSymbol;
+import swf.exporters.swflite.SpriteSymbol;
+import swf.exporters.swflite.timeline.Frame;
+import swf.exporters.swflite.timeline.FrameObject;
+import swf.exporters.swflite.timeline.FrameObjectType;
+import swf.exporters.swflite.timeline.SymbolTimeline;
+import utest.Assert;
+import utest.Test;
+
+@:access(openfl.display.DisplayObject)
+@:access(swf.exporters.animate)
+@:access(swf.exporters.swflite)
+class TimelineTest extends Test
+{
+ public function testTimelinesCompile():Void
+ {
+ // both timelines must compile on every target, including Flash
+ Assert.notNull(Type.getClassName(AnimateTimeline));
+ Assert.notNull(Type.getClassName(SymbolTimeline));
+ }
+
+ #if !flash
+ public function testAnimateClippingCharacterWithoutTargetIsMask():Void
+ {
+ var clip = createAnimateClip([[animateObject(AnimateFrameObjectType.CREATE, 0, 1, 5)]]);
+ Assert.isTrue(clip.getChildAt(0).__isMask);
+ }
+
+ public function testSWFLiteClippingCharacterWithoutTargetIsMask():Void
+ {
+ var clip = createSWFLiteClip([[swfLiteObject(FrameObjectType.CREATE, 0, 1, 5)]]);
+ Assert.isTrue(clip.getChildAt(0).__isMask);
+ }
+
+ public function testAnimateClipDepthChangeCreatesNewInstance():Void
+ {
+ var clip = createAnimateClip([
+ [animateObject(AnimateFrameObjectType.CREATE, 0, 1, 0)],
+ [animateObject(AnimateFrameObjectType.DESTROY, 0, 1, 0), animateObject(AnimateFrameObjectType.CREATE, 1, 1, 3)]
+ ]);
+ checkClipDepthChange(clip);
+ }
+
+ public function testSWFLiteClipDepthChangeCreatesNewInstance():Void
+ {
+ var clip = createSWFLiteClip([
+ [swfLiteObject(FrameObjectType.CREATE, 0, 1, 0)],
+ [swfLiteObject(FrameObjectType.DESTROY, 0, 1, 0), swfLiteObject(FrameObjectType.CREATE, 1, 1, 3)]
+ ]);
+ checkClipDepthChange(clip);
+ }
+
+ public function testAnimateNamedChildrenAreAssignedPerClass():Void
+ {
+ // instance fields are cached by class, so each class must still get its own fields
+ var library = createAnimateLibrary();
+ var symbolA = addAnimateSprite(library, 10, Type.getClassName(TimelineFieldClipA), [[animateObject(AnimateFrameObjectType.CREATE, 0, 1, 0, "first")]]);
+ var symbolB = addAnimateSprite(library, 11, Type.getClassName(TimelineFieldClipB), [[animateObject(AnimateFrameObjectType.CREATE, 0, 1, 0, "second")]]);
+
+ var a1:TimelineFieldClipA = cast symbolA.__createObject(library);
+ var b:TimelineFieldClipB = cast symbolB.__createObject(library);
+ var a2:TimelineFieldClipA = cast symbolA.__createObject(library);
+
+ Assert.notNull(a1.first);
+ Assert.notNull(a2.first);
+ Assert.notNull(b.second);
+ Assert.notEquals(a1.first, a2.first);
+ library.unload();
+ }
+
+ private static function checkClipDepthChange(clip:MovieClip):Void
+ {
+ clip.gotoAndStop(1);
+ var first = clip.getChildAt(0);
+ // __isMask is not initialized on display objects that never were masks
+ Assert.isTrue(first.__isMask != true);
+
+ // the same character at the same depth, now with a clip depth, is a new mask instance
+ clip.gotoAndStop(2);
+ var second = clip.getChildAt(0);
+ Assert.notEquals(first, second);
+ Assert.isTrue(second.__isMask);
+ }
+
+ private static function createAnimateLibrary():AnimateLibrary
+ {
+ var library = new AnimateLibrary("timeline-test", "timeline-test");
+ library.frameRate = 24;
+ library.symbols = new Map();
+ library.symbolsByClassName = new Map();
+ library.bitmapSymbols = [];
+
+ var shape = new AnimateShapeSymbol();
+ shape.id = 1;
+ shape.commands = [];
+ library.symbols.set(shape.id, shape);
+ return library;
+ }
+
+ private static function addAnimateSprite(library:AnimateLibrary, id:Int, className:String, frames:Array>):AnimateSpriteSymbol
+ {
+ var sprite = new AnimateSpriteSymbol();
+ sprite.id = id;
+ sprite.className = className;
+ for (objects in frames)
+ {
+ var frame = new AnimateFrame();
+ frame.objects = objects;
+ sprite.frames.push(frame);
+ }
+ library.symbols.set(id, sprite);
+ return sprite;
+ }
+
+ private static function createAnimateClip(frames:Array>):MovieClip
+ {
+ var library = createAnimateLibrary();
+ var clip:MovieClip = cast addAnimateSprite(library, 10, null, frames).__createObject(library);
+ library.unload();
+ return clip;
+ }
+
+ private static function animateObject(type:AnimateFrameObjectType, id:Int, symbol:Int, clipDepth:Int, name:String = null):AnimateFrameObject
+ {
+ var object = new AnimateFrameObject();
+ object.type = type;
+ object.id = id;
+ object.symbol = symbol;
+ object.depth = 1;
+ object.clipDepth = clipDepth;
+ object.name = name;
+ return object;
+ }
+
+ private static function createSWFLiteClip(frames:Array>):MovieClip
+ {
+ var swf = new SWFLite();
+ swf.frameRate = 24;
+
+ var shape = new ShapeSymbol();
+ shape.id = 1;
+ shape.commands = [];
+ swf.symbols.set(shape.id, shape);
+
+ var sprite = new SpriteSymbol();
+ sprite.id = 10;
+ for (objects in frames)
+ {
+ var frame = new Frame();
+ frame.objects = objects;
+ sprite.frames.push(frame);
+ }
+ swf.symbols.set(sprite.id, sprite);
+
+ return cast sprite.__createObject(swf);
+ }
+
+ private static function swfLiteObject(type:FrameObjectType, id:Int, symbol:Int, clipDepth:Int):FrameObject
+ {
+ var object = new FrameObject();
+ object.type = type;
+ object.id = id;
+ object.symbol = symbol;
+ object.depth = 1;
+ object.clipDepth = clipDepth;
+ return object;
+ }
+ #end
+}
+
+class TimelineFieldClipA extends MovieClip
+{
+ public var first:DisplayObject;
+
+ public function new()
+ {
+ super();
+ }
+}
+
+class TimelineFieldClipB extends MovieClip
+{
+ public var second:DisplayObject;
+
+ public function new()
+ {
+ super();
+ }
+}
diff --git a/tests/tools/src/ToolsTestMain.hx b/tests/tools/src/ToolsTestMain.hx
new file mode 100644
index 000000000..44d6c7525
--- /dev/null
+++ b/tests/tools/src/ToolsTestMain.hx
@@ -0,0 +1,8 @@
+// Tests for the command-line tools, which need sys and hxp, so they are separate from the OpenFL tests
+class ToolsTestMain
+{
+ public static function main():Void
+ {
+ utest.UTest.run([new tools.AnimateLibraryExporterTest(), new tools.RoundTripTest(), new tools.ToolsTest()]);
+ }
+}
diff --git a/tests/tools/src/tools/AnimateLibraryExporterTest.hx b/tests/tools/src/tools/AnimateLibraryExporterTest.hx
new file mode 100644
index 000000000..95976b45d
--- /dev/null
+++ b/tests/tools/src/tools/AnimateLibraryExporterTest.hx
@@ -0,0 +1,256 @@
+package tools;
+
+import haxe.Json;
+import haxe.io.Bytes;
+import haxe.zip.Reader;
+import hxp.Path;
+import lime.tools.Asset;
+import openfl.utils.ByteArray;
+import swf.SWFRoot;
+import swf.data.consts.SoundCompression;
+import swf.data.consts.SoundRate;
+import swf.data.consts.SoundSize;
+import swf.data.consts.SoundType;
+import swf.exporters.AnimateLibraryExporter;
+import swf.tags.TagDefineShape;
+import swf.tags.TagDefineSound;
+import sys.FileSystem;
+import sys.io.File;
+import tests.SWFBuilder;
+import tests.ShapeBuilder;
+import utest.Assert;
+import utest.Test;
+
+@:access(swf.exporters.AnimateLibraryExporter)
+class AnimateLibraryExporterTest extends Test
+{
+ public function testGeneratedClassesAreTemplates():Void
+ {
+ var exporter = export(new SWFBuilder().add(SWFBuilder.createSprite(1)).link(1, "Clip").parse());
+
+ var output:Array = [];
+ Assert.same(["Clip"], exporter.generateClasses("generated", output));
+ Assert.equals(1, output.length);
+ Assert.isTrue(output[0].type == lime.tools.AssetType.TEMPLATE);
+ }
+
+ public function testIncludedClassesOnlyDependOnTheLibrary():Void
+ {
+ // Clip has a child field of type Child, so it depends on Child and on Child's base class
+ Assert.same(["Child", "Clip"], getIncludedClasses(null));
+ Assert.same([], getIncludedClasses("com.example.Missing"));
+ Assert.same(["Child", "Clip"], getIncludedClasses("Clip"));
+ }
+
+ public function testPrefixedGeneratedClasses():Void
+ {
+ var exporter = createChildAndClip("Child");
+ var output:Array = [];
+ var included = [];
+ var generated = exporter.generateClasses("generated", output, "lib", included);
+ generated.sort(Reflect.compare);
+ included.sort(Reflect.compare);
+ Assert.same(["LibChild", "LibClip"], generated);
+ Assert.same(["LibChild", "LibClip"], included);
+
+ var clip:String = null;
+ for (asset in output)
+ {
+ if (Path.withoutDirectory(asset.targetPath) == "LibClip.hx") clip = asset.data;
+ }
+ // the base class from this library is generated with the same prefix
+ Assert.isTrue(clip.indexOf("class LibClip extends LibChild") > -1);
+ // generated classes register themselves, since their names do not match the linkage
+ Assert.isTrue(clip.indexOf('__registerGeneratedClass("' + exporter.libraryData.uuid + '", 2, LibClip)') > -1);
+ }
+
+ public function testSymbolClassZeroIsRoot():Void
+ {
+ // the document class is linked to character 0, and is not always named *_fla.MainTimeline
+ var exporter = export(new SWFBuilder().link(0, "MainDocument").parse());
+ Assert.equals("MainDocument", exporter.libraryData.root.className);
+ }
+
+ public function testLineGradientStyleIsSerialized():Void
+ {
+ var tag = new TagDefineShape();
+ tag.characterId = 1;
+ tag.shapes = new ShapeBuilder()
+ .addGradientLine(20, [0xFFFF0000, 0xFF0000FF], [0, 255])
+ .move(0, 0, -1, -1, 1)
+ .lineTo(1000, 0)
+ .end();
+
+ var exporter = export(new SWFBuilder().parse());
+ var symbol = exporter.addShape(tag);
+ var commands:Array = symbol.commands;
+
+ // LINE_GRADIENT_STYLE, type, colors, alphas, ratios, matrix, spread, interpolation, focal point
+ var index = -1;
+ for (i in 0...commands.length - 8)
+ {
+ if (commands[i] == 9 && Std.isOfType(commands[i + 2], Array))
+ {
+ index = i;
+ break;
+ }
+ }
+ Assert.isTrue(index > -1);
+ Assert.same([0xFF0000, 0x0000FF], commands[index + 2]);
+ Assert.same([0, 255], commands[index + 4]);
+ }
+
+ public function testLinkedSoundExport():Void
+ {
+ var pcm16 = createPCM(441, 2);
+ var pcm8 = createPCM(1001, 1);
+ var frames = createMP3Frames();
+ var mp3 = new ByteArray();
+ // an empty ID3v2 tag, which DefineSound does not keep
+ for (value in [0x49, 0x44, 0x33, 3, 0, 0, 0, 0, 0, 0])
+ {
+ mp3.writeByte(value);
+ }
+ mp3.writeBytes(frames);
+
+ var path = tempPath("sounds.zip");
+ new AnimateLibraryExporter(new SWFBuilder()
+ .add(TagDefineSound.create(1, SoundCompression.UNCOMPRESSED_LITTLE_ENDIAN, SoundRate.KHZ_11, SoundSize.BIT_16, SoundType.MONO, 441, pcm16))
+ .add(TagDefineSound.create(2, SoundCompression.UNCOMPRESSED_NATIVE_ENDIAN, SoundRate.KHZ_22, SoundSize.BIT_8, SoundType.STEREO, 500, pcm8))
+ .add(TagDefineSound.createWithMP3(3, mp3))
+ .add(TagDefineSound.create(4, SoundCompression.ADPCM, SoundRate.KHZ_22, SoundSize.BIT_16, SoundType.MONO, 4, createPCM(4, 1)))
+ .add(TagDefineSound.create(5, SoundCompression.SPEEX, SoundRate.KHZ_5, SoundSize.BIT_16, SoundType.MONO, 4, createPCM(4, 1)))
+ .link(1, "TestSound")
+ .link(2, "TestSound8")
+ .link(3, "TestMp3")
+ .link(4, "TestAdpcm")
+ .link(5, "TestSpeex")
+ .parse(), path);
+
+ var entries = readZip(path);
+ checkWAV(entries.get("sounds/1.wav"), 1, 11025, 16, pcm16.length);
+ checkWAV(entries.get("sounds/2.wav"), 2, 22050, 8, pcm8.length);
+ var expectedMP3 = Bytes.alloc(frames.length);
+ for (i in 0...frames.length)
+ {
+ expectedMP3.set(i, frames[i]);
+ }
+ Assert.equals(0, entries.get("sounds/3.mp3").compare(expectedMP3));
+ Assert.isFalse(entries.exists("sounds/4.wav"));
+
+ var symbols = new Map();
+ for (symbol in (Json.parse(entries.get("data.json").toString()).symbols : Array))
+ {
+ if (symbol != null) symbols.set(symbol.id, symbol);
+ }
+ Assert.equals("TestSound", symbols.get(1).className);
+ Assert.equals("sounds/1.wav", symbols.get(1).path);
+ Assert.equals(7, symbols.get(1).type);
+ Assert.equals("TestMp3", symbols.get(3).className);
+ Assert.isFalse(symbols.exists(4));
+ Assert.isFalse(symbols.exists(5));
+
+ var soundAssets = [];
+ for (asset in (Json.parse(entries.get("library.json").toString()).assets : Array))
+ {
+ if (asset.type == "SOUND") soundAssets.push(asset.path);
+ }
+ soundAssets.sort(Reflect.compare);
+ Assert.same(["sounds/1.wav", "sounds/2.wav", "sounds/3.mp3"], soundAssets);
+ }
+
+ private static function getIncludedClasses(childBaseClassName:String):Array
+ {
+ var exporter = createChildAndClip(null);
+ exporter.libraryData.symbols.get(1).baseClassName = childBaseClassName;
+
+ var included = [];
+ exporter.generateClasses("generated", [], "", included);
+ included.sort(Reflect.compare);
+ return included;
+ }
+
+ // Clip has a child field of type Child
+ private static function createChildAndClip(clipBaseClassName:String):AnimateLibraryExporter
+ {
+ var exporter = export(new SWFBuilder().parse());
+ exporter.libraryData.symbols = new Map();
+ exporter.libraryData.symbols.set(1, {type: 5, id: 1, className: "Child", baseClassName: null, frames: []});
+ exporter.libraryData.symbols.set(2, {
+ type: 5,
+ id: 2,
+ className: "Clip",
+ baseClassName: clipBaseClassName,
+ frames: [{objects: [{name: "child", symbol: 1}]}]
+ });
+ return exporter;
+ }
+
+ private static function export(swf:SWFRoot, path:String = null):AnimateLibraryExporter
+ {
+ return new AnimateLibraryExporter(swf, path != null ? path : tempPath("library.zip"));
+ }
+
+ private static function checkWAV(wav:Bytes, channels:Int, sampleRate:Int, bitsPerSample:Int, dataLength:Int):Void
+ {
+ Assert.equals("RIFF", wav.getString(0, 4));
+ // RIFF chunks are padded to an even length
+ Assert.equals(44 + dataLength + dataLength % 2, wav.length);
+ Assert.equals(wav.length - 8, wav.getInt32(4));
+ Assert.equals("WAVE", wav.getString(8, 4));
+ Assert.equals(1, wav.getUInt16(20));
+ Assert.equals(channels, wav.getUInt16(22));
+ Assert.equals(sampleRate, wav.getInt32(24));
+ Assert.equals(sampleRate * channels * Std.int(bitsPerSample / 8), wav.getInt32(28));
+ Assert.equals(bitsPerSample, wav.getUInt16(34));
+ Assert.equals("data", wav.getString(36, 4));
+ Assert.equals(dataLength, wav.getInt32(40));
+ }
+
+ private static function createMP3Frames():ByteArray
+ {
+ // two silent MPEG-1 Layer III frames at 128 kbit/s and 44.1 kHz, 417 bytes each
+ var frames = new ByteArray();
+ for (frame in 0...2)
+ {
+ for (value in [0xFF, 0xFB, 0x90, 0x00])
+ {
+ frames.writeByte(value);
+ }
+ for (i in 0...413)
+ {
+ frames.writeByte(0);
+ }
+ }
+ return frames;
+ }
+
+ private static function createPCM(length:Int, seed:Int):ByteArray
+ {
+ var data = new ByteArray();
+ for (i in 0...length)
+ {
+ data.writeByte((i * seed) & 0xFF);
+ }
+ return data;
+ }
+
+ private static function readZip(path:String):Map
+ {
+ var entries = new Map();
+ var input = File.read(path, true);
+ for (entry in Reader.readZip(input))
+ {
+ entries.set(entry.fileName, entry.compressed ? Reader.unzip(entry) : entry.data);
+ }
+ input.close();
+ return entries;
+ }
+
+ public static function tempPath(name:String):String
+ {
+ var directory = Path.combine(Path.directory(Sys.programPath()), "tmp");
+ if (!FileSystem.exists(directory)) FileSystem.createDirectory(directory);
+ return Path.combine(directory, name);
+ }
+}
diff --git a/tests/tools/src/tools/RoundTripTest.hx b/tests/tools/src/tools/RoundTripTest.hx
new file mode 100644
index 000000000..6a75da3dd
--- /dev/null
+++ b/tests/tools/src/tools/RoundTripTest.hx
@@ -0,0 +1,245 @@
+package tools;
+
+import openfl.display.DisplayObject;
+import openfl.display.MovieClip;
+import swf.data.SWFMatrix;
+import swf.data.SWFRectangle;
+import swf.exporters.AnimateLibraryExporter;
+import swf.exporters.animate.AnimateLibrary;
+import swf.tags.TagDefineShape;
+import swf.tags.TagDefineSprite;
+import swf.tags.TagEnd;
+import swf.tags.TagPlaceObject2;
+import swf.tags.TagRemoveObject2;
+import swf.tags.TagShowFrame;
+import sys.io.File;
+import tests.MorphShapeTest;
+import tests.SWFBuilder;
+import tests.ShapeBuilder;
+import utest.Assert;
+import utest.Async;
+import utest.Test;
+
+// Exports a SWF with the Animate exporter, loads the result with AnimateLibrary, and plays it,
+// so the runtime is tested with the data the exporter really writes
+@:access(openfl.display.DisplayObject)
+@:access(swf.exporters.AnimateLibraryExporter)
+class RoundTripTest extends Test
+{
+ public function testTimelineRoundTrip(async:Async):Void
+ {
+ var sprite = new TagDefineSprite();
+ sprite.characterId = 10;
+ sprite.frameCount = 4;
+ var frames:Array> = [
+ // a named child
+ [place(1, 1, "first")],
+ // the same child, moved
+ [move(1, 2000)],
+ // another character at the same depth
+ [remove(1), place(1, 2)],
+ // a clipping character above it, with a masked child
+ [clip(2, 1, 3), place(3, 2)]
+ ];
+ for (frame in frames)
+ {
+ for (tag in frame)
+ {
+ sprite.tags.push(tag);
+ }
+ sprite.tags.push(new TagShowFrame());
+ }
+ sprite.tags.push(new TagEnd());
+
+ var swf = new SWFBuilder().add(square(1, 400, 0xFF0000)).add(square(2, 800, 0x0000FF)).add(sprite).link(10, "RoundTripClip").parse();
+ var path = AnimateLibraryExporterTest.tempPath("round-trip.zip");
+ new AnimateLibraryExporter(swf, path);
+
+ AnimateLibrary.loadFromBytes(File.getBytes(path)).onComplete(function(library)
+ {
+ var clip = library.getMovieClip("RoundTripClip");
+ Assert.notNull(clip);
+ Assert.equals(4, clip.totalFrames);
+
+ var expected = [
+ "first 20x20 at 0",
+ "first 20x20 at 100",
+ "40x40 at 0",
+ "40x40 at 0 | 20x20 at 0 mask | 40x40 at 0 masked"
+ ];
+ for (frame in 1...5)
+ {
+ clip.gotoAndStop(frame);
+ Assert.equals(expected[frame - 1], describe(clip), "frame " + frame);
+ }
+
+ // rewinding rebuilds the earlier frames
+ for (frame in [2, 1])
+ {
+ clip.gotoAndStop(frame);
+ Assert.equals(expected[frame - 1], describe(clip), "frame " + frame + " after rewinding");
+ }
+
+ library.unload();
+ async.done();
+ }).onError(function(error)
+ {
+ Assert.fail("Could not load the exported library: " + error);
+ async.done();
+ });
+ }
+
+ public function testMorphShapeRoundTrip(async:Async):Void
+ {
+ // a morph shape whose start and end edges do not match is skipped
+ var broken = MorphShapeTest.createMorphShape(2, 400, 800);
+ broken.endEdges = new ShapeBuilder().move(0, 0).lineTo(800, 0).lineTo(-800, 0).edges();
+
+ var sprite = new TagDefineSprite();
+ sprite.characterId = 10;
+ sprite.frameCount = 4;
+ var frames:Array> = [[place(1, 1)], [morph(1, 32768)], [morph(1, 65535)], [place(2, 2)]];
+ for (frame in frames)
+ {
+ for (tag in frame)
+ {
+ sprite.tags.push(tag);
+ }
+ sprite.tags.push(new TagShowFrame());
+ }
+ sprite.tags.push(new TagEnd());
+
+ var swf = new SWFBuilder().add(MorphShapeTest.createMorphShape(1, 400, 800)).add(broken).add(sprite).link(10, "MorphClip").parse();
+ var path = AnimateLibraryExporterTest.tempPath("morph-round-trip.zip");
+ var exporter = new AnimateLibraryExporter(swf, path);
+
+ // each ratio is drawn once, and placements without a ratio start at 0
+ var morphShape:Dynamic = exporter.libraryData.symbols.get(1);
+ Assert.equals(8, morphShape.type);
+ Assert.same([0, 32768, 65535], morphShape.ratios);
+ Assert.equals(3, morphShape.shapes.length);
+ Assert.isFalse(exporter.libraryData.symbols.exists(2));
+ var ratios = [];
+ for (frame in (exporter.libraryData.symbols.get(10).frames : Array))
+ {
+ for (object in (frame.objects : Array))
+ {
+ if (object.symbol == 1) ratios.push(object.ratio);
+ }
+ }
+ Assert.same([0, 32768, 65535], ratios);
+
+ AnimateLibrary.loadFromBytes(File.getBytes(path)).onComplete(function(library)
+ {
+ var clip = library.getMovieClip("MorphClip");
+ var states = [];
+ for (frame in [1, 2, 3, 4, 2, 1])
+ {
+ clip.gotoAndStop(frame);
+ states.push(describe(clip));
+ }
+ Assert.equals("20x20 at 0, 30x30 at 0, 40x40 at 0, 40x40 at 0, 30x30 at 0, 20x20 at 0", states.join(", "));
+
+ library.unload();
+ async.done();
+ }).onError(function(error)
+ {
+ Assert.fail("Could not load the exported library: " + error);
+ async.done();
+ });
+ }
+
+ private static function describe(clip:MovieClip):String
+ {
+ var children = [];
+ for (i in 0...clip.numChildren)
+ {
+ var child = clip.getChildAt(i);
+ var name = StringTools.startsWith(child.name, "instance") ? "" : child.name + " ";
+ var flags = (child.__isMask == true ? " mask" : "") + (isMasked(child) ? " masked" : "");
+ children.push(name + Math.round(child.width) + "x" + Math.round(child.height) + " at " + Math.round(child.x) + flags);
+ }
+ return children.join(" | ");
+ }
+
+ private static function isMasked(child:DisplayObject):Bool
+ {
+ #if (openfl >= "9.5.0")
+ return child.mask != null || child.clippingLayer != null;
+ #else
+ return child.mask != null;
+ #end
+ }
+
+ private static function square(id:Int, size:Int, rgb:Int):TagDefineShape
+ {
+ var tag = new TagDefineShape();
+ tag.characterId = id;
+ tag.shapeBounds = new SWFRectangle();
+ tag.shapeBounds.xmin = 0;
+ tag.shapeBounds.ymin = 0;
+ tag.shapeBounds.xmax = size;
+ tag.shapeBounds.ymax = size;
+ tag.shapes = new ShapeBuilder()
+ .addSolidFill(0xFF000000 | rgb)
+ .move(0, 0, 0, 1, 0)
+ .lineTo(size, 0)
+ .lineTo(0, size)
+ .lineTo(-size, 0)
+ .lineTo(0, -size)
+ .end();
+ return tag;
+ }
+
+ private static function place(depth:Int, characterId:Int, name:String = null):TagPlaceObject2
+ {
+ var tag = new TagPlaceObject2();
+ tag.depth = depth;
+ tag.hasCharacter = true;
+ tag.characterId = characterId;
+ tag.hasMatrix = true;
+ tag.matrix = new SWFMatrix();
+ if (name != null)
+ {
+ tag.hasName = true;
+ tag.instanceName = name;
+ }
+ return tag;
+ }
+
+ private static function move(depth:Int, x:Int):TagPlaceObject2
+ {
+ var tag = new TagPlaceObject2();
+ tag.depth = depth;
+ tag.hasMove = true;
+ tag.hasMatrix = true;
+ tag.matrix = new SWFMatrix();
+ tag.matrix.translateX = x;
+ return tag;
+ }
+
+ private static function morph(depth:Int, ratio:Int):TagPlaceObject2
+ {
+ var tag = new TagPlaceObject2();
+ tag.depth = depth;
+ tag.hasMove = true;
+ tag.hasRatio = true;
+ tag.ratio = ratio;
+ return tag;
+ }
+
+ private static function clip(depth:Int, characterId:Int, clipDepth:Int):TagPlaceObject2
+ {
+ var tag = place(depth, characterId);
+ tag.hasClipDepth = true;
+ tag.clipDepth = clipDepth;
+ return tag;
+ }
+
+ private static function remove(depth:Int):TagRemoveObject2
+ {
+ var tag = new TagRemoveObject2();
+ tag.depth = depth;
+ return tag;
+ }
+}
diff --git a/tests/tools/src/tools/ToolsTest.hx b/tests/tools/src/tools/ToolsTest.hx
new file mode 100644
index 000000000..476b1395f
--- /dev/null
+++ b/tests/tools/src/tools/ToolsTest.hx
@@ -0,0 +1,157 @@
+package tools;
+
+import haxe.Json;
+import hxp.Path;
+import hxp.System;
+import lime.tools.Asset;
+import lime.tools.AssetType;
+import lime.tools.HXProject;
+import lime.tools.Library;
+import lime.tools.Platform;
+import sys.FileSystem;
+import sys.io.File;
+import tests.SWFBuilder;
+import utest.Assert;
+import utest.Test;
+
+@:access(Tools)
+class ToolsTest extends Test
+{
+ public function testFlashClassPaths():Void
+ {
+ var swfPath = writeSWF(createDirectory("flash-paths"), new SWFBuilder()
+ .add(SWFBuilder.createSprite(1))
+ .add(SWFBuilder.createSprite(2))
+ .link(1, "SkyPandaTiles_fla.Clip")
+ .link(2, "Plain"));
+
+ // Haxe packages start with a lowercase letter, and the prefix is only added once
+ Assert.same(["../haxe/_generated/Plain.hx", "../haxe/_generated/skyPandaTiles_fla/Clip.hx"], getFlashClassPaths(swfPath, ""));
+ Assert.same(["../haxe/_generated/LibPlain.hx", "../haxe/_generated/skyPandaTiles_fla/LibClip.hx"], getFlashClassPaths(swfPath, "lib"));
+ }
+
+ public function testFlashLibraryCache():Void
+ {
+ var directory = createDirectory("flash-cache");
+ var project = createProject(Platform.FLASH, writeSWF(directory, createClipSWF()), true);
+ Tools.targetDirectory = directory;
+
+ var output = Tools.processLibraries(project);
+ Assert.isTrue(output.haxeflags.indexOf("Clip") > -1);
+ Assert.notNull(findTemplate(output, "Clip.hx"));
+
+ markCachedTemplates(directory);
+ var cached = Tools.processLibraries(project);
+ Assert.isTrue(cached.haxeflags.indexOf("Clip") > -1);
+ Assert.equals("// cached", findTemplate(cached, "Clip.hx").data);
+
+ // a different prefix makes the cache out of date
+ project.libraries[0].prefix = "lib";
+ var renamed = Tools.processLibraries(project);
+ Assert.isTrue(renamed.haxeflags.indexOf("LibClip") > -1);
+ Assert.notNull(findTemplate(renamed, "LibClip.hx"));
+ }
+
+ public function testAnimateGeneratedClassesAreCompiled():Void
+ {
+ var directory = createDirectory("animate-generate");
+ var project = createProject(Platform.HTML5, writeSWF(directory, createClipSWF()), true);
+ Tools.targetDirectory = directory;
+
+ var output = Tools.processLibraries(project);
+ Assert.isTrue(output.haxeflags.indexOf("Clip") > -1);
+ Assert.notNull(findTemplate(output, "Clip.hx"));
+
+ // templates come back from the cache, otherwise Lime would delete them as stale files
+ markCachedTemplates(directory);
+ var cached = Tools.processLibraries(project);
+ Assert.isTrue(cached.haxeflags.indexOf("Clip") > -1);
+ Assert.equals("// cached", findTemplate(cached, "Clip.hx").data);
+
+ // classes are only compiled when generate="true"
+ project.libraries[0].generate = null;
+ var unset = Tools.processLibraries(project);
+ Assert.equals(-1, unset.haxeflags.indexOf("Clip"));
+ Assert.notNull(findTemplate(unset, "Clip.hx"));
+
+ project.libraries[0].generate = false;
+ var disabled = Tools.processLibraries(project);
+ Assert.equals(-1, disabled.haxeflags.indexOf("Clip"));
+ Assert.isNull(findTemplate(disabled, "Clip.hx"));
+ }
+
+ public function testVersionComesFromHaxelibJson():Void
+ {
+ // run.n is next to haxelib.json, here the repository is three directories up from tests/tools/bin
+ var root = Path.combine(Path.directory(Sys.programPath()), "../../..");
+ var expected:String = Json.parse(File.getContent(Path.combine(root, "haxelib.json"))).version;
+ Assert.equals(expected, Tools.getVersion(root));
+ Assert.equals("0.0.0", Tools.getVersion(createDirectory("no-haxelib-json")));
+ }
+
+ private static function createClipSWF():SWFBuilder
+ {
+ return new SWFBuilder().add(SWFBuilder.createSprite(1)).link(1, "Clip");
+ }
+
+ private static function createProject(target:Platform, swfPath:String, generate:Null):HXProject
+ {
+ var project = new HXProject();
+ project.target = target;
+ project.libraries.push(new Library(swfPath, "lib", "swf", true, false, generate, ""));
+ return project;
+ }
+
+ private static function createDirectory(name:String):String
+ {
+ var directory = AnimateLibraryExporterTest.tempPath(name);
+ if (FileSystem.exists(directory)) System.removeDirectory(directory);
+ System.mkdir(directory);
+ return directory;
+ }
+
+ private static function findTemplate(project:HXProject, fileName:String):Asset
+ {
+ for (asset in project.assets)
+ {
+ if (asset.type == AssetType.TEMPLATE && Path.withoutDirectory(asset.targetPath) == fileName)
+ {
+ return asset;
+ }
+ }
+ return null;
+ }
+
+ private static function getFlashClassPaths(swfPath:String, prefix:String):Array
+ {
+ var output = new HXProject();
+ Tools.generateSWFClasses(new HXProject(), output, new Asset(swfPath, "library.swf", AssetType.BINARY), prefix);
+
+ var paths = [for (asset in output.assets) StringTools.replace(asset.targetPath, "\\", "/")];
+ paths.sort(Reflect.compare);
+ return paths;
+ }
+
+ private static function markCachedTemplates(directory:String):Void
+ {
+ // so the next build can tell whether the templates came from the cache
+ var cacheFile = Path.combine(directory, "obj/libraries/lib.classes.json");
+ Assert.isTrue(FileSystem.exists(cacheFile));
+
+ var cache:Dynamic = Json.parse(File.getContent(cacheFile));
+ for (template in (cache.templates : Array))
+ {
+ template.data = "// cached";
+ }
+ File.saveContent(cacheFile, Json.stringify(cache));
+ }
+
+ private static function writeSWF(directory:String, swf:SWFBuilder):String
+ {
+ var path = Path.combine(directory, "library.swf");
+ File.saveBytes(path, swf.build());
+ // caches are only used when they are newer than the SWF file
+ Sys.sleep(1.1);
+ return path;
+ }
+}
diff --git a/tests/tools/tools.hxml b/tests/tools/tools.hxml
new file mode 100644
index 000000000..e207ef4bf
--- /dev/null
+++ b/tests/tools/tools.hxml
@@ -0,0 +1,14 @@
+-cp src
+-cp ../src
+-cp ../../src
+-cp ../../scripts
+-lib openfl
+-lib lime
+-lib hxp
+-lib format
+-lib utest
+-D lime-cairo
+-D lime-cffi
+-D optional-cffi
+-main ToolsTestMain
+-neko bin/tools.n