mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
LottieCpp improvements
This commit is contained in:
parent
8605198d72
commit
c752e2d895
@ -32,6 +32,8 @@ swift_library(
|
||||
"//submodules/WallpaperBackgroundNode",
|
||||
"//submodules/Components/MultilineTextWithEntitiesComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramUI/Components/LottieMetal",
|
||||
"//submodules/TelegramAnimatedStickerNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -15,6 +15,7 @@ objc_library(
|
||||
copts = [
|
||||
"-Werror",
|
||||
"-I{}/Sources".format(package_name()),
|
||||
"-O2",
|
||||
],
|
||||
hdrs = glob([
|
||||
"PublicHeaders/**/*.h",
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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()) {
|
||||
|
@ -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));
|
||||
|
@ -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()) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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()));
|
||||
|
@ -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));
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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()) {
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
@ -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()));
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -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()) {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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()));
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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()) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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()];
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 */
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
@ -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) {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user