diff --git a/Tests/LottieMetalMacTest/.gitignore b/Tests/LottieMetalMacTest/.gitignore
new file mode 100644
index 0000000000..31360466c1
--- /dev/null
+++ b/Tests/LottieMetalMacTest/.gitignore
@@ -0,0 +1,2 @@
+TestData/*.json
+
diff --git a/Tests/LottieMetalMacTest/BUILD b/Tests/LottieMetalMacTest/BUILD
new file mode 100644
index 0000000000..110af1aa5a
--- /dev/null
+++ b/Tests/LottieMetalMacTest/BUILD
@@ -0,0 +1,173 @@
+load("@build_bazel_rules_apple//apple:macos.bzl",
+ "macos_application",
+)
+
+load("@build_bazel_rules_swift//swift:swift.bzl",
+ "swift_library",
+)
+
+load("//build-system/bazel-utils:plist_fragment.bzl",
+ "plist_fragment",
+)
+
+load(
+ "@build_bazel_rules_apple//apple:resources.bzl",
+ "apple_resource_bundle",
+ "apple_resource_group",
+)
+
+load(
+ "@rules_xcodeproj//xcodeproj:defs.bzl",
+ "top_level_target",
+ "top_level_targets",
+ "xcodeproj",
+ "xcode_provisioning_profile",
+)
+
+load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")
+
+load(
+ "@build_configuration//:variables.bzl",
+ "telegram_bazel_path",
+)
+
+filegroup(
+ name = "AppResources",
+ srcs = glob([
+ "Resources/**/*",
+ ], exclude = ["Resources/**/.*"]),
+)
+
+plist_fragment(
+ name = "BuildNumberInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleVersion
+ 1
+ """
+)
+
+plist_fragment(
+ name = "VersionInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleShortVersionString
+ 1.0
+ """
+)
+
+plist_fragment(
+ name = "AppNameInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleDisplayName
+ Test
+ """
+)
+
+plist_fragment(
+ name = "MacAppInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ Telegram
+ CFBundlePackageType
+ APPL
+ NSMainStoryboardFile
+ Main
+ NSPrincipalClass
+ NSApplication
+ """
+)
+
+filegroup(
+ name = "TestDataBundleFiles",
+ srcs = glob([
+ "TestData/*.json",
+ ]),
+ visibility = ["//visibility:public"],
+)
+
+plist_fragment(
+ name = "TestDataBundleInfoPlist",
+ extension = "plist",
+ template =
+ """
+ CFBundleIdentifier
+ org.telegram.TestDataBundle
+ CFBundleDevelopmentRegion
+ en
+ CFBundleName
+ TestDataBundle
+ """
+)
+
+apple_resource_bundle(
+ name = "TestDataBundle",
+ infoplists = [
+ ":TestDataBundleInfoPlist",
+ ],
+ resources = [
+ ":TestDataBundleFiles",
+ ],
+)
+
+swift_library(
+ name = "MacLib",
+ srcs = glob([
+ "MacSources/**/*.swift",
+ ]),
+ data = [
+ "Resources/Main.storyboard",
+ ],
+)
+
+macos_application(
+ name = "LottieMetalMacTest",
+ app_icons = [],
+ bundle_id = "com.example.hello-world-swift",
+ infoplists = [
+ ":MacAppInfoPlist",
+ ":BuildNumberInfoPlist",
+ ":VersionInfoPlist",
+ ],
+ minimum_os_version = "10.13",
+ deps = [
+ ":MacLib"
+ ],
+ visibility = ["//visibility:public"],
+)
+
+xcodeproj(
+ name = "LottieMetalMacTest_xcodeproj",
+ build_mode = "bazel",
+ bazel_path = telegram_bazel_path,
+ project_name = "LottieMetalMacTest",
+ tags = ["manual"],
+ top_level_targets = top_level_targets(
+ labels = [
+ ":LottieMetalMacTest",
+ ],
+ ),
+ xcode_configurations = {
+ "Debug": {
+ "//command_line_option:compilation_mode": "dbg",
+ },
+ "Release": {
+ "//command_line_option:compilation_mode": "opt",
+ },
+ },
+ default_xcode_configuration = "Debug"
+)
diff --git a/Tests/LottieMetalMacTest/MacSources/AppDelegate.swift b/Tests/LottieMetalMacTest/MacSources/AppDelegate.swift
new file mode 100644
index 0000000000..377719dffc
--- /dev/null
+++ b/Tests/LottieMetalMacTest/MacSources/AppDelegate.swift
@@ -0,0 +1,19 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import Cocoa
+
+@NSApplicationMain
+@objc(AppDelegate)
+class AppDelegate: NSObject, NSApplicationDelegate {}
diff --git a/Tests/LottieMetalMacTest/MacSources/ViewController.swift b/Tests/LottieMetalMacTest/MacSources/ViewController.swift
new file mode 100644
index 0000000000..9811f38752
--- /dev/null
+++ b/Tests/LottieMetalMacTest/MacSources/ViewController.swift
@@ -0,0 +1,11 @@
+import Cocoa
+
+@objc(ViewController)
+class ViewController: NSViewController {
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ self.view.layer?.backgroundColor = NSColor.blue.cgColor
+ }
+}
+
diff --git a/Tests/LottieMetalMacTest/Resources/Main.storyboard b/Tests/LottieMetalMacTest/Resources/Main.storyboard
new file mode 100644
index 0000000000..d9938c1396
--- /dev/null
+++ b/Tests/LottieMetalMacTest/Resources/Main.storyboard
@@ -0,0 +1,697 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tests/LottieMetalTest/MacSources/ViewController.swift b/Tests/LottieMetalTest/MacSources/ViewController.swift
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD b/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD
index 1b46d73bdf..0aedc6e7b2 100644
--- a/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD
+++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/BUILD
@@ -24,6 +24,7 @@ objc_library(
],
deps = [
"//submodules/TelegramUI/Components/LottieCpp",
+ "//Tests/LottieMetalTest/thorvg",
],
sdk_frameworks = [
"Foundation",
diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h
index 7a3dded9ce..4293902250 100644
--- a/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h
+++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/PublicHeaders/SoftwareLottieRenderer/SoftwareLottieRenderer.h
@@ -11,7 +11,14 @@ extern "C" {
#endif
CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path);
-UIImage * _Nullable renderLottieAnimationContainer(LottieAnimationContainer * _Nonnull animationContainer, CGSize size, bool useReferenceRendering);
+
+@interface SoftwareLottieRenderer : NSObject
+
+- (instancetype _Nonnull)initWithAnimationContainer:(LottieAnimationContainer * _Nonnull)animationContainer;
+
+- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering;
+
+@end
#ifdef __cplusplus
}
diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm
index 3d5b3ce137..a4903b4bb6 100644
--- a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm
+++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/SoftwareLottieRenderer.mm
@@ -2,148 +2,380 @@
#import "Canvas.h"
#import "CoreGraphicsCanvasImpl.h"
+#import "ThorVGCanvasImpl.h"
-namespace {
+#include
-static void drawLottieRenderableItem(std::shared_ptr context, LottieRenderContent * _Nonnull item) {
- if (item.path == nil) {
- return;
+namespace lottie {
+
+static void processRenderContentItem(std::shared_ptr const &contentItem, std::optional &effectiveLocalBounds, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) {
+ for (const auto &shadingVariant : contentItem->shadings) {
+ CGRect shapeBounds = bezierPathsBoundingBoxParallel(bezierPathsBoundingBoxContext, shadingVariant->explicitPath.value());
+ if (shadingVariant->stroke) {
+ shapeBounds = shapeBounds.insetBy(-shadingVariant->stroke->lineWidth / 2.0, -shadingVariant->stroke->lineWidth / 2.0);
+ if (effectiveLocalBounds) {
+ effectiveLocalBounds = effectiveLocalBounds->unionWith(shapeBounds);
+ } else {
+ effectiveLocalBounds = shapeBounds;
+ }
+ } else if (shadingVariant->fill) {
+ if (effectiveLocalBounds) {
+ effectiveLocalBounds = effectiveLocalBounds->unionWith(shapeBounds);
+ } else {
+ effectiveLocalBounds = shapeBounds;
+ }
+ }
}
- std::shared_ptr path = lottie::CGPath::makePath();
- [item.path enumerateItems:^(LottiePathItem * _Nonnull pathItem) {
- switch (pathItem->type) {
- case LottiePathItemTypeMoveTo: {
- path->moveTo(lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y));
- break;
- }
- case LottiePathItemTypeLineTo: {
- path->addLineTo(lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y));
- break;
- }
- case LottiePathItemTypeCurveTo: {
- path->addCurveTo(lottie::Vector2D(pathItem->points[2].x, pathItem->points[2].y), lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y), lottie::Vector2D(pathItem->points[1].x, pathItem->points[1].y));
- break;
- }
- case LottiePathItemTypeClose: {
- path->closeSubpath();
- break;
- }
- default: {
- break;
- }
- }
- }];
-
- if (item.stroke != nil) {
- if ([item.stroke.shading isKindOfClass:[LottieRenderContentSolidShading class]]) {
- LottieRenderContentSolidShading *solidShading = (LottieRenderContentSolidShading *)item.stroke.shading;
-
- lottieRendering::LineJoin lineJoin = lottieRendering::LineJoin::Bevel;
- switch (item.stroke.lineJoin) {
- case kCGLineJoinBevel: {
- lineJoin = lottieRendering::LineJoin::Bevel;
- break;
- }
- case kCGLineJoinRound: {
- lineJoin = lottieRendering::LineJoin::Round;
- break;
- }
- case kCGLineJoinMiter: {
- lineJoin = lottieRendering::LineJoin::Miter;
- break;
- }
- default: {
- break;
- }
- }
-
- lottieRendering::LineCap lineCap = lottieRendering::LineCap::Square;
- switch (item.stroke.lineCap) {
- case kCGLineCapButt: {
- lineCap = lottieRendering::LineCap::Butt;
- break;
- }
- case kCGLineCapRound: {
- lineCap = lottieRendering::LineCap::Round;
- break;
- }
- case kCGLineCapSquare: {
- lineCap = lottieRendering::LineCap::Square;
- break;
- }
- default: {
- break;
- }
- }
-
- std::vector dashPattern;
- if (item.stroke.dashPattern != nil) {
- for (NSNumber *value in item.stroke.dashPattern) {
- dashPattern.push_back([value doubleValue]);
- }
- }
-
- context->strokePath(path, item.stroke.lineWidth, lineJoin, lineCap, item.stroke.dashPhase, dashPattern, lottieRendering::Color(solidShading.color.r, solidShading.color.g, solidShading.color.b, solidShading.color.a));
- } else if ([item.stroke.shading isKindOfClass:[LottieRenderContentGradientShading class]]) {
- __unused LottieRenderContentGradientShading *gradientShading = (LottieRenderContentGradientShading *)item.stroke.shading;
- }
- } else if (item.fill != nil) {
- lottieRendering::FillRule rule = lottieRendering::FillRule::NonZeroWinding;
- switch (item.fill.fillRule) {
- case LottieFillRuleEvenOdd: {
- rule = lottieRendering::FillRule::EvenOdd;
- break;
- }
- case LottieFillRuleWinding: {
- rule = lottieRendering::FillRule::NonZeroWinding;
- break;
- }
- default: {
- break;
- }
- }
-
- if ([item.fill.shading isKindOfClass:[LottieRenderContentSolidShading class]]) {
- LottieRenderContentSolidShading *solidShading = (LottieRenderContentSolidShading *)item.fill.shading;
-
- context->fillPath(path, rule, lottieRendering::Color(solidShading.color.r, solidShading.color.g, solidShading.color.b, solidShading.color.a));
- } else if ([item.fill.shading isKindOfClass:[LottieRenderContentGradientShading class]]) {
- LottieRenderContentGradientShading *gradientShading = (LottieRenderContentGradientShading *)item.fill.shading;
-
- std::vector colors;
- std::vector locations;
- for (LottieColorStop *colorStop in gradientShading.colorStops) {
- colors.push_back(lottieRendering::Color(colorStop.color.r, colorStop.color.g, colorStop.color.b, colorStop.color.a));
- locations.push_back(colorStop.location);
- }
-
- lottieRendering::Gradient gradient(colors, locations);
- lottie::Vector2D start(gradientShading.start.x, gradientShading.start.y);
- lottie::Vector2D end(gradientShading.end.x, gradientShading.end.y);
-
- switch (gradientShading.gradientType) {
- case LottieGradientTypeLinear: {
- context->linearGradientFillPath(path, rule, gradient, start, end);
- break;
- }
- case LottieGradientTypeRadial: {
- context->radialGradientFillPath(path, rule, gradient, start, 0.0, start, start.distanceTo(end));
- break;
- }
- default: {
- break;
- }
- }
- }
+ for (const auto &subItem : contentItem->subItems) {
+ processRenderContentItem(subItem, effectiveLocalBounds, bezierPathsBoundingBoxContext);
}
}
-static void renderLottieRenderNode(LottieRenderNode * _Nonnull node, std::shared_ptr parentContext, lottie::Vector2D const &globalSize, double parentAlpha) {
- float normalizedOpacity = node.opacity;
+static void processRenderTree(std::shared_ptr const &node, Vector2D const &globalSize, CATransform3D const &parentTransform, bool isInvertedMask, BezierPathsBoundingBoxContext &bezierPathsBoundingBoxContext) {
+ if (node->isHidden() || node->alpha() == 0.0f) {
+ node->renderData.isValid = false;
+ return;
+ }
+
+ if (node->masksToBounds()) {
+ if (node->bounds().empty()) {
+ node->renderData.isValid = false;
+ return;
+ }
+ }
+
+ auto currentTransform = parentTransform;
+
+ Vector2D localTranslation(node->position().x + -node->bounds().x, node->position().y + -node->bounds().y);
+ CATransform3D localTransform = node->transform();
+ localTransform = localTransform.translated(localTranslation);
+
+ currentTransform = localTransform * currentTransform;
+
+ if (!currentTransform.isInvertible()) {
+ node->renderData.isValid = false;
+ return;
+ }
+
+ std::optional effectiveLocalBounds;
+
+ double alpha = node->alpha();
+
+ if (node->_contentItem) {
+ processRenderContentItem(node->_contentItem, effectiveLocalBounds, bezierPathsBoundingBoxContext);
+ }
+
+ bool isInvertedMatte = isInvertedMask;
+ if (isInvertedMatte) {
+ effectiveLocalBounds = node->bounds();
+ }
+
+ if (effectiveLocalBounds && effectiveLocalBounds->empty()) {
+ effectiveLocalBounds = std::nullopt;
+ }
+
+ std::optional effectiveLocalRect;
+ if (effectiveLocalBounds.has_value()) {
+ effectiveLocalRect = effectiveLocalBounds;
+ }
+
+ std::optional subnodesGlobalRect;
+ bool masksToBounds = node->masksToBounds();
+
+ int drawContentDescendants = 0;
+
+ for (const auto &item : node->subnodes()) {
+ processRenderTree(item, globalSize, currentTransform, false, bezierPathsBoundingBoxContext);
+ if (item->renderData.isValid) {
+ drawContentDescendants += item->renderData.drawContentDescendants;
+
+ if (item->_contentItem) {
+ drawContentDescendants += 1;
+ }
+
+ if (!item->renderData.localRect.empty()) {
+ if (effectiveLocalRect.has_value()) {
+ effectiveLocalRect = effectiveLocalRect->unionWith(item->renderData.localRect);
+ } else {
+ effectiveLocalRect = item->renderData.localRect;
+ }
+ }
+
+ if (subnodesGlobalRect) {
+ subnodesGlobalRect = subnodesGlobalRect->unionWith(item->renderData.globalRect);
+ } else {
+ subnodesGlobalRect = item->renderData.globalRect;
+ }
+ }
+ }
+
+ if (masksToBounds && effectiveLocalRect.has_value()) {
+ if (node->bounds().contains(effectiveLocalRect.value())) {
+ masksToBounds = false;
+ }
+ }
+
+ std::optional fuzzyGlobalRect;
+
+ if (effectiveLocalBounds) {
+ CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform);
+ if (fuzzyGlobalRect) {
+ fuzzyGlobalRect = fuzzyGlobalRect->unionWith(effectiveGlobalBounds);
+ } else {
+ fuzzyGlobalRect = effectiveGlobalBounds;
+ }
+ }
+
+ if (subnodesGlobalRect) {
+ if (fuzzyGlobalRect) {
+ fuzzyGlobalRect = fuzzyGlobalRect->unionWith(subnodesGlobalRect.value());
+ } else {
+ fuzzyGlobalRect = subnodesGlobalRect;
+ }
+ }
+
+ if (!fuzzyGlobalRect) {
+ node->renderData.isValid = false;
+ return;
+ }
+
+ CGRect globalRect(
+ std::floor(fuzzyGlobalRect->x),
+ std::floor(fuzzyGlobalRect->y),
+ std::ceil(fuzzyGlobalRect->width + fuzzyGlobalRect->x - floor(fuzzyGlobalRect->x)),
+ std::ceil(fuzzyGlobalRect->height + fuzzyGlobalRect->y - floor(fuzzyGlobalRect->y))
+ );
+
+ if (!CGRect(0.0, 0.0, globalSize.x, globalSize.y).intersects(globalRect)) {
+ node->renderData.isValid = false;
+ return;
+ }
+
+ if (masksToBounds && effectiveLocalBounds) {
+ CGRect effectiveGlobalBounds = effectiveLocalBounds->applyingTransform(currentTransform);
+ if (effectiveGlobalBounds.contains(CGRect(0.0, 0.0, globalSize.x, globalSize.y))) {
+ masksToBounds = false;
+ }
+ }
+
+ if (node->mask()) {
+ processRenderTree(node->mask(), globalSize, currentTransform, node->invertMask(), bezierPathsBoundingBoxContext);
+ if (node->mask()->renderData.isValid) {
+ if (!node->mask()->renderData.globalRect.intersects(globalRect)) {
+ node->renderData.isValid = false;
+ return;
+ }
+ } else {
+ node->renderData.isValid = false;
+ return;
+ }
+ }
+
+ CGRect localRect = effectiveLocalRect.value_or(CGRect(0.0, 0.0, 0.0, 0.0)).applyingTransform(localTransform);
+
+ node->renderData.isValid = true;
+
+ node->renderData.layer._bounds = node->bounds();
+ node->renderData.layer._position = node->position();
+ node->renderData.layer._transform = node->transform();
+ node->renderData.layer._opacity = alpha;
+ node->renderData.layer._masksToBounds = masksToBounds;
+ node->renderData.layer._isHidden = node->isHidden();
+
+ node->renderData.globalRect = globalRect;
+ node->renderData.localRect = localRect;
+ node->renderData.globalTransform = currentTransform;
+ node->renderData.drawsContent = effectiveLocalBounds.has_value();
+ node->renderData.drawContentDescendants = drawContentDescendants;
+ node->renderData.isInvertedMatte = isInvertedMatte;
+}
+
+}
+
+namespace {
+
+static void drawLottieContentItem(std::shared_ptr context, std::shared_ptr item) {
+ for (const auto &shading : item->shadings) {
+ if (shading->explicitPath->empty()) {
+ continue;
+ }
+
+ std::shared_ptr path = lottie::CGPath::makePath();
+
+ const auto iterate = [&](LottiePathItem const *pathItem) {
+ switch (pathItem->type) {
+ case LottiePathItemTypeMoveTo: {
+ path->moveTo(lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y));
+ break;
+ }
+ case LottiePathItemTypeLineTo: {
+ path->addLineTo(lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y));
+ break;
+ }
+ case LottiePathItemTypeCurveTo: {
+ path->addCurveTo(lottie::Vector2D(pathItem->points[2].x, pathItem->points[2].y), lottie::Vector2D(pathItem->points[0].x, pathItem->points[0].y), lottie::Vector2D(pathItem->points[1].x, pathItem->points[1].y));
+ break;
+ }
+ case LottiePathItemTypeClose: {
+ path->closeSubpath();
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ };
+
+ LottiePathItem pathItem;
+ for (const auto &path : shading->explicitPath.value()) {
+ std::optional previousElement;
+ for (const auto &element : path.elements()) {
+ if (previousElement.has_value()) {
+ if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) {
+ pathItem.type = LottiePathItemTypeLineTo;
+ pathItem.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
+ iterate(&pathItem);
+ } else {
+ pathItem.type = LottiePathItemTypeCurveTo;
+ pathItem.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
+ pathItem.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y);
+ pathItem.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y);
+ iterate(&pathItem);
+ }
+ } else {
+ pathItem.type = LottiePathItemTypeMoveTo;
+ pathItem.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
+ iterate(&pathItem);
+ }
+ previousElement = element;
+ }
+ if (path.closed().value_or(true)) {
+ pathItem.type = LottiePathItemTypeClose;
+ iterate(&pathItem);
+ }
+ }
+
+ if (shading->stroke) {
+ if (shading->stroke->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Solid) {
+ lottie::RenderTreeNodeContentItem::SolidShading *solidShading = (lottie::RenderTreeNodeContentItem::SolidShading *)shading->stroke->shading.get();
+
+ if (solidShading->opacity != 0.0) {
+ lottieRendering::LineJoin lineJoin = lottieRendering::LineJoin::Bevel;
+ switch (shading->stroke->lineJoin) {
+ case lottie::LineJoin::Bevel: {
+ lineJoin = lottieRendering::LineJoin::Bevel;
+ break;
+ }
+ case lottie::LineJoin::Round: {
+ lineJoin = lottieRendering::LineJoin::Round;
+ break;
+ }
+ case lottie::LineJoin::Miter: {
+ lineJoin = lottieRendering::LineJoin::Miter;
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ lottieRendering::LineCap lineCap = lottieRendering::LineCap::Square;
+ switch (shading->stroke->lineCap) {
+ case lottie::LineCap::Butt: {
+ lineCap = lottieRendering::LineCap::Butt;
+ break;
+ }
+ case lottie::LineCap::Round: {
+ lineCap = lottieRendering::LineCap::Round;
+ break;
+ }
+ case lottie::LineCap::Square: {
+ lineCap = lottieRendering::LineCap::Square;
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ std::vector dashPattern;
+ if (!shading->stroke->dashPattern.empty()) {
+ dashPattern = shading->stroke->dashPattern;
+ }
+
+ context->strokePath(path, shading->stroke->lineWidth, lineJoin, lineCap, shading->stroke->dashPhase, dashPattern, lottieRendering::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity));
+ } else if (shading->stroke->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Gradient) {
+ //TODO:gradient stroke
+ }
+ }
+ } else if (shading->fill) {
+ lottieRendering::FillRule rule = lottieRendering::FillRule::NonZeroWinding;
+ switch (shading->fill->rule) {
+ case lottie::FillRule::EvenOdd: {
+ rule = lottieRendering::FillRule::EvenOdd;
+ break;
+ }
+ case lottie::FillRule::NonZeroWinding: {
+ rule = lottieRendering::FillRule::NonZeroWinding;
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ if (shading->fill->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Solid) {
+ lottie::RenderTreeNodeContentItem::SolidShading *solidShading = (lottie::RenderTreeNodeContentItem::SolidShading *)shading->fill->shading.get();
+ if (solidShading->opacity != 0.0) {
+ context->fillPath(path, rule, lottieRendering::Color(solidShading->color.r, solidShading->color.g, solidShading->color.b, solidShading->color.a * solidShading->opacity));
+ }
+ } else if (shading->fill->shading->type() == lottie::RenderTreeNodeContentItem::ShadingType::Gradient) {
+ lottie::RenderTreeNodeContentItem::GradientShading *gradientShading = (lottie::RenderTreeNodeContentItem::GradientShading *)shading->fill->shading.get();
+
+ if (gradientShading->opacity != 0.0) {
+ std::vector colors;
+ std::vector locations;
+ for (const auto &color : gradientShading->colors) {
+ colors.push_back(lottieRendering::Color(color.r, color.g, color.b, color.a * gradientShading->opacity));
+ }
+ locations = gradientShading->locations;
+
+ lottieRendering::Gradient gradient(colors, locations);
+ lottie::Vector2D start(gradientShading->start.x, gradientShading->start.y);
+ lottie::Vector2D end(gradientShading->end.x, gradientShading->end.y);
+
+ switch (gradientShading->gradientType) {
+ case lottie::GradientType::Linear: {
+ context->linearGradientFillPath(path, rule, gradient, start, end);
+ break;
+ }
+ case lottie::GradientType::Radial: {
+ context->radialGradientFillPath(path, rule, gradient, start, 0.0, start, start.distanceTo(end));
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (const auto &subItem : item->subItems) {
+ drawLottieContentItem(context, subItem);
+ }
+}
+
+static void renderLottieRenderNode(std::shared_ptr node, std::shared_ptr parentContext, lottie::Vector2D const &globalSize, double parentAlpha) {
+ if (!node->renderData.isValid) {
+ return;
+ }
+ float normalizedOpacity = node->renderData.layer.opacity();
double layerAlpha = ((double)normalizedOpacity) * parentAlpha;
- if (node.isHidden || normalizedOpacity == 0.0f) {
+ if (node->renderData.layer.isHidden() || normalizedOpacity == 0.0f) {
return;
}
@@ -154,44 +386,44 @@ static void renderLottieRenderNode(LottieRenderNode * _Nonnull node, std::shared
std::shared_ptr tempContext;
bool needsTempContext = false;
- if (node.mask != nil) {
+ if (node->mask() && node->mask()->renderData.isValid) {
needsTempContext = true;
} else {
- needsTempContext = layerAlpha != 1.0 || node.masksToBounds;
+ needsTempContext = layerAlpha != 1.0 || node->renderData.layer.masksToBounds();
}
if (needsTempContext) {
- if (node.mask != nil || node.masksToBounds) {
- auto maskBackingStorage = parentContext->makeLayer((int)(node.globalRect.size.width), (int)(node.globalRect.size.height));
+ if ((node->mask() && node->mask()->renderData.isValid) || node->renderData.layer.masksToBounds()) {
+ auto maskBackingStorage = parentContext->makeLayer((int)(node->renderData.globalRect.width), (int)(node->renderData.globalRect.height));
- maskBackingStorage->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node.globalRect.origin.x, -node.globalRect.origin.y)));
- maskBackingStorage->concatenate(lottie::fromNativeTransform(node.globalTransform));
+ maskBackingStorage->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node->renderData.globalRect.x, -node->renderData.globalRect.y)));
+ maskBackingStorage->concatenate(node->renderData.globalTransform);
- if (node.masksToBounds) {
- maskBackingStorage->fill(lottie::CGRect(node.bounds.origin.x, node.bounds.origin.y, node.bounds.size.width, node.bounds.size.height), lottieRendering::Color(1.0, 1.0, 1.0, 1.0));
+ if (node->renderData.layer.masksToBounds()) {
+ maskBackingStorage->fill(lottie::CGRect(node->renderData.layer.bounds().x, node->renderData.layer.bounds().y, node->renderData.layer.bounds().width, node->renderData.layer.bounds().height), lottieRendering::Color(1.0, 1.0, 1.0, 1.0));
}
- if (node.mask != nil) {
- renderLottieRenderNode(node.mask, maskBackingStorage, globalSize, 1.0);
+ if (node->mask() && node->mask()->renderData.isValid) {
+ renderLottieRenderNode(node->mask(), maskBackingStorage, globalSize, 1.0);
}
maskContext = maskBackingStorage;
}
- auto tempContextValue = parentContext->makeLayer((int)(node.globalRect.size.width), (int)(node.globalRect.size.height));
+ auto tempContextValue = parentContext->makeLayer((int)(node->renderData.globalRect.width), (int)(node->renderData.globalRect.height));
tempContext = tempContextValue;
currentContext = tempContextValue;
- currentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node.globalRect.origin.x, -node.globalRect.origin.y)));
+ currentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node->renderData.globalRect.x, -node->renderData.globalRect.y)));
currentContext->saveState();
- currentContext->concatenate(lottie::fromNativeTransform(node.globalTransform));
+ currentContext->concatenate(node->renderData.globalTransform);
} else {
currentContext = parentContext;
}
- parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(node.position.x, node.position.y)));
- parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node.bounds.origin.x, -node.bounds.origin.y)));
- parentContext->concatenate(lottie::fromNativeTransform(node.transform));
+ parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(node->renderData.layer.position().x, node->renderData.layer.position().y)));
+ parentContext->concatenate(lottie::CATransform3D::identity().translated(lottie::Vector2D(-node->renderData.layer.bounds().x, -node->renderData.layer.bounds().y)));
+ parentContext->concatenate(node->renderData.layer.transform());
double renderAlpha = 1.0;
if (tempContext) {
@@ -202,17 +434,19 @@ static void renderLottieRenderNode(LottieRenderNode * _Nonnull node, std::shared
currentContext->setAlpha(renderAlpha);
- if (node.renderContent != nil) {
- drawLottieRenderableItem(currentContext, node.renderContent);
+ if (node->_contentItem) {
+ drawLottieContentItem(currentContext, node->_contentItem);
}
- if (node.isInvertedMatte) {
- currentContext->fill(lottie::CGRect(node.bounds.origin.x, node.bounds.origin.y, node.bounds.size.width, node.bounds.size.height), lottieRendering::Color(0.0, 0.0, 0.0, 1.0));
+ if (node->renderData.isInvertedMatte) {
+ currentContext->fill(lottie::CGRect(node->renderData.layer.bounds().x, node->renderData.layer.bounds().y, node->renderData.layer.bounds().width, node->renderData.layer.bounds().height), lottieRendering::Color(0.0, 0.0, 0.0, 1.0));
currentContext->setBlendMode(lottieRendering::BlendMode::DestinationOut);
}
- for (LottieRenderNode *subnode in node.subnodes) {
- renderLottieRenderNode(subnode, currentContext, globalSize, renderAlpha);
+ for (const auto &subnode : node->subnodes()) {
+ if (subnode->renderData.isValid) {
+ renderLottieRenderNode(subnode, currentContext, globalSize, renderAlpha);
+ }
}
if (tempContext) {
@@ -220,12 +454,12 @@ static void renderLottieRenderNode(LottieRenderNode * _Nonnull node, std::shared
if (maskContext) {
tempContext->setBlendMode(lottieRendering::BlendMode::DestinationIn);
- tempContext->draw(maskContext, lottie::CGRect(node.globalRect.origin.x, node.globalRect.origin.y, node.globalRect.size.width, node.globalRect.size.height));
+ tempContext->draw(maskContext, lottie::CGRect(node->renderData.globalRect.x, node->renderData.globalRect.y, node->renderData.globalRect.width, node->renderData.globalRect.height));
}
- parentContext->concatenate(lottie::fromNativeTransform(node.globalTransform).inverted());
+ parentContext->concatenate(node->renderData.globalTransform.inverted());
parentContext->setAlpha(layerAlpha);
- parentContext->draw(tempContext, lottie::CGRect(node.globalRect.origin.x, node.globalRect.origin.y, node.globalRect.size.width, node.globalRect.size.height));
+ parentContext->draw(tempContext, node->renderData.globalRect);
}
parentContext->restoreState();
@@ -238,24 +472,54 @@ CGRect getPathNativeBoundingBox(CGPathRef _Nonnull path) {
return CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
}
-UIImage * _Nullable renderLottieAnimationContainer(LottieAnimationContainer * _Nonnull animationContainer, CGSize size, bool useReferenceRendering) {
- LottieAnimation *animation = animationContainer.animation;
- LottieRenderNode *lottieNode = [animationContainer getCurrentRenderTreeForSize:size];
+@interface SoftwareLottieRenderer() {
+ LottieAnimationContainer *_animationContainer;
+ std::shared_ptr _bezierPathsBoundingBoxContext;
+}
+
+@end
+
+@implementation SoftwareLottieRenderer
+
+- (instancetype _Nonnull)initWithAnimationContainer:(LottieAnimationContainer * _Nonnull)animationContainer {
+ self = [super init];
+ if (self != nil) {
+ _animationContainer = animationContainer;
+ _bezierPathsBoundingBoxContext = std::make_shared();
+ }
+ return self;
+}
+
+- (UIImage * _Nullable)renderForSize:(CGSize)size useReferenceRendering:(bool)useReferenceRendering {
+ LottieAnimation *animation = _animationContainer.animation;
+ std::shared_ptr renderNode = [_animationContainer internalGetRootRenderTreeNode];
+ if (!renderNode) {
+ return nil;
+ }
+
+ processRenderTree(renderNode, lottie::Vector2D((int)size.width, (int)size.height), lottie::CATransform3D::identity().scaled(lottie::Vector2D(size.width / (double)animation.size.width, size.height / (double)animation.size.height)), false, *_bezierPathsBoundingBoxContext.get());
if (useReferenceRendering) {
auto context = std::make_shared((int)size.width, (int)size.height);
- if (lottieNode) {
- CGPoint scale = CGPointMake(size.width / (CGFloat)animation.size.width, size.height / (CGFloat)animation.size.height);
- context->concatenate(lottie::CATransform3D::makeScale(scale.x, scale.y, 1.0));
-
- renderLottieRenderNode(lottieNode, context, lottie::Vector2D(context->width(), context->height()), 1.0);
- }
+ CGPoint scale = CGPointMake(size.width / (CGFloat)animation.size.width, size.height / (CGFloat)animation.size.height);
+ context->concatenate(lottie::CATransform3D::makeScale(scale.x, scale.y, 1.0));
+
+ renderLottieRenderNode(renderNode, context, lottie::Vector2D(context->width(), context->height()), 1.0);
auto image = context->makeImage();
return [[UIImage alloc] initWithCGImage:std::static_pointer_cast(image)->nativeImage()];
} else {
+ /*auto context = std::make_shared((int)size.width, (int)size.height);
+
+ CGPoint scale = CGPointMake(size.width / (CGFloat)animation.size.width, size.height / (CGFloat)animation.size.height);
+ context->concatenate(lottie::CATransform3D::makeScale(scale.x, scale.y, 1.0));
+
+ renderLottieRenderNode(renderNode, context, lottie::Vector2D(context->width(), context->height()), 1.0);*/
+
return nil;
}
}
+
+@end
diff --git a/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h
new file mode 100644
index 0000000000..6166b708a0
--- /dev/null
+++ b/Tests/LottieMetalTest/SoftwareLottieRenderer/Sources/ThorVGCanvasImpl.h
@@ -0,0 +1,65 @@
+#ifndef ThorVGCanvasImpl_h
+#define ThorVGCanvasImpl_h
+
+#include "Canvas.h"
+
+#include
+
+namespace lottieRendering {
+
+class ThorVGCanvasImpl: public Canvas {
+public:
+ ThorVGCanvasImpl(int width, int height);
+ virtual ~ThorVGCanvasImpl();
+
+ virtual int width() const override;
+ virtual int height() const override;
+
+ virtual std::shared_ptr