diff --git a/Example/json/anim.json b/Example/json/anim.json new file mode 100644 index 0000000..56b543b --- /dev/null +++ b/Example/json/anim.json @@ -0,0 +1,146 @@ +{ + "class": "CAReplicatorLayer", + "layerModel": { + "sublayers": [ + { + "layerModel": { + "position": [ + 100, + 100 + ], + "bounds": [ + [ + 0, + 0 + ], + [ + 200, + 200 + ] + ], + "animations": { + "ripple": { + "class": "CAAnimationGroup", + "animationModel": { + "beginTime": 0, + "duration": 3, + "timeOffset": 0, + "autoreverses": false, + "fillMode": { + "rawValue": "removed" + }, + "isRemovedOnCompletion": false, + "speed": 1, + "repeatDuration": 0, + "repeatCount": 3.4028235e+38, + "animations": [ + { + "animationModel": { + "autoreverses": false, + "repeatDuration": 0, + "timeOffset": 0, + "fillMode": { + "rawValue": "forwards" + }, + "isRemovedOnCompletion": false, + "fromValue": { + "type": "float", + "value": 0 + }, + "speed": 1, + "isAdditive": false, + "duration": 3, + "repeatCount": 0, + "isCumulative": false, + "toValue": { + "value": 1, + "type": "int" + }, + "keyPath": "transform.scale.xy", + "beginTime": 0 + }, + "class": "CABasicAnimation" + }, + { + "class": "CAKeyframeAnimation", + "animationModel": { + "repeatCount": 0, + "repeatDuration": 0, + "keyPath": "opacity", + "fillMode": { + "rawValue": "forwards" + }, + "beginTime": 0, + "isCumulative": false, + "calculationMode": { + "rawValue": "linear" + }, + "autoreverses": false, + "isRemovedOnCompletion": false, + "keyTimes": [ + 0, + 0.5, + 1 + ], + "values": [ + { + "type": "float", + "value": 0.8 + }, + { + "type": "float", + "value": 0.4 + }, + { + "type": "float", + "value": 0 + } + ], + "isAdditive": false, + "speed": 1, + "timeOffset": 0, + "duration": 3 + } + } + ], + "timingFunction": "linear" + } + } + }, + "backgroundColor": { + "code": "FF00FFFF" + }, + "allowsEdgeAntialiasing": false, + "contentsFormat": { + "rawValue": "RGBA8" + }, + "cornerRadius": 100, + "opacity": 0, + "allowsGroupOpacity": true + }, + "class": "CALayer" + } + ], + "instanceCount": 3, + "contentsFormat": { + "rawValue": "RGBA8" + }, + "allowsEdgeAntialiasing": false, + "instanceDelay": 1, + "position": [ + 196.5, + 426 + ], + "bounds": [ + [ + 0, + 0 + ], + [ + 200, + 200 + ] + ], + "allowsGroupOpacity": true + } +} diff --git a/Sources/SDCALayer/Extension/CAMediaTimingFunction+.swift b/Sources/SDCALayer/Extension/CAMediaTimingFunction+.swift new file mode 100644 index 0000000..748e8fe --- /dev/null +++ b/Sources/SDCALayer/Extension/CAMediaTimingFunction+.swift @@ -0,0 +1,35 @@ +// +// CAMediaTimingFunction+.swift +// +// +// Created by p-x9 on 2024/04/06. +// +// + +import QuartzCore + +extension CAMediaTimingFunction { + var controlPoints: [CGPoint] { + var controlPoints = [CGPoint]() + for index in 1..<3 { + let controlPoint = UnsafeMutablePointer.allocate(capacity: 2) + getControlPoint(at: Int(index), values: controlPoint) + let x: Float = controlPoint[0] + let y: Float = controlPoint[1] + controlPoint.deallocate() + controlPoints.append(CGPoint(x: CGFloat(x), y: CGFloat(y))) + } + return controlPoints + } + + var name: CAMediaTimingFunctionName? { + switch description { + case "linear": .linear + case "easeIn": .easeIn + case "easeOut": .easeOut + case "easeInEaseOut": .easeInEaseOut + case "default": .default + default: nil + } + } +} diff --git a/Sources/SDCALayer/Extension/CGColor+.swift b/Sources/SDCALayer/Extension/CGColor+.swift index bf813ef..3d2bff0 100644 --- a/Sources/SDCALayer/Extension/CGColor+.swift +++ b/Sources/SDCALayer/Extension/CGColor+.swift @@ -35,7 +35,7 @@ extension CGColor { } private var rgbaComponents: [CGFloat] { - guard let colorSpace = CGColorSpace(name: CGColorSpace.extendedSRGB), + guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB), let converted = self.converted(to: colorSpace, intent: .defaultIntent, options: nil), let components = converted.components, converted.numberOfComponents == 4 else { diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAAnimation+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAAnimation+ICodable.swift new file mode 100644 index 0000000..e253cee --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAAnimation+ICodable.swift @@ -0,0 +1,31 @@ +// +// CAAnimation+ICodable.swift +// +// +// Created by p-x9 on 2024/04/06. +// +// + +import QuartzCore +import IndirectlyCodable + +extension CAAnimation: IndirectlyCodable { + public typealias Model = JCAAnimation + + public func codable() -> JCAAnimation? { + guard let animationClass = NSClassFromString(Self.codableTypeName) as? JCAAnimation.Type else { + return nil + } + + return animationClass.init(with: self) + } + + @objc + open class var codableTypeName: String { + String(reflecting: Model.self) + } +} + +extension CAMediaTimingFillMode: RawIndirectlyCodable { + public typealias Model = JCAMediaTimingFillMode +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAAnimationGroup+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAAnimationGroup+ICodable.swift new file mode 100644 index 0000000..1898858 --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAAnimationGroup+ICodable.swift @@ -0,0 +1,17 @@ +// +// CAAnimationGroup+ICodable.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +extension CAAnimationGroup { + public typealias Model = JCAAnimationGroup + + open override class var codableTypeName: String { + String(reflecting: Model.self) + } +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CABasicAnimation+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CABasicAnimation+ICodable.swift new file mode 100644 index 0000000..ef6772b --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CABasicAnimation+ICodable.swift @@ -0,0 +1,17 @@ +// +// CABasicAnimation+ICodable.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +extension CABasicAnimation { + public typealias Model = JCABasicAnimation + + open override class var codableTypeName: String { + String(reflecting: Model.self) + } +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAKeyframeAnimation+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAKeyframeAnimation+ICodable.swift new file mode 100644 index 0000000..fb1f815 --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAKeyframeAnimation+ICodable.swift @@ -0,0 +1,25 @@ +// +// CAKeyframeAnimation+ICodable.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +extension CAKeyframeAnimation { + public typealias Model = JCAKeyframeAnimation + + open override class var codableTypeName: String { + String(reflecting: Model.self) + } +} + +extension CAAnimationCalculationMode: RawIndirectlyCodable { + public typealias Model = JCAAnimationCalculationMode +} + +extension CAAnimationRotationMode: RawIndirectlyCodable { + public typealias Model = JCAAnimationRotationMode +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAPropertyAnimation+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAPropertyAnimation+ICodable.swift new file mode 100644 index 0000000..376536d --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CAPropertyAnimation+ICodable.swift @@ -0,0 +1,17 @@ +// +// CAPropertyAnimation+ICodable.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +extension CAPropertyAnimation { + public typealias Model = JCAPropertyAnimation + + open override class var codableTypeName: String { + String(reflecting: Model.self) + } +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CASpringAnimation+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CASpringAnimation+ICodable.swift new file mode 100644 index 0000000..90bb938 --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/Animation/CASpringAnimation+ICodable.swift @@ -0,0 +1,17 @@ +// +// CASpringAnimation+ICodable.swift +// +// +// Created by p-x9 on 2024/04/09. +// +// + +import QuartzCore + +extension CASpringAnimation { + public typealias Model = JCASpringAnimation + + open override class var codableTypeName: String { + String(reflecting: Model.self) + } +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CAMediaTimingFunction+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/CAMediaTimingFunction+ICodable.swift new file mode 100644 index 0000000..a1bde8a --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/CAMediaTimingFunction+ICodable.swift @@ -0,0 +1,18 @@ +// +// CAMediaTimingFunction+ICodable.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore +import IndirectlyCodable + +extension CAMediaTimingFunction: IndirectlyCodable { + public typealias Model = JCAMediaTimingFunction + + public func codable() -> Model? { + .init(with: self) + } +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CAValueFunction+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/CAValueFunction+ICodable.swift new file mode 100644 index 0000000..25556ff --- /dev/null +++ b/Sources/SDCALayer/Extension/IndirectlyCodable/CAValueFunction+ICodable.swift @@ -0,0 +1,18 @@ +// +// CAValueFunction+ICodable.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore +import IndirectlyCodable + +extension CAValueFunction: IndirectlyCodable { + public typealias Model = JCAValueFunction + + public func codable() -> Model? { + .init(with: self) + } +} diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CAGradientLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAGradientLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CAGradientLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAGradientLayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CALayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CALayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CALayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CALayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CAReplicatorLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAReplicatorLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CAReplicatorLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAReplicatorLayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CAScrollLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAScrollLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CAScrollLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAScrollLayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CAShapeLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAShapeLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CAShapeLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CAShapeLayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CATextLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CATextLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CATextLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CATextLayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CATiledLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CATiledLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CATiledLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CATiledLayer+ICodable.swift diff --git a/Sources/SDCALayer/Extension/IndirectlyCodable/CATransformLayer+ICodable.swift b/Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CATransformLayer+ICodable.swift similarity index 100% rename from Sources/SDCALayer/Extension/IndirectlyCodable/CATransformLayer+ICodable.swift rename to Sources/SDCALayer/Extension/IndirectlyCodable/Layer/CATransformLayer+ICodable.swift diff --git a/Sources/SDCALayer/Model/Animation/JCAAnimation.swift b/Sources/SDCALayer/Model/Animation/JCAAnimation.swift new file mode 100644 index 0000000..c6b1e38 --- /dev/null +++ b/Sources/SDCALayer/Model/Animation/JCAAnimation.swift @@ -0,0 +1,78 @@ +// +// JCAAnimation.swift +// +// +// Created by p-x9 on 2024/04/06. +// +// + +import Foundation +import QuartzCore +import KeyPathValue +import IndirectlyCodable + +open class JCAAnimation: CAAnimationConvertible, Codable { + public typealias Target = CAAnimation + + open class var targetTypeName: String { + String(reflecting: Target.self) + } + + static private let propertyMap: PropertyMap = .init([ + .init(\.beginTime, \.beginTime), + .init(\.duration, \.duration), + .init(\.speed, \.speed), + .init(\.timeOffset, \.timeOffset), + .init(\.repeatCount, \.repeatCount), + .init(\.repeatDuration, \.repeatDuration), + .init(\.autoreverses, \.autoreverses), + .init(\.fillMode, \.fillMode), + + .init(\.timingFunction, \.timingFunction), + .init(\.isRemovedOnCompletion, \.isRemovedOnCompletion) + ]) + + /* CAMediaTiming */ + public var beginTime: CFTimeInterval? + public var duration: CFTimeInterval? + public var speed: Float? + public var timeOffset: CFTimeInterval? + public var repeatCount: Float? + public var repeatDuration: CFTimeInterval? + public var autoreverses: Bool? + public var fillMode: JCAMediaTimingFillMode? + + public var timingFunction: JCAMediaTimingFunction? + public var isRemovedOnCompletion: Bool? + + public init() {} + + public required convenience init(with object: Target) { + self.init() + applyProperties(with: object) + } + + open func applyProperties(to target: Target) { + Self.propertyMap.apply(to: target, from: self) + } + + open func applyProperties(with target: Target) { + Self.propertyMap.apply(to: self, from: target) + } + + public func convertToAnimation() -> Target? { + let animation = CAAnimation() + + self.applyProperties(to: animation) + return animation + } +} + +public final class JCAMediaTimingFillMode: RawIndirectlyCodableModel { + public typealias Target = CAMediaTimingFillMode + + public var rawValue: Target.RawValue + public required init(rawValue: Target.RawValue) { + self.rawValue = rawValue + } +} diff --git a/Sources/SDCALayer/Model/Animation/JCAAnimationGroup.swift b/Sources/SDCALayer/Model/Animation/JCAAnimationGroup.swift new file mode 100644 index 0000000..b59dd9c --- /dev/null +++ b/Sources/SDCALayer/Model/Animation/JCAAnimationGroup.swift @@ -0,0 +1,72 @@ +// +// JCAAnimationGroup.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +open class JCAAnimationGroup: JCAAnimation { + public typealias Target = CAAnimationGroup + + private enum CodingKeys: String, CodingKey { + case animations + } + + open override class var targetTypeName: String { + String(reflecting: Target.self) + } + + public var animations: [SDCAAnimation]? + + public override init() { + super.init() + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + animations = try container.decodeIfPresent([SDCAAnimation].self, forKey: .animations) + } + + open override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(animations, forKey: .animations) + } + + public required convenience init(with object: Target) { + self.init() + + applyProperties(with: object) + } + + open override func applyProperties(to target: CAAnimation) { + super.applyProperties(to: target) + + guard let target = target as? CAAnimationGroup else { return } + target.animations = animations?.compactMap { + $0.converted() + } + } + + open override func applyProperties(with target: CAAnimation) { + super.applyProperties(with: target) + + guard let target = target as? CAAnimationGroup else { return } + animations = target.animations?.compactMap { + .init(model: $0.codable()) + } + } + + open override func convertToAnimation() -> CAAnimation? { + let animation = CAAnimationGroup() + + self.applyProperties(to: animation) + return animation + } +} diff --git a/Sources/SDCALayer/Model/Animation/JCABasicAnimation.swift b/Sources/SDCALayer/Model/Animation/JCABasicAnimation.swift new file mode 100644 index 0000000..aa1ab92 --- /dev/null +++ b/Sources/SDCALayer/Model/Animation/JCABasicAnimation.swift @@ -0,0 +1,80 @@ +// +// JCABasicAnimation.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +open class JCABasicAnimation: JCAPropertyAnimation { + public typealias Target = CABasicAnimation + + private enum CodingKeys: String, CodingKey { + case fromValue + case toValue + case byValue + } + + open override class var targetTypeName: String { + String(reflecting: Target.self) + } + + public var fromValue: JCAAnimationAny? + public var toValue: JCAAnimationAny? + public var byValue: JCAAnimationAny? + + public override init() { + super.init() + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + fromValue = try container.decodeIfPresent(JCAAnimationAny.self, forKey: .fromValue) + toValue = try container.decodeIfPresent(JCAAnimationAny.self, forKey: .toValue) + byValue = try container.decodeIfPresent(JCAAnimationAny.self, forKey: .byValue) + } + + open override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(fromValue, forKey: .fromValue) + try container.encodeIfPresent(toValue, forKey: .toValue) + try container.encodeIfPresent(byValue, forKey: .byValue) + } + + public required convenience init(with object: Target) { + self.init() + + applyProperties(with: object) + } + + open override func applyProperties(to target: CAAnimation) { + super.applyProperties(to: target) + + guard let target = target as? CABasicAnimation else { return } + target.fromValue = fromValue?.value + target.toValue = toValue?.value + target.byValue = byValue?.value + } + + open override func applyProperties(with target: CAAnimation) { + super.applyProperties(with: target) + + guard let target = target as? CABasicAnimation else { return } + fromValue = .init(target.fromValue) + toValue = .init(target.toValue) + byValue = .init(target.byValue) + } + + open override func convertToAnimation() -> CAAnimation? { + let animation = CABasicAnimation() + + self.applyProperties(to: animation) + return animation + } +} diff --git a/Sources/SDCALayer/Model/Animation/JCAKeyframeAnimation.swift b/Sources/SDCALayer/Model/Animation/JCAKeyframeAnimation.swift new file mode 100644 index 0000000..e63c380 --- /dev/null +++ b/Sources/SDCALayer/Model/Animation/JCAKeyframeAnimation.swift @@ -0,0 +1,150 @@ +// +// JCAKeyframeAnimation.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +open class JCAKeyframeAnimation: JCAPropertyAnimation { + public typealias Target = CAKeyframeAnimation + + private enum CodingKeys: String, CodingKey { + case values + case path + case keyTimes + case timingFunctions + case calculationMode + case tensionValues + case continuityValues + case biasValues + case rotationMode + } + + open override class var targetTypeName: String { + String(reflecting: Target.self) + } + + static private let propertyMap: PropertyMap = .init([ +// .init(\.values, \.values), + .init(\.path, \.path), +// .init(\.keyTimes, \.keyTimes), + .init(\.timingFunctions, \.timingFunctions), + .init(\.calculationMode, \.calculationMode), +// .init(\.tensionValues, \.tensionValues), +// .init(\.continuityValues, \.continuityValues), +// .init(\.biasValues, \.biasValues), + .init(\.rotationMode, \.rotationMode), + ]) + + public var values: [JCAAnimationAny]? + public var path: JCGPath? + public var keyTimes: [Float]? + public var timingFunctions: [JCAMediaTimingFunction]? + public var calculationMode: JCAAnimationCalculationMode? + + public var tensionValues: [Float]? + public var continuityValues: [Float]? + public var biasValues: [Float]? + + public var rotationMode: JCAAnimationRotationMode? + + public override init() { + super.init() + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + values = try container.decodeIfPresent([JCAAnimationAny].self, forKey: .values) + path = try container.decodeIfPresent(JCGPath.self, forKey: .path) + keyTimes = try container.decodeIfPresent([Float].self, forKey: .keyTimes) + timingFunctions = try container.decodeIfPresent([JCAMediaTimingFunction].self, forKey: .timingFunctions) + calculationMode = try container.decodeIfPresent(JCAAnimationCalculationMode.self, forKey: .calculationMode) + + tensionValues = try container.decodeIfPresent([Float].self, forKey: .tensionValues) + continuityValues = try container.decodeIfPresent([Float].self, forKey: .continuityValues) + biasValues = try container.decodeIfPresent([Float].self, forKey: .biasValues) + + rotationMode = try container.decodeIfPresent(JCAAnimationRotationMode.self, forKey: .rotationMode) + } + + open override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(values, forKey: .values) + try container.encodeIfPresent(path, forKey: .path) + try container.encodeIfPresent(keyTimes, forKey: .keyTimes) + try container.encodeIfPresent(timingFunctions, forKey: .timingFunctions) + try container.encodeIfPresent(calculationMode, forKey: .calculationMode) + + try container.encodeIfPresent(tensionValues, forKey: .tensionValues) + try container.encodeIfPresent(continuityValues, forKey: .continuityValues) + try container.encodeIfPresent(biasValues, forKey: .biasValues) + + try container.encodeIfPresent(rotationMode, forKey: .rotationMode) + } + + public required convenience init(with object: Target) { + self.init() + + applyProperties(with: object) + } + + open override func applyProperties(to target: CAAnimation) { + super.applyProperties(to: target) + + guard let target = target as? CAKeyframeAnimation else { return } + + Self.propertyMap.apply(to: target, from: self) + + target.values = values?.map(\.value) + target.keyTimes = keyTimes?.map { NSNumber(value: $0) } + target.tensionValues = tensionValues?.map { NSNumber(value: $0) } + target.continuityValues = continuityValues?.map { NSNumber(value: $0) } + target.biasValues = biasValues?.map { NSNumber(value: $0) } + } + + open override func applyProperties(with target: CAAnimation) { + super.applyProperties(with: target) + + guard let target = target as? CAKeyframeAnimation else { return } + + Self.propertyMap.apply(to: self, from: target) + + values = target.values?.compactMap { .init($0) } + keyTimes = target.keyTimes?.map { $0.floatValue } + tensionValues = target.tensionValues?.map { $0.floatValue } + continuityValues = target.continuityValues?.map { $0.floatValue } + biasValues = target.biasValues?.map { $0.floatValue } + } + + open override func convertToAnimation() -> CAAnimation? { + let animation = CAKeyframeAnimation() + + self.applyProperties(to: animation) + return animation + } +} + +public final class JCAAnimationCalculationMode: RawIndirectlyCodableModel { + public typealias Target = CAAnimationCalculationMode + + public var rawValue: Target.RawValue + public required init(rawValue: Target.RawValue) { + self.rawValue = rawValue + } +} + +public final class JCAAnimationRotationMode: RawIndirectlyCodableModel { + public typealias Target = CAAnimationRotationMode + + public var rawValue: Target.RawValue + public required init(rawValue: Target.RawValue) { + self.rawValue = rawValue + } +} diff --git a/Sources/SDCALayer/Model/Animation/JCAPropertyAnimation.swift b/Sources/SDCALayer/Model/Animation/JCAPropertyAnimation.swift new file mode 100644 index 0000000..53d2fd6 --- /dev/null +++ b/Sources/SDCALayer/Model/Animation/JCAPropertyAnimation.swift @@ -0,0 +1,91 @@ +// +// JCAPropertyAnimation.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +open class JCAPropertyAnimation: JCAAnimation { + public typealias Target = CAPropertyAnimation + + private enum CodingKeys: String, CodingKey { + case keyPath + case isAdditive + case isCumulative + case valueFunction + } + + open override class var targetTypeName: String { + String(reflecting: Target.self) + } + + static private let propertyMap: PropertyMap = .init([ + .init(\.keyPath, \.keyPath), + .init(\.isAdditive, \.isAdditive), + .init(\.isCumulative, \.isCumulative), + .init(\.valueFunction, \.valueFunction), + ]) + + public var keyPath: String? + public var isAdditive: Bool? + public var isCumulative: Bool? + public var valueFunction: JCAValueFunction? + + public override init() { + super.init() + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + + keyPath = try container.decodeIfPresent(String.self, forKey: .keyPath) + isAdditive = try container.decodeIfPresent(Bool.self, forKey: .isAdditive) + isCumulative = try container.decodeIfPresent(Bool.self, forKey: .isCumulative) + valueFunction = try container.decodeIfPresent(JCAValueFunction.self, forKey: .valueFunction) + } + + open override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(keyPath, forKey: .keyPath) + try container.encodeIfPresent(isAdditive, forKey: .isAdditive) + try container.encodeIfPresent(isCumulative, forKey: .isCumulative) + try container.encodeIfPresent(valueFunction, forKey: .valueFunction) + } + + public required convenience init(with object: Target) { + self.init() + + applyProperties(with: object) + } + + open override func applyProperties(to target: CAAnimation) { + super.applyProperties(to: target) + + guard let target = target as? CAPropertyAnimation else { return } + + Self.propertyMap.apply(to: target, from: self) + } + + open override func applyProperties(with target: CAAnimation) { + super.applyProperties(with: target) + + guard let target = target as? CAPropertyAnimation else { return } + + Self.propertyMap.apply(to: self, from: target) + } + + open override func convertToAnimation() -> CAAnimation? { + let animation = CAPropertyAnimation() + + self.applyProperties(to: animation) + return animation + } +} diff --git a/Sources/SDCALayer/Model/Animation/JCASpringAnimation.swift b/Sources/SDCALayer/Model/Animation/JCASpringAnimation.swift new file mode 100644 index 0000000..2b06782 --- /dev/null +++ b/Sources/SDCALayer/Model/Animation/JCASpringAnimation.swift @@ -0,0 +1,94 @@ +// +// JCASpringAnimation.swift +// +// +// Created by p-x9 on 2024/04/09. +// +// + +import QuartzCore + +open class JCASpringAnimation: JCAAnimation { + public typealias Target = CASpringAnimation + + private enum CodingKeys: String, CodingKey { + case mass + case stiffness + case damping + case initialVelocity + } + + open override class var targetTypeName: String { + String(reflecting: Target.self) + } + + static private let propertyMap: PropertyMap = .init([ + .init(\.mass, \.mass), + .init(\.stiffness, \.stiffness), + .init(\.damping, \.damping), + .init(\.initialVelocity, \.initialVelocity), + ]) + + public var mass: CGFloat? + public var stiffness: CGFloat? + public var damping: CGFloat? + public var initialVelocity: CGFloat? + +// @available(iOS 17.0, *) +// public var allowsOverdamping: Bool? + + public override init() { + super.init() + } + + public required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container = try decoder.container(keyedBy: CodingKeys.self) + + mass = try container.decodeIfPresent(CGFloat.self, forKey: .mass) + stiffness = try container.decodeIfPresent(CGFloat.self, forKey: .stiffness) + damping = try container.decodeIfPresent(CGFloat.self, forKey: .damping) + initialVelocity = try container.decodeIfPresent(CGFloat.self, forKey: .initialVelocity) + } + + open override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(mass, forKey: .mass) + try container.encodeIfPresent(stiffness, forKey: .stiffness) + try container.encodeIfPresent(damping, forKey: .damping) + try container.encodeIfPresent(initialVelocity, forKey: .initialVelocity) + } + + public required convenience init(with object: Target) { + self.init() + + applyProperties(with: object) + } + + open override func applyProperties(to target: CAAnimation) { + super.applyProperties(to: target) + + guard let target = target as? CASpringAnimation else { return } + + Self.propertyMap.apply(to: target, from: self) + } + + open override func applyProperties(with target: CAAnimation) { + super.applyProperties(with: target) + + guard let target = target as? CASpringAnimation else { return } + + Self.propertyMap.apply(to: self, from: target) + } + + open override func convertToAnimation() -> CAAnimation? { + let animation = CASpringAnimation() + + self.applyProperties(to: animation) + return animation + } +} diff --git a/Sources/SDCALayer/Model/JCAAnimationAny.swift b/Sources/SDCALayer/Model/JCAAnimationAny.swift new file mode 100644 index 0000000..0437a10 --- /dev/null +++ b/Sources/SDCALayer/Model/JCAAnimationAny.swift @@ -0,0 +1,116 @@ +// +// JCAAnimationAny.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore +import IndirectlyCodable + +public struct JCAAnimationAny: Codable { + public enum ValueType: String, Codable { + case int + case float + case point + case rect + case color + } + var type: ValueType + var value: Any + + public enum CodingKeys: String, CodingKey { + case type + case value + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try container.decode(ValueType.self, forKey: .type) + var value: Any + switch type { + case .int: + value = try container.decode(Int.self, forKey: .value) + case .float: + value = try container.decode(Double.self, forKey: .value) + case .point: + value = try container.decode(CGPoint.self, forKey: .value) + case .rect: + value = try container.decode(CGRect.self, forKey: .value) + case .color: + value = try container.decode(JCGColor.self, forKey: .value) + } + if let v = value as? any IndirectlyCodableModel, + let converted = v.converted() { + value = converted + } + self.value = value + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + + switch value { + case let v as any FixedWidthInteger: + try container.encode(Int(v), forKey: .value) + case let v as any BinaryFloatingPoint: + try container.encode(Double(v), forKey: .value) + case let v as CGPoint: + try container.encode(v, forKey: .value) + case let v as CGRect: + try container.encode(v, forKey: .value) + case let v as CGColor where CFGetTypeID(v) == CGColor.typeID: + try container.encodeIfPresent(v.codable(), forKey: .value) + default: + throw EncodingError.invalidValue( + value, + .init(codingPath: [CodingKeys.value], debugDescription: "Unsupported Value Type") + ) + } + } +} + +extension JCAAnimationAny { + init?(_ value: Any?) { + guard var value = value else { return nil } + value = value as Any + if let v = value as? any IndirectlyCodable, + let codable = v.codable() { + value = codable + } + + switch value { + case let v as any FixedWidthInteger: + self.value = v + self.type = .int + + case let v as any BinaryFloatingPoint: + self.value = v + self.type = .float + + case let v as CGPoint: + self.value = v + self.type = .point + + case let v as CGRect: + self.value = v + self.type = .rect + + case let v as NSInteger: + self.value = Int(v) + self.type = .int + + case let v as NSNumber: + self.value = v.doubleValue + self.type = .float + + case let v as CGColor: + self.value = v + self.type = .color + default: + return nil + } + } +} diff --git a/Sources/SDCALayer/Model/JCAMediaTimingFunction.swift b/Sources/SDCALayer/Model/JCAMediaTimingFunction.swift new file mode 100644 index 0000000..1e28a71 --- /dev/null +++ b/Sources/SDCALayer/Model/JCAMediaTimingFunction.swift @@ -0,0 +1,91 @@ +// +// JCAMediaTimingFunction.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore +import IndirectlyCodable + +public enum JCAMediaTimingFunction: IndirectlyCodableModel, Codable { + public typealias Target = CAMediaTimingFunction + + case linear + case easeIn + case easeOut + case easeInEaseOut + case `default` + case other(c1: CGPoint, c2: CGPoint) + + public init(with target: Target) { + if let name = target.name { + switch name { + case .linear: self = .linear + case .easeIn: self = .easeIn + case .easeOut: self = .easeOut + case .easeInEaseOut: self = .easeInEaseOut + case .default: self = .default + default: + let points = target.controlPoints + self = .other(c1: points[0], c2: points[1]) + } + } else { + let points = target.controlPoints + self = .other(c1: points[0], c2: points[1]) + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let string = try? container.decode(String.self) { + switch string { + case "linear": self = .linear + case "easeIn": self = .easeIn + case "easeOut": self = .easeOut + case "easeInEaseOut": self = .easeInEaseOut + case "default": self = .default + default: throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Invalid function name: \(string)" + ) + } + } else { + let points = try container.decode([CGPoint].self) + self = .other(c1: points[0], c2: points[1]) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .linear: + try container.encode("linear") + case .easeIn: + try container.encode("easeIn") + case .easeOut: + try container.encode("easeOut") + case .easeInEaseOut: + try container.encode("easeInEaseOut") + case .default: + try container.encode("default") + case let .other(c1: c1, c2: c2): + try container.encode([ + c1, c2 + ]) + } + } + + public func converted() -> CAMediaTimingFunction? { + switch self { + case .linear: return .init(name: .linear) + case .easeIn: return .init(name: .easeIn) + case .easeOut: return .init(name: .easeOut) + case .easeInEaseOut: return .init(name: .easeInEaseOut) + case .default: return .init(name: .default) + case .other(let c1, let c2): + return .init(controlPoints: Float(c1.x), Float(c1.y), Float(c2.x), Float(c2.y)) + } + } +} diff --git a/Sources/SDCALayer/Model/JCAValueFunction.swift b/Sources/SDCALayer/Model/JCAValueFunction.swift new file mode 100644 index 0000000..9772604 --- /dev/null +++ b/Sources/SDCALayer/Model/JCAValueFunction.swift @@ -0,0 +1,61 @@ +// +// JCAValueFunction.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore +import IndirectlyCodable + +public enum JCAValueFunction: String, IndirectlyCodableModel, Codable { + public typealias Target = CAValueFunction + + case rotateX + case rotateY + case rotateZ + + case scale + case scaleX + case scaleY + case scaleZ + + case translate + case translateX + case translateY + case translateZ + + public init(with target: CAValueFunction) { + switch target.name { + case .rotateX: self = .rotateX + case .rotateY: self = .rotateY + case .rotateZ: self = .rotateZ + case .scale: self = .scale + case .scaleX: self = .scaleX + case .scaleY: self = .scaleY + case .scaleZ: self = .scaleZ + case .translate: self = .translate + case .translateX: self = .translateX + case .translateY: self = .translateY + case .translateZ: self = .translateZ + default: + fatalError() + } + } + public func converted() -> CAValueFunction? { + switch self { + case .rotateX: .init(name: .rotateX) + case .rotateY: .init(name: .rotateY) + case .rotateZ: .init(name: .rotateZ) + case .scale: .init(name: .scale) + case .scaleX: .init(name: .scaleX) + case .scaleY: .init(name: .scaleY) + case .scaleZ: .init(name: .scaleZ) + case .translate: .init(name: .translate) + case .translateX: .init(name: .translateX) + case .translateY: .init(name: .translateY) + case .translateZ: .init(name: .translateZ) + } + } +} diff --git a/Sources/SDCALayer/Model/Layer/JCALayer.swift b/Sources/SDCALayer/Model/Layer/JCALayer.swift index 8aa2e4d..fade2df 100644 --- a/Sources/SDCALayer/Model/Layer/JCALayer.swift +++ b/Sources/SDCALayer/Model/Layer/JCALayer.swift @@ -137,6 +137,8 @@ open class JCALayer: CALayerConvertible, Codable { public var name: String? + public var animations: [String: SDCAAnimation]? + public init() {} public required convenience init(with object: CALayer) { @@ -153,6 +155,14 @@ open class JCALayer: CALayerConvertible, Codable { .forEach { target.addSublayer($0) } + + if let animations { + for (key, animation) in animations { + if let animation = animation.converted() { + target.add(animation, forKey: key) + } + } + } } open func applyProperties(with target: CALayer) { @@ -162,6 +172,19 @@ open class JCALayer: CALayerConvertible, Codable { self.sublayers = target.sublayers?.compactMap { SDCALayer(model: $0.codable()) } + + if let animationKeys = target.animationKeys(), + !animationKeys.isEmpty { + var animations: [String: SDCAAnimation] = [:] + for key in animationKeys { + guard let animation = target.animation(forKey: key), + let model = animation.codable() else { + continue + } + animations[key] = .init(model: model) + } + self.animations = animations + } } open func convertToLayer() -> CALayer? { diff --git a/Sources/SDCALayer/Protocol/CAAnimationConvertible.swift b/Sources/SDCALayer/Protocol/CAAnimationConvertible.swift new file mode 100644 index 0000000..33e2b0a --- /dev/null +++ b/Sources/SDCALayer/Protocol/CAAnimationConvertible.swift @@ -0,0 +1,20 @@ +// +// CAAnimationConvertible.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore + +public protocol CAAnimationConvertible: IndirectlyCodableModel where Target: CAAnimation { + func convertToAnimation() -> Target? +} + +public extension CAAnimationConvertible { + func converted() -> Target? { + self.convertToAnimation() + } +} + diff --git a/Sources/SDCALayer/Protocol/CALayerConvertible.swift b/Sources/SDCALayer/Protocol/CALayerConvertible.swift index 3868782..cf0b724 100644 --- a/Sources/SDCALayer/Protocol/CALayerConvertible.swift +++ b/Sources/SDCALayer/Protocol/CALayerConvertible.swift @@ -6,9 +6,9 @@ // // -import Foundation +import QuartzCore -public protocol CALayerConvertible: IndirectlyCodableModel { +public protocol CALayerConvertible: IndirectlyCodableModel where Target: CALayer { func convertToLayer() -> Target? } diff --git a/Sources/SDCALayer/SDCAAnimation.swift b/Sources/SDCALayer/SDCAAnimation.swift new file mode 100644 index 0000000..f84268e --- /dev/null +++ b/Sources/SDCALayer/SDCAAnimation.swift @@ -0,0 +1,94 @@ +// +// SDCAAnimation.swift +// +// +// Created by p-x9 on 2024/04/07. +// +// + +import QuartzCore +import IndirectlyCodable + +public class SDCAAnimation: CAAnimationConvertible { + public typealias Target = CAAnimation + + enum CodingKeys: String, CodingKey { + case `class`, animationModel + } + + public var `class`: String? + public var animationModel: (any CAAnimationConvertible)? + + static func animationModelClass(for className: String?) -> Codable.Type? { + guard let className, + let layerClass = NSClassFromString(className) as? any IndirectlyCodable.Type, + let layerModelClass = NSClassFromString(layerClass.codableTypeName) as? any Codable.Type else { + return nil + } + return layerModelClass + } + + public static func load(fromJSON json: String) -> SDCAAnimation? { + SDCAAnimation.value(fromJSON: json) + } + + public static func load(fromYAML yaml: String) -> SDCAAnimation? { + SDCAAnimation.value(fromYAML: yaml) + } + + public var json: String? { + self.jsonString + } + + public var yaml: String? { + self.yamlString + } + + public init(model: (any CAAnimationConvertible)) { + self.class = type(of: model).targetTypeName + self.animationModel = model + } + + public convenience init?(model: (any CAAnimationConvertible)?) { + guard let model else { return nil } + + self.init(model: model) + } + + public init(class: String?, model: any CAAnimationConvertible) { + self.class = `class` + self.animationModel = model + } + + public required convenience init(with object: CAAnimation) { + guard let model = object.codable() else { + fatalError("not supported Layer type") + } + self.init(model: model) + } + + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + `class` = try container.decodeIfPresent(String.self, forKey: .class) + guard let layerClass = Self.animationModelClass(for: `class`) as? any CAAnimationConvertible.Type else { + return + } + + animationModel = try container.decodeIfPresent(layerClass, forKey: .animationModel) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(`class`, forKey: .class) + + guard let animationModel else { + return + } + try container.encode(animationModel, forKey: .animationModel) + } + + public func convertToAnimation() -> Target? { + animationModel?.converted() as? CAAnimation + } +} diff --git a/Sources/SDCALayer/Util/PropertyMap.swift b/Sources/SDCALayer/Util/PropertyMap.swift index d2c5708..6d51c5c 100644 --- a/Sources/SDCALayer/Util/PropertyMap.swift +++ b/Sources/SDCALayer/Util/PropertyMap.swift @@ -77,6 +77,14 @@ extension PropertyMap.MappingElement { forwardMapElement = (sourceKeyPath, .init(destinationKeyPath)) reverseMapElement = (destinationKeyPath, .init(sourceKeyPath)) } + + public init( + _ sourceKeyPath: ReferenceWritableKeyPath, + _ destinationKeyPath: ReferenceWritableKeyPath + ) where SourceValue: Sequence, DestinationValue: Sequence, SourceValue.Element: IndirectlyCodable, DestinationValue.Element: IndirectlyCodableModel, SourceValue.Element.Model == DestinationValue.Element, SourceValue.Element == DestinationValue.Element.Target { + forwardMapElement = (sourceKeyPath, .init(destinationKeyPath)) + reverseMapElement = (destinationKeyPath, .init(sourceKeyPath)) + } } extension PropertyMap {