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 @@ -[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE.md) [![Haxelib Version](https://img.shields.io/github/tag/openfl/swf.svg?style=flat&label=haxelib)](http://lib.haxe.org/p/swf) +[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE.md) [![Haxelib Version](https://img.shields.io/github/tag/openfl/swf.svg?style=flat&label=haxelib)](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 @@ + +