diff --git a/Sources/OpenSwiftUICore/Animation/AnimatableArray.swift b/Sources/OpenSwiftUICore/Animation/AnimatableArray.swift new file mode 100644 index 000000000..dd3b2a04a --- /dev/null +++ b/Sources/OpenSwiftUICore/Animation/AnimatableArray.swift @@ -0,0 +1,68 @@ +// +// AnimatableArray.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete + +package struct AnimatableArray: VectorArithmetic where Element: VectorArithmetic { + package var elements: [Element] + + package init(_ elements: [Element]) { + self.elements = elements + } + + package static var zero: AnimatableArray { .init([]) } + + package static func += (lhs: inout AnimatableArray, rhs: AnimatableArray) { + let count = Swift.min(lhs.elements.count, rhs.elements.count) + for i in 0.., rhs: AnimatableArray) { + let count = Swift.min(lhs.elements.count, rhs.elements.count) + for i in 0.., rhs: AnimatableArray) -> AnimatableArray { + var result = lhs + result += rhs + return result + } + + @_transparent + package static func - (lhs: AnimatableArray, rhs: AnimatableArray) -> AnimatableArray { + var result = lhs + result -= rhs + return result + } + + package mutating func scale(by rhs: Double) { + for i in elements.indices { + elements[i].scale(by: rhs) + } + } + + package var magnitudeSquared: Double { + elements.reduce(0) { partialResult, element in + partialResult + element.magnitudeSquared + } + } +} + +extension Array where Element: Animatable { + package var animatableData: AnimatableArray { + get { AnimatableArray(map(\.animatableData)) } + set { + let count = Swift.min(count, newValue.elements.count) + for i in 0..: VectorArithmetic where First: VectorArithmetic, Second: VectorArithmetic { + /// The first value. public var first: First + + /// The second value. public var second: Second - + + /// Creates an animated pair with the provided values. @inlinable public init(_ first: First, _ second: Second) { self.first = first @@ -18,15 +22,14 @@ public struct AnimatablePair: VectorArithmetic where First: Vecto } @inlinable - subscript() -> (First, Second) { + package subscript() -> (First, Second) { get { (first, second) } set { (first, second) = newValue } } @_transparent public static var zero: AnimatablePair { - @_transparent - get { .init(First.zero, Second.zero) } + .init(First.zero, Second.zero) } @_transparent @@ -57,13 +60,11 @@ public struct AnimatablePair: VectorArithmetic where First: Vecto second.scale(by: rhs) } + /// The dot-product of this animated pair with itself. @_transparent public var magnitudeSquared: Double { - @_transparent - get { first.magnitudeSquared + second.magnitudeSquared } - } - - public static func == (a: AnimatablePair, b: AnimatablePair) -> Bool { - a.first == b.first && a.second == b.second + first.magnitudeSquared + second.magnitudeSquared } } + +extension AnimatablePair: Sendable where First: Sendable, Second: Sendable {} diff --git a/Sources/OpenSwiftUICore/Animation/AnyAnimatableData.swift b/Sources/OpenSwiftUICore/Animation/AnyAnimatableData.swift new file mode 100644 index 000000000..2af746581 --- /dev/null +++ b/Sources/OpenSwiftUICore/Animation/AnyAnimatableData.swift @@ -0,0 +1,142 @@ +// +// AnyAnimatableData.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete +// ID: 7ABB4C511D8E2C0F1768F58E8C14509E (SwiftUICore) + +@frozen +public struct _AnyAnimatableData: VectorArithmetic { + package var vtable: _AnyAnimatableDataVTable.Type + package var value: Any + + @inline(__always) + init(vtable: _AnyAnimatableDataVTable.Type, value: Any) { + self.vtable = vtable + self.value = value + } + + package init(_ container: T) where T: Animatable { + vtable = VTable.self + value = container.animatableData + } + + package func update(_ container: inout T) where T: Animatable { + guard vtable == VTable.self else { return } + container.animatableData = value as! T.AnimatableData + } + + public static var zero: _AnyAnimatableData { + _AnyAnimatableData(vtable: ZeroVTable.self, value: ZeroVTable.zero) + } + + public static func == (lhs: _AnyAnimatableData, rhs: _AnyAnimatableData) -> Bool { + if lhs.vtable == rhs.vtable { + lhs.vtable.isEqual(lhs.value, rhs.value) + } else { + false + } + } + + public static func += (lhs: inout _AnyAnimatableData, rhs: _AnyAnimatableData) { + if lhs.vtable == rhs.vtable { + lhs.vtable.add(&lhs.value, rhs.value) + } else if lhs.vtable == ZeroVTable.self { + lhs = rhs + } + } + + public static func -= (lhs: inout _AnyAnimatableData, rhs: _AnyAnimatableData) { + if lhs.vtable == rhs.vtable { + lhs.vtable.subtract(&lhs.value, rhs.value) + } else if lhs.vtable == ZeroVTable.self { + lhs = rhs + lhs.vtable.negate(&lhs.value) + } + } + + @_transparent + public static func + (lhs: _AnyAnimatableData, rhs: _AnyAnimatableData) -> _AnyAnimatableData { + var ret = lhs + ret += rhs + return ret + } + + @_transparent + public static func - (lhs: _AnyAnimatableData, rhs: _AnyAnimatableData) -> _AnyAnimatableData { + var ret = lhs + ret -= rhs + return ret + } + + public mutating func scale(by rhs: Double) { + vtable.scale(&value, by: rhs) + } + + public var magnitudeSquared: Double { + vtable.magnitudeSquared(value) + } +} + +@available(*, unavailable) +extension _AnyAnimatableData: Sendable {} + +@usableFromInline +package class _AnyAnimatableDataVTable { + package class var zero: Any { + preconditionFailure("") + } + + package class func isEqual(_ lhs: Any, _ rhs: Any) -> Bool { false } + package class func add(_ lhs: inout Any, _ rhs: Any) {} + package class func subtract(_ lhs: inout Any, _ rhs: Any) {} + package class func negate(_ lhs: inout Any) {} + package class func scale(_ lhs: inout Any, by rhs: Double) {} + package class func magnitudeSquared(_ lhs: Any) -> Double { .zero } +} + +@available(*, unavailable) +extension _AnyAnimatableDataVTable: Sendable {} + +private final class VTable: _AnyAnimatableDataVTable where Value: Animatable { + override class var zero: Any { + Value.AnimatableData.zero + } + + override class func isEqual(_ lhs: Any, _ rhs: Any) -> Bool { + lhs as! Value.AnimatableData == rhs as! Value.AnimatableData + } + + override class func add(_ lhs: inout Any, _ rhs: Any) { + var value = lhs as! Value.AnimatableData + value += rhs as! Value.AnimatableData + lhs = value + } + + override class func subtract(_ lhs: inout Any, _ rhs: Any) { + var value = lhs as! Value.AnimatableData + value -= rhs as! Value.AnimatableData + lhs = value + } + + override class func negate(_ lhs: inout Any) { + var value = lhs as! Value.AnimatableData + value = .zero - value + lhs = value + } + + override class func scale(_ lhs: inout Any, by rhs: Double) { + var value = lhs as! Value.AnimatableData + value.scale(by: rhs) + lhs = value + } + + override class func magnitudeSquared(_ lhs: Any) -> Double { + (lhs as! Value.AnimatableData).magnitudeSquared + } +} + +private final class ZeroVTable: _AnyAnimatableDataVTable { + override class var zero: Any { () } +} diff --git a/Sources/OpenSwiftUICore/Animation/EmptyAnimatableData.swift b/Sources/OpenSwiftUICore/Animation/EmptyAnimatableData.swift index f809f31d1..e4c0ead49 100644 --- a/Sources/OpenSwiftUICore/Animation/EmptyAnimatableData.swift +++ b/Sources/OpenSwiftUICore/Animation/EmptyAnimatableData.swift @@ -1,10 +1,14 @@ // // EmptyAnimatableData.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete +/// An empty type for animatable data. +/// +/// This type is suitable for use as the `animatableData` property of +/// types that do not have any animatable properties. @frozen public struct EmptyAnimatableData: VectorArithmetic { @inlinable @@ -33,3 +37,13 @@ public struct EmptyAnimatableData: VectorArithmetic { public static func == (_: EmptyAnimatableData, _: EmptyAnimatableData) -> Bool { true } } + +public import Foundation + +extension Double: Animatable { + public typealias AnimatableData = Swift.Double +} + +extension CGFloat: Animatable { + public typealias AnimatableData = CGFloat +} diff --git a/Sources/OpenSwiftUICore/Animation/Animation.swift b/Sources/OpenSwiftUICore/Animation/TODO/Animation.swift similarity index 100% rename from Sources/OpenSwiftUICore/Animation/Animation.swift rename to Sources/OpenSwiftUICore/Animation/TODO/Animation.swift diff --git a/Sources/OpenSwiftUICore/Animation/VectorArithmetic.swift b/Sources/OpenSwiftUICore/Animation/VectorArithmetic.swift index d665f6d2c..71cfb78bc 100644 --- a/Sources/OpenSwiftUICore/Animation/VectorArithmetic.swift +++ b/Sources/OpenSwiftUICore/Animation/VectorArithmetic.swift @@ -1,18 +1,29 @@ // // VectorArithmetic.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for RELEASE_2023 +// Audited for iOS 18.0 // Status: Complete public import Foundation +/// A type that can serve as the animatable data of an animatable type. +/// +/// `VectorArithmetic` extends the `AdditiveArithmetic` protocol with scalar +/// multiplication and a way to query the vector magnitude of the value. Use +/// this type as the `animatableData` associated type of a type that conforms to +/// the ``Animatable`` protocol. public protocol VectorArithmetic: AdditiveArithmetic { + /// Multiplies each component of this value by the given value. mutating func scale(by rhs: Double) + + /// Returns the dot-product of this vector arithmetic instance with itself. var magnitudeSquared: Double { get } } extension VectorArithmetic { + /// Returns a value with each component of this value multiplied by the + /// given value. @_alwaysEmitIntoClient public func scaled(by rhs: Double) -> Self { var result = self @@ -20,9 +31,11 @@ extension VectorArithmetic { return result } + /// Interpolates this value with `other` by the specified `amount`. + /// + /// This is equivalent to `self = self + (other - self) * amount`. @_alwaysEmitIntoClient public mutating func interpolate(towards other: Self, amount: Double) { - // lhs + (rhs - lhs) * t var result = other result -= self result.scale(by: amount) @@ -30,6 +43,9 @@ extension VectorArithmetic { self = result } + /// Returns this value interpolated with `other` by the specified `amount`. + /// + /// This result is equivalent to `self + (other - self) * amount`. @_alwaysEmitIntoClient public func interpolated(towards other: Self, amount: Double) -> Self { var result = self @@ -41,21 +57,17 @@ extension VectorArithmetic { extension Float: VectorArithmetic { @_transparent public mutating func scale(by rhs: Double) { self *= Float(rhs) } + @_transparent - public var magnitudeSquared: Double { - @_transparent - get { Double(self * self) } - } + public var magnitudeSquared: Double { Double(self * self) } } extension Double: VectorArithmetic { @_transparent public mutating func scale(by rhs: Double) { self *= rhs } + @_transparent - public var magnitudeSquared: Double { - @_transparent - get { self * self } - } + public var magnitudeSquared: Double { self * self } } extension CGFloat: VectorArithmetic { @@ -63,8 +75,20 @@ extension CGFloat: VectorArithmetic { public mutating func scale(by rhs: Double) { self *= CGFloat(rhs) } @_transparent - public var magnitudeSquared: Double { - @_transparent - get { Double(self * self) } - } + public var magnitudeSquared: Double { Double(self * self) } +} + +package func mix(_ lhs: T, _ rhs: T, by t: Double) -> T where T: VectorArithmetic { + var result = rhs + result -= lhs + result.scale(by: t) + result += lhs + return result +} + +extension VectorArithmetic { + package static var unitScale: Double { 128.0 } + package static var inverseUnitScale: Swift.Double { 1 / unitScale } + package mutating func applyUnitScale() { scale(by: Self.unitScale) } + package mutating func unapplyUnitScale() { scale(by: Self.inverseUnitScale) } } diff --git a/Sources/OpenSwiftUICore/Animation/_VectorMath.swift b/Sources/OpenSwiftUICore/Animation/_VectorMath.swift index bdf5d7e9d..3f694b032 100644 --- a/Sources/OpenSwiftUICore/Animation/_VectorMath.swift +++ b/Sources/OpenSwiftUICore/Animation/_VectorMath.swift @@ -1,10 +1,12 @@ // // _VectorMath.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 +// Audited for iOS 18.0 // Status: Complete +/// Adds the "vector space" numeric operations for any type that +/// conforms to Animatable. public protocol _VectorMath: Animatable {} extension _VectorMath { @@ -73,3 +75,18 @@ extension _VectorMath { return result } } + +extension _VectorMath { + package mutating func normalize() { + let magnitudeSquared = animatableData.magnitudeSquared + if magnitudeSquared != 0 { + self *= (1.0 / magnitudeSquared) + } + } + + package func normalized() -> Self { + var result = self + result.normalize() + return result + } +} diff --git a/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift b/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift index 38507f569..9d36d6058 100644 --- a/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift +++ b/Sources/OpenSwiftUICore/Graphic/Color/ColorResolved.swift @@ -150,21 +150,28 @@ extension Color.Resolved : Animatable { // ResolvedGradient.Color.Space.convertIn(self) preconditionFailure("TODO") } else { - let factor: Float = 128.0 - return AnimatablePair(linearRed * factor, AnimatablePair(linearGreen * factor, AnimatablePair(linearBlue * factor, opacity * factor))) + return AnimatablePair( + linearRed.scaled(by: .unitScale), + AnimatablePair( + linearGreen.scaled(by: .unitScale), + AnimatablePair( + linearBlue.scaled(by: .unitScale), + opacity.scaled(by: .unitScale) + ) + ) + ) } } set { - let factor: Float = 0.0078125 if Self.legacyInterpolation { // ResolvedGradient.Color.Space.convertOut(self) preconditionFailure("TODO") } else { - linearRed = newValue.first * factor - linearGreen = newValue.second.first * factor - linearBlue = newValue.second.second.first * factor - opacity = newValue.second.second.second * factor + linearRed = newValue.first.scaled(by: .inverseUnitScale) + linearGreen = newValue.second.first.scaled(by: .inverseUnitScale) + linearBlue = newValue.second.second.first.scaled(by: .inverseUnitScale) + opacity = newValue.second.second.second.scaled(by: .inverseUnitScale) } } } @@ -173,13 +180,20 @@ extension Color.Resolved : Animatable { extension Color.ResolvedVibrant: Animatable { package var animatableData: AnimatablePair>> { get { - let factor: Float = 128.0 - return AnimatablePair(scale * factor, AnimatablePair(bias.0 * factor, AnimatablePair(bias.1 * factor, bias.2 * factor))) + AnimatablePair( + scale.scaled(by: .unitScale), + AnimatablePair( + bias.0.scaled(by: .unitScale), + AnimatablePair( + bias.1.scaled(by: .unitScale), + bias.2.scaled(by: .unitScale) + ) + ) + ) } set { - let factor: Float = 0.0078125 - scale = newValue.first * factor - bias = (newValue.second.first * factor, newValue.second.second.first * factor, newValue.second.second.second * factor) + scale = newValue.first.scaled(by: .inverseUnitScale) + bias = (newValue.second.first.scaled(by: .inverseUnitScale), newValue.second.second.first.scaled(by: .inverseUnitScale), newValue.second.second.second.scaled(by: .inverseUnitScale)) } } } diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift b/Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift index 767010cc8..cc2b082b7 100644 --- a/Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/Angle.swift @@ -73,10 +73,10 @@ extension Angle: Animatable, _VectorMath { public typealias AnimatableData = Double public var animatableData: AnimatableData { - get { radians * 128.0 } - set { radians = newValue / 128.0 } + get { radians.scaled(by: .unitScale) } + set { radians = newValue.scaled(by: .inverseUnitScale) } } - + @inlinable public static var zero: Angle { .init() } } diff --git a/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift b/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift index 0b5b5b1b9..1ed495e62 100644 --- a/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift +++ b/Sources/OpenSwiftUICore/Layout/Geometry/UnitPoint.swift @@ -220,8 +220,8 @@ extension UnitPoint { extension UnitPoint: Animatable { public var animatableData: AnimatablePair { - get { AnimatablePair(x * 128.0, y * 128.0) } - set { x = newValue.first / 128.0 ; y = newValue.second / 128.0 } + get { AnimatablePair(x.scaled(by: .unitScale), x.scaled(by: .unitScale)) } + set { x = newValue.first.scaled(by: .inverseUnitScale) ; y = newValue.second.scaled(by: .inverseUnitScale) } } } diff --git a/Tests/OpenSwiftUICompatibilityTests/View/Graphic/AngleTests.swift b/Tests/OpenSwiftUICompatibilityTests/View/Graphic/AngleTests.swift index 7ae44f16e..e07672c17 100644 --- a/Tests/OpenSwiftUICompatibilityTests/View/Graphic/AngleTests.swift +++ b/Tests/OpenSwiftUICompatibilityTests/View/Graphic/AngleTests.swift @@ -9,7 +9,7 @@ struct AngleTests { let a1 = Angle(radians: radians) #expect(a1.radians == radians) #expect(a1.degrees == degrees) - #expect(a1.animatableData == radians * 128) + #expect(a1.animatableData == radians * 128.0) let a2 = Angle(degrees: degrees) #expect(a2.radians == radians) #expect(a2.degrees == degrees)