diff --git a/Sources/OpenSwiftUICore/Animation/Animation/Animation.swift b/Sources/OpenSwiftUICore/Animation/Animation/Animation.swift index 328868ceb..e199e2548 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/Animation.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/Animation.swift @@ -234,7 +234,7 @@ extension Animation: CustomStringConvertible, CustomDebugStringConvertible, Cust } public var customMirror: Mirror { - Mirror(box, children: ["base": box.base]) + Mirror(self, children: ["base": box.base]) } } diff --git a/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift b/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift index 290ed3cee..53253012d 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift @@ -108,7 +108,7 @@ public struct UnitCurve { case .circularEaseIn: return abs(progress / sqrt(1 - progress * progress)) case .circularEaseOut: - return abs((progress - 1.0) / sqrt(-(progress - 2.0) * (progress - 1.0))) + return abs((progress - 1.0) / sqrt(-(progress - 2.0) * progress)) case .circularEaseInOut: if progress >= 0.5 { return abs((progress + progress - 2) / sqrt(((progress * -4.0 + 8.0) * progress) - 3.0)) @@ -279,21 +279,21 @@ extension UnitCurve { } package func value(at time: Double) -> Double { - let t = solveX(time, epsilon: .ulpOfOne) - return round(t * (cy + t * (by + ay * t)) * pow(2, 20)) * .ulpOfOne + let t = solveX(time, epsilon: pow(2, -20)) + return round(t * (cy + t * (by + ay * t)) * pow(2, 20)) * pow(2, -20) } package func velocity(at time: Double) -> Double { - let t = solveX(time, epsilon: .ulpOfOne) - let x = cx + (bx + bx) * t + (ax * 3 * t) - let y = cy + (by + by) * t + (ay * 3 * t) + let t = solveX(time, epsilon: pow(2, -20)) + let x = cx + ((bx + bx) + (ax * 3 * t)) * t + let y = cy + ((by + by) + (ay * 3 * t)) * t guard x != y else { return 1.0 } guard x != 0 else { return y < 0 ? -.infinity : .infinity } - return round((y / x) * pow(2, 20)) * .ulpOfOne + return round((y / x) * pow(2, 20)) * pow(2, -20) } // TODO: Implemented by Copilot. Verify this via unit test later diff --git a/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompatibilityTests.swift new file mode 100644 index 000000000..2b263da53 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompatibilityTests.swift @@ -0,0 +1,19 @@ +// +// AnimationTests.swift +// OpenSwiftUICompatibilityTests + +import Testing + +struct AnimationCompatibilityTests { + @Test + func description() { + let animation = Animation.default + #expect(animation.description == "DefaultAnimation()") + #if OPENSWIFTUI_COMPATIBILITY_TEST + #expect(animation.debugDescription == "AnyAnimator(SwiftUI.DefaultAnimation())") + #else + #expect(animation.debugDescription == "AnyAnimator(OpenSwiftUICore.DefaultAnimation())") + #endif + #expect(animation.customMirror.description == "Mirror for Animation") + } +} diff --git a/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/UnitCurveCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/UnitCurveCompatibilityTests.swift new file mode 100644 index 000000000..3ee1225e4 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/UnitCurveCompatibilityTests.swift @@ -0,0 +1,250 @@ +// +// UnitCurveCompatibilityTests.swift +// OpenSwiftUICompatibilityTests + +import Testing + +// MARK: - UnitCurveTests + +struct UnitCurveCompatibilityTests { + @Test + func linearCurve() { + let curve = UnitCurve.linear + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.25)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.75)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 1.0)) + } + + @Test + func easeInCurve() { + let curve = UnitCurve.easeIn + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.09, absoluteTolerance: 0.01)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.32, absoluteTolerance: 0.01)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.62, absoluteTolerance: 0.01)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 0.67, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 1.07, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 1.37, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 0.0)) + } + + @Test + func easeOutCurve() { + let curve = UnitCurve.easeOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.38, absoluteTolerance: 0.01)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.68, absoluteTolerance: 0.01)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.91, absoluteTolerance: 0.01)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 1.0, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 1.37, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 1.07, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 0.67, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 0.0)) + } + + @Test + func easeInOutCurve() { + let curve = UnitCurve.easeInOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.13, absoluteTolerance: 0.01)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.87, absoluteTolerance: 0.01)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 1.06, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 1.72, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 1.06, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 0.0)) + } + + @Test + func circularEaseInCurve() { + let curve = UnitCurve.circularEaseIn + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.032, absoluteTolerance: 0.001)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.134, absoluteTolerance: 0.001)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.339, absoluteTolerance: 0.001)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 0.258, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 0.577, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 1.134, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: .infinity)) + } + + @Test + func circularEaseOutCurve() { + let curve = UnitCurve.circularEaseOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.661, absoluteTolerance: 0.001)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.866, absoluteTolerance: 0.001)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.968, absoluteTolerance: 0.001)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: .infinity)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 1.134, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 0.577, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 0.258, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 0.0)) + } + + @Test + func circularEaseInOutCurve() { + let curve = UnitCurve.circularEaseInOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.25).isApproximatelyEqual(to: 0.067, absoluteTolerance: 0.001)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.value(at: 0.75).isApproximatelyEqual(to: 0.933, absoluteTolerance: 0.001)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.velocity(at: 0.25).isApproximatelyEqual(to: 0.577, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: .infinity, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 0.75).isApproximatelyEqual(to: 0.577, absoluteTolerance: 0.001)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 0.0)) + } + + // MARK: - Bezier Curve Creation + + @Test + func bezierCurveCreation() { + let startPoint = UnitPoint(x: 0.25, y: 0.1) + let endPoint = UnitPoint(x: 0.75, y: 0.9) + let curve = UnitCurve.bezier(startControlPoint: startPoint, endControlPoint: endPoint) + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5, absoluteTolerance: 0.01)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 1.20, absoluteTolerance: 0.01)) + } + + @Test + func bezierCurveWithExtremeControlPoints() { + let startPoint = UnitPoint(x: -0.5, y: 2.0) + let endPoint = UnitPoint(x: 1.5, y: -1.0) + let curve = UnitCurve.bezier(startControlPoint: startPoint, endControlPoint: endPoint) + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: -1.0)) + } + + // MARK: - Value Function + + @Test + func valueAtBoundaryConditions() { + let curve = UnitCurve.easeInOut + + #expect(curve.value(at: -0.5).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.value(at: 1.5).isApproximatelyEqual(to: 1.0)) + } + + @Test + func valueAtVariousProgressPoints() { + let curve = UnitCurve.linear + + for progress in stride(from: 0.0, through: 1.0, by: 0.1) { + let value = curve.value(at: progress) + #expect(value >= 0.0) + #expect(value <= 1.0) + } + } + + // MARK: - Velocity Function + + @Test + func velocityAtBoundaryConditions() { + let curve = UnitCurve.linear + + #expect(curve.velocity(at: -0.5).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 0.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: 1.5).isApproximatelyEqual(to: 1.0)) + } + + @Test + func velocityForCircularCurves() { + let easeIn = UnitCurve.circularEaseIn + let easeOut = UnitCurve.circularEaseOut + let easeInOut = UnitCurve.circularEaseInOut + + #expect(easeIn.velocity(at: 0.5) > 0.0) + #expect(easeOut.velocity(at: 0.5) > 0.0) + #expect(easeInOut.velocity(at: 0.5) > 0.0) + } + + // MARK: - Inverse Property + + @Test + func linearCurveInverse() { + let curve = UnitCurve.linear + let inverse = curve.inverse + + #expect(inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(inverse.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + } + + @Test + func circularCurveInverse() { + let easeIn = UnitCurve.circularEaseIn + let easeOut = UnitCurve.circularEaseOut + + #expect(easeIn.inverse.value(at: 0.3).isApproximatelyEqual(to: easeOut.value(at: 0.3))) + #expect(easeIn.inverse.value(at: 0.7).isApproximatelyEqual(to: easeOut.value(at: 0.7))) + + #expect(easeIn.inverse.velocity(at: 0.3).isApproximatelyEqual(to: easeOut.velocity(at: 0.3))) + #expect(easeIn.inverse.velocity(at: 0.7).isApproximatelyEqual(to: easeOut.velocity(at: 0.7))) + } + + @Test + func curveInverse() { + let easeIn = UnitCurve.easeIn + let easeOut = UnitCurve.easeOut + + #expect(!easeIn.inverse.value(at: 0.3).isApproximatelyEqual(to: easeOut.value(at: 0.3))) + #expect(!easeIn.inverse.value(at: 0.7).isApproximatelyEqual(to: easeOut.value(at: 0.7))) + + #expect(!easeIn.inverse.velocity(at: 0.3).isApproximatelyEqual(to: easeOut.velocity(at: 0.3))) + #expect(!easeIn.inverse.velocity(at: 0.7).isApproximatelyEqual(to: easeOut.velocity(at: 0.7))) + } + + @Test + func bezierCurveInverse() { + let startPoint = UnitPoint(x: 0.25, y: 0.1) + let endPoint = UnitPoint(x: 0.75, y: 0.9) + let curve = UnitCurve.bezier(startControlPoint: startPoint, endControlPoint: endPoint) + + let inverseStartPoint = UnitPoint(x: 0.1, y: 0.25) + let inverseEndPoint = UnitPoint(x: 0.9, y: 0.75) + let inverse = UnitCurve.bezier(startControlPoint: inverseStartPoint, endControlPoint: inverseEndPoint) + + #expect(inverse.value(at: 0.3).isAlmostEqual(to: curve.inverse.value(at: 0.3))) + #expect(inverse.value(at: 0.7).isAlmostEqual(to: curve.inverse.value(at: 0.7))) + } +} diff --git a/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift new file mode 100644 index 000000000..10edebd0a --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift @@ -0,0 +1,79 @@ +// +// UnitCurveTests.swift +// OpenSwiftUICoreTests + +@testable import OpenSwiftUICore +import Testing + +// MARK: - UnitCurveTests + +struct UnitCurveTests { + // MARK: - CubicSolver + + @Test + func cubicSolverInitialization() { + let startPoint = UnitPoint(x: 0.25, y: 0.1) + let endPoint = UnitPoint(x: 0.75, y: 0.9) + let solver = UnitCurve.CubicSolver(startControlPoint: startPoint, endControlPoint: endPoint) + + #expect(solver.startControlPoint.x.isApproximatelyEqual(to: 0.25)) + #expect(solver.startControlPoint.y.isApproximatelyEqual(to: 0.1)) + #expect(solver.endControlPoint.x.isApproximatelyEqual(to: 0.75)) + #expect(solver.endControlPoint.y.isApproximatelyEqual(to: 0.9)) + } + + @Test + func cubicSolverValueCalculation() { + let startPoint = UnitPoint(x: 0.25, y: 0.1) + let endPoint = UnitPoint(x: 0.75, y: 0.9) + let solver = UnitCurve.CubicSolver(startControlPoint: startPoint, endControlPoint: endPoint) + + #expect(solver.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(solver.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(solver.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + } + + @Test + func cubicSolverVelocityCalculation() { + let startPoint = UnitPoint(x: 0.25, y: 0.1) + let endPoint = UnitPoint(x: 0.75, y: 0.9) + let solver = UnitCurve.CubicSolver(startControlPoint: startPoint, endControlPoint: endPoint) + #expect(solver.velocity(at: 0.5).isApproximatelyEqual(to: 1.199, absoluteTolerance: 0.001)) + } + + // MARK: - Edge Cases + + @Test + func extremeProgressValues() { + let curve = UnitCurve.easeInOut + + #expect(curve.value(at: -1000.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1000.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.velocity(at: -1000.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.velocity(at: 1000.0).isApproximatelyEqual(to: 0.0)) + } + + @Test + func bezierWithIdenticalControlPoints() { + let point = UnitPoint(x: 0.5, y: 0.5) + let curve = UnitCurve.bezier(startControlPoint: point, endControlPoint: point) + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.velocity(at: 0.5).isApproximatelyEqual(to: 1.0)) + } + + @Test + func circularEaseInOutTransition() { + let curve = UnitCurve.circularEaseInOut + + let belowMid = curve.value(at: 0.49) + let mid = curve.value(at: 0.5) + let aboveMid = curve.value(at: 0.51) + + #expect(belowMid < mid) + #expect(mid < aboveMid) + #expect(mid.isApproximatelyEqual(to: 0.5)) + } +}