LottieCpp improvements

This commit is contained in:
Isaac 2024-05-07 20:05:50 +04:00
parent 8605198d72
commit c752e2d895
67 changed files with 4000 additions and 336 deletions

View File

@ -32,6 +32,8 @@ swift_library(
"//submodules/WallpaperBackgroundNode",
"//submodules/Components/MultilineTextWithEntitiesComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/LottieMetal",
"//submodules/TelegramAnimatedStickerNode",
],
visibility = [
"//visibility:public",

View File

@ -16,6 +16,8 @@ import ComponentDisplayAdapters
import WallpaperBackgroundNode
import ReactionSelectionNode
import EntityKeyboard
import LottieMetal
import TelegramAnimatedStickerNode
func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
let sourceWindowFrame = fromView.convert(frame, to: nil)
@ -127,7 +129,7 @@ final class ChatSendMessageContextScreenComponent: Component {
private let messageEffectDisposable = MetaDisposable()
private var selectedMessageEffect: AvailableMessageEffects.MessageEffect?
private var standaloneReactionAnimation: StandaloneReactionAnimation?
private var standaloneReactionAnimation: LottieMetalAnimatedStickerNode?
private var presentationAnimationState: PresentationAnimationState = .initial
private var appliedAnimationState: PresentationAnimationState = .initial
@ -509,7 +511,7 @@ final class ChatSendMessageContextScreenComponent: Component {
ReactionContextNode.randomGenericReactionEffect(context: component.context)
)
|> deliverOnMainQueue).startStrict(next: { [weak self] messageEffect, path in
guard let self, let component = self.component, let environment = self.environment else {
guard let self, let component = self.component else {
return
}
guard let messageEffect else {
@ -517,17 +519,6 @@ final class ChatSendMessageContextScreenComponent: Component {
}
let effectId = messageEffect.id
let reactionItem = ReactionItem(
reaction: ReactionItem.Reaction(rawValue: updateReaction.reaction),
appearAnimation: messageEffect.effectSticker,
stillAnimation: messageEffect.effectSticker,
listAnimation: messageEffect.effectSticker,
largeListAnimation: messageEffect.effectSticker,
applicationAnimation: nil,
largeApplicationAnimation: nil,
isCustom: true
)
if let selectedMessageEffect = self.selectedMessageEffect {
if selectedMessageEffect.id == effectId {
self.selectedMessageEffect = nil
@ -570,12 +561,7 @@ final class ChatSendMessageContextScreenComponent: Component {
})
}
let genericReactionEffect = path
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: genericReactionEffect)
standaloneReactionAnimation.frame = self.bounds
self.standaloneReactionAnimation = standaloneReactionAnimation
self.addSubnode(standaloneReactionAnimation)
let _ = path
var customEffectResource: MediaResource?
if let effectAnimation = messageEffect.effectAnimation {
@ -586,24 +572,34 @@ final class ChatSendMessageContextScreenComponent: Component {
customEffectResource = effectFile.resource
}
}
guard let customEffectResource else {
return
}
standaloneReactionAnimation.animateReactionSelection(
context: component.context,
theme: environment.theme,
animationCache: component.context.animationCache,
reaction: reactionItem,
customEffectResource: customEffectResource,
avatarPeers: [],
playHaptic: true,
isLarge: true,
playCenterReaction: false,
targetView: targetView,
addStandaloneReactionAnimation: { _ in
},
completion: { [weak standaloneReactionAnimation] in
standaloneReactionAnimation?.removeFromSupernode()
let standaloneReactionAnimation = LottieMetalAnimatedStickerNode()
standaloneReactionAnimation.isUserInteractionEnabled = false
let effectSize = CGSize(width: 380.0, height: 380.0)
var effectFrame = effectSize.centered(around: targetView.convert(targetView.bounds.center, to: self))
effectFrame.origin.x -= effectFrame.width * 0.3
self.standaloneReactionAnimation = standaloneReactionAnimation
standaloneReactionAnimation.frame = effectFrame
standaloneReactionAnimation.updateLayout(size: effectFrame.size)
self.addSubnode(standaloneReactionAnimation)
let source = AnimatedStickerResourceSource(account: component.context.account, resource: customEffectResource, fitzModifier: nil)
standaloneReactionAnimation.setup(source: source, width: Int(effectSize.width), height: Int(effectSize.height), playbackMode: .once, mode: .direct(cachePathPrefix: nil))
standaloneReactionAnimation.completed = { [weak self, weak standaloneReactionAnimation] _ in
guard let self else {
return
}
)
if let standaloneReactionAnimation {
standaloneReactionAnimation.removeFromSupernode()
if self.standaloneReactionAnimation === standaloneReactionAnimation {
self.standaloneReactionAnimation = nil
}
}
}
standaloneReactionAnimation.visibility = true
}))
}
reactionContextNode.displayTail = true

View File

@ -347,6 +347,7 @@ public final class MetalEngineSubjectContext {
fileprivate var computeOperations: [ComputeOperation] = []
fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:]
fileprivate var freeResourcesOnCompletion: [MetalEngineResource] = []
fileprivate var customCompletions: [() -> Void] = []
fileprivate init(device: MTLDevice, impl: MetalEngine.Impl) {
self.device = device
@ -446,6 +447,10 @@ public final class MetalEngineSubjectContext {
return commands(commandBuffer, state)
})
}
public func addCustomCompletion(_ customCompletion: @escaping () -> Void) {
self.customCompletions.append(customCompletion)
}
}
public final class MetalEngineSubjectInternalData {
@ -1039,13 +1044,17 @@ public final class MetalEngine {
}
}
if !subjectContext.freeResourcesOnCompletion.isEmpty {
if !subjectContext.freeResourcesOnCompletion.isEmpty || !subjectContext.customCompletions.isEmpty {
let freeResourcesOnCompletion = subjectContext.freeResourcesOnCompletion
let customCompletions = subjectContext.customCompletions
commandBuffer.addCompletedHandler { _ in
DispatchQueue.main.async {
for resource in freeResourcesOnCompletion {
resource.free()
}
for customCompletion in customCompletions {
customCompletion()
}
}
}
}

View File

@ -5793,7 +5793,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let source = AnimatedStickerResourceSource(account: item.context.account, resource: resource, fitzModifier: nil)
let animationSize = CGSize(width: 180.0, height: 180.0)
let animationSize = CGSize(width: 380.0, height: 380.0)
let animationNodeFrame: CGRect
var messageEffectView: UIView?
@ -5825,18 +5825,18 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let pathPrefix = item.context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(resource.id)
let additionalAnimationNode = LottieMetalAnimatedStickerNode()
additionalAnimationNode.updateLayout(size: animationSize)
additionalAnimationNode.setup(source: source, width: Int(animationSize.width * 1.6), height: Int(animationSize.height * 1.6), playbackMode: .once, mode: .direct(cachePathPrefix: pathPrefix))
var animationFrame: CGRect
if isStickerEffect {
let scale: CGFloat = 0.245
let offsetScale: CGFloat = 0.5
animationFrame = animationNodeFrame.offsetBy(dx: incomingMessage ? animationNodeFrame.width * offsetScale : -animationNodeFrame.width * offsetScale, dy: -25.0).insetBy(dx: -animationNodeFrame.width * scale, dy: -animationNodeFrame.height * scale)
animationFrame = animationNodeFrame.offsetBy(dx: incomingMessage ? animationNodeFrame.width * offsetScale : -animationNodeFrame.width * offsetScale, dy: -25.0)
} else {
animationFrame = animationNodeFrame.insetBy(dx: -animationNodeFrame.width, dy: -animationNodeFrame.height)
.offsetBy(dx: incomingMessage ? animationNodeFrame.width - 10.0 : -animationNodeFrame.width + 10.0, dy: 0.0)
animationFrame = animationFrame.offsetBy(dx: CGFloat.random(in: -30.0 ... 30.0), dy: CGFloat.random(in: -30.0 ... 30.0))
}
animationFrame = animationFrame.offsetBy(dx: 0.0, dy: self.insets.top)
additionalAnimationNode.frame = animationFrame
if incomingMessage {

View File

@ -15,6 +15,7 @@ objc_library(
copts = [
"-Werror",
"-I{}/Sources".format(package_name()),
"-O2",
],
hdrs = glob([
"PublicHeaders/**/*.h",

View File

@ -12,6 +12,7 @@ extern "C" {
@interface LottieAnimation : NSObject
@property (nonatomic, readonly) NSInteger frameCount;
@property (nonatomic, readonly) NSInteger framesPerSecond;
@property (nonatomic, readonly) CGSize size;
- (instancetype _Nullable)initWithData:(NSData * _Nonnull)data;

View File

@ -42,15 +42,16 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, readonly, direct) CGFloat location;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct));
@end
@interface LottiePath : NSObject
- (CGRect)boundingBox __attribute__((objc_direct));
- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate __attribute__((objc_direct));
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct));
@end
@ -64,6 +65,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, readonly, direct) CGFloat opacity;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity __attribute__((objc_direct));
@end
@ -76,6 +78,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, readonly, direct) CGPoint end;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray<LottieColorStop *> * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct));
@end
@ -85,6 +88,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, readonly, direct) LottieFillRule fillRule;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct));
@end
@ -99,6 +103,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, strong, readonly, direct) NSArray<NSNumber *> * _Nullable dashPattern;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray<NSNumber *> * _Nullable)dashPattern __attribute__((objc_direct));
@end
@ -109,6 +114,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, strong, readonly, direct) LottieRenderContentFill * _Nullable fill;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct));
@end
@ -130,6 +136,7 @@ typedef NS_ENUM(NSUInteger, LottieGradientType) {
@property (nonatomic, readonly, direct) LottieRenderNode * _Nullable mask;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray<LottieRenderNode *> * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct));
@end

View File

@ -43,8 +43,8 @@ public:
std::shared_ptr<AssetLibrary> assetLibrary_,
std::optional<std::vector<Marker>> markers_,
std::optional<std::vector<FitzModifier>> fitzModifiers_,
std::optional<json11::Json> meta_,
std::optional<json11::Json> comps_
std::optional<lottiejson11::Json> meta_,
std::optional<lottiejson11::Json> comps_
) :
startFrame(startFrame_),
endFrame(endFrame_),
@ -75,7 +75,7 @@ public:
Animation(const Animation&) = delete;
Animation& operator=(Animation&) = delete;
static std::shared_ptr<Animation> fromJson(json11::Json::object const &json) noexcept(false) {
static std::shared_ptr<Animation> fromJson(lottiejson11::Json::object const &json) noexcept(false) {
auto name = getOptionalString(json, "nm");
auto version = getString(json, "v");
@ -168,14 +168,14 @@ public:
);
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
if (name.has_value()) {
result.insert(std::make_pair("nm", name.value()));
}
result.insert(std::make_pair("v", json11::Json(version)));
result.insert(std::make_pair("v", lottiejson11::Json(version)));
if (tgs.has_value()) {
result.insert(std::make_pair("tgs", tgs.value()));
@ -184,34 +184,34 @@ public:
if (type.has_value()) {
switch (type.value()) {
case CoordinateSpace::Type2d:
result.insert(std::make_pair("ddd", json11::Json(0)));
result.insert(std::make_pair("ddd", lottiejson11::Json(0)));
break;
case CoordinateSpace::Type3d:
result.insert(std::make_pair("ddd", json11::Json(1)));
result.insert(std::make_pair("ddd", lottiejson11::Json(1)));
break;
}
}
result.insert(std::make_pair("ip", json11::Json(startFrame)));
result.insert(std::make_pair("op", json11::Json(endFrame)));
result.insert(std::make_pair("fr", json11::Json(framerate)));
result.insert(std::make_pair("w", json11::Json(width)));
result.insert(std::make_pair("h", json11::Json(height)));
result.insert(std::make_pair("ip", lottiejson11::Json(startFrame)));
result.insert(std::make_pair("op", lottiejson11::Json(endFrame)));
result.insert(std::make_pair("fr", lottiejson11::Json(framerate)));
result.insert(std::make_pair("w", lottiejson11::Json(width)));
result.insert(std::make_pair("h", lottiejson11::Json(height)));
json11::Json::array layersArray;
lottiejson11::Json::array layersArray;
for (const auto &layer : layers) {
json11::Json::object layerJson;
lottiejson11::Json::object layerJson;
layer->toJson(layerJson);
layersArray.push_back(layerJson);
}
result.insert(std::make_pair("layers", json11::Json(layersArray)));
result.insert(std::make_pair("layers", lottiejson11::Json(layersArray)));
if (glyphs.has_value()) {
json11::Json::array glyphArray;
lottiejson11::Json::array glyphArray;
for (const auto &glyph : glyphs.value()) {
glyphArray.push_back(glyph->toJson());
}
result.insert(std::make_pair("chars", json11::Json(glyphArray)));
result.insert(std::make_pair("chars", lottiejson11::Json(glyphArray)));
}
if (fonts.has_value()) {
@ -223,19 +223,19 @@ public:
}
if (markers.has_value()) {
json11::Json::array markerArray;
lottiejson11::Json::array markerArray;
for (const auto &marker : markers.value()) {
markerArray.push_back(marker.toJson());
}
result.insert(std::make_pair("markers", json11::Json(markerArray)));
result.insert(std::make_pair("markers", lottiejson11::Json(markerArray)));
}
if (fitzModifiers.has_value()) {
json11::Json::array fitzModifierArray;
lottiejson11::Json::array fitzModifierArray;
for (const auto &fitzModifier : fitzModifiers.value()) {
fitzModifierArray.push_back(fitzModifier.toJson());
}
result.insert(std::make_pair("fitz", json11::Json(fitzModifierArray)));
result.insert(std::make_pair("fitz", lottiejson11::Json(fitzModifierArray)));
}
if (meta.has_value()) {
@ -305,8 +305,8 @@ public:
std::optional<std::vector<FitzModifier>> fitzModifiers;
std::optional<json11::Json> meta;
std::optional<json11::Json> comps;
std::optional<lottiejson11::Json> meta;
std::optional<lottiejson11::Json> comps;
};
}

View File

@ -14,7 +14,7 @@ public:
id(id_) {
}
explicit Asset(json11::Json::object const &json) noexcept(false) {
explicit Asset(lottiejson11::Json::object const &json) noexcept(false) {
auto idData = getAny(json, "id");
if (idData.is_string()) {
id = idData.string_value();
@ -30,7 +30,7 @@ public:
Asset(const Asset&) = delete;
Asset& operator=(Asset&) = delete;
virtual void toJson(json11::Json::object &json) const {
virtual void toJson(lottiejson11::Json::object &json) const {
json.insert(std::make_pair("id", id));
if (objectName.has_value()) {

View File

@ -22,7 +22,7 @@ public:
precompAssets(precompAssets_) {
}
explicit AssetLibrary(json11::Json const &json) noexcept(false) {
explicit AssetLibrary(lottiejson11::Json const &json) noexcept(false) {
if (!json.is_array()) {
throw LottieParsingException();
}
@ -45,11 +45,11 @@ public:
}
}
json11::Json::array toJson() const {
json11::Json::array result;
lottiejson11::Json::array toJson() const {
lottiejson11::Json::array result;
for (const auto &asset : assetList) {
json11::Json::object assetJson;
lottiejson11::Json::object assetJson;
asset->toJson(assetJson);
result.push_back(assetJson);
}

View File

@ -23,7 +23,7 @@ public:
virtual ~ImageAsset() = default;
explicit ImageAsset(json11::Json::object const &json) noexcept(false) :
explicit ImageAsset(lottiejson11::Json::object const &json) noexcept(false) :
Asset(json) {
name = getString(json, "p");
directory = getString(json, "u");
@ -34,7 +34,7 @@ public:
_t = getOptionalString(json, "t");
}
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
Asset::toJson(json);
json.insert(std::make_pair("p", name));

View File

@ -21,7 +21,7 @@ public:
virtual ~PrecompAsset() = default;
explicit PrecompAsset(json11::Json::object const &json) noexcept(false) :
explicit PrecompAsset(lottiejson11::Json::object const &json) noexcept(false) :
Asset(json) {
frameRate = getOptionalDouble(json, "fr");
@ -36,12 +36,12 @@ public:
}
}
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
Asset::toJson(json);
json11::Json::array layerArray;
lottiejson11::Json::array layerArray;
for (const auto &layer : layers) {
json11::Json::object layerJson;
lottiejson11::Json::object layerJson;
layer->toJson(layerJson);
layerArray.push_back(layerJson);
}

View File

@ -27,7 +27,7 @@ public:
isSingle(false) {
}
KeyframeGroup(json11::Json::object const &json) noexcept(false) {
KeyframeGroup(lottiejson11::Json::object const &json) noexcept(false) {
isAnimated = getOptionalInt(json, "a");
expression = getOptionalAny(json, "x");
expressionIndex = getOptionalInt(json, "ix");
@ -101,15 +101,15 @@ public:
}
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
assert(!keyframes.empty());
if (keyframes.size() == 1 && isSingle) {
result.insert(std::make_pair("k", keyframes[0].value.toJson()));
} else {
json11::Json::array containerData;
lottiejson11::Json::array containerData;
for (const auto &keyframe : rawKeyframeData) {
containerData.push_back(keyframe.toJson());
@ -138,7 +138,7 @@ public:
std::vector<Keyframe<T>> keyframes;
std::optional<int> isAnimated;
std::optional<json11::Json> expression;
std::optional<lottiejson11::Json> expression;
std::optional<int> expressionIndex;
std::vector<KeyframeData<T>> rawKeyframeData;
bool isSingle = false;

View File

@ -9,7 +9,7 @@ namespace lottie {
/// A layer that holds an image.
class ImageLayerModel: public LayerModel {
public:
explicit ImageLayerModel(json11::Json::object const &json) noexcept(false) :
explicit ImageLayerModel(lottiejson11::Json::object const &json) noexcept(false) :
LayerModel(json) {
referenceID = getString(json, "refId");
@ -18,7 +18,7 @@ public:
virtual ~ImageLayerModel() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
LayerModel::toJson(json);
json.insert(std::make_pair("refId", referenceID));

View File

@ -2,7 +2,7 @@
namespace lottie {
LayerType parseLayerType(json11::Json::object const &json, std::string const &key) {
LayerType parseLayerType(lottiejson11::Json::object const &json, std::string const &key) {
if (const auto layerTypeValue = getOptionalInt(json, "ty")) {
switch (layerTypeValue.value()) {
case 0:

View File

@ -21,7 +21,7 @@ enum class LayerType {
Text
};
LayerType parseLayerType(json11::Json::object const &json, std::string const &key);
LayerType parseLayerType(lottiejson11::Json::object const &json, std::string const &key);
int serializeLayerType(LayerType value);
enum class MatteType: int {
@ -53,7 +53,7 @@ enum class BlendMode: int {
/// A base top container for shapes, images, and other view objects.
class LayerModel {
public:
explicit LayerModel(json11::Json::object const &json) noexcept(false) {
explicit LayerModel(lottiejson11::Json::object const &json) noexcept(false) {
name = getOptionalString(json, "nm");
index = getOptionalInt(json, "ind");
@ -179,7 +179,7 @@ public:
virtual ~LayerModel() = default;
virtual void toJson(json11::Json::object &json) const {
virtual void toJson(lottiejson11::Json::object &json) const {
if (name.has_value()) {
json.insert(std::make_pair("nm", name.value()));
}
@ -219,7 +219,7 @@ public:
}
if (masks.has_value()) {
json11::Json::array maskArray;
lottiejson11::Json::array maskArray;
for (const auto &mask : masks.value()) {
maskArray.push_back(mask->toJson());
}
@ -308,9 +308,9 @@ public:
std::optional<bool> hasMask;
std::optional<int> td;
std::optional<json11::Json> effectsData;
std::optional<lottiejson11::Json> effectsData;
std::optional<std::string> layerClass;
std::optional<json11::Json> _extraHidden;
std::optional<lottiejson11::Json> _extraHidden;
};
}

View File

@ -8,7 +8,7 @@
namespace lottie {
std::shared_ptr<LayerModel> parseLayerModel(json11::Json::object const &json) noexcept(false) {
std::shared_ptr<LayerModel> parseLayerModel(lottiejson11::Json::object const &json) noexcept(false) {
LayerType layerType = parseLayerType(json, "ty");
switch (layerType) {

View File

@ -7,7 +7,7 @@
namespace lottie {
std::shared_ptr<LayerModel> parseLayerModel(json11::Json::object const &json) noexcept(false);
std::shared_ptr<LayerModel> parseLayerModel(lottiejson11::Json::object const &json) noexcept(false);
}

View File

@ -13,7 +13,7 @@ namespace lottie {
/// A layer that holds another animation composition.
class PreCompLayerModel: public LayerModel {
public:
PreCompLayerModel(json11::Json::object const &json) :
PreCompLayerModel(lottiejson11::Json::object const &json) :
LayerModel(json) {
referenceID = getString(json, "refId");
if (const auto timeRemappingData = getOptionalObject(json, "tm")) {
@ -25,7 +25,7 @@ public:
virtual ~PreCompLayerModel() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
LayerModel::toJson(json);
json.insert(std::make_pair("refId", referenceID));

View File

@ -12,7 +12,7 @@ namespace lottie {
/// A layer that holds vector shape objects.
class ShapeLayerModel: public LayerModel {
public:
ShapeLayerModel(json11::Json::object const &json) noexcept(false) :
ShapeLayerModel(lottiejson11::Json::object const &json) noexcept(false) :
LayerModel(json) {
auto shapeItemsData = getObjectArray(json, "shapes");
for (const auto &shapeItemData : shapeItemsData) {
@ -22,12 +22,12 @@ public:
virtual ~ShapeLayerModel() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
LayerModel::toJson(json);
json11::Json::array shapeItemArray;
lottiejson11::Json::array shapeItemArray;
for (const auto &item : items) {
json11::Json::object itemJson;
lottiejson11::Json::object itemJson;
item->toJson(itemJson);
shapeItemArray.push_back(itemJson);
}

View File

@ -9,7 +9,7 @@ namespace lottie {
/// A layer that holds a solid color.
class SolidLayerModel: public LayerModel {
public:
explicit SolidLayerModel(json11::Json::object const &json) noexcept(false) :
explicit SolidLayerModel(lottiejson11::Json::object const &json) noexcept(false) :
LayerModel(json) {
colorHex = getString(json, "sc");
width = getDouble(json, "sw");
@ -18,7 +18,7 @@ public:
virtual ~SolidLayerModel() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
LayerModel::toJson(json);
json.insert(std::make_pair("sc", colorHex));

View File

@ -12,7 +12,7 @@ namespace lottie {
/// A layer that holds text.
class TextLayerModel: public LayerModel {
public:
TextLayerModel(json11::Json::object const &json) :
TextLayerModel(lottiejson11::Json::object const &json) :
LayerModel(json),
text(KeyframeGroup<TextDocument>(TextDocument(
"",
@ -46,10 +46,10 @@ public:
virtual ~TextLayerModel() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
LayerModel::toJson(json);
json11::Json::object textContainer;
lottiejson11::Json::object textContainer;
textContainer.insert(std::make_pair("d", text.toJson()));
if (_extraM.has_value()) {
textContainer.insert(std::make_pair("m", _extraM.value()));
@ -57,7 +57,7 @@ public:
if (_extraP.has_value()) {
textContainer.insert(std::make_pair("p", _extraP.value()));
}
json11::Json::array animatorArray;
lottiejson11::Json::array animatorArray;
for (const auto &animator : animators) {
animatorArray.push_back(animator->toJson());
}
@ -73,9 +73,9 @@ public:
/// Text animators
std::vector<std::shared_ptr<TextAnimator>> animators;
std::optional<json11::Json> _extraM;
std::optional<json11::Json> _extraP;
std::optional<json11::Json> _extraA;
std::optional<lottiejson11::Json> _extraM;
std::optional<lottiejson11::Json> _extraP;
std::optional<lottiejson11::Json> _extraA;
};
}

View File

@ -23,7 +23,7 @@ public:
value(value_) {
}
explicit DashElement(json11::Json::object const &json) noexcept(false) :
explicit DashElement(lottiejson11::Json::object const &json) noexcept(false) :
type(DashElementType::Offset),
value(KeyframeGroup<Vector1D>(Vector1D(0.0))) {
auto typeRawValue = getString(json, "n");
@ -42,8 +42,8 @@ public:
name = getOptionalString(json, "nm");
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
switch (type) {
case DashElementType::Offset:

View File

@ -7,7 +7,7 @@ namespace lottie {
class FitzModifier {
public:
explicit FitzModifier(json11::Json::object const &json) noexcept(false) {
explicit FitzModifier(lottiejson11::Json::object const &json) noexcept(false) {
original = getInt(json, "o");
type12 = getOptionalInt(json, "f12");
type3 = getOptionalInt(json, "f3");
@ -16,8 +16,8 @@ public:
type6 = getOptionalInt(json, "f6");
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
result.insert(std::make_pair("o", (double)original));
if (type12.has_value()) {

View File

@ -18,14 +18,14 @@ public:
frameTime(frameTime_) {
}
explicit Marker(json11::Json::object const &json) noexcept(false) {
explicit Marker(lottiejson11::Json::object const &json) noexcept(false) {
name = getString(json, "cm");
frameTime = getDouble(json, "tm");
dr = getOptionalInt(json, "dr");
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
result.insert(std::make_pair("cm", name));
result.insert(std::make_pair("tm", frameTime));

View File

@ -19,7 +19,7 @@ enum class MaskMode {
class Mask {
public:
explicit Mask(json11::Json::object const &json) noexcept(false) :
explicit Mask(lottiejson11::Json::object const &json) noexcept(false) :
opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
shape(KeyframeGroup<BezierPath>(BezierPath())),
inverted(false),
@ -61,8 +61,8 @@ public:
name = getOptionalString(json, "nm");
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
if (_mode.has_value()) {
switch (_mode.value()) {

View File

@ -43,7 +43,7 @@ public:
_rotationZ(rotationZ_) {
}
explicit Transform(json11::Json::object const &json) noexcept(false) {
explicit Transform(lottiejson11::Json::object const &json) noexcept(false) {
// AnchorPoint
if (const auto anchorPointDictionary = getOptionalObject(json, "a")) {
_anchorPoint = KeyframeGroup<Vector3D>(anchorPointDictionary.value());
@ -112,8 +112,8 @@ public:
_extraSk = getOptionalAny(json, "sk");
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
if (_anchorPoint.has_value()) {
result.insert(std::make_pair("a", _anchorPoint->toJson()));
@ -139,7 +139,7 @@ public:
result.insert(std::make_pair("p", _position->toJson()));
break;
case PositionInternalRepresentation::NestedXY:
json11::Json::object nestedPosition;
lottiejson11::Json::object nestedPosition;
assert(_positionX.has_value());
assert(_positionY.has_value());
assert(!_position.has_value());
@ -258,8 +258,8 @@ private:
std::optional<bool> _extra_positionS;
std::optional<std::string> _extraTy;
std::optional<json11::Json> _extraSa;
std::optional<json11::Json> _extraSk;
std::optional<lottiejson11::Json> _extraSa;
std::optional<lottiejson11::Json> _extraSk;
};
}

View File

@ -17,7 +17,7 @@ enum class PathDirection: int {
/// An item that define an ellipse shape
class Ellipse: public ShapeItem {
public:
explicit Ellipse(json11::Json::object const &json) noexcept(false) :
explicit Ellipse(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
position(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
size(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))) {
@ -43,7 +43,7 @@ public:
virtual ~Ellipse() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
if (direction.has_value()) {

View File

@ -17,7 +17,7 @@ enum class FillRule: int {
class Fill: public ShapeItem {
public:
explicit Fill(json11::Json::object const &json) noexcept(false) :
explicit Fill(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
opacity(KeyframeGroup<Vector1D>(Vector1D(0.0))),
color(KeyframeGroup<Color>(Color(0.0, 0.0, 0.0, 0.0))) {
@ -45,7 +45,7 @@ public:
virtual ~Fill() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("o", opacity.toJson()));

View File

@ -17,7 +17,7 @@ enum class GradientType: int {
/// An item that define a gradient fill
class GradientFill: public ShapeItem {
public:
explicit GradientFill(json11::Json::object const &json) noexcept(false) :
explicit GradientFill(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
startPoint(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
@ -60,7 +60,7 @@ public:
virtual ~GradientFill() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("o", opacity.toJson()));
@ -75,7 +75,7 @@ public:
json.insert(std::make_pair("a", highlightAngle->toJson()));
}
json11::Json::object colorsContainer;
lottiejson11::Json::object colorsContainer;
colorsContainer.insert(std::make_pair("p", numberOfColors));
colorsContainer.insert(std::make_pair("k", colors.toJson()));
json.insert(std::make_pair("g", colorsContainer));

View File

@ -13,7 +13,7 @@ namespace lottie {
/// An item that define a gradient stroke
class GradientStroke: public ShapeItem {
public:
explicit GradientStroke(json11::Json::object const &json) noexcept(false) :
explicit GradientStroke(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
startPoint(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
@ -109,7 +109,7 @@ public:
virtual ~GradientStroke() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("o", opacity.toJson()));
@ -133,13 +133,13 @@ public:
json.insert(std::make_pair("ml", miterLimit.value()));
}
json11::Json::object colorsContainer;
lottiejson11::Json::object colorsContainer;
colorsContainer.insert(std::make_pair("p", numberOfColors));
colorsContainer.insert(std::make_pair("k", colors.toJson()));
json.insert(std::make_pair("g", colorsContainer));
if (dashPattern.has_value()) {
json11::Json::array dashElements;
lottiejson11::Json::array dashElements;
for (const auto &dashElement : dashPattern.value()) {
dashElements.push_back(dashElement.toJson());
}

View File

@ -12,7 +12,7 @@ namespace lottie {
/// An item that define an ellipse shape
class Group: public ShapeItem {
public:
explicit Group(json11::Json::object const &json) noexcept(false) :
explicit Group(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json) {
auto itemsData = getObjectArray(json, "it");
for (const auto &itemData : itemsData) {
@ -24,12 +24,12 @@ public:
virtual ~Group() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json11::Json::array itemArray;
lottiejson11::Json::array itemArray;
for (const auto &item : items) {
json11::Json::object itemJson;
lottiejson11::Json::object itemJson;
item->toJson(itemJson);
itemArray.push_back(itemJson);
}

View File

@ -18,7 +18,7 @@ enum class MergeMode: int {
/// An item that define an ellipse shape
class Merge: public ShapeItem {
public:
explicit Merge(json11::Json::object const &json) noexcept(false) :
explicit Merge(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
mode(MergeMode::None) {
auto modeRawValue = getInt(json, "mm");
@ -48,7 +48,7 @@ public:
virtual ~Merge() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("mm", (int)mode));

View File

@ -11,7 +11,7 @@ namespace lottie {
/// An item that define an ellipse shape
class Rectangle: public ShapeItem {
public:
explicit Rectangle(json11::Json::object const &json) noexcept(false) :
explicit Rectangle(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
position(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
size(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
@ -70,7 +70,7 @@ public:
cornerRadius(cornerRadius_) {
}
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
if (direction.has_value()) {

View File

@ -10,7 +10,7 @@ namespace lottie {
/// An item that define a repeater
class Repeater: public ShapeItem {
public:
explicit Repeater(json11::Json::object const &json) noexcept(false) :
explicit Repeater(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json) {
if (const auto copiesData = getOptionalObject(json, "c")) {
copies = KeyframeGroup<Vector1D>(copiesData.value());
@ -39,7 +39,7 @@ public:
virtual ~Repeater() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
if (copies.has_value()) {
@ -49,7 +49,7 @@ public:
json.insert(std::make_pair("o", offset->toJson()));
}
json11::Json::object transformContainer;
lottiejson11::Json::object transformContainer;
if (startOpacity.has_value()) {
json.insert(std::make_pair("so", startOpacity->toJson()));
}

View File

@ -11,7 +11,7 @@ namespace lottie {
/// An item that define an ellipse shape
class RoundedRectangle: public ShapeItem {
public:
explicit RoundedRectangle(json11::Json::object const &json) noexcept(false) :
explicit RoundedRectangle(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
cornerRadius(KeyframeGroup<Vector1D>(Vector1D(0.0))) {
if (const auto directionRawValue = getOptionalInt(json, "d")) {
@ -42,7 +42,7 @@ public:
virtual ~RoundedRectangle() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
if (direction.has_value()) {

View File

@ -13,7 +13,7 @@ namespace lottie {
/// An item that defines an custom shape
class Shape: public ShapeItem {
public:
explicit Shape(json11::Json::object const &json) noexcept(false) :
explicit Shape(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
path(KeyframeGroup<BezierPath>(getObject(json, "ks"))) {
if (const auto directionRawValue = getOptionalInt(json, "d")) {
@ -35,7 +35,7 @@ public:
virtual ~Shape() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("ks", path.toJson()));

View File

@ -17,7 +17,7 @@
namespace lottie {
std::shared_ptr<ShapeItem> parseShapeItem(json11::Json::object const &json) noexcept(false) {
std::shared_ptr<ShapeItem> parseShapeItem(lottiejson11::Json::object const &json) noexcept(false) {
auto typeRawValue = getString(json, "ty");
if (typeRawValue == "el") {
return std::make_shared<Ellipse>(json);

View File

@ -27,13 +27,13 @@ enum class ShapeType {
/// An item belonging to a Shape Layer
class ShapeItem {
public:
ShapeItem(json11::Json const &jsonAny) noexcept(false) :
ShapeItem(lottiejson11::Json const &jsonAny) noexcept(false) :
type(ShapeType::Ellipse) {
if (!jsonAny.is_object()) {
throw LottieParsingException();
}
json11::Json::object const &json = jsonAny.object_items();
lottiejson11::Json::object const &json = jsonAny.object_items();
name = getOptionalString(json, "nm");
matchName = getOptionalString(json, "mn");
@ -106,7 +106,7 @@ public:
ShapeItem(const ShapeItem&) = delete;
ShapeItem& operator=(ShapeItem&) = delete;
virtual void toJson(json11::Json::object &json) const {
virtual void toJson(lottiejson11::Json::object &json) const {
if (name.has_value()) {
json.insert(std::make_pair("nm", name.value()));
}
@ -202,7 +202,7 @@ public:
std::optional<std::string> layerClass;
};
std::shared_ptr<ShapeItem> parseShapeItem(json11::Json::object const &json) noexcept(false);
std::shared_ptr<ShapeItem> parseShapeItem(lottiejson11::Json::object const &json) noexcept(false);
}

View File

@ -10,7 +10,7 @@ namespace lottie {
/// An item that define a shape transform
class ShapeTransform: public ShapeItem {
public:
explicit ShapeTransform(json11::Json::object const &json) noexcept(false) :
explicit ShapeTransform(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json) {
if (const auto anchorData = getOptionalObject(json, "a")) {
anchor = KeyframeGroup<Vector3D>(anchorData.value());
@ -37,7 +37,7 @@ public:
virtual ~ShapeTransform() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
if (anchor.has_value()) {

View File

@ -19,7 +19,7 @@ enum class StarType: int {
/// An item that define a star shape
class Star: public ShapeItem {
public:
explicit Star(json11::Json::object const &json) noexcept(false) :
explicit Star(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
position(KeyframeGroup<Vector3D>(Vector3D(0.0, 0.0, 0.0))),
outerRadius(KeyframeGroup<Vector1D>(Vector1D(0.0))),
@ -75,7 +75,7 @@ public:
virtual ~Star() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
if (direction.has_value()) {

View File

@ -15,7 +15,7 @@ namespace lottie {
/// An item that define an ellipse shape
class Stroke: public ShapeItem {
public:
explicit Stroke(json11::Json::object const &json) noexcept(false) :
explicit Stroke(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
opacity(KeyframeGroup<Vector1D>(Vector1D(100.0))),
color(KeyframeGroup<Color>(Color(0.0, 0.0, 0.0, 0.0))),
@ -81,7 +81,7 @@ public:
virtual ~Stroke() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("o", opacity.toJson()));
@ -96,7 +96,7 @@ public:
}
if (dashPattern.has_value()) {
json11::Json::array dashElements;
lottiejson11::Json::array dashElements;
for (const auto &dashElement : dashPattern.value()) {
dashElements.push_back(dashElement.toJson());
}
@ -134,7 +134,7 @@ public:
std::optional<std::vector<DashElement>> dashPattern;
std::optional<bool> fillEnabled;
std::optional<json11::Json> ml2;
std::optional<lottiejson11::Json> ml2;
};
}

View File

@ -16,7 +16,7 @@ enum class TrimType: int {
/// An item that defines trim
class Trim: public ShapeItem {
public:
explicit Trim(json11::Json::object const &json) noexcept(false) :
explicit Trim(lottiejson11::Json::object const &json) noexcept(false) :
ShapeItem(json),
start(KeyframeGroup<Vector1D>(Vector1D(0.0))),
end(KeyframeGroup<Vector1D>(Vector1D(0.0))),
@ -41,7 +41,7 @@ public:
virtual ~Trim() = default;
virtual void toJson(json11::Json::object &json) const override {
virtual void toJson(lottiejson11::Json::object &json) const override {
ShapeItem::toJson(json);
json.insert(std::make_pair("s", start.toJson()));

View File

@ -22,7 +22,7 @@ public:
ascent(ascent_) {
}
explicit Font(json11::Json::object const &json) noexcept(false) {
explicit Font(lottiejson11::Json::object const &json) noexcept(false) {
name = getString(json, "fName");
familyName = getString(json, "fFamily");
path = getOptionalString(json, "fPath");
@ -33,8 +33,8 @@ public:
origin = getOptionalInt(json, "origin");
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
result.insert(std::make_pair("fName", name));
result.insert(std::make_pair("fFamily", familyName));
@ -74,7 +74,7 @@ public:
fonts(fonts_) {
}
explicit FontList(json11::Json::object const &json) noexcept(false) {
explicit FontList(lottiejson11::Json::object const &json) noexcept(false) {
if (const auto fontsData = getOptionalObjectArray(json, "list")) {
for (const auto &fontData : fontsData.value()) {
fonts.emplace_back(fontData);
@ -82,14 +82,14 @@ public:
}
}
json11::Json::object toJson() const {
json11::Json::array fontArray;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::array fontArray;
for (const auto &font : fonts) {
fontArray.push_back(font.toJson());
}
json11::Json::object result;
lottiejson11::Json::object result;
result.insert(std::make_pair("list", fontArray));
return result;
}

View File

@ -27,7 +27,7 @@ public:
shapes(shapes_) {
}
explicit Glyph(json11::Json::object const &json) noexcept(false) :
explicit Glyph(lottiejson11::Json::object const &json) noexcept(false) :
character(""),
fontSize(0.0),
fontFamily(""),
@ -52,8 +52,8 @@ public:
}
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
result.insert(std::make_pair("ch", character));
result.insert(std::make_pair("size", fontSize));
@ -62,13 +62,13 @@ public:
result.insert(std::make_pair("w", width));
if (internalHasData || shapes.has_value()) {
json11::Json::object shapeContainer;
lottiejson11::Json::object shapeContainer;
if (shapes.has_value()) {
json11::Json::array shapeArray;
lottiejson11::Json::array shapeArray;
for (const auto &shape : shapes.value()) {
json11::Json::object shapeJson;
lottiejson11::Json::object shapeJson;
shape->toJson(shapeJson);
shapeArray.push_back(shapeJson);
}

View File

@ -41,18 +41,18 @@ public:
tracking(tracking_) {
}
explicit TextAnimator(json11::Json const &jsonAny) {
explicit TextAnimator(lottiejson11::Json const &jsonAny) {
if (!jsonAny.is_object()) {
throw LottieParsingException();
}
json11::Json::object const &json = jsonAny.object_items();
lottiejson11::Json::object const &json = jsonAny.object_items();
if (const auto nameData = getOptionalString(json, "nm")) {
name = nameData.value();
}
_extraS = getOptionalAny(json, "s");
json11::Json::object const &animatorContainer = getObject(json, "a");
lottiejson11::Json::object const &animatorContainer = getObject(json, "a");
if (const auto fillColorData = getOptionalObject(animatorContainer, "fc")) {
fillColor = KeyframeGroup<Color>(fillColorData.value());
@ -89,8 +89,8 @@ public:
}
}
json11::Json::object toJson() const {
json11::Json::object animatorContainer;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object animatorContainer;
if (fillColor.has_value()) {
animatorContainer.insert(std::make_pair("fc", fillColor->toJson()));
@ -126,7 +126,7 @@ public:
animatorContainer.insert(std::make_pair("o", opacity->toJson()));
}
json11::Json::object result;
lottiejson11::Json::object result;
result.insert(std::make_pair("a", animatorContainer));
if (name.has_value()) {
@ -175,7 +175,7 @@ public:
/// Tracking
std::optional<KeyframeGroup<Vector1D>> tracking;
std::optional<json11::Json> _extraS;
std::optional<lottiejson11::Json> _extraS;
};
}

View File

@ -48,7 +48,7 @@ public:
textFrameSize(textFrameSize_) {
}
explicit TextDocument(json11::Json const &jsonAny) noexcept(false) :
explicit TextDocument(lottiejson11::Json const &jsonAny) noexcept(false) :
text(""),
fontSize(0.0),
fontFamily(""),
@ -59,7 +59,7 @@ public:
throw LottieParsingException();
}
json11::Json::object const &json = jsonAny.object_items();
lottiejson11::Json::object const &json = jsonAny.object_items();
text = getString(json, "t");
fontSize = getDouble(json, "s");
@ -103,8 +103,8 @@ public:
}
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
result.insert(std::make_pair("t", text));
result.insert(std::make_pair("s", fontSize));

View File

@ -26,7 +26,7 @@ const char* LottieParsingException::what() const throw() {
return "Lottie parsing exception";
}
json11::Json getAny(json11::Json::object const &object, std::string const &key) noexcept(false) {
lottiejson11::Json getAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -34,7 +34,7 @@ json11::Json getAny(json11::Json::object const &object, std::string const &key)
return value->second;
}
std::optional<json11::Json> getOptionalAny(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<lottiejson11::Json> getOptionalAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;
@ -42,7 +42,7 @@ std::optional<json11::Json> getOptionalAny(json11::Json::object const &object, s
return value->second;
}
json11::Json::object getObject(json11::Json::object const &object, std::string const &key) noexcept(false) {
lottiejson11::Json::object getObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -53,7 +53,7 @@ json11::Json::object getObject(json11::Json::object const &object, std::string c
return value->second.object_items();
}
std::optional<json11::Json::object> getOptionalObject(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<lottiejson11::Json::object> getOptionalObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;
@ -64,7 +64,7 @@ std::optional<json11::Json::object> getOptionalObject(json11::Json::object const
return value->second.object_items();
}
std::vector<json11::Json::object> getObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::vector<lottiejson11::Json::object> getObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -73,7 +73,7 @@ std::vector<json11::Json::object> getObjectArray(json11::Json::object const &obj
throw LottieParsingException();
}
std::vector<json11::Json::object> result;
std::vector<lottiejson11::Json::object> result;
for (const auto &item : value->second.array_items()) {
if (!item.is_object()) {
throw LottieParsingException();
@ -84,7 +84,7 @@ std::vector<json11::Json::object> getObjectArray(json11::Json::object const &obj
return result;
}
std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<std::vector<lottiejson11::Json::object>> getOptionalObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;
@ -93,7 +93,7 @@ std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::
throw LottieParsingException();
}
std::vector<json11::Json::object> result;
std::vector<lottiejson11::Json::object> result;
for (const auto &item : value->second.array_items()) {
if (!item.is_object()) {
throw LottieParsingException();
@ -104,7 +104,7 @@ std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::
return result;
}
std::vector<json11::Json> getAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::vector<lottiejson11::Json> getAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -116,7 +116,7 @@ std::vector<json11::Json> getAnyArray(json11::Json::object const &object, std::s
return value->second.array_items();
}
std::optional<std::vector<json11::Json>> getOptionalAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<std::vector<lottiejson11::Json>> getOptionalAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw std::nullopt;
@ -128,7 +128,7 @@ std::optional<std::vector<json11::Json>> getOptionalAnyArray(json11::Json::objec
return value->second.array_items();
}
std::string getString(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::string getString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -139,7 +139,7 @@ std::string getString(json11::Json::object const &object, std::string const &key
return value->second.string_value();
}
std::optional<std::string> getOptionalString(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<std::string> getOptionalString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;
@ -150,7 +150,7 @@ std::optional<std::string> getOptionalString(json11::Json::object const &object,
return value->second.string_value();
}
int32_t getInt(json11::Json::object const &object, std::string const &key) noexcept(false) {
int32_t getInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -161,7 +161,7 @@ int32_t getInt(json11::Json::object const &object, std::string const &key) noexc
return value->second.int_value();
}
std::optional<int32_t> getOptionalInt(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<int32_t> getOptionalInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;
@ -172,7 +172,7 @@ std::optional<int32_t> getOptionalInt(json11::Json::object const &object, std::s
return value->second.int_value();
}
double getDouble(json11::Json::object const &object, std::string const &key) noexcept(false) {
double getDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -183,7 +183,7 @@ double getDouble(json11::Json::object const &object, std::string const &key) noe
return value->second.number_value();
}
std::optional<double> getOptionalDouble(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<double> getOptionalDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;
@ -194,7 +194,7 @@ std::optional<double> getOptionalDouble(json11::Json::object const &object, std:
return value->second.number_value();
}
bool getBool(json11::Json::object const &object, std::string const &key) noexcept(false) {
bool getBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
throw LottieParsingException();
@ -205,7 +205,7 @@ bool getBool(json11::Json::object const &object, std::string const &key) noexcep
return value->second.bool_value();
}
std::optional<bool> getOptionalBool(json11::Json::object const &object, std::string const &key) noexcept(false) {
std::optional<bool> getOptionalBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false) {
auto value = object.find(key);
if (value == object.end()) {
return std::nullopt;

View File

@ -23,29 +23,29 @@ public:
virtual const char* what() const throw();
};
json11::Json getAny(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<json11::Json> getOptionalAny(json11::Json::object const &object, std::string const &key) noexcept(false);
lottiejson11::Json getAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<lottiejson11::Json> getOptionalAny(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
json11::Json::object getObject(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<json11::Json::object> getOptionalObject(json11::Json::object const &object, std::string const &key) noexcept(false);
lottiejson11::Json::object getObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<lottiejson11::Json::object> getOptionalObject(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::vector<json11::Json::object> getObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<std::vector<json11::Json::object>> getOptionalObjectArray(json11::Json::object const &object, std::string const &key) noexcept(false);
std::vector<lottiejson11::Json::object> getObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<std::vector<lottiejson11::Json::object>> getOptionalObjectArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::vector<json11::Json> getAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<std::vector<json11::Json>> getOptionalAnyArray(json11::Json::object const &object, std::string const &key) noexcept(false);
std::vector<lottiejson11::Json> getAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<std::vector<lottiejson11::Json>> getOptionalAnyArray(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::string getString(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<std::string> getOptionalString(json11::Json::object const &object, std::string const &key) noexcept(false);
std::string getString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<std::string> getOptionalString(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
int32_t getInt(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<int32_t> getOptionalInt(json11::Json::object const &object, std::string const &key) noexcept(false);
int32_t getInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<int32_t> getOptionalInt(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
double getDouble(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<double> getOptionalDouble(json11::Json::object const &object, std::string const &key) noexcept(false);
double getDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<double> getOptionalDouble(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
bool getBool(json11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<bool> getOptionalBool(json11::Json::object const &object, std::string const &key) noexcept(false);
bool getBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
std::optional<bool> getOptionalBool(lottiejson11::Json::object const &object, std::string const &key) noexcept(false);
}

View File

@ -32,9 +32,9 @@ public:
closed(false) {
}
explicit BezierPathContents(json11::Json const &jsonAny) noexcept(false) :
explicit BezierPathContents(lottiejson11::Json const &jsonAny) noexcept(false) :
elements({}) {
json11::Json::object const *json = nullptr;
lottiejson11::Json::object const *json = nullptr;
if (jsonAny.is_object()) {
json = &jsonAny.object_items();
} else if (jsonAny.is_array()) {
@ -97,12 +97,12 @@ public:
BezierPathContents(const BezierPathContents&) = delete;
BezierPathContents& operator=(BezierPathContents&) = delete;
json11::Json toJson() const {
json11::Json::object result;
lottiejson11::Json toJson() const {
lottiejson11::Json::object result;
json11::Json::array vertices;
json11::Json::array inPoints;
json11::Json::array outPoints;
lottiejson11::Json::array vertices;
lottiejson11::Json::array inPoints;
lottiejson11::Json::array outPoints;
for (const auto &element : elements) {
vertices.push_back(element.vertex.point.toJson());
@ -118,7 +118,7 @@ public:
result.insert(std::make_pair("c", closed.value()));
}
return json11::Json(result);
return lottiejson11::Json(result);
}
std::shared_ptr<CGPath> cgPath() const {
@ -445,11 +445,11 @@ public:
_contents(std::make_shared<BezierPathContents>()) {
}
explicit BezierPath(json11::Json const &jsonAny) noexcept(false) :
explicit BezierPath(lottiejson11::Json const &jsonAny) noexcept(false) :
_contents(std::make_shared<BezierPathContents>(jsonAny)) {
}
json11::Json toJson() const {
lottiejson11::Json toJson() const {
return _contents->toJson();
}

View File

@ -149,7 +149,7 @@ public:
spatialOutTangent(spatialOutTangent_) {
}
explicit KeyframeData(json11::Json const &json) noexcept(false) {
explicit KeyframeData(lottiejson11::Json const &json) noexcept(false) {
if (!json.is_object()) {
throw LottieParsingException();
}
@ -186,8 +186,8 @@ public:
}
}
json11::Json::object toJson() const {
json11::Json::object result;
lottiejson11::Json::object toJson() const {
lottiejson11::Json::object result;
if (startValue.has_value()) {
result.insert(std::make_pair("s", startValue->toJson()));
@ -240,7 +240,7 @@ public:
/// The spacial out tangent of the vector.
std::optional<Vector3D> spatialOutTangent;
std::optional<json11::Json> nData;
std::optional<lottiejson11::Json> nData;
bool isHold() const {
if (hold.has_value()) {

View File

@ -62,7 +62,7 @@ struct Color {
a = a_ / denominatorValue;
}
explicit Color(json11::Json const &jsonAny) noexcept(false) :
explicit Color(lottiejson11::Json const &jsonAny) noexcept(false) :
r(0.0), g(0.0), b(0.0), a(0.0) {
if (!jsonAny.is_array()) {
throw LottieParsingException();
@ -125,13 +125,13 @@ struct Color {
a = a1;
}
json11::Json toJson() const {
json11::Json::array result;
lottiejson11::Json toJson() const {
lottiejson11::Json::array result;
result.push_back(json11::Json(r));
result.push_back(json11::Json(g));
result.push_back(json11::Json(b));
result.push_back(json11::Json(a));
result.push_back(lottiejson11::Json(r));
result.push_back(lottiejson11::Json(g));
result.push_back(lottiejson11::Json(b));
result.push_back(lottiejson11::Json(a));
return result;
}

View File

@ -11,7 +11,7 @@ struct GradientColorSet {
GradientColorSet() {
}
explicit GradientColorSet(json11::Json const &jsonAny) noexcept(false) {
explicit GradientColorSet(lottiejson11::Json const &jsonAny) noexcept(false) {
if (!jsonAny.is_array()) {
throw LottieParsingException();
}
@ -24,8 +24,8 @@ struct GradientColorSet {
}
}
json11::Json toJson() const {
json11::Json::array result;
lottiejson11::Json toJson() const {
lottiejson11::Json::array result;
for (auto value : colors) {
result.push_back(value);

View File

@ -18,7 +18,7 @@ struct Vector1D {
value(value_) {
}
explicit Vector1D(json11::Json const &json) noexcept(false) {
explicit Vector1D(lottiejson11::Json const &json) noexcept(false) {
if (json.is_number()) {
value = json.number_value();
} else if (json.is_array()) {
@ -34,8 +34,8 @@ struct Vector1D {
}
}
json11::Json toJson() const {
return json11::Json(value);
lottiejson11::Json toJson() const {
return lottiejson11::Json(value);
}
double value;
@ -68,7 +68,7 @@ struct Vector2D {
y(y_) {
}
explicit Vector2D(json11::Json const &json) noexcept(false) {
explicit Vector2D(lottiejson11::Json const &json) noexcept(false) {
x = 0.0;
y = 0.0;
@ -121,13 +121,13 @@ struct Vector2D {
}
}
json11::Json toJson() const {
json11::Json::object result;
lottiejson11::Json toJson() const {
lottiejson11::Json::object result;
result.insert(std::make_pair("x", x));
result.insert(std::make_pair("y", y));
return json11::Json(result);
return lottiejson11::Json(result);
}
double x;
@ -200,7 +200,7 @@ struct Vector3D {
z(z_) {
}
explicit Vector3D(json11::Json const &json) noexcept(false) {
explicit Vector3D(lottiejson11::Json const &json) noexcept(false) {
if (!json.is_array()) {
throw LottieParsingException();
}
@ -236,14 +236,14 @@ struct Vector3D {
}
}
json11::Json toJson() const {
json11::Json::array result;
lottiejson11::Json toJson() const {
lottiejson11::Json::array result;
result.push_back(json11::Json(x));
result.push_back(json11::Json(y));
result.push_back(json11::Json(z));
result.push_back(lottiejson11::Json(x));
result.push_back(lottiejson11::Json(y));
result.push_back(lottiejson11::Json(z));
return json11::Json(result);
return lottiejson11::Json(result);
}
double x = 0.0;

View File

@ -17,7 +17,7 @@
self = [super init];
if (self != nil) {
std::string errorText;
auto json = json11::Json::parse(std::string((uint8_t const *)data.bytes, ((uint8_t const *)data.bytes) + data.length), errorText);
auto json = lottiejson11::Json::parse(std::string((uint8_t const *)data.bytes, ((uint8_t const *)data.bytes) + data.length), errorText);
if (!json.is_object()) {
return nil;
}
@ -35,13 +35,17 @@
return (NSInteger)(_animation->endFrame - _animation->startFrame);
}
- (NSInteger)framesPerSecond {
return (NSInteger)(_animation->framerate);
}
- (CGSize)size {
return CGSizeMake(_animation->width, _animation->height);
}
- (NSData * _Nonnull)toJson {
json11::Json::object json = _animation->toJson();
std::string jsonString = json11::Json(json).dump();
lottiejson11::Json::object json = _animation->toJson();
std::string jsonString = lottiejson11::Json(json).dump();
return [[NSData alloc] initWithBytes:jsonString.data() length:jsonString.size()];
}

View File

@ -321,13 +321,6 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
Vector2D localTranslation(node->position().x + -node->bounds().x, node->position().y + -node->bounds().y);
CATransform3D localTransform = node->transform();
localTransform = localTransform.translated(localTranslation);
//if (localTransform.isIdentity()) {
// localTransform.m41 += localTranslation.x;
// localTransform.m42 += localTranslation.y;
//} else {
// localTransform.m41 += localTranslation.x;
// localTransform.m42 += localTranslation.y;
//}
currentTransform = localTransform * currentTransform;
@ -554,8 +547,6 @@ static std::shared_ptr<OutputRenderNode> convertRenderTree(std::shared_ptr<Rende
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());
auto node = convertRenderTree(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 (node) {

View File

@ -0,0 +1,147 @@
#ifndef LottieRenderTree_h
#define LottieRenderTree_h
#import <QuartzCore/QuartzCore.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef NS_ENUM(NSUInteger, LottiePathItemType) {
LottiePathItemTypeMoveTo,
LottiePathItemTypeLineTo,
LottiePathItemTypeCurveTo,
LottiePathItemTypeClose
};
typedef struct {
LottiePathItemType type;
CGPoint points[4];
} LottiePathItem;
typedef struct {
CGFloat r;
CGFloat g;
CGFloat b;
CGFloat a;
} LottieColor;
typedef NS_ENUM(NSUInteger, LottieFillRule) {
LottieFillRuleEvenOdd,
LottieFillRuleWinding
};
typedef NS_ENUM(NSUInteger, LottieGradientType) {
LottieGradientTypeLinear,
LottieGradientTypeRadial
};
@interface LottieColorStop : NSObject
@property (nonatomic, readonly, direct) LottieColor color;
@property (nonatomic, readonly, direct) CGFloat location;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithColor:(LottieColor)color location:(CGFloat)location __attribute__((objc_direct));
@end
@interface LottiePath : NSObject
- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate __attribute__((objc_direct));
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct));
@end
@interface LottieRenderContentShading : NSObject
@end
@interface LottieRenderContentSolidShading : LottieRenderContentShading
@property (nonatomic, readonly, direct) LottieColor color;
@property (nonatomic, readonly, direct) CGFloat opacity;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity __attribute__((objc_direct));
@end
@interface LottieRenderContentGradientShading : LottieRenderContentShading
@property (nonatomic, readonly, direct) CGFloat opacity;
@property (nonatomic, readonly, direct) LottieGradientType gradientType;
@property (nonatomic, strong, readonly, direct) NSArray<LottieColorStop *> * _Nonnull colorStops;
@property (nonatomic, readonly, direct) CGPoint start;
@property (nonatomic, readonly, direct) CGPoint end;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray<LottieColorStop *> * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct));
@end
@interface LottieRenderContentFill : NSObject
@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading;
@property (nonatomic, readonly, direct) LottieFillRule fillRule;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct));
@end
@interface LottieRenderContentStroke : NSObject
@property (nonatomic, strong, readonly, direct) LottieRenderContentShading * _Nonnull shading;
@property (nonatomic, readonly, direct) CGFloat lineWidth;
@property (nonatomic, readonly, direct) CGLineJoin lineJoin;
@property (nonatomic, readonly, direct) CGLineCap lineCap;
@property (nonatomic, readonly, direct) CGFloat miterLimit;
@property (nonatomic, readonly, direct) CGFloat dashPhase;
@property (nonatomic, strong, readonly, direct) NSArray<NSNumber *> * _Nullable dashPattern;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray<NSNumber *> * _Nullable)dashPattern __attribute__((objc_direct));
@end
@interface LottieRenderContent : NSObject
@property (nonatomic, strong, readonly, direct) LottiePath * _Nonnull path;
@property (nonatomic, strong, readonly, direct) LottieRenderContentStroke * _Nullable stroke;
@property (nonatomic, strong, readonly, direct) LottieRenderContentFill * _Nullable fill;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct));
@end
@interface LottieRenderNode : NSObject
@property (nonatomic, readonly, direct) CGPoint position;
@property (nonatomic, readonly, direct) CGRect bounds;
@property (nonatomic, readonly, direct) CATransform3D transform;
@property (nonatomic, readonly, direct) CGFloat opacity;
@property (nonatomic, readonly, direct) bool masksToBounds;
@property (nonatomic, readonly, direct) bool isHidden;
@property (nonatomic, readonly, direct) CGRect globalRect;
@property (nonatomic, readonly, direct) CATransform3D globalTransform;
@property (nonatomic, readonly, direct) LottieRenderContent * _Nullable renderContent;
@property (nonatomic, readonly, direct) bool hasSimpleContents;
@property (nonatomic, readonly, direct) bool isInvertedMatte;
@property (nonatomic, readonly, direct) NSArray<LottieRenderNode *> * _Nonnull subnodes;
@property (nonatomic, readonly, direct) LottieRenderNode * _Nullable mask;
- (instancetype _Nonnull)init NS_UNAVAILABLE;
- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray<LottieRenderNode *> * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct));
@end
#ifdef __cplusplus
}
#endif
#endif /* LottieRenderTree_h */

View File

@ -1,4 +1,4 @@
#include <LottieCpp/LottieRenderTree.h>
#include "LottieRenderTree.h"
#include "LottieRenderTreeInternal.h"
#include "Lottie/Public/Primitives/CGPath.hpp"
@ -15,6 +15,7 @@ namespace {
@interface LottiePath () {
std::vector<lottie::BezierPath> _paths;
NSData *_customData;
}
@end
@ -29,83 +30,128 @@ namespace {
return self;
}
/*- (instancetype _Nonnull)initWithCGPath:(CGPathRef _Nonnull)cgPath {
- (instancetype _Nonnull)initWithCustomData:(NSData * _Nonnull)customData __attribute__((objc_direct)) {
self = [super init];
if (self != nil) {
CGMutablePathRef mutableCopy = CGPathCreateMutableCopy(cgPath);
_path = std::make_shared<lottie::CGPathCocoaImpl>(mutableCopy);
CFRelease(mutableCopy);
_customData = customData;
}
return self;
}*/
- (CGRect)boundingBox {
lottie::CGRect result = bezierPathsBoundingBox(_paths);
return CGRectMake(result.x, result.y, result.width, result.height);
}
- (void)enumerateItems:(void (^ _Nonnull)(LottiePathItem * _Nonnull))iterate {
LottiePathItem item;
for (const auto &path : _paths) {
std::optional<lottie::PathElement> previousElement;
for (const auto &element : path.elements()) {
if (previousElement.has_value()) {
if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) {
if (_customData != nil) {
int dataOffset = 0;
int dataLength = (int)_customData.length;
uint8_t const *dataBytes = (uint8_t const *)_customData.bytes;
while (dataOffset < dataLength) {
uint8_t itemType = dataBytes[dataOffset];
dataOffset += 1;
switch (itemType) {
case 0: {
Float32 px;
memcpy(&px, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 py;
memcpy(&py, dataBytes + dataOffset, 4);
dataOffset += 4;
item.type = LottiePathItemTypeMoveTo;
item.points[0] = CGPointMake(px, py);
iterate(&item);
break;
}
case 1: {
Float32 px;
memcpy(&px, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 py;
memcpy(&py, dataBytes + dataOffset, 4);
dataOffset += 4;
item.type = LottiePathItemTypeLineTo;
item.points[0] = CGPointMake(px, py);
iterate(&item);
break;
}
case 2: {
Float32 p1x;
memcpy(&p1x, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 p1y;
memcpy(&p1y, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 p2x;
memcpy(&p2x, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 p2y;
memcpy(&p2y, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 px;
memcpy(&px, dataBytes + dataOffset, 4);
dataOffset += 4;
Float32 py;
memcpy(&py, dataBytes + dataOffset, 4);
dataOffset += 4;
item.type = LottiePathItemTypeCurveTo;
item.points[0] = CGPointMake(p1x, p1y);
item.points[1] = CGPointMake(p2x, p2y);
item.points[2] = CGPointMake(px, py);
iterate(&item);
break;
}
case 3: {
item.type = LottiePathItemTypeClose;
iterate(&item);
break;
}
default: {
break;
}
}
}
} else {
for (const auto &path : _paths) {
std::optional<lottie::PathElement> previousElement;
for (const auto &element : path.elements()) {
if (previousElement.has_value()) {
if (previousElement->vertex.outTangentRelative().isZero() && element.vertex.inTangentRelative().isZero()) {
item.type = LottiePathItemTypeLineTo;
item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
iterate(&item);
} else {
item.type = LottiePathItemTypeCurveTo;
item.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
item.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y);
item.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y);
iterate(&item);
}
} else {
item.type = LottiePathItemTypeMoveTo;
item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
iterate(&item);
} else {
item.type = LottiePathItemTypeCurveTo;
item.points[2] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
item.points[1] = CGPointMake(element.vertex.inTangent.x, element.vertex.inTangent.y);
item.points[0] = CGPointMake(previousElement->vertex.outTangent.x, previousElement->vertex.outTangent.y);
iterate(&item);
}
} else {
item.type = LottiePathItemTypeMoveTo;
item.points[0] = CGPointMake(element.vertex.point.x, element.vertex.point.y);
iterate(&item);
previousElement = element;
}
previousElement = element;
}
if (path.closed().value_or(true)) {
item.type = LottiePathItemTypeClose;
iterate(&item);
}
}
/*_path->enumerate([iterate](lottie::CGPathItem const &element) {
LottiePathItem item;
switch (element.type) {
case lottie::CGPathItem::Type::MoveTo: {
item.type = LottiePathItemTypeMoveTo;
item.points[0] = CGPointMake(element.points[0].x, element.points[0].y);
iterate(&item);
break;
}
case lottie::CGPathItem::Type::LineTo: {
item.type = LottiePathItemTypeLineTo;
item.points[0] = CGPointMake(element.points[0].x, element.points[0].y);
iterate(&item);
break;
}
case lottie::CGPathItem::Type::CurveTo: {
item.type = LottiePathItemTypeCurveTo;
item.points[0] = CGPointMake(element.points[0].x, element.points[0].y);
item.points[1] = CGPointMake(element.points[1].x, element.points[1].y);
item.points[2] = CGPointMake(element.points[2].x, element.points[2].y);
iterate(&item);
break;
}
case lottie::CGPathItem::Type::Close: {
if (path.closed().value_or(true)) {
item.type = LottiePathItemTypeClose;
iterate(&item);
break;
}
}
});*/
}
}
@end
@ -155,6 +201,15 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
return self;
}
- (instancetype _Nonnull)initWithColor:(LottieColor)color opacity:(CGFloat)opacity {
self = [super init];
if (self != nil) {
_color = color;
_opacity = opacity;
}
return self;
}
@end
@implementation LottieRenderContentGradientShading
@ -187,6 +242,18 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
return self;
}
- (instancetype _Nonnull)initWithOpacity:(CGFloat)opacity gradientType:(LottieGradientType)gradientType colorStops:(NSArray<LottieColorStop *> * _Nonnull)colorStops start:(CGPoint)start end:(CGPoint)end __attribute__((objc_direct)) {
self = [super init];
if (self != nil) {
_opacity = opacity;
_gradientType = gradientType;
_colorStops = colorStops;
_start = start;
_end = end;
}
return self;
}
@end
@implementation LottieRenderContentFill
@ -222,6 +289,15 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
return self;
}
- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading fillRule:(LottieFillRule)fillRule __attribute__((objc_direct)) {
self = [super init];
if (self != nil) {
_shading = shading;
_fillRule = fillRule;
}
return self;
}
@end
@implementation LottieRenderContentStroke
@ -298,6 +374,20 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
return self;
}
- (instancetype _Nonnull)initWithShading:(LottieRenderContentShading * _Nonnull)shading lineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit dashPhase:(CGFloat)dashPhase dashPattern:(NSArray<NSNumber *> * _Nullable)dashPattern __attribute__((objc_direct)) {
self = [super init];
if (self != nil) {
_shading = shading;
_lineWidth = lineWidth;
_lineJoin = lineJoin;
_lineCap = lineCap;
_miterLimit = miterLimit;
_dashPhase = dashPhase;
_dashPattern = dashPattern;
}
return self;
}
@end
@implementation LottieRenderContent
@ -316,10 +406,40 @@ static LottieColor lottieColorFromColor(lottie::Color color) {
return self;
}
- (instancetype _Nonnull)initWithPath:(LottiePath * _Nonnull)path stroke:(LottieRenderContentStroke * _Nullable)stroke fill:(LottieRenderContentFill * _Nullable)fill __attribute__((objc_direct)) {
self = [super init];
if (self != nil) {
_path = path;
_stroke = stroke;
_fill = fill;
}
return self;
}
@end
@implementation LottieRenderNode
- (instancetype _Nonnull)initWithPosition:(CGPoint)position bounds:(CGRect)bounds transform:(CATransform3D)transform opacity:(CGFloat)opacity masksToBounds:(bool)masksToBounds isHidden:(bool)isHidden globalRect:(CGRect)globalRect globalTransform:(CATransform3D)globalTransform renderContent:(LottieRenderContent * _Nullable)renderContent hasSimpleContents:(bool)hasSimpleContents isInvertedMatte:(bool)isInvertedMatte subnodes:(NSArray<LottieRenderNode *> * _Nonnull)subnodes mask:(LottieRenderNode * _Nullable)mask __attribute__((objc_direct)) {
self = [super init];
if (self != nil) {
_position = position;
_bounds = bounds;
_transform = transform;
_opacity = opacity;
_masksToBounds = masksToBounds;
_isHidden = isHidden;
_globalRect = globalRect;
_globalTransform= globalTransform;
_renderContent = renderContent;
_hasSimpleContents = hasSimpleContents;
_isInvertedMatte = isInvertedMatte;
_subnodes = subnodes;
_mask = mask;
}
return self;
}
@end
@implementation LottieRenderNode (Internal)

View File

@ -1,5 +1,46 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "LottieMetalSources",
srcs = glob([
"Metal/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "LottieMetalSourcesBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.LottieMetalSources</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>LottieMetal</string>
"""
)
apple_resource_bundle(
name = "LottieMetalSourcesBundle",
infoplists = [
":LottieMetalSourcesBundleInfoPlist",
],
resources = [
":LottieMetalSources",
],
)
swift_library(
name = "LottieMetal",
module_name = "LottieMetal",
@ -9,6 +50,9 @@ swift_library(
copts = [
"-warnings-as-errors",
],
data = [
":LottieMetalSourcesBundle",
],
deps = [
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/AsyncDisplayKit",
@ -17,6 +61,7 @@ swift_library(
"//submodules/GZip",
"//submodules/MetalEngine",
"//submodules/TelegramUI/Components/LottieCpp",
"//submodules/Components/HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",

View File

@ -0,0 +1,872 @@
#include <metal_stdlib>
using namespace metal;
typedef struct
{
packed_float2 position;
packed_float2 texCoord;
} QuadVertex;
typedef struct
{
packed_float2 position;
} Vertex;
typedef struct
{
float4 position [[position]];
float2 texCoord;
float2 transformedPosition;
} QuadOut;
typedef struct
{
float4 position [[position]];
float direction;
} FillVertexOut;
float calculateNormalDirection(float2 a, float2 b, float2 c) {
float2 ab = b - a;
float2 ac = c - a;
return ab.x * ac.y - ab.y * ac.x;
}
vertex QuadOut quad_vertex_shader(
device QuadVertex const *vertices [[buffer(0)]],
uint vertexId [[vertex_id]],
device matrix<float, 4> const &transform [[buffer(1)]]
) {
QuadVertex in = vertices[vertexId];
QuadOut out;
float4 position = transform * float4(float2(in.position), 0.0, 1.0);
out.position = position;
out.texCoord = in.texCoord;
out.transformedPosition = (transform * float4(float2(in.position), 0.0, 1.0)).xy;
return out;
}
vertex FillVertexOut fill_vertex_shader(
device Vertex const *vertices [[buffer(0)]],
uint vertexId [[vertex_id]],
device matrix<float, 4> const &transform [[buffer(1)]],
device packed_float2 const &baseVertex [[buffer(2)]]
) {
FillVertexOut out;
uint triangleIndex = vertexId / 3;
uint vertexInTriangleIndex = vertexId % 3;
//[0, 1], [1, 2], [2, 3]...
//0, 1, 2
float2 sourcePosition;
float2 v1 = float2(vertices[triangleIndex].position);
float2 v2 = float2(vertices[triangleIndex + 1].position);
sourcePosition = select(
select(
v2,
v1,
vertexInTriangleIndex == 1
),
baseVertex,
vertexInTriangleIndex == 0
);
float normalDirection = calculateNormalDirection(baseVertex, v1, v2);
float4 position = transform * float4(sourcePosition, 0.0, 1.0);
out.position = position;
out.direction = sign(normalDirection);
return out;
}
struct ShapeOut {
half4 color [[color(1)]];
};
fragment ShapeOut fragment_shader(
FillVertexOut in [[stage_in]],
ShapeOut current,
device const int32_t &mode [[buffer(1)]]
) {
ShapeOut out = current;
if (mode == 0) {
half result = select(out.color.r, half(127.0 / 255.0), out.color.r == 0.0);
result += half(in.direction) * 3.0 / 255.0;
out.color.r = result;
} else {
out.color.r = out.color.r == 0.0 ? 1.0 : 0.0;
}
return out;
}
fragment ShapeOut clear_mask_fragment(
QuadOut in [[stage_in]]
) {
ShapeOut out;
out.color = half4(0.0);
return out;
}
struct ColorOut {
half4 color [[color(0)]];
};
fragment ColorOut merge_color_fill_fragment_shader(
ShapeOut colorIn,
device const float4 &color [[buffer(0)]],
device const int32_t &mode [[buffer(1)]]
) {
ColorOut out;
half4 sampledColor = half4(color);
sampledColor.r = sampledColor.r * sampledColor.a;
sampledColor.g = sampledColor.g * sampledColor.a;
sampledColor.b = sampledColor.b * sampledColor.a;
if (mode == 0) {
half diff = abs(colorIn.color.r - 127.0 / 255.0);
float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
float outColorFactor = select(
0.0,
diffSelect,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
} else {
float outColorFactor = select(
0.0,
1.0,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
}
if (out.color.a == 0.0) {
//discard_fragment();
}
return out;
}
typedef struct
{
packed_float4 color;
float location;
} GradientColorStop;
float linearGradientStep(float edge0, float edge1, float x) {
float t = clamp((x - edge0) / (edge1 - edge0), float(0), float(1));
return t;
}
fragment ColorOut merge_linear_gradient_fill_fragment_shader(
QuadOut quadIn [[stage_in]],
ShapeOut colorIn,
device const GradientColorStop *colorStops [[buffer(0)]],
device const int32_t &mode [[buffer(1)]],
device const uint &numColorStops [[buffer(2)]],
device const packed_float2 &localStartPosition [[buffer(3)]],
device const packed_float2 &localEndPosition [[buffer(4)]]
) {
ColorOut out;
float4 sourceColor;
if (numColorStops <= 1) {
sourceColor = colorStops[0].color;
} else {
float2 localPixelPosition = quadIn.transformedPosition.xy;
float2 gradientVector = normalize(localEndPosition - localStartPosition);
float2 pointVector = localPixelPosition - localStartPosition;
float pixelDistance = dot(pointVector, gradientVector) / dot(gradientVector, gradientVector);
float gradientLength = length(localEndPosition - localStartPosition);
float pixelValue = clamp(pixelDistance / gradientLength, 0.0, 1.0);
sourceColor = mix(colorStops[0].color, colorStops[1].color, linearGradientStep(
colorStops[0].location,
colorStops[1].location,
pixelValue
));
for (int i = 1; i < (int)numColorStops - 1; i++) {
sourceColor = mix(sourceColor, colorStops[i + 1].color, linearGradientStep(
colorStops[i].location,
colorStops[i + 1].location,
pixelValue
));
}
}
half4 sampledColor = half4(sourceColor);
sampledColor.r = sampledColor.r * sampledColor.a;
sampledColor.g = sampledColor.g * sampledColor.a;
sampledColor.b = sampledColor.b * sampledColor.a;
if (mode == 0) {
half diff = abs(colorIn.color.r - 127.0 / 255.0);
float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
float outColorFactor = select(
0.0,
diffSelect,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
} else {
float outColorFactor = select(
0.0,
1.0,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
}
if (out.color.a == 0.0) {
//discard_fragment();
}
return out;
}
fragment ColorOut merge_radial_gradient_fill_fragment_shader(
QuadOut quadIn [[stage_in]],
ShapeOut colorIn,
device const GradientColorStop *colorStops [[buffer(0)]],
device const int32_t &mode [[buffer(1)]],
device const uint &numColorStops [[buffer(2)]],
device const packed_float2 &localStartPosition [[buffer(3)]],
device const packed_float2 &localEndPosition [[buffer(4)]]
) {
ColorOut out;
float4 sourceColor;
if (numColorStops <= 1) {
sourceColor = colorStops[0].color;
} else {
float pixelDistance = distance(quadIn.transformedPosition.xy, localStartPosition);
float gradientLength = length(localEndPosition - localStartPosition);
float pixelValue = clamp(pixelDistance / gradientLength, 0.0, 1.0);
sourceColor = colorStops[0].color;
for (int i = 0; i < (int)numColorStops - 1; i++) {
float currentStopLocation = colorStops[i].location;
float nextStopLocation = colorStops[i + 1].location;
float4 nextStopColor = colorStops[i + 1].color;
sourceColor = mix(sourceColor, nextStopColor, linearGradientStep(
currentStopLocation,
nextStopLocation,
pixelValue
));
}
}
half4 sampledColor = half4(sourceColor);
sampledColor.r = sampledColor.r * sampledColor.a;
sampledColor.g = sampledColor.g * sampledColor.a;
sampledColor.b = sampledColor.b * sampledColor.a;
if (mode == 0) {
half diff = abs(colorIn.color.r - 127.0 / 255.0);
float diffSelect = select(0.0, 1.0, diff > (2.0 / 255.0));
float outColorFactor = select(
0.0,
diffSelect,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
} else {
float outColorFactor = select(
0.0,
1.0,
colorIn.color.r > 1.0 / 255.0
);
out.color = sampledColor * outColorFactor;
}
if (out.color.a == 0.0) {
//discard_fragment();
}
return out;
}
typedef struct {
packed_float2 position;
} StrokePositionIn;
typedef struct {
packed_float2 point;
} StrokePointIn;
typedef struct {
float id;
} StrokeRoundJoinVertexIn;
typedef struct {
packed_float4 position;
} StrokeMiterJoinVertexIn;
typedef struct {
packed_float3 position;
} StrokeBevelJoinVertexIn;
typedef struct {
packed_float2 position;
} StrokeCapVertexIn;
typedef struct
{
float4 position [[position]];
} StrokeVertexOut;
fragment ColorOut stroke_fragment_shader(
StrokeVertexOut in [[stage_in]],
ShapeOut colorIn,
device const float4 &color [[buffer(0)]]
) {
ColorOut out;
half4 result = half4(color);
result.r *= result.a;
result.g *= result.a;
result.b *= result.a;
out.color = result;
return out;
}
typedef struct {
int32_t bufferOffset; // 4
packed_float2 start; // 4 * 2
packed_float2 end; // 4 * 2
packed_float2 cp1; // 4 * 2
packed_float2 cp2; // 4 * 2
float offset; // 4
} BezierInputItem;
kernel void evaluateBezier(
device BezierInputItem const *inputItems [[buffer(0)]],
device float *vertexData [[buffer(1)]],
device uint const &itemCount [[buffer(2)]],
uint2 index [[ thread_position_in_grid ]]
) {
if (index.x >= itemCount) {
return;
}
BezierInputItem item = inputItems[index.x];
float2 p0 = item.start;
float2 p1 = item.cp1;
float2 p2 = item.cp2;
float2 p3 = item.end;
float t = (((float)index.y) + 1.0) / (8.0);
float oneMinusT = 1.0 - t;
float2 value = oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3;
vertexData[item.bufferOffset + 2 * index.y] = value.x;
vertexData[item.bufferOffset + 2 * index.y + 1] = value.y;
}
fragment half4 quad_offscreen_fragment(
QuadOut in [[stage_in]],
texture2d<half, access::sample> texture[[texture(0)]],
device float const &opacity [[buffer(1)]]
) {
constexpr sampler s(address::clamp_to_edge, filter::linear);
half4 color = texture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
color *= half(opacity);
return color;
}
fragment half4 quad_offscreen_fragment_with_mask(
QuadOut in [[stage_in]],
texture2d<half, access::sample> texture[[texture(0)]],
texture2d<half, access::sample> maskTexture[[texture(1)]],
device float const &opacity [[buffer(1)]],
device uint const &maskMode [[buffer(2)]]
) {
constexpr sampler s(address::clamp_to_edge, filter::linear);
half4 color = texture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
half4 maskColor = maskTexture.sample(s, float2(in.texCoord.x, 1.0 - in.texCoord.y));
if (maskMode == 0) {
color *= maskColor.a;
} else {
color *= 1.0 - maskColor.a;
}
color *= half(opacity);
return color;
}
bool myIsNan(float val) {
return (val < 0.0 || 0.0 < val || val == 0.0) ? false : true;
}
bool isLinePointInvalid(float4 p) {
return p.w == 0.0 || myIsNan(p.x);
}
// Adapted from https://github.com/rreusser/regl-gpu-lines
vertex StrokeVertexOut strokeTerminalVertex(
uint instanceId [[instance_id]],
uint index [[vertex_id]],
device StrokePointIn const *points [[buffer(0)]],
device matrix<float, 4> const &transform [[buffer(1)]],
device packed_float2 const &_vertCnt2 [[buffer(2)]],
device packed_float2 const &_capJoinRes2 [[buffer(3)]],
device uint const &isJoinRound [[buffer(4)]],
device uint const &isCapRound [[buffer(5)]],
device float const &miterLimit [[buffer(6)]],
device float const &width [[buffer(7)]]
) {
const float2 ROUND_CAP_SCALE = float2(1.0, 1.0);
const float2 SQUARE_CAP_SCALE = float2(2.0, 2.0 / sqrt(3.0));
float2 _capScale = isCapRound ? ROUND_CAP_SCALE : SQUARE_CAP_SCALE;
const float pi = 3.141592653589793;
float2 xyB = points[instanceId * 3 + 0].point;
float2 xyC = points[instanceId * 3 + 1].point;
float2 xyD = points[instanceId * 3 + 2].point;
StrokeVertexOut out;
float4 pB = float4(xyB, 0.0, 1.0);
float4 pC = float4(xyC, 0.0, 1.0);
float4 pD = float4(xyD, 0.0, 1.0);
// A sensible default for early returns
out.position = pB;
bool aInvalid = false;
bool bInvalid = isLinePointInvalid(pB);
bool cInvalid = isLinePointInvalid(pC);
bool dInvalid = isLinePointInvalid(pD);
// Vertex count for each part (first half of join, second (mirrored) half). Note that not all of
// these vertices may be used, for example if we have enough for a round cap but only draw a miter
// join.
float2 v = _vertCnt2 + 3.0;
// Total vertex count
float N = dot(v, float2(1));
// If we're past the first half-join and half of the segment, then we swap all vertices and start
// over from the opposite end.
bool mirror = index >= v.x;
// When rendering dedicated endpoints, this allows us to insert an end cap *alone* (without the attached
// segment and join)
if (dInvalid && mirror) {
return out;
}
// Convert to screen-pixel coordinates
// Save w so we can perspective re-multiply at the end to get varyings depth-correct
float pw = mirror ? pC.w : pB.w;
pB = float4(float3(pB.xy, pB.z) / pB.w, 1);
pC = float4(float3(pC.xy, pC.z) / pC.w, 1);
pD = float4(float3(pD.xy, pD.z) / pD.w, 1);
// If it's a cap, mirror A back onto C to accomplish a round
float4 pA = pC;
// Reject if invalid or if outside viewing planes
if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {
return out;
}
// Swap everything computed so far if computing mirrored half
if (mirror) {
float4 vTmp = pC; pC = pB; pB = vTmp;
vTmp = pD; pD = pA; pA = vTmp;
bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;
}
bool isCap = !mirror;
// Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a
// degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.
if (aInvalid) { pA = 2.0 * pB - pC; }
if (dInvalid) { pD = 2.0 * pC - pB; }
bool roundOrCap = isJoinRound || isCap;
// Tangent and normal vectors
float2 tBC = pC.xy - pB.xy;
float lBC = length(tBC);
tBC /= lBC;
float2 nBC = float2(-tBC.y, tBC.x);
float2 tAB = pB.xy - pA.xy;
float lAB = length(tAB);
if (lAB > 0.0) tAB /= lAB;
float2 nAB = float2(-tAB.y, tAB.x);
float2 tCD = pD.xy - pC.xy;
float lCD = length(tCD);
if (lCD > 0.0) tCD /= lCD;
float2 nCD = float2(-tCD.y, tCD.x);
// Clamp for safety, since we take the arccos
float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);
// This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation
// of the middle segment. The fix appears straightforward, but this took a few hours to get right.
const float tol = 1e-4;
float mirrorSign = mirror ? -1.0 : 1.0;
float dirB = -dot(tBC, nAB);
float dirC = dot(tBC, nCD);
bool bCollinear = abs(dirB) < tol;
bool cCollinear = abs(dirC) < tol;
bool bIsHairpin = bCollinear && cosB < 0.0;
// bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;
dirB = bCollinear ? -mirrorSign : sign(dirB);
dirC = cCollinear ? -mirrorSign : sign(dirC);
float2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
// Compute our primary "join index", that is, the index starting at the very first point of the join.
// The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
float i = mirror ? N - index : index;
// Decide the resolution of whichever feature we're drawing. n is twice the number of points used since
// that's the only form in which we use this number.
float res = (isCap ? _capJoinRes2.x : _capJoinRes2.y);
// Shift the index to send unused vertices to an index below zero, which will then just get clamped to
// zero and result in repeated points, i.e. degenerate triangles.
i -= max(0.0, (mirror ? _vertCnt2.y : _vertCnt2.x) - res);
// Use the direction to offset the index by one. This has the effect of flipping the winding number so
// that it's always consistent no matter which direction the join turns.
i += (dirB < 0.0 ? -1.0 : 0.0);
// Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly
// in the middle, where the mirrored and unmirrored halves meet.
i -= mirror ? 1.0 : 0.0;
// Clamp to zero and repeat unused excess vertices.
i = max(0.0, i);
// Start with a default basis pointing along the segment with normal vector outward
float2 xBasis = tBC;
float2 yBasis = nBC * dirB;
// Default point is 0 along the segment, 1 (width unit) normal to it
float2 xy = float2(0);
if (i == res + 1.0) {
// pick off this one specific index to be the interior miter point
// If not div-by-zero, then sinB / (1 + cosB)
float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;
xy = float2(min(abs(m), min(lBC, lAB) / width), -1);
} else {
// Draw half of a join
float m2 = dot(miter, miter);
float lm = sqrt(m2);
yBasis = miter / lm;
xBasis = dirB * float2(yBasis.y, -yBasis.x);
bool isBevel = 1.0 > miterLimit * m2;
if (((int)i) % 2 == 0) {
// Outer joint points
if (roundOrCap || i != 0.0) {
// Round joins
float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);
xy = float2(cos(theta), sin(theta));
if (isCap) {
// A special multiplier factor for turning 3-point rounds into square caps (but leave the
// y == 0.0 point unaffected)
if (xy.y > 0.001) xy *= _capScale;
}
} else {
// Miter joins
yBasis = bIsHairpin ? float2(0) : miter;
xy.y = isBevel ? 1.0 : 1.0 / m2;
}
} else {
// Offset the center vertex position to get bevel SDF correct
if (isBevel && !roundOrCap) {
xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);
}
}
}
// Point offset from main vertex position
float2 dP = float2x2(xBasis, yBasis) * xy;
out.position = pB;
out.position.xy += width * dP;
out.position *= pw;
out.position = transform * out.position;
return out;
}
vertex StrokeVertexOut strokeInnerVertex(
uint instanceId [[instance_id]],
uint index [[vertex_id]],
device StrokePointIn const *points [[buffer(0)]],
device matrix<float, 4> const &transform [[buffer(1)]],
device packed_float2 const &_vertCnt2 [[buffer(2)]],
device packed_float2 const &_capJoinRes2 [[buffer(3)]],
device uint const &isJoinRound [[buffer(4)]],
device uint const &isCapRound [[buffer(5)]],
device float const &miterLimit [[buffer(6)]],
device float const &width [[buffer(7)]]
) {
const float2 ROUND_CAP_SCALE = float2(1.0, 1.0);
const float2 SQUARE_CAP_SCALE = float2(2.0, 2.0 / sqrt(3.0));
float2 _capScale = isCapRound ? ROUND_CAP_SCALE : SQUARE_CAP_SCALE;
const float pi = 3.141592653589793;
float2 xyA = points[instanceId + 0].point;
float2 xyB = points[instanceId + 1].point;
float2 xyC = points[instanceId + 2].point;
float2 xyD = points[instanceId + 3].point;
StrokeVertexOut out;
float4 pA = float4(xyA, 0.0, 1.0);
float4 pB = float4(xyB, 0.0, 1.0);
float4 pC = float4(xyC, 0.0, 1.0);
float4 pD = float4(xyD, 0.0, 1.0);
// A sensible default for early returns
out.position = pB;
bool aInvalid = isLinePointInvalid(pA);
bool bInvalid = isLinePointInvalid(pB);
bool cInvalid = isLinePointInvalid(pC);
bool dInvalid = isLinePointInvalid(pD);
// Vertex count for each part (first half of join, second (mirrored) half). Note that not all of
// these vertices may be used, for example if we have enough for a round cap but only draw a miter
// join.
float2 v = _vertCnt2 + 3.0;
// Total vertex count
float N = dot(v, float2(1));
// If we're past the first half-join and half of the segment, then we swap all vertices and start
// over from the opposite end.
bool mirror = index >= v.x;
// When rendering dedicated endoints, this allows us to insert an end cap *alone* (without the attached
// segment and join)
// Convert to screen-pixel coordinates
// Save w so we can perspective re-multiply at the end to get varyings depth-correct
float pw = mirror ? pC.w : pB.w;
pA = float4(float3(pA.xy, pA.z) / pA.w, 1);
pB = float4(float3(pB.xy, pB.z) / pB.w, 1);
pC = float4(float3(pC.xy, pC.z) / pC.w, 1);
pD = float4(float3(pD.xy, pD.z) / pD.w, 1);
// If it's a cap, mirror A back onto C to accomplish a round
// Reject if invalid or if outside viewing planes
if (bInvalid || cInvalid || max(abs(pB.z), abs(pC.z)) > 1.0) {
return out;
}
// Swap everything computed so far if computing mirrored half
if (mirror) {
float4 vTmp = pC; pC = pB; pB = vTmp;
vTmp = pD; pD = pA; pA = vTmp;
bool bTmp = dInvalid; dInvalid = aInvalid; aInvalid = bTmp;
}
const bool isCap = false;
// Either flip A onto C (and D onto B) to produce a 180 degree-turn cap, or extrapolate to produce a
// degenerate (no turn) join, depending on whether we're inserting caps or just leaving ends hanging.
if (aInvalid) { pA = 2.0 * pB - pC; }
if (dInvalid) { pD = 2.0 * pC - pB; }
bool roundOrCap = isJoinRound || isCap;
// Tangent and normal vectors
float2 tBC = pC.xy - pB.xy;
float lBC = length(tBC);
tBC /= lBC;
float2 nBC = float2(-tBC.y, tBC.x);
float2 tAB = pB.xy - pA.xy;
float lAB = length(tAB);
if (lAB > 0.0) tAB /= lAB;
float2 nAB = float2(-tAB.y, tAB.x);
float2 tCD = pD.xy - pC.xy;
float lCD = length(tCD);
if (lCD > 0.0) tCD /= lCD;
float2 nCD = float2(-tCD.y, tCD.x);
// Clamp for safety, since we take the arccos
float cosB = clamp(dot(tAB, tBC), -1.0, 1.0);
// This section is somewhat fragile. When lines are collinear, signs flip randomly and break orientation
// of the middle segment. The fix appears straightforward, but this took a few hours to get right.
const float tol = 1e-4;
float mirrorSign = mirror ? -1.0 : 1.0;
float dirB = -dot(tBC, nAB);
float dirC = dot(tBC, nCD);
bool bCollinear = abs(dirB) < tol;
bool cCollinear = abs(dirC) < tol;
bool bIsHairpin = bCollinear && cosB < 0.0;
// bool cIsHairpin = cCollinear && dot(tBC, tCD) < 0.0;
dirB = bCollinear ? -mirrorSign : sign(dirB);
dirC = cCollinear ? -mirrorSign : sign(dirC);
float2 miter = bIsHairpin ? -tBC : 0.5 * (nAB + nBC) * dirB;
// Compute our primary "join index", that is, the index starting at the very first point of the join.
// The second half of the triangle strip instance is just the first, reversed, and with vertices swapped!
float i = mirror ? N - index : index;
// Decide the resolution of whichever feature we're drawing. n is twice the number of points used since
// that's the only form in which we use this number.
float res = (isCap ? _capJoinRes2.x : _capJoinRes2.y);
// Shift the index to send unused vertices to an index below zero, which will then just get clamped to
// zero and result in repeated points, i.e. degenerate triangles.
i -= max(0.0, (mirror ? _vertCnt2.y : _vertCnt2.x) - res);
// Use the direction to offset the index by one. This has the effect of flipping the winding number so
// that it's always consistent no matter which direction the join turns.
i += (dirB < 0.0 ? -1.0 : 0.0);
// Vertices of the second (mirrored) half of the join are offset by one to get it to connect correctly
// in the middle, where the mirrored and unmirrored halves meet.
i -= mirror ? 1.0 : 0.0;
// Clamp to zero and repeat unused excess vertices.
i = max(0.0, i);
// Start with a default basis pointing along the segment with normal vector outward
float2 xBasis = tBC;
float2 yBasis = nBC * dirB;
// Default point is 0 along the segment, 1 (width unit) normal to it
float2 xy = float2(0);
if (i == res + 1.0) {
// pick off this one specific index to be the interior miter point
// If not div-by-zero, then sinB / (1 + cosB)
float m = cosB > -0.9999 ? (tAB.x * tBC.y - tAB.y * tBC.x) / (1.0 + cosB) : 0.0;
xy = float2(min(abs(m), min(lBC, lAB) / width), -1);
} else {
// Draw half of a join
float m2 = dot(miter, miter);
float lm = sqrt(m2);
yBasis = miter / lm;
xBasis = dirB * float2(yBasis.y, -yBasis.x);
bool isBevel = 1.0 > miterLimit * m2;
if (((int)i) % 2 == 0) {
// Outer joint points
if (roundOrCap || i != 0.0) {
// Round joins
float theta = -0.5 * (acos(cosB) * (clamp(i, 0.0, res) / res) - pi) * (isCap ? 2.0 : 1.0);
xy = float2(cos(theta), sin(theta));
if (isCap) {
// A special multiplier factor for turning 3-point rounds into square caps (but leave the
// y == 0.0 point unaffected)
if (xy.y > 0.001) xy *= _capScale;
}
} else {
// Miter joins
yBasis = bIsHairpin ? float2(0) : miter;
xy.y = isBevel ? 1.0 : 1.0 / m2;
}
} else {
// Offset the center vertex position to get bevel SDF correct
if (isBevel && !roundOrCap) {
xy.y = -1.0 + sqrt((1.0 + cosB) * 0.5);
}
}
}
// Point offset from main vertex position
float2 dP = float2x2(xBasis, yBasis) * xy;
// The varying generation code handles clamping, if needed
out.position = pB;
out.position.xy += width * dP;
out.position *= pw;
out.position = transform * out.position;
return out;
}
constant static float2 quadVertices[6] = {
float2(0.0, 0.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 0.0),
float2(0.0, 1.0),
float2(1.0, 1.0)
};
struct MetalEngineRectangle {
float2 origin;
float2 size;
};
struct MetalEngineQuadVertexOut {
float4 position [[position]];
float2 uv;
};
vertex MetalEngineQuadVertexOut blitVertex(
const device MetalEngineRectangle &rect [[ buffer(0) ]],
unsigned int vid [[ vertex_id ]]
) {
float2 quadVertex = quadVertices[vid];
MetalEngineQuadVertexOut out;
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
out.position.x = -1.0 + out.position.x * 2.0;
out.position.y = -1.0 + out.position.y * 2.0;
out.uv = float2(quadVertex.x, 1.0 - quadVertex.y);
return out;
}
fragment half4 blitFragment(
MetalEngineQuadVertexOut in [[stage_in]],
texture2d<half> texture [[ texture(0) ]]
) {
constexpr sampler sampler(coord::normalized, address::repeat, filter::linear);
half4 color = texture.sample(sampler, in.uv);
return half4(color.r, color.g, color.b, color.a);
}

View File

@ -7,17 +7,566 @@ import AnimatedStickerNode
import MetalEngine
import LottieCpp
import GZip
import MetalKit
import HierarchyTrackingLayer
private final class BundleMarker: NSObject {
}
private var metalLibraryValue: MTLLibrary?
func metalLibrary(device: MTLDevice) -> MTLLibrary? {
if let metalLibraryValue {
return metalLibraryValue
}
let mainBundle = Bundle(for: BundleMarker.self)
guard let path = mainBundle.path(forResource: "LottieMetalSourcesBundle", ofType: "bundle") else {
return nil
}
guard let bundle = Bundle(path: path) else {
return nil
}
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
return nil
}
metalLibraryValue = library
return library
}
private func generateTexture(device: MTLDevice, sideSize: Int, msaaSampleCount: Int) -> MTLTexture {
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.sampleCount = msaaSampleCount
if msaaSampleCount == 1 {
textureDescriptor.textureType = .type2D
} else {
textureDescriptor.textureType = .type2DMultisample
}
textureDescriptor.width = sideSize
textureDescriptor.height = sideSize
textureDescriptor.pixelFormat = .bgra8Unorm
//textureDescriptor.storageMode = .memoryless
textureDescriptor.storageMode = .private
textureDescriptor.usage = [.renderTarget, .shaderRead]
return device.makeTexture(descriptor: textureDescriptor)!
}
final class LottieContentLayer: MetalEngineSubjectLayer, MetalEngineSubject {
private var animationContainer: LottieAnimationContainer?
var frameIndex: Int = 0
var internalData: MetalEngineSubjectInternalData?
private var renderBufferHeap: MTLHeap?
private var offscreenHeap: MTLHeap?
private var multisampleTextureQueue: [MTLTexture] = []
private let currentBezierIndicesBuffer = PathRenderBuffer()
private let currentBuffer = PathRenderBuffer()
final class PrepareState: ComputeState {
let pathRenderContext: PathRenderContext
init?(device: MTLDevice) {
guard let pathRenderContext = PathRenderContext(device: device, msaaSampleCount: 1) else {
return nil
}
self.pathRenderContext = pathRenderContext
}
}
final class RenderState: RenderToLayerState {
let pipelineState: MTLRenderPipelineState
required init?(device: MTLDevice) {
guard let library = metalLibrary(device: device) else {
return nil
}
guard let vertexFunction = library.makeFunction(name: "blitVertex"), let fragmentFunction = library.makeFunction(name: "blitFragment") else {
return nil
}
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertexFunction
pipelineDescriptor.fragmentFunction = fragmentFunction
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
return nil
}
self.pipelineState = pipelineState
}
}
init(animationContainer: LottieAnimationContainer) {
self.animationContainer = animationContainer
#if DEBUG && false
let startTime = CFAbsoluteTimeGetCurrent()
let buffer = WriteBuffer()
for i in 0 ..< animationContainer.animation.frameCount {
animationContainer.update(i)
serializeNode(buffer: buffer, node: animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0)))
}
buffer.trim()
let deltaTime = (CFAbsoluteTimeGetCurrent() - startTime)
let zippedData = TGGZipData(buffer.data, 1.0)
print("Serialized in \(deltaTime * 1000.0) size: \(zippedData.count / (1 * 1024 * 1024)) MB")
#endif
super.init()
self.isOpaque = false
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func fillPath(frameState: PathFrameState, path: LottiePath, shading: PathShading, rule: LottieFillRule, transform: CATransform3D) {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: rule, shading: shading, transform: transform)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
fillState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
case .lineTo:
let point = pathItem.pointee.points.0
fillState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
fillState.addCurve(
to: SIMD2<Float>(Float(point.x), Float(point.y)),
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
)
case .close:
fillState.close()
@unknown default:
break
}
}
fillState.close()
frameState.add(fill: fillState)
}
private func strokePath(frameState: PathFrameState, path: LottiePath, width: CGFloat, join: CGLineJoin, cap: CGLineCap, miterLimit: CGFloat, color: LottieColor, transform: CATransform3D) {
let strokeState = PathRenderStrokeState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, lineWidth: Float(width), lineJoin: join, lineCap: cap, miterLimit: Float(miterLimit), color: color, transform: transform)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
strokeState.begin(point: SIMD2<Float>(Float(point.x), Float(point.y)))
case .lineTo:
let point = pathItem.pointee.points.0
strokeState.addLine(to: SIMD2<Float>(Float(point.x), Float(point.y)))
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
strokeState.addCurve(
to: SIMD2<Float>(Float(point.x), Float(point.y)),
cp1: SIMD2<Float>(Float(cp1.x), Float(cp1.y)),
cp2: SIMD2<Float>(Float(cp2.x), Float(cp2.y))
)
case .close:
strokeState.close()
@unknown default:
break
}
}
strokeState.complete()
frameState.add(stroke: strokeState)
}
func update(context: MetalEngineSubjectContext) {
if self.bounds.isEmpty {
return
}
let size = CGSize(width: 800.0, height: 800.0)
let msaaSampleCount = 1
let renderSpec = RenderLayerSpec(size: RenderSize(width: Int(size.width), height: Int(size.height)))
guard let animationContainer = self.animationContainer else {
return
}
animationContainer.update(self.frameIndex)
func defaultTransformForSize(_ size: CGSize) -> CATransform3D {
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, 2.0 / size.width, 2.0 / size.height, 1.0)
transform = CATransform3DTranslate(transform, -size.width * 0.5, -size.height * 0.5, 0.0)
transform = CATransform3DTranslate(transform, 0.0, size.height, 0.0)
transform = CATransform3DScale(transform, 1.0, -1.0, 1.0)
return transform
}
let canvasSize = size
var transform = defaultTransformForSize(canvasSize)
concat(CATransform3DMakeScale(canvasSize.width / animationContainer.animation.size.width, canvasSize.height / animationContainer.animation.size.height, 1.0))
var transformStack: [CATransform3D] = []
func saveState() {
transformStack.append(transform)
}
func restoreState() {
transform = transformStack.removeLast()
}
func concat(_ other: CATransform3D) {
transform = CATransform3DConcat(other, transform)
}
func renderNodeContent(frameState: PathFrameState, item: LottieRenderContent, alpha: Double) {
if let fill = item.fill {
if let solidShading = fill.shading as? LottieRenderContentSolidShading {
self.fillPath(
frameState: frameState,
path: item.path,
shading: .color(LottieColor(r: solidShading.color.r, g: solidShading.color.g, b: solidShading.color.b, a: solidShading.color.a * solidShading.opacity * alpha)),
rule: fill.fillRule,
transform: transform
)
} else if let gradientShading = fill.shading as? LottieRenderContentGradientShading {
let gradientType: PathShading.Gradient.GradientType
switch gradientShading.gradientType {
case .linear:
gradientType = .linear
case .radial:
gradientType = .radial
@unknown default:
gradientType = .linear
}
var colorStops: [PathShading.Gradient.ColorStop] = []
for colorStop in gradientShading.colorStops {
colorStops.append(PathShading.Gradient.ColorStop(
color: LottieColor(r: colorStop.color.r, g: colorStop.color.g, b: colorStop.color.b, a: colorStop.color.a * gradientShading.opacity * alpha),
location: Float(colorStop.location)
))
}
let gradientShading = PathShading.Gradient(
gradientType: gradientType,
colorStops: colorStops,
start: SIMD2<Float>(Float(gradientShading.start.x), Float(gradientShading.start.y)),
end: SIMD2<Float>(Float(gradientShading.end.x), Float(gradientShading.end.y))
)
self.fillPath(
frameState: frameState,
path: item.path,
shading: .gradient(gradientShading),
rule: fill.fillRule,
transform: transform
)
}
} else if let stroke = item.stroke {
if let solidShading = stroke.shading as? LottieRenderContentSolidShading {
let color = solidShading.color
strokePath(
frameState: frameState,
path: item.path,
width: stroke.lineWidth,
join: stroke.lineJoin,
cap: stroke.lineCap,
miterLimit: stroke.miterLimit,
color: LottieColor(r: color.r, g: color.g, b: color.b, a: color.a * solidShading.opacity * alpha),
transform: transform
)
}
}
}
func renderNode(frameState: PathFrameState, node: LottieRenderNode, globalSize: CGSize, parentAlpha: CGFloat) {
let normalizedOpacity = node.opacity
let layerAlpha = normalizedOpacity * parentAlpha
if node.isHidden || normalizedOpacity == 0.0 {
return
}
saveState()
var needsTempContext = false
if node.mask != nil {
needsTempContext = true
} else {
needsTempContext = (layerAlpha != 1.0 && !node.hasSimpleContents) || node.masksToBounds
}
var maskSurface: PathFrameState.MaskSurface?
if needsTempContext {
if node.mask != nil || node.masksToBounds {
var maskMode: PathFrameState.MaskSurface.Mode = .regular
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
if node.masksToBounds {
let fillState = PathRenderFillState(buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer, fillRule: .evenOdd, shading: .color(.init(r: 1.0, g: 1.0, b: 1.0, a: 1.0)), transform: transform)
fillState.begin(point: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.minY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.minX), Float(node.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.maxY)))
fillState.addLine(to: SIMD2<Float>(Float(node.bounds.maxX), Float(node.bounds.minY)))
fillState.close()
frameState.add(fill: fillState)
}
if let maskNode = node.mask {
if maskNode.isInvertedMatte {
maskMode = .inverse
}
renderNode(frameState: frameState, node: maskNode, globalSize: globalSize, parentAlpha: 1.0)
}
restoreState()
maskSurface = frameState.popOffscreenMask(mode: maskMode)
}
frameState.pushOffscreen(width: Int(node.globalRect.width), height: Int(node.globalRect.height))
saveState()
transform = defaultTransformForSize(node.globalRect.size)
concat(CATransform3DMakeTranslation(-node.globalRect.minX, -node.globalRect.minY, 0.0))
concat(node.globalTransform)
} else {
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
concat(node.transform)
}
var renderAlpha: CGFloat = 1.0
if needsTempContext {
renderAlpha = 1.0
} else {
renderAlpha = layerAlpha
}
if let renderContent = node.renderContent {
renderNodeContent(frameState: frameState, item: renderContent, alpha: renderAlpha)
}
for subnode in node.subnodes {
renderNode(frameState: frameState, node: subnode, globalSize: globalSize, parentAlpha: renderAlpha)
}
if needsTempContext {
restoreState()
concat(CATransform3DMakeTranslation(node.position.x, node.position.y, 0.0))
concat(CATransform3DMakeTranslation(-node.bounds.origin.x, -node.bounds.origin.y, 0.0))
concat(node.transform)
concat(CATransform3DInvert(node.globalTransform))
frameState.popOffscreen(rect: node.globalRect, transform: transform, opacity: Float(layerAlpha), mask: maskSurface)
}
restoreState()
}
self.currentBuffer.reset()
self.currentBezierIndicesBuffer.reset()
let frameState = PathFrameState(width: Int(size.width), height: Int(size.height), msaaSampleCount: 1, buffer: self.currentBuffer, bezierDataBuffer: self.currentBezierIndicesBuffer)
let node = animationContainer.getCurrentRenderTree(for: CGSize(width: 512.0, height: 512.0))
renderNode(frameState: frameState, node: node, globalSize: canvasSize, parentAlpha: 1.0)
final class ComputeOutput {
let pathRenderContext: PathRenderContext
let renderBufferHeap: MTLHeap
let multisampleTexture: MTLTexture
let takenMultisampleTextures: [MTLTexture]
init(pathRenderContext: PathRenderContext, renderBufferHeap: MTLHeap, multisampleTexture: MTLTexture, takenMultisampleTextures: [MTLTexture]) {
self.pathRenderContext = pathRenderContext
self.renderBufferHeap = renderBufferHeap
self.multisampleTexture = multisampleTexture
self.takenMultisampleTextures = takenMultisampleTextures
}
}
var customCompletion: (() -> Void)?
let computeOutput = context.compute(state: PrepareState.self, commands: { commandBuffer, state -> ComputeOutput? in
let renderBufferHeap: MTLHeap
if let current = self.renderBufferHeap {
renderBufferHeap = current
} else {
let heapDescriptor = MTLHeapDescriptor()
heapDescriptor.size = 32 * 1024 * 1024
heapDescriptor.storageMode = .shared
heapDescriptor.cpuCacheMode = .writeCombined
if #available(iOS 13.0, *) {
heapDescriptor.hazardTrackingMode = .tracked
}
guard let value = MetalEngine.shared.device.makeHeap(descriptor: heapDescriptor) else {
print()
return nil
}
self.renderBufferHeap = value
renderBufferHeap = value
}
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return nil
}
frameState.prepare(heap: renderBufferHeap)
frameState.encodeCompute(context: state.pathRenderContext, computeEncoder: computeEncoder)
computeEncoder.endEncoding()
let multisampleTexture: MTLTexture
if !self.multisampleTextureQueue.isEmpty {
multisampleTexture = self.multisampleTextureQueue.removeFirst()
} else {
multisampleTexture = generateTexture(device: MetalEngine.shared.device, sideSize: Int(size.width), msaaSampleCount: 1)
}
let tempTexture: MTLTexture
if !self.multisampleTextureQueue.isEmpty {
tempTexture = self.multisampleTextureQueue.removeFirst()
} else {
tempTexture = generateTexture(device: MetalEngine.shared.device, sideSize: Int(size.width), msaaSampleCount: 1)
}
let renderPassDescriptor = MTLRenderPassDescriptor()
renderPassDescriptor.colorAttachments[0].texture = multisampleTexture
if msaaSampleCount == 1 {
renderPassDescriptor.colorAttachments[0].storeAction = .store
} else {
//renderPassDescriptor.colorAttachments[0].resolveTexture = self.currentDrawable?.texture
renderPassDescriptor.colorAttachments[0].storeAction = .multisampleResolve
preconditionFailure()
}
renderPassDescriptor.colorAttachments[0].loadAction = .clear
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
renderPassDescriptor.colorAttachments[1].texture = tempTexture
renderPassDescriptor.colorAttachments[1].loadAction = .clear
renderPassDescriptor.colorAttachments[1].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
renderPassDescriptor.colorAttachments[1].storeAction = .dontCare
if msaaSampleCount == 4 {
renderPassDescriptor.setSamplePositions([
MTLSamplePosition(x: 0.25, y: 0.25),
MTLSamplePosition(x: 0.75, y: 0.25),
MTLSamplePosition(x: 0.75, y: 0.75),
MTLSamplePosition(x: 0.25, y: 0.75)
])
}
var offscreenHeapMemorySize = frameState.calculateOffscreenHeapMemorySize(device: MetalEngine.shared.device)
offscreenHeapMemorySize = max(offscreenHeapMemorySize, 1 * 1024 * 1024)
let offscreenHeap: MTLHeap
if let current = self.offscreenHeap, current.size >= offscreenHeapMemorySize * 3 {
offscreenHeap = current
} else {
print("Creating offscreen heap \(offscreenHeapMemorySize * 3 / (1024 * 1024)) MB (3 * \(offscreenHeapMemorySize / (1024 * 1024)) MB)")
let heapDescriptor = MTLHeapDescriptor()
heapDescriptor.size = offscreenHeapMemorySize * 3
heapDescriptor.storageMode = .private
heapDescriptor.cpuCacheMode = .defaultCache
if #available(iOS 13.0, *) {
heapDescriptor.hazardTrackingMode = .tracked
}
offscreenHeap = MetalEngine.shared.device.makeHeap(descriptor: heapDescriptor)!
self.offscreenHeap = offscreenHeap
}
frameState.encodeOffscreen(context: state.pathRenderContext, heap: offscreenHeap, commandBuffer: commandBuffer, canvasSize: canvasSize)
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
self.multisampleTextureQueue.append(multisampleTexture)
self.multisampleTextureQueue.append(tempTexture)
return nil
}
frameState.encodeRender(context: state.pathRenderContext, encoder: renderEncoder, canvasSize: canvasSize)
renderEncoder.endEncoding()
return ComputeOutput(
pathRenderContext: state.pathRenderContext,
renderBufferHeap: renderBufferHeap,
multisampleTexture: multisampleTexture,
takenMultisampleTextures: [multisampleTexture, tempTexture]
)
})
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: self, inputs: computeOutput, commands: { [weak self] encoder, placement, computeOutput in
guard let computeOutput else {
return
}
let effectiveRect = placement.effectiveRect
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
encoder.setFragmentTexture(computeOutput.multisampleTexture, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
let takenMultisampleTextures = computeOutput.takenMultisampleTextures
customCompletion = {
guard let self else {
return
}
for texture in takenMultisampleTextures {
self.multisampleTextureQueue.append(texture)
}
}
})
context.addCustomCompletion({
customCompletion?()
})
}
}
public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedStickerNode {
private final class LoadFrameTask {
var isCancelled: Bool = false
}
private let hierarchyTrackingLayer: HierarchyTrackingLayer
public var automaticallyLoadFirstFrame: Bool = false
public var automaticallyLoadLastFrame: Bool = false
public var playToCompletionOnStop: Bool = false
private var layoutSize: CGSize?
private var lottieInstance: LottieAnimationContainer?
private var renderLayer: LottieContentLayer?
private var displayLinkSubscription: SharedDisplayLinkDriver.Link?
private var didStart: Bool = false
public var started: () -> Void = {}
@ -73,12 +622,26 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
private var playbackMode: AnimatedStickerPlaybackMode = .loop
override public init() {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init()
self.backgroundColor = .blue
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.updatePlayback()
}
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.updatePlayback()
}
}
deinit {
self.sourceDisposable?.dispose()
}
public func cloneCurrentFrame(from otherNode: AnimatedStickerNode?) {
@ -124,6 +687,55 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
self.isPlaying = isPlaying
self.isPlayingChanged(self.isPlaying)
}
if isPlaying, let lottieInstance = self.lottieInstance {
if self.displayLinkSubscription == nil {
let fps: Int
if lottieInstance.animation.framesPerSecond == 30 {
fps = 30
} else {
fps = 60
}
self.displayLinkSubscription = SharedDisplayLinkDriver.shared.add(framesPerSecond: .fps(fps), { [weak self] deltaTime in
guard let self, let lottieInstance = self.lottieInstance, let renderLayer = self.renderLayer else {
return
}
if renderLayer.frameIndex == lottieInstance.animation.frameCount - 1 {
switch self.playbackMode {
case .loop:
self.completed(false)
case let .count(count):
if count <= 1 {
if !self.didComplete {
self.didComplete = true
self.completed(true)
}
return
} else {
self.playbackMode = .count(count - 1)
self.completed(false)
}
case .once:
if !self.didComplete {
self.didComplete = true
self.completed(true)
}
return
case .still:
break
}
}
self.frameIndex = (self.frameIndex + 1) % lottieInstance.animation.frameCount
renderLayer.frameIndex = self.frameIndex
renderLayer.setNeedsUpdate()
})
self.renderLayer?.setNeedsUpdate()
}
} else {
self.displayLinkSubscription = nil
}
}
private func advanceFrameIfPossible() {
@ -173,6 +785,13 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
private func setupPlayback(lottieInstance: LottieAnimationContainer) {
self.lottieInstance = lottieInstance
let renderLayer = LottieContentLayer(animationContainer: lottieInstance)
self.renderLayer = renderLayer
if let layoutSize = self.layoutSize {
renderLayer.frame = CGRect(origin: CGPoint(), size: layoutSize)
}
self.layer.addSublayer(renderLayer)
self.updatePlayback()
}
@ -205,6 +824,10 @@ public final class LottieMetalAnimatedStickerNode: ASDisplayNode, AnimatedSticke
}
public func updateLayout(size: CGSize) {
self.layoutSize = size
if let renderLayer = self.renderLayer {
renderLayer.frame = CGRect(origin: CGPoint(), size: size)
}
}
public func setOverlayColor(_ color: UIColor?, replace: Bool, animated: Bool) {

View File

@ -0,0 +1,374 @@
import Foundation
import MetalKit
import LottieCpp
private func alignUp(size: Int, align: Int) -> Int {
precondition(((align - 1) & align) == 0, "Align must be a power of two")
let alignmentMask = align - 1
return (size + alignmentMask) & ~alignmentMask
}
final class PathFrameState {
struct RenderItem {
enum Content {
case fill(PathRenderFillState)
case stroke(PathRenderStrokeState)
case offscreen(surface: Surface, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?)
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer, canvasSize: CGSize) {
switch self {
case let .fill(fill):
fill.encode(context: context, encoder: encoder, buffer: buffer)
case let .stroke(stroke):
stroke.encode(context: context, encoder: encoder, buffer: buffer)
case let .offscreen(surface, rect, transform, opacity, mask):
surface.encode(context: context, encoder: encoder, canvasSize: canvasSize, rect: rect, transform: transform, opacity: opacity, mask: mask)
}
}
}
let content: Content
init(content: Content) {
self.content = content
}
}
final class MaskSurface {
enum Mode {
case regular
case inverse
}
let surface: Surface
let mode: Mode
init(surface: Surface, mode: Mode) {
self.surface = surface
self.mode = mode
}
}
final class Surface {
let width: Int
let height: Int
private let msaaSampleCount: Int
private var texture: MTLTexture?
private(set) var items: [RenderItem] = []
init(width: Int, height: Int, msaaSampleCount: Int) {
self.width = width
self.height = height
self.msaaSampleCount = msaaSampleCount
}
func add(fill: PathRenderFillState) {
self.items.append(RenderItem(content: .fill(fill)))
}
func add(stroke: PathRenderStrokeState) {
self.items.append(RenderItem(content: .stroke(stroke)))
}
func add(surface: Surface, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?) {
self.items.append(RenderItem(content: .offscreen(surface: surface, rect: rect, transform: transform, opacity: opacity, mask: mask)))
}
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, canvasSize: CGSize, rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface?) {
guard let texture = self.texture else {
print("Trying to encode offscreen blit pass, but no texture is present")
return
}
if mask != nil {
encoder.setRenderPipelineState(context.drawOffscreenWithMaskPipelineState)
} else {
encoder.setRenderPipelineState(context.drawOffscreenPipelineState)
}
let identityTransform = CATransform3DIdentity
var identityTransformMatrix = SIMD16<Float>(
Float(identityTransform.m11), Float(identityTransform.m12), Float(identityTransform.m13), Float(identityTransform.m14),
Float(identityTransform.m21), Float(identityTransform.m22), Float(identityTransform.m23), Float(identityTransform.m24),
Float(identityTransform.m31), Float(identityTransform.m32), Float(identityTransform.m33), Float(identityTransform.m34),
Float(identityTransform.m41), Float(identityTransform.m42), Float(identityTransform.m43), Float(identityTransform.m44)
)
let boundingBox = rect.applying(CATransform3DGetAffineTransform(transform))
var quadVertices: [SIMD4<Float>] = [
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.minY), 0.0, 0.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.maxY), 1.0, 1.0)
]
encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
encoder.setFragmentTexture(texture, index: 0)
if let mask {
guard let maskTexture = mask.surface.texture else {
print("Trying to encode offscreen blit pass, but no mask texture is present")
return
}
encoder.setFragmentTexture(maskTexture, index: 1)
}
var opacity = opacity
encoder.setFragmentBytes(&opacity, length: 4, index: 1)
if let mask {
var maskMode: UInt32 = mask.mode == .regular ? 0 : 1;
encoder.setFragmentBytes(&maskMode, length: 4, index: 2)
}
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
}
func offscreenTextureDescriptor() -> MTLTextureDescriptor {
let textureDescriptor = MTLTextureDescriptor()
textureDescriptor.textureType = .type2D
textureDescriptor.width = self.width
textureDescriptor.height = self.height
textureDescriptor.pixelFormat = .bgra8Unorm
textureDescriptor.storageMode = .private
textureDescriptor.usage = [.renderTarget, .shaderRead]
return textureDescriptor
}
func offscreenTempTextureDescriptor() -> MTLTextureDescriptor {
let tempTextureDescriptor = MTLTextureDescriptor()
tempTextureDescriptor.sampleCount = self.msaaSampleCount
if self.msaaSampleCount == 1 {
tempTextureDescriptor.textureType = .type2D
} else {
tempTextureDescriptor.textureType = .type2DMultisample
}
tempTextureDescriptor.width = self.width
tempTextureDescriptor.height = self.height
tempTextureDescriptor.pixelFormat = .bgra8Unorm
tempTextureDescriptor.storageMode = .private
tempTextureDescriptor.usage = [.renderTarget, .shaderRead]
return tempTextureDescriptor
}
func calculateOffscreenHeapMemorySize(device: MTLDevice) -> Int {
var result = 0
var sizeAndAlign = device.heapTextureSizeAndAlign(descriptor: self.offscreenTextureDescriptor())
result += sizeAndAlign.size
sizeAndAlign = device.heapTextureSizeAndAlign(descriptor: self.offscreenTempTextureDescriptor())
result += sizeAndAlign.size * 2
for item in self.items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
result += surface.calculateOffscreenHeapMemorySize(device: device)
if let mask {
result += mask.surface.calculateOffscreenHeapMemorySize(device: device)
}
}
}
return result
}
func encodeOffscreen(context: PathRenderContext, heap: MTLHeap, commandBuffer: MTLCommandBuffer, materializedBuffer: MTLBuffer, canvasSize: CGSize) {
guard let resultTexture = heap.makeTexture(descriptor: self.offscreenTextureDescriptor()) else {
return
}
for item in self.items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
if let mask {
mask.surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
}
self.texture = resultTexture
guard let offscreenTexture = heap.makeTexture(descriptor: self.offscreenTempTextureDescriptor()) else {
return
}
guard let tempTexture = heap.makeTexture(descriptor: self.offscreenTempTextureDescriptor()) else {
return
}
let offscreenRenderPassDescriptor = MTLRenderPassDescriptor()
if msaaSampleCount == 1 {
offscreenRenderPassDescriptor.colorAttachments[0].texture = resultTexture
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .store
} else {
offscreenRenderPassDescriptor.colorAttachments[0].texture = offscreenTexture
offscreenRenderPassDescriptor.colorAttachments[0].storeAction = .multisampleResolve
offscreenRenderPassDescriptor.colorAttachments[0].resolveTexture = resultTexture
}
offscreenRenderPassDescriptor.colorAttachments[0].loadAction = .clear
offscreenRenderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
offscreenRenderPassDescriptor.colorAttachments[1].texture = tempTexture
offscreenRenderPassDescriptor.colorAttachments[1].loadAction = .clear
offscreenRenderPassDescriptor.colorAttachments[1].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
offscreenRenderPassDescriptor.colorAttachments[1].storeAction = .dontCare
if self.msaaSampleCount == 4 {
offscreenRenderPassDescriptor.setSamplePositions([
MTLSamplePosition(x: 0.25, y: 0.25),
MTLSamplePosition(x: 0.75, y: 0.25),
MTLSamplePosition(x: 0.75, y: 0.75),
MTLSamplePosition(x: 0.25, y: 0.75)
])
}
guard let offscreenRenderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: offscreenRenderPassDescriptor) else {
return
}
for item in self.items {
item.content.encode(context: context, encoder: offscreenRenderEncoder, buffer: materializedBuffer, canvasSize: canvasSize)
}
offscreenRenderEncoder.endEncoding()
}
}
let msaaSampleCount: Int
let buffer: PathRenderBuffer
let bezierDataBuffer: PathRenderBuffer
private var surfaceStack: [Surface] = []
private var materializedBuffer: MTLBuffer?
private var materializedBezierIndexBuffer: MTLBuffer?
init(width: Int, height: Int, msaaSampleCount: Int, buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer) {
self.msaaSampleCount = msaaSampleCount
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.surfaceStack.append(Surface(width: width, height: height, msaaSampleCount: msaaSampleCount))
}
func pushOffscreen(width: Int, height: Int) {
self.surfaceStack.append(Surface(width: width, height: height, msaaSampleCount: self.msaaSampleCount))
}
func popOffscreen(rect: CGRect, transform: CATransform3D, opacity: Float, mask: MaskSurface? = nil) {
self.surfaceStack[self.surfaceStack.count - 2].add(surface: self.surfaceStack[self.surfaceStack.count - 1], rect: rect, transform: transform, opacity: opacity, mask: mask)
self.surfaceStack.removeLast()
}
func popOffscreenMask(mode: MaskSurface.Mode) -> MaskSurface {
return MaskSurface(
surface: self.surfaceStack.removeLast(),
mode: mode
)
}
func add(fill: PathRenderFillState) {
self.surfaceStack.last!.add(fill: fill)
}
func add(stroke: PathRenderStrokeState) {
self.surfaceStack.last!.add(stroke: stroke)
}
func prepare(heap: MTLHeap) {
if self.buffer.length == 0 {
return
}
var bufferOptions: MTLResourceOptions = [.storageModeShared, .cpuCacheModeWriteCombined]
if #available(iOS 13.0, *) {
bufferOptions.insert(.hazardTrackingModeTracked)
}
guard let materializedBuffer = heap.makeBuffer(length: self.buffer.length, options: bufferOptions) else {
print("Could not create materialized buffer")
return
}
materializedBuffer.label = "materializedBuffer"
self.materializedBuffer = materializedBuffer
memcpy(materializedBuffer.contents(), self.buffer.memory, self.buffer.length)
if self.bezierDataBuffer.length != 0 {
guard let materializedBezierIndexBuffer = heap.makeBuffer(length: self.bezierDataBuffer.length, options: bufferOptions) else {
print("Could not create materialized bezier index buffer")
return
}
self.materializedBezierIndexBuffer = materializedBezierIndexBuffer
materializedBezierIndexBuffer.label = "materializedBezierIndexBuffer"
memcpy(materializedBezierIndexBuffer.contents(), self.bezierDataBuffer.memory, self.bezierDataBuffer.length)
}
}
func calculateOffscreenHeapMemorySize(device: MTLDevice) -> Int {
var result = 0
for item in self.surfaceStack[0].items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
result += surface.calculateOffscreenHeapMemorySize(device: device)
if let mask {
result += mask.surface.calculateOffscreenHeapMemorySize(device: device)
}
}
}
return result
}
func encodeOffscreen(context: PathRenderContext, heap: MTLHeap, commandBuffer: MTLCommandBuffer, canvasSize: CGSize) {
guard let materializedBuffer = self.materializedBuffer else {
return
}
assert(self.surfaceStack.count == 1)
for item in self.surfaceStack[0].items {
if case let .offscreen(surface, _, _, _, mask) = item.content {
if let mask {
mask.surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
surface.encodeOffscreen(context: context, heap: heap, commandBuffer: commandBuffer, materializedBuffer: materializedBuffer, canvasSize: canvasSize)
}
}
}
func encodeRender(context: PathRenderContext, encoder: MTLRenderCommandEncoder, canvasSize: CGSize) {
guard let materializedBuffer = self.materializedBuffer else {
return
}
assert(self.surfaceStack.count == 1)
for item in self.surfaceStack[0].items {
item.content.encode(context: context, encoder: encoder, buffer: materializedBuffer, canvasSize: canvasSize)
}
}
func encodeCompute(context: PathRenderContext, computeEncoder: MTLComputeCommandEncoder) {
guard let materializedBuffer = self.materializedBuffer, let materializedBezierIndexBuffer = self.materializedBezierIndexBuffer else {
return
}
let itemSize = 4 + 4 * 4 * 2 + 4
let itemCount = self.bezierDataBuffer.length / itemSize
computeEncoder.setComputePipelineState(context.prepareBezierPipelineState)
let threadGroupWidth = 16
let threadGroupHeight = 8
computeEncoder.useResource(materializedBuffer, usage: .write)
computeEncoder.setBuffer(materializedBezierIndexBuffer, offset: 0, index: 0)
computeEncoder.setBuffer(materializedBuffer, offset: 0, index: 1)
var itemCountSize: UInt32 = UInt32(itemCount)
computeEncoder.setBytes(&itemCountSize, length: 4, index: 2)
let dispatchSize = alignUp(size: itemCount, align: threadGroupWidth)
computeEncoder.dispatchThreadgroups(MTLSize(width: dispatchSize, height: 1, depth: 1), threadsPerThreadgroup: MTLSize(width: 1, height: threadGroupHeight, depth: 1))
}
}

View File

@ -0,0 +1,77 @@
import Foundation
import MetalKit
import LottieCpp
final class PathRenderBuffer {
private(set) var memory: UnsafeMutableRawPointer
private(set) var capacity: Int = 8 * 1024 * 1024
private(set) var length: Int = 0
init() {
self.memory = malloc(self.capacity)!
}
func reset() {
self.length = 0
}
func append(bytes: UnsafeRawPointer, length: Int) {
assert(length % 4 == 0)
if self.length + length > self.capacity {
self.capacity = self.capacity * 2
preconditionFailure()
}
memcpy(self.memory.advanced(by: self.length), bytes, length)
self.length += length
}
func appendZero(count: Int) {
if self.length + length > self.capacity {
self.capacity = self.capacity * 2
preconditionFailure()
}
self.length += count
}
func append(float: Float) {
var value: Float = float
self.append(bytes: &value, length: 4)
}
func append(float2: SIMD2<Float>) {
var value: SIMD2<Float> = float2
self.append(bytes: &value, length: 4 * 2)
}
func append(float3: SIMD3<Float>) {
var value = float3.x
self.append(bytes: &value, length: 4)
value = float3.y
self.append(bytes: &value, length: 4)
value = float3.z
self.append(bytes: &value, length: 4)
}
func append(int: Int32) {
var value = int
self.append(bytes: &value, length: 4)
}
func appendBezierData(
bufferOffset: Int,
start: SIMD2<Float>,
end: SIMD2<Float>,
cp1: SIMD2<Float>,
cp2: SIMD2<Float>,
offset: Float
) {
self.append(int: Int32(bufferOffset))
self.append(float2: start)
self.append(float2: end)
self.append(float2: cp1)
self.append(float2: cp2)
self.append(float: offset)
}
}

View File

@ -0,0 +1,208 @@
import Foundation
import MetalKit
import LottieCpp
final class PathRenderContext {
let device: MTLDevice
let msaaSampleCount: Int
let prepareBezierPipelineState: MTLComputePipelineState
let shapePipelineState: MTLRenderPipelineState
let clearPipelineState: MTLRenderPipelineState
let mergeColorFillPipelineState: MTLRenderPipelineState
let mergeLinearGradientFillPipelineState: MTLRenderPipelineState
let mergeRadialGradientFillPipelineState: MTLRenderPipelineState
let strokeTerminalPipelineState: MTLRenderPipelineState
let strokeInnerPipelineState: MTLRenderPipelineState
let drawOffscreenPipelineState: MTLRenderPipelineState
let drawOffscreenWithMaskPipelineState: MTLRenderPipelineState
let maximumThreadGroupWidth: Int
init?(device: MTLDevice, msaaSampleCount: Int) {
self.device = device
self.msaaSampleCount = msaaSampleCount
self.maximumThreadGroupWidth = device.maxThreadsPerThreadgroup.width
guard let library = metalLibrary(device: device) else {
return nil
}
guard let quadVertexFunction = library.makeFunction(name: "quad_vertex_shader") else {
print("Unable to find vertex function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let shapeVertexFunction = library.makeFunction(name: "fill_vertex_shader") else {
print("Unable to find vertex function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let shapeFragmentFunction = library.makeFunction(name: "fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let clearFragmentFunction = library.makeFunction(name: "clear_mask_fragment") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let mergeColorFillFragmentFunction = library.makeFunction(name: "merge_color_fill_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let mergeLinearGradientFillFragmentFunction = library.makeFunction(name: "merge_linear_gradient_fill_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let mergeRadialGradientFillFragmentFunction = library.makeFunction(name: "merge_radial_gradient_fill_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let strokeFragmentFunction = library.makeFunction(name: "stroke_fragment_shader") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let strokeTerminalVertexFunction = library.makeFunction(name: "strokeTerminalVertex") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let strokeInnerVertexFunction = library.makeFunction(name: "strokeInnerVertex") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let prepareBezierPipelineFunction = library.makeFunction(name: "evaluateBezier") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let quadOffscreenFragmentFunction = library.makeFunction(name: "quad_offscreen_fragment") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
guard let quadOffscreenWithMaskFragmentFunction = library.makeFunction(name: "quad_offscreen_fragment_with_mask") else {
print("Unable to find fragment function. Are you sure you defined it and spelled the name right?")
return nil
}
self.prepareBezierPipelineState = try! device.makeComputePipelineState(function: prepareBezierPipelineFunction)
let shapePipelineDescriptor = MTLRenderPipelineDescriptor()
shapePipelineDescriptor.vertexFunction = shapeVertexFunction
shapePipelineDescriptor.fragmentFunction = shapeFragmentFunction
shapePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
shapePipelineDescriptor.colorAttachments[0].writeMask = []
shapePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
shapePipelineDescriptor.colorAttachments[1].writeMask = [.all]
shapePipelineDescriptor.rasterSampleCount = msaaSampleCount
guard let shapePipelineState = try? device.makeRenderPipelineState(descriptor: shapePipelineDescriptor) else {
preconditionFailure()
}
self.shapePipelineState = shapePipelineState
let clearPipelineDescriptor = MTLRenderPipelineDescriptor()
clearPipelineDescriptor.vertexFunction = quadVertexFunction
clearPipelineDescriptor.fragmentFunction = clearFragmentFunction
clearPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
clearPipelineDescriptor.colorAttachments[0].writeMask = []
clearPipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
clearPipelineDescriptor.colorAttachments[1].writeMask = .all
clearPipelineDescriptor.rasterSampleCount = msaaSampleCount
guard let clearPipelineState = try? device.makeRenderPipelineState(descriptor: clearPipelineDescriptor) else {
preconditionFailure()
}
self.clearPipelineState = clearPipelineState
let mergePipelineDescriptor = MTLRenderPipelineDescriptor()
mergePipelineDescriptor.vertexFunction = quadVertexFunction
mergePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
mergePipelineDescriptor.colorAttachments[0].writeMask = [.all]
mergePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
mergePipelineDescriptor.rasterSampleCount = msaaSampleCount
mergePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
mergePipelineDescriptor.colorAttachments[1].writeMask = []
mergePipelineDescriptor.fragmentFunction = mergeColorFillFragmentFunction
guard let mergeColorFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
preconditionFailure()
}
self.mergeColorFillPipelineState = mergeColorFillPipelineState
mergePipelineDescriptor.fragmentFunction = mergeLinearGradientFillFragmentFunction
guard let mergeLinearGradientFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
preconditionFailure()
}
self.mergeLinearGradientFillPipelineState = mergeLinearGradientFillPipelineState
mergePipelineDescriptor.fragmentFunction = mergeRadialGradientFillFragmentFunction
guard let mergeRadialGradientFillPipelineState = try? device.makeRenderPipelineState(descriptor: mergePipelineDescriptor) else {
preconditionFailure()
}
self.mergeRadialGradientFillPipelineState = mergeRadialGradientFillPipelineState
let strokePipelineDescriptor = MTLRenderPipelineDescriptor()
strokePipelineDescriptor.fragmentFunction = strokeFragmentFunction
strokePipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
strokePipelineDescriptor.colorAttachments[0].writeMask = [.all]
strokePipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
strokePipelineDescriptor.colorAttachments[0].rgbBlendOperation = mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation
strokePipelineDescriptor.colorAttachments[0].alphaBlendOperation = mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation
strokePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor
strokePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor
strokePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor
strokePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor
strokePipelineDescriptor.rasterSampleCount = msaaSampleCount
strokePipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
strokePipelineDescriptor.colorAttachments[1].writeMask = []
strokePipelineDescriptor.vertexFunction = strokeTerminalVertexFunction
guard let strokeTerminalPipelineState = try? device.makeRenderPipelineState(descriptor: strokePipelineDescriptor) else {
preconditionFailure()
}
self.strokeTerminalPipelineState = strokeTerminalPipelineState
strokePipelineDescriptor.vertexFunction = strokeInnerVertexFunction
guard let strokeInnerPipelineState = try? device.makeRenderPipelineState(descriptor: strokePipelineDescriptor) else {
preconditionFailure()
}
self.strokeInnerPipelineState = strokeInnerPipelineState
let drawOffscreenPipelineDescriptor = MTLRenderPipelineDescriptor()
drawOffscreenPipelineDescriptor.vertexFunction = quadVertexFunction
drawOffscreenPipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
drawOffscreenPipelineDescriptor.colorAttachments[0].writeMask = [.all]
drawOffscreenPipelineDescriptor.colorAttachments[1].pixelFormat = .bgra8Unorm
drawOffscreenPipelineDescriptor.colorAttachments[1].writeMask = []
drawOffscreenPipelineDescriptor.rasterSampleCount = msaaSampleCount
drawOffscreenPipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
drawOffscreenPipelineDescriptor.colorAttachments[0].rgbBlendOperation = mergePipelineDescriptor.colorAttachments[0].rgbBlendOperation
drawOffscreenPipelineDescriptor.colorAttachments[0].alphaBlendOperation = mergePipelineDescriptor.colorAttachments[0].alphaBlendOperation
drawOffscreenPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor
drawOffscreenPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor
drawOffscreenPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor
drawOffscreenPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = mergePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor
drawOffscreenPipelineDescriptor.fragmentFunction = quadOffscreenFragmentFunction
guard let drawOffscreenPipelineState = try? device.makeRenderPipelineState(descriptor: drawOffscreenPipelineDescriptor) else {
preconditionFailure()
}
self.drawOffscreenPipelineState = drawOffscreenPipelineState
drawOffscreenPipelineDescriptor.fragmentFunction = quadOffscreenWithMaskFragmentFunction
guard let drawOffscreenWithMaskPipelineState = try? device.makeRenderPipelineState(descriptor: drawOffscreenPipelineDescriptor) else {
preconditionFailure()
}
self.drawOffscreenWithMaskPipelineState = drawOffscreenWithMaskPipelineState
}
}

View File

@ -0,0 +1,277 @@
import Foundation
import MetalKit
import simd
import LottieCpp
enum PathShading {
final class Gradient {
enum GradientType {
case linear
case radial
}
struct ColorStop {
var color: LottieColor
var location: Float
init(color: LottieColor, location: Float) {
self.color = color
self.location = location
}
}
let gradientType: GradientType
let colorStops: [ColorStop]
let start: SIMD2<Float>
let end: SIMD2<Float>
init(gradientType: GradientType, colorStops: [ColorStop], start: SIMD2<Float>, end: SIMD2<Float>) {
self.gradientType = gradientType
self.colorStops = colorStops
self.start = start
self.end = end
}
}
case color(LottieColor)
case gradient(Gradient)
}
final class PathRenderSubpathFillState {
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
let bufferOffset: Int
private(set) var vertexCount: Int = 0
private var firstPosition: SIMD2<Float>
private var lastPosition: SIMD2<Float>
private(set) var minPosition: SIMD2<Float>
private(set) var maxPosition: SIMD2<Float>
private var isClosed: Bool = false
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, point: SIMD2<Float>) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.bufferOffset = buffer.length
self.firstPosition = point
self.lastPosition = point
self.minPosition = point
self.maxPosition = point
self.add(point: point)
}
func add(point: SIMD2<Float>) {
self.buffer.append(float2: point)
self.minPosition.x = min(self.minPosition.x, point.x)
self.minPosition.y = min(self.minPosition.y, point.y)
self.maxPosition.x = max(self.maxPosition.x, point.x)
self.maxPosition.y = max(self.maxPosition.y, point.y)
self.lastPosition = point
self.vertexCount += 1
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
let stepCount = 8
self.bezierDataBuffer.appendBezierData(
bufferOffset: self.buffer.length / 4,
start: self.lastPosition,
end: point,
cp1: cp1,
cp2: cp2,
offset: 0.0
)
self.buffer.appendZero(count: 4 * 2 * stepCount)
self.vertexCount += stepCount
let (curveMin, curveMax) = bezierBounds(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point)
self.minPosition.x = min(self.minPosition.x, curveMin.x)
self.minPosition.y = min(self.minPosition.y, curveMin.y)
self.maxPosition.x = max(self.maxPosition.x, curveMax.x)
self.maxPosition.y = max(self.maxPosition.y, curveMax.y)
self.lastPosition = point
}
func close() {
if self.isClosed {
assert(false)
} else {
self.isClosed = true
if self.lastPosition != self.firstPosition {
self.add(point: self.firstPosition)
}
}
}
}
final class PathRenderFillState {
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
private let fillRule: LottieFillRule
private let shading: PathShading
private let transform: CATransform3D
private var currentSubpath: PathRenderSubpathFillState?
private(set) var subpaths: [PathRenderSubpathFillState] = []
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, fillRule: LottieFillRule, shading: PathShading, transform: CATransform3D) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.fillRule = fillRule
self.shading = shading
self.transform = transform
}
func begin(point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.close()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
self.currentSubpath = PathRenderSubpathFillState(buffer: self.buffer, bezierDataBuffer: self.bezierDataBuffer, point: point)
}
func addLine(to point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.add(point: point)
}
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.addCurve(to: point, cp1: cp1, cp2: cp2)
}
}
func close() {
if let currentSubpath = self.currentSubpath {
currentSubpath.close()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
}
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer) {
if self.subpaths.isEmpty {
return
}
var minPosition: SIMD2<Float> = self.subpaths[0].minPosition
var maxPosition: SIMD2<Float> = self.subpaths[0].maxPosition
for subpath in self.subpaths {
minPosition.x = min(minPosition.x, subpath.minPosition.x)
minPosition.y = min(minPosition.y, subpath.minPosition.y)
maxPosition.x = max(maxPosition.x, subpath.maxPosition.x)
maxPosition.y = max(maxPosition.y, subpath.maxPosition.y)
}
let localBoundingBox = CGRect(x: CGFloat(minPosition.x), y: CGFloat(minPosition.y), width: CGFloat(maxPosition.x - minPosition.x), height: CGFloat(maxPosition.y - minPosition.y))
if localBoundingBox.isEmpty {
return
}
var transformMatrix = simd_float4x4(
SIMD4<Float>(Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14)),
SIMD4<Float>(Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24)),
SIMD4<Float>(Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34)),
SIMD4<Float>(Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44))
)
let identityTransform = CATransform3DIdentity
var identityTransformMatrix = SIMD16<Float>(
Float(identityTransform.m11), Float(identityTransform.m12), Float(identityTransform.m13), Float(identityTransform.m14),
Float(identityTransform.m21), Float(identityTransform.m22), Float(identityTransform.m23), Float(identityTransform.m24),
Float(identityTransform.m31), Float(identityTransform.m32), Float(identityTransform.m33), Float(identityTransform.m34),
Float(identityTransform.m41), Float(identityTransform.m42), Float(identityTransform.m43), Float(identityTransform.m44)
)
let transform = CATransform3DGetAffineTransform(self.transform)
let boundingBox = localBoundingBox.applying(transform)
let baseVertex = boundingBox.origin.applying(transform.inverted())
encoder.setRenderPipelineState(context.clearPipelineState)
var quadVertices: [SIMD4<Float>] = [
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.minY), 0.0, 0.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.minY), 1.0, 0.0),
SIMD4<Float>(Float(boundingBox.minX), Float(boundingBox.maxY), 0.0, 1.0),
SIMD4<Float>(Float(boundingBox.maxX), Float(boundingBox.maxY), 1.0, 1.0)
]
encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
encoder.setRenderPipelineState(context.shapePipelineState)
encoder.setVertexBytes(&transformMatrix, length: 4 * 4 * 4, index: 1)
var baseVertexData = SIMD2<Float>(Float(baseVertex.x), Float(baseVertex.y))
encoder.setVertexBytes(&baseVertexData, length: 4 * 2, index: 2)
var modeBytes: Int32 = self.fillRule == .winding ? 0 : 1
encoder.setFragmentBytes(&modeBytes, length: 4, index: 1)
for subpath in self.subpaths {
encoder.setVertexBuffer(buffer, offset: subpath.bufferOffset, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: (subpath.vertexCount - 1) * 3)
}
encoder.setVertexBytes(&quadVertices, length: MemoryLayout<SIMD4<Float>>.size * quadVertices.count, index: 0)
encoder.setVertexBytes(&identityTransformMatrix, length: 4 * 4 * 4, index: 1)
switch self.shading {
case let .color(color):
encoder.setRenderPipelineState(context.mergeColorFillPipelineState)
var colorVector = SIMD4(Float(color.r), Float(color.g), Float(color.b), Float(color.a))
encoder.setFragmentBytes(&colorVector, length: MemoryLayout<SIMD4<Float>>.size, index: 0)
case let .gradient(gradient):
switch gradient.gradientType {
case .linear:
encoder.setRenderPipelineState(context.mergeLinearGradientFillPipelineState)
case .radial:
encoder.setRenderPipelineState(context.mergeRadialGradientFillPipelineState)
}
var modeBytes: Int32 = self.fillRule == .winding ? 0 : 1
encoder.setFragmentBytes(&modeBytes, length: 4, index: 1)
let colorStopSize = 4 * 4 + 4
var colorStopsData = Data(count: colorStopSize * gradient.colorStops.count)
colorStopsData.withUnsafeMutableBytes { buffer in
let bytes = buffer.baseAddress!.assumingMemoryBound(to: Float.self)
for i in 0 ..< gradient.colorStops.count {
let colorStop = gradient.colorStops[i]
bytes[i * 5 + 0] = Float(colorStop.color.r)
bytes[i * 5 + 1] = Float(colorStop.color.g)
bytes[i * 5 + 2] = Float(colorStop.color.b)
bytes[i * 5 + 3] = Float(colorStop.color.a)
bytes[i * 5 + 4] = colorStop.location
}
encoder.setFragmentBytes(buffer.baseAddress!, length: buffer.count, index: 0)
}
var numColorStops: UInt32 = UInt32(gradient.colorStops.count)
encoder.setFragmentBytes(&numColorStops, length: 4, index: 2)
var startPosition = transformMatrix * SIMD4<Float>(gradient.start.x, gradient.start.y, 0.0, 1.0)
encoder.setFragmentBytes(&startPosition, length: 4 * 2, index: 3)
var endPosition = transformMatrix * SIMD4<Float>(gradient.end.x, gradient.end.y, 0.0, 1.0)
encoder.setFragmentBytes(&endPosition, length: 4 * 2, index: 4)
}
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: quadVertices.count)
}
}

View File

@ -0,0 +1,425 @@
import Foundation
import MetalKit
import LottieCpp
func evaluateBezier(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>, t: Float) -> SIMD2<Float> {
let t2 = t * t
let t3 = t * t * t
let A = (3 * t2 - 3 * t3)
let B = (3 * t3 - 6 * t2 + 3 * t)
let C = (3 * t2 - t3 - 3 * t + 1)
let value = t3 * p3 + A * p2 + B * p1 + C * p0
return value
}
func evaluateBezier(p0: Float, p1: Float, p2: Float, p3: Float, t: Float) -> Float {
let oneMinusT = 1.0 - t
let value = oneMinusT * oneMinusT * oneMinusT * p0 + 3.0 * t * oneMinusT * oneMinusT * p1 + 3.0 * t * t * oneMinusT * p2 + t * t * t * p3
return value
}
func solveQuadratic(p0: Float, p1: Float, p2: Float, p3: Float) -> (Float, Float) {
let i = p1 - p0
let j = p2 - p1
let k = p3 - p2
let a = (3 * i) - (6 * j) + (3 * k)
let b = (6 * j) - (6 * i)
let c = (3 * i)
let sqrtPart = (b * b) - (4 * a * c)
let hasSolution = sqrtPart >= 0
if !hasSolution {
return (.nan, .nan)
}
let t1 = (-b + sqrt(sqrtPart)) / (2 * a)
let t2 = (-b - sqrt(sqrtPart)) / (2 * a)
var s1: Float = .nan
var s2: Float = .nan
if t1 >= 0.0 && t1 <= 1.0 {
s1 = evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t1)
}
if t2 >= 0.0 && t2 <= 1.0 {
s2 = evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t2)
}
return (s1, s2)
}
func bezierBounds(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>) -> (minPosition: SIMD2<Float>, maxPosition: SIMD2<Float>) {
let (solX1, solX2) = solveQuadratic(p0: p0.x, p1: p1.x, p2: p2.x, p3: p3.x)
let (solY1, solY2) = solveQuadratic(p0: p0.y, p1: p1.y, p2: p2.y, p3: p3.y)
var minX = min(p0.x, p3.x)
var maxX = max(p0.x, p3.x)
if !solX1.isNaN {
minX = min(minX, solX1)
maxX = max(maxX, solX1)
}
if !solX2.isNaN {
minX = min(minX, solX2)
maxX = max(maxX, solX2)
}
var minY = min(p0.y, p3.y)
var maxY = max(p0.y, p3.y)
if !solY1.isNaN {
minY = min(minY, solY1)
maxY = max(maxY, solY1)
}
if !solY2.isNaN {
minY = min(minY, solY2)
maxY = max(maxY, solY2)
}
return (SIMD2<Float>(minX, minY), SIMD2<Float>(maxX, maxY))
}
final class PathRenderSubpathStrokeState {
struct TerminalState {
var bufferOffset: Int
var segmentCount: Int
}
enum UnresolvedPosition {
case position(SIMD2<Float>)
case curve(p0: SIMD2<Float>, p1: SIMD2<Float>, p2: SIMD2<Float>, p3: SIMD2<Float>, t: Float)
func resolve() -> SIMD2<Float> {
switch self {
case let .position(value):
return value
case let .curve(p0, p1, p2, p3, t):
return evaluateBezier(p0: p0, p1: p1, p2: p2, p3: p3, t: t)
}
}
}
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
let bufferOffset: Int
private(set) var vertexCount: Int = 0
private(set) var terminalState: TerminalState?
private(set) var curveJoinVertexRanges: [Range<Int>] = []
private var firstPosition: SIMD2<Float>
private var secondPosition: UnresolvedPosition
private var thirdPosition: UnresolvedPosition
private var lastPosition: SIMD2<Float>
private var lastMinus1Position: UnresolvedPosition
private var lastMinus2Position: UnresolvedPosition
private(set) var isClosed: Bool = false
private(set) var isCompleted: Bool = false
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, point: SIMD2<Float>) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.bufferOffset = buffer.length
self.firstPosition = point
self.secondPosition = .position(point)
self.thirdPosition = .position(point)
self.lastPosition = point
self.lastMinus1Position = .position(point)
self.lastMinus2Position = .position(point)
self.add(point: point)
}
func add(point: SIMD2<Float>) {
self.buffer.append(float2: point)
self.lastMinus2Position = self.lastMinus1Position
self.lastMinus1Position = .position(self.lastPosition)
self.lastPosition = point
self.vertexCount += 1
if self.vertexCount == 2 {
self.secondPosition = .position(point)
} else if self.vertexCount == 3 {
self.thirdPosition = .position(point)
}
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
let stepCount = 8
self.bezierDataBuffer.appendBezierData(
bufferOffset: self.buffer.length / 4,
start: self.lastPosition,
end: point,
cp1: cp1,
cp2: cp2,
offset: 0.0
)
self.buffer.appendZero(count: 4 * 2 * stepCount)
if self.vertexCount == 1 {
self.secondPosition = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(1) / Float(stepCount))
self.thirdPosition = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(2) / Float(stepCount))
}
self.vertexCount += stepCount
self.lastMinus2Position = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(stepCount - 2) / Float(stepCount))
self.lastMinus1Position = .curve(p0: self.lastPosition, p1: cp1, p2: cp2, p3: point, t: Float(stepCount - 1) / Float(stepCount))
self.lastPosition = point
}
func close() {
if self.isClosed {
assert(false)
} else {
self.isClosed = true
if self.lastPosition != self.firstPosition {
self.add(point: self.firstPosition)
}
}
}
func complete() {
if self.isCompleted {
assert(false)
} else {
if self.isClosed {
if self.vertexCount >= 3 {
self.buffer.append(float2: self.secondPosition.resolve())
self.buffer.append(float2: self.thirdPosition.resolve())
self.vertexCount += 2
}
} else {
if self.vertexCount == 2 {
let terminalBufferOffset = self.buffer.length
let resolvedSecond = self.secondPosition.resolve()
self.buffer.append(float2: self.firstPosition)
self.buffer.append(float2: self.firstPosition * 0.5 + resolvedSecond * 0.5)
self.buffer.append(float2: resolvedSecond)
self.buffer.append(float2: resolvedSecond)
self.buffer.append(float2: self.firstPosition * 0.5 + resolvedSecond * 0.5)
self.buffer.append(float2: self.firstPosition)
self.terminalState = TerminalState(bufferOffset: terminalBufferOffset, segmentCount: 2)
} else if self.vertexCount >= 3 {
let terminalBufferOffset = self.buffer.length
self.buffer.append(float2: self.firstPosition)
self.buffer.append(float2: self.secondPosition.resolve())
self.buffer.append(float2: self.thirdPosition.resolve())
self.buffer.append(float2: self.lastPosition)
self.buffer.append(float2: self.lastMinus1Position.resolve())
self.buffer.append(float2: self.lastMinus2Position.resolve())
self.terminalState = TerminalState(bufferOffset: terminalBufferOffset, segmentCount: 2)
}
}
}
}
}
final class PathRenderStrokeState {
private let buffer: PathRenderBuffer
private let bezierDataBuffer: PathRenderBuffer
private let lineWidth: Float
private let lineJoin: CGLineJoin
private let lineCap: CGLineCap
private let miterLimit: Float
private let color: LottieColor
private let transform: CATransform3D
private var currentSubpath: PathRenderSubpathStrokeState?
private(set) var subpaths: [PathRenderSubpathStrokeState] = []
init(buffer: PathRenderBuffer, bezierDataBuffer: PathRenderBuffer, lineWidth: Float, lineJoin: CGLineJoin, lineCap: CGLineCap, miterLimit: Float, color: LottieColor, transform: CATransform3D) {
self.buffer = buffer
self.bezierDataBuffer = bezierDataBuffer
self.lineWidth = lineWidth
self.lineJoin = lineJoin
self.lineCap = lineCap
self.miterLimit = miterLimit
self.color = color
self.transform = transform
}
func begin(point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.complete()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
self.currentSubpath = PathRenderSubpathStrokeState(buffer: self.buffer, bezierDataBuffer: self.bezierDataBuffer, point: point)
}
func addLine(to point: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.add(point: point)
}
}
func addCurve(to point: SIMD2<Float>, cp1: SIMD2<Float>, cp2: SIMD2<Float>) {
if let currentSubpath = self.currentSubpath {
currentSubpath.addCurve(to: point, cp1: cp1, cp2: cp2)
}
}
func close() {
if let currentSubpath = self.currentSubpath {
currentSubpath.close()
currentSubpath.complete()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
}
func complete() {
if let currentSubpath = self.currentSubpath {
currentSubpath.complete()
self.subpaths.append(currentSubpath)
self.currentSubpath = nil
}
}
func encode(context: PathRenderContext, encoder: MTLRenderCommandEncoder, buffer: MTLBuffer) {
if self.subpaths.isEmpty {
return
}
encoder.setVertexBuffer(buffer, offset: 0, index: 0)
var colorVector = SIMD4(Float(color.r), Float(color.g), Float(color.b), Float(color.a))
encoder.setFragmentBytes(&colorVector, length: MemoryLayout<SIMD4<Float>>.size, index: 0)
var transformMatrix = SIMD16<Float>(
Float(transform.m11), Float(transform.m12), Float(transform.m13), Float(transform.m14),
Float(transform.m21), Float(transform.m22), Float(transform.m23), Float(transform.m24),
Float(transform.m31), Float(transform.m32), Float(transform.m33), Float(transform.m34),
Float(transform.m41), Float(transform.m42), Float(transform.m43), Float(transform.m44)
)
encoder.setVertexBytes(&transformMatrix, length: 4 * 4 * 4, index: 1)
let capRes2: Float
switch self.lineCap {
case .butt:
capRes2 = 2.0
case .square:
capRes2 = 6.0
case .round:
capRes2 = 24.0
@unknown default:
capRes2 = 2.0
}
let joinRes2: Float = self.lineJoin == .round ? 16.0 : 2.0
func computeCount(isEndpoints: Bool, insertCaps: Bool) -> SIMD2<Float> {
if insertCaps {
if isEndpoints {
return SIMD2<Float>(capRes2, max(capRes2, joinRes2))
} else {
return SIMD2<Float>(max(capRes2, joinRes2), max(capRes2, joinRes2))
}
} else {
if isEndpoints {
return SIMD2<Float>(capRes2, joinRes2)
} else {
return SIMD2<Float>(joinRes2, joinRes2)
}
}
}
var hasTerminalStates = false
for subpath in self.subpaths {
let segmentCount = subpath.vertexCount - 1
if segmentCount <= 0 {
continue
}
if subpath.vertexCount >= 4 {
encoder.setRenderPipelineState(context.strokeInnerPipelineState)
encoder.setVertexBufferOffset(subpath.bufferOffset, index: 0)
var vertCnt2 = computeCount(isEndpoints: false, insertCaps: false)
encoder.setVertexBytes(&vertCnt2, length: 4 * 2, index: 2)
var capJoinRes2 = SIMD2<Float>(capRes2, joinRes2)
encoder.setVertexBytes(&capJoinRes2, length: 4 * 2, index: 3)
var isRoundJoinValue: UInt32 = self.lineJoin == .round ? 1 : 0
encoder.setVertexBytes(&isRoundJoinValue, length: 4, index: 4)
var isRoundCapValue: UInt32 = self.lineCap == .round ? 1 : 0
encoder.setVertexBytes(&isRoundCapValue, length: 4, index: 5)
var miterLimitValue: Float = self.lineJoin == .miter ? self.miterLimit : 1.0
encoder.setVertexBytes(&miterLimitValue, length: 4, index: 6)
var lineWidthValue: Float = self.lineWidth * 0.5
encoder.setVertexBytes(&lineWidthValue, length: 4, index: 7)
let vertexCount = 6 + Int(vertCnt2.x) + Int(vertCnt2.y) + 2
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount, instanceCount: subpath.vertexCount - 4 + 1, baseInstance: 0)
}
if subpath.terminalState != nil {
hasTerminalStates = true
}
}
if hasTerminalStates {
encoder.setRenderPipelineState(context.strokeTerminalPipelineState)
for subpath in self.subpaths {
let segmentCount = subpath.vertexCount - 1
if segmentCount <= 0 {
continue
}
if !subpath.isClosed {
if let terminalState = subpath.terminalState {
encoder.setVertexBufferOffset(terminalState.bufferOffset, index: 0)
var vertCnt2 = computeCount(isEndpoints: true, insertCaps: false)
encoder.setVertexBytes(&vertCnt2, length: 4 * 2, index: 2)
var capJoinRes2 = SIMD2<Float>(capRes2, joinRes2)
encoder.setVertexBytes(&capJoinRes2, length: 4 * 2, index: 3)
var isRoundJoinValue: UInt32 = self.lineJoin == .round ? 1 : 0
encoder.setVertexBytes(&isRoundJoinValue, length: 4, index: 4)
var isRoundCapValue: UInt32 = self.lineCap == .round ? 1 : 0
encoder.setVertexBytes(&isRoundCapValue, length: 4, index: 5)
var miterLimitValue: Float = self.lineJoin == .miter ? self.miterLimit : 1.0
encoder.setVertexBytes(&miterLimitValue, length: 4, index: 6)
var lineWidthValue: Float = self.lineWidth * 0.5
encoder.setVertexBytes(&lineWidthValue, length: 4, index: 7)
let vertexCount = 6 + Int(vertCnt2.x) + Int(vertCnt2.y) + 2
encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: vertexCount, instanceCount: terminalState.segmentCount, baseInstance: 0)
}
}
}
}
}
}

View File

@ -0,0 +1,485 @@
import Foundation
import LottieCpp
final class WriteBuffer {
private(set) var data: Data
private var capacity: Int
var length: Int
init() {
self.capacity = 1024
self.data = Data(count: self.capacity)
self.length = 0
}
func trim() {
self.data.count = self.length
self.capacity = self.data.count
}
func write(bytes: UnsafeRawBufferPointer) {
if self.data.count < self.length + bytes.count {
self.data.count = self.data.count * 2
}
self.data.withUnsafeMutableBytes { buffer -> Void in
memcpy(buffer.baseAddress!.advanced(by: self.length), bytes.baseAddress!, bytes.count)
}
self.length += bytes.count
}
func write(uInt32 value: UInt32) {
var value = value
withUnsafeBytes(of: &value, { bytes in
self.write(bytes: bytes)
})
}
func write(uInt16 value: UInt16) {
var value = value
withUnsafeBytes(of: &value, { bytes in
self.write(bytes: bytes)
})
}
func write(uInt8 value: UInt8) {
var value = value
withUnsafeBytes(of: &value, { bytes in
self.write(bytes: bytes)
})
}
func write(float value: Float) {
var value = value
withUnsafeBytes(of: &value, { bytes in
self.write(bytes: bytes)
})
}
func write(point: CGPoint) {
self.write(float: Float(point.x))
self.write(float: Float(point.y))
}
func write(size: CGSize) {
self.write(float: Float(size.width))
self.write(float: Float(size.height))
}
func write(rect: CGRect) {
self.write(point: rect.origin)
self.write(size: rect.size)
}
func write(transform: CATransform3D) {
self.write(float: Float(transform.m11))
self.write(float: Float(transform.m12))
self.write(float: Float(transform.m13))
self.write(float: Float(transform.m14))
self.write(float: Float(transform.m21))
self.write(float: Float(transform.m22))
self.write(float: Float(transform.m23))
self.write(float: Float(transform.m24))
self.write(float: Float(transform.m31))
self.write(float: Float(transform.m32))
self.write(float: Float(transform.m33))
self.write(float: Float(transform.m34))
self.write(float: Float(transform.m41))
self.write(float: Float(transform.m42))
self.write(float: Float(transform.m43))
self.write(float: Float(transform.m44))
}
}
final class ReadBuffer {
private let data: Data
private var offset: Int
init(data: Data) {
self.data = data
self.offset = 0
}
func read(bytes: UnsafeMutableRawBufferPointer) {
if self.offset + bytes.count <= self.data.count {
self.data.withUnsafeBytes { buffer -> Void in
memcpy(bytes.baseAddress!, buffer.baseAddress!.advanced(by: self.offset), bytes.count)
}
self.offset += bytes.count
} else {
preconditionFailure()
}
}
func readUInt32() -> UInt32 {
var value: UInt32 = 0
withUnsafeMutableBytes(of: &value, { bytes in
self.read(bytes: bytes)
})
return value
}
func readUInt16() -> UInt16 {
var value: UInt16 = 0
withUnsafeMutableBytes(of: &value, { bytes in
self.read(bytes: bytes)
})
return value
}
func readUInt8() -> UInt8 {
var value: UInt8 = 0
withUnsafeMutableBytes(of: &value, { bytes in
self.read(bytes: bytes)
})
return value
}
func readFloat() -> Float {
var value: Float = 0
withUnsafeMutableBytes(of: &value, { bytes in
self.read(bytes: bytes)
})
return value
}
func readPoint() -> CGPoint {
return CGPoint(x: CGFloat(self.readFloat()), y: CGFloat(self.readFloat()))
}
func readSize() -> CGSize {
return CGSize(width: CGFloat(self.readFloat()), height: CGFloat(self.readFloat()))
}
func readRect() -> CGRect {
return CGRect(origin: self.readPoint(), size: self.readSize())
}
func readTransform() -> CATransform3D {
return CATransform3D(
m11: CGFloat(self.readFloat()),
m12: CGFloat(self.readFloat()),
m13: CGFloat(self.readFloat()),
m14: CGFloat(self.readFloat()),
m21: CGFloat(self.readFloat()),
m22: CGFloat(self.readFloat()),
m23: CGFloat(self.readFloat()),
m24: CGFloat(self.readFloat()),
m31: CGFloat(self.readFloat()),
m32: CGFloat(self.readFloat()),
m33: CGFloat(self.readFloat()),
m34: CGFloat(self.readFloat()),
m41: CGFloat(self.readFloat()),
m42: CGFloat(self.readFloat()),
m43: CGFloat(self.readFloat()),
m44: CGFloat(self.readFloat())
)
}
}
private extension LottieColor {
init(argb: UInt32) {
self.init(r: CGFloat((argb >> 16) & 0xff) / 255.0, g: CGFloat((argb >> 8) & 0xff) / 255.0, b: CGFloat(argb & 0xff) / 255.0, a: CGFloat((argb >> 24) & 0xff) / 255.0)
}
var argb: UInt32 {
return (UInt32(self.a * 255.0) << 24) | (UInt32(max(0.0, self.r) * 255.0) << 16) | (UInt32(max(0.0, self.g) * 255.0) << 8) | (UInt32(max(0.0, self.b) * 255.0))
}
}
private struct NodeFlags: OptionSet {
var rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
static let masksToBounds = NodeFlags(rawValue: 1 << 0)
static let isHidden = NodeFlags(rawValue: 1 << 1)
static let hasSimpleContents = NodeFlags(rawValue: 1 << 2)
static let isInvertedMatte = NodeFlags(rawValue: 1 << 3)
static let hasRenderContent = NodeFlags(rawValue: 1 << 4)
static let hasSubnodes = NodeFlags(rawValue: 1 << 5)
static let hasMask = NodeFlags(rawValue: 1 << 6)
}
private struct LottieContentFlags: OptionSet {
var rawValue: UInt8
init(rawValue: UInt8) {
self.rawValue = rawValue
}
static let hasStroke = LottieContentFlags(rawValue: 1 << 0)
static let hasFill = LottieContentFlags(rawValue: 1 << 1)
}
func serializePath(buffer: WriteBuffer, path: LottiePath) {
let lengthOffset = buffer.length
buffer.write(uInt32: 0)
path.enumerateItems { pathItem in
switch pathItem.pointee.type {
case .moveTo:
let point = pathItem.pointee.points.0
buffer.write(uInt8: 0)
buffer.write(point: point)
case .lineTo:
let point = pathItem.pointee.points.0
buffer.write(uInt8: 1)
buffer.write(point: point)
case .curveTo:
let cp1 = pathItem.pointee.points.0
let cp2 = pathItem.pointee.points.1
let point = pathItem.pointee.points.2
buffer.write(uInt8: 2)
buffer.write(point: cp1)
buffer.write(point: cp2)
buffer.write(point: point)
case .close:
buffer.write(uInt8: 3)
@unknown default:
break
}
}
let dataLength = buffer.length - lengthOffset - 4
let previousLength = buffer.length
buffer.length = lengthOffset
buffer.write(uInt32: UInt32(dataLength))
buffer.length = previousLength
}
func deserializePath(buffer: ReadBuffer) -> LottiePath {
let itemDataLength = Int(buffer.readUInt32())
var itemData = Data(count: itemDataLength)
itemData.withUnsafeMutableBytes { bytes in
buffer.read(bytes: bytes)
}
return LottiePath(customData: itemData)
}
func serializeContentShading(buffer: WriteBuffer, shading: LottieRenderContentShading) {
if let shading = shading as? LottieRenderContentSolidShading {
buffer.write(uInt8: 0)
buffer.write(uInt32: shading.color.argb)
buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0)))
} else if let shading = shading as? LottieRenderContentGradientShading {
buffer.write(uInt8: 1)
buffer.write(uInt8: UInt8(clamping: Int(shading.opacity * 255.0)))
buffer.write(uInt8: UInt8(shading.gradientType.rawValue))
let colorStopCount = min(shading.colorStops.count, 255)
buffer.write(uInt8: UInt8(colorStopCount))
for i in 0 ..< colorStopCount {
buffer.write(uInt32: shading.colorStops[i].color.argb)
buffer.write(float: Float(shading.colorStops[i].location))
}
buffer.write(point: shading.start)
buffer.write(point: shading.end)
} else {
buffer.write(uInt8: 0)
buffer.write(uInt8: UInt8(clamping: Int(1.0 * 255.0)))
}
}
func deserializeContentShading(buffer: ReadBuffer) -> LottieRenderContentShading {
switch buffer.readUInt8() {
case 0:
return LottieRenderContentSolidShading(
color: LottieColor(argb: buffer.readUInt32()),
opacity: CGFloat(buffer.readUInt8()) / 255.0
)
case 1:
let opacity = CGFloat(buffer.readUInt8()) / 255.0
let gradientType = LottieGradientType(rawValue: UInt(buffer.readUInt8()))!
var colorStops: [LottieColorStop] = []
let colorStopCount = Int(buffer.readUInt8())
for _ in 0 ..< colorStopCount {
colorStops.append(LottieColorStop(
color: LottieColor(argb: buffer.readUInt32()),
location: CGFloat(buffer.readFloat())
))
}
let start = buffer.readPoint()
let end = buffer.readPoint()
return LottieRenderContentGradientShading(
opacity: opacity,
gradientType: gradientType,
colorStops: colorStops,
start: start,
end: end
)
default:
preconditionFailure()
}
}
func serializeStroke(buffer: WriteBuffer, stroke: LottieRenderContentStroke) {
serializeContentShading(buffer: buffer, shading: stroke.shading)
buffer.write(float: Float(stroke.lineWidth))
buffer.write(uInt8: UInt8(stroke.lineJoin.rawValue))
buffer.write(uInt8: UInt8(stroke.lineCap.rawValue))
buffer.write(float: Float(stroke.miterLimit))
}
func deserializeStroke(buffer: ReadBuffer) -> LottieRenderContentStroke {
return LottieRenderContentStroke(
shading: deserializeContentShading(buffer: buffer),
lineWidth: CGFloat(buffer.readFloat()),
lineJoin: CGLineJoin(rawValue: Int32(buffer.readUInt8()))!,
lineCap: CGLineCap(rawValue: Int32(buffer.readUInt8()))!,
miterLimit: CGFloat(buffer.readFloat()),
dashPhase: 0.0,
dashPattern: nil
)
}
func serializeFill(buffer: WriteBuffer, fill: LottieRenderContentFill) {
serializeContentShading(buffer: buffer, shading: fill.shading)
buffer.write(uInt8: UInt8(fill.fillRule.rawValue))
}
func deserializeFill(buffer: ReadBuffer) -> LottieRenderContentFill {
return LottieRenderContentFill(
shading: deserializeContentShading(buffer: buffer),
fillRule: LottieFillRule(rawValue: UInt(buffer.readUInt8()))!
)
}
func serializeRenderContent(buffer: WriteBuffer, renderContent: LottieRenderContent) {
var flags: LottieContentFlags = []
if renderContent.stroke != nil {
flags.insert(.hasStroke)
}
if renderContent.fill != nil {
flags.insert(.hasFill)
}
buffer.write(uInt8: flags.rawValue)
serializePath(buffer: buffer, path: renderContent.path)
if let stroke = renderContent.stroke {
serializeStroke(buffer: buffer, stroke: stroke)
}
if let fill = renderContent.fill {
serializeFill(buffer: buffer, fill: fill)
}
}
func deserializeRenderContent(buffer: ReadBuffer) -> LottieRenderContent {
let flags = LottieContentFlags(rawValue: buffer.readUInt8())
let path = deserializePath(buffer: buffer)
var stroke: LottieRenderContentStroke?
if flags.contains(.hasStroke) {
stroke = deserializeStroke(buffer: buffer)
}
var fill: LottieRenderContentFill?
if flags.contains(.hasFill) {
fill = deserializeFill(buffer: buffer)
}
return LottieRenderContent(
path: path,
stroke: stroke,
fill: fill
)
}
func serializeNode(buffer: WriteBuffer, node: LottieRenderNode) {
var flags: NodeFlags = []
if node.masksToBounds {
flags.insert(.masksToBounds)
}
if node.isHidden {
flags.insert(.isHidden)
}
if node.hasSimpleContents {
flags.insert(.hasSimpleContents)
}
if node.isInvertedMatte {
flags.insert(.isInvertedMatte)
}
if node.renderContent != nil {
flags.insert(.hasRenderContent)
}
if !node.subnodes.isEmpty {
flags.insert(.hasSubnodes)
}
if node.mask != nil {
flags.insert(.hasMask)
}
buffer.write(uInt8: flags.rawValue)
buffer.write(point: node.position)
buffer.write(rect: node.bounds)
buffer.write(transform: node.transform)
buffer.write(uInt8: UInt8(clamping: Int(node.opacity * 255.0)))
buffer.write(rect: node.globalRect)
buffer.write(transform: node.globalTransform)
if let renderContent = node.renderContent {
serializeRenderContent(buffer: buffer, renderContent: renderContent)
}
if !node.subnodes.isEmpty {
let count = min(node.subnodes.count, 4095)
buffer.write(uInt16: UInt16(count))
for i in 0 ..< count {
serializeNode(buffer: buffer, node: node.subnodes[i])
}
}
if let mask = node.mask {
serializeNode(buffer: buffer, node: mask)
}
}
func deserializeNode(buffer: ReadBuffer) -> LottieRenderNode {
let flags = NodeFlags(rawValue: buffer.readUInt8())
let position = buffer.readPoint()
let bounds = buffer.readRect()
let transform = buffer.readTransform()
let opacity = CGFloat(buffer.readUInt8()) / 255.0
let globalRect = buffer.readRect()
let globalTransform = buffer.readTransform()
var renderContent: LottieRenderContent?
if flags.contains(.hasRenderContent) {
renderContent = deserializeRenderContent(buffer: buffer)
}
var subnodes: [LottieRenderNode] = []
if flags.contains(.hasSubnodes) {
let count = Int(buffer.readUInt16())
for _ in 0 ..< count {
subnodes.append(deserializeNode(buffer: buffer))
}
}
var mask: LottieRenderNode?
if flags.contains(.hasMask) {
mask = deserializeNode(buffer: buffer)
}
return LottieRenderNode(
position: position,
bounds: bounds,
transform: transform,
opacity: opacity,
masksToBounds: flags.contains(.masksToBounds),
isHidden: flags.contains(.isHidden),
globalRect: globalRect,
globalTransform: globalTransform,
renderContent: renderContent,
hasSimpleContents: flags.contains(.hasSimpleContents),
isInvertedMatte: flags.contains(.isInvertedMatte),
subnodes: subnodes,
mask: mask
)
}