From 3693b5553533a941dc3256c8df4ecee85a2f9736 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Jul 2025 02:13:40 +0800 Subject: [PATCH 1/4] Add AnimationTests --- .../Animation/Animation/Animation.swift | 2 +- .../Animation/Animation/AnimationTests.swift | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift 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/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift new file mode 100644 index 000000000..2b6b7d829 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift @@ -0,0 +1,19 @@ +// +// AnimationTests.swift +// OpenSwiftUICompatibilityTests + +import Testing + +struct AnimationTests { + @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") + } +} From b48abd63f16fae1db1196c281476977fad4f7c1c Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Jul 2025 02:40:13 +0800 Subject: [PATCH 2/4] Add UnitCurveTests and fix bug in UnitCurve --- .../Animation/Animation/UnitCurve.swift | 10 +- .../Animation/Animation/UnitCurveTests.swift | 309 ++++++++++++++++++ 2 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift diff --git a/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift b/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift index 290ed3cee..860bcd96c 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,12 +279,12 @@ 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 t = solveX(time, epsilon: pow(2, -20)) let x = cx + (bx + bx) * t + (ax * 3 * t) let y = cy + (by + by) * t + (ay * 3 * t) guard x != y else { @@ -293,7 +293,7 @@ extension UnitCurve { 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/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift new file mode 100644 index 000000000..fa185adc4 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift @@ -0,0 +1,309 @@ +// +// UnitCurveTests.swift +// OpenSwiftUICoreTests + +@testable import OpenSwiftUICore +import Testing + +// MARK: - UnitCurveTests [Implmeneted by Copilot] + +struct UnitCurveTests { + // MARK: - Static Curve Properties + + @Test + func linearCurve() { + let curve = UnitCurve.linear + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #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.5).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: 1.0).isApproximatelyEqual(to: 1.0)) + + let midValue = curve.value(at: 0.5) + #expect(midValue > 0.0) + #expect(midValue < 0.5) + } + + @Test + func easeOutCurve() { + let curve = UnitCurve.easeOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + let midValue = curve.value(at: 0.5) + #expect(midValue > 0.5) + #expect(midValue < 1.0) + } + + @Test + func easeInOutCurve() { + let curve = UnitCurve.easeInOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + let quarterValue = curve.value(at: 0.25) + #expect(quarterValue > 0.0) + #expect(quarterValue < 0.25) + + let threeQuarterValue = curve.value(at: 0.75) + #expect(threeQuarterValue > 0.75) + #expect(threeQuarterValue < 1.0) + } + + @Test + func easeInEaseOutDeprecated() { + let curve = UnitCurve.easeInEaseOut + let easeInOut = UnitCurve.easeInOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: easeInOut.value(at: 0.0))) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: easeInOut.value(at: 0.5))) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: easeInOut.value(at: 1.0))) + } + + @Test + func circularEaseInCurve() { + let curve = UnitCurve.circularEaseIn + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + let midValue = curve.value(at: 0.5) + #expect(midValue > 0.0) + #expect(midValue < 0.5) + } + + @Test + func circularEaseOutCurve() { + let curve = UnitCurve.circularEaseOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + + let midValue = curve.value(at: 0.5) + #expect(midValue > 0.5) + #expect(midValue < 1.0) + } + + @Test + func circularEaseInOutCurve() { + let curve = UnitCurve.circularEaseInOut + + #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.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)) + } + + @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)) + } + + // 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 + let easeInOut = UnitCurve.circularEaseInOut + + #expect(easeIn.inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(easeIn.inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(easeOut.inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(easeOut.inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + #expect(easeInOut.inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(easeInOut.inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + } + + @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 inverse = curve.inverse + + #expect(inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) + #expect(inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) + } + + // 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)) + + let midValue = solver.value(at: 0.5) + #expect(midValue >= 0.0) + #expect(midValue <= 1.0) + } + + @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) + + let velocity = solver.velocity(at: 0.5) + #expect(velocity.isFinite) + } + + // MARK: - Hashable and Sendable + + @Test + func hashableConformance() { + let curve1 = UnitCurve.linear + let curve2 = UnitCurve.linear + let curve3 = UnitCurve.easeIn + + #expect(curve1.hashValue == curve2.hashValue) + #expect(curve1.hashValue != curve3.hashValue) + } + + @Test + func equatableConformance() { + let curve1 = UnitCurve.linear + let curve2 = UnitCurve.linear + let curve3 = UnitCurve.easeIn + + #expect(curve1 == curve2) + #expect(curve1 != curve3) + } + + // 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) >= 0.0) + #expect(curve.velocity(at: 1000.0) >= 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: 0.5).isApproximatelyEqual(to: 0.5)) + #expect(curve.value(at: 1.0).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)) + } +} From cee39af42b8b4c6c8875fe59228e3a67fbdce088 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Jul 2025 03:06:46 +0800 Subject: [PATCH 3/4] Add UnitCurveCompatibilityTests --- ...wift => AnimationCompatibilityTests.swift} | 2 +- .../UnitCurveCompatibilityTests.swift | 250 ++++++++++++++++++ .../Animation/Animation/UnitCurveTests.swift | 244 +---------------- 3 files changed, 258 insertions(+), 238 deletions(-) rename Tests/OpenSwiftUICompatibilityTests/Animation/Animation/{AnimationTests.swift => AnimationCompatibilityTests.swift} (93%) create mode 100644 Tests/OpenSwiftUICompatibilityTests/Animation/Animation/UnitCurveCompatibilityTests.swift diff --git a/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompatibilityTests.swift similarity index 93% rename from Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift rename to Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompatibilityTests.swift index 2b6b7d829..2b263da53 100644 --- a/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationTests.swift +++ b/Tests/OpenSwiftUICompatibilityTests/Animation/Animation/AnimationCompatibilityTests.swift @@ -4,7 +4,7 @@ import Testing -struct AnimationTests { +struct AnimationCompatibilityTests { @Test func description() { let animation = Animation.default 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 index fa185adc4..bef5bf669 100644 --- a/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift +++ b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift @@ -5,213 +5,9 @@ @testable import OpenSwiftUICore import Testing -// MARK: - UnitCurveTests [Implmeneted by Copilot] +// MARK: - UnitCurveTests struct UnitCurveTests { - // MARK: - Static Curve Properties - - @Test - func linearCurve() { - let curve = UnitCurve.linear - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) - #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.5).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: 1.0).isApproximatelyEqual(to: 1.0)) - - let midValue = curve.value(at: 0.5) - #expect(midValue > 0.0) - #expect(midValue < 0.5) - } - - @Test - func easeOutCurve() { - let curve = UnitCurve.easeOut - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - - let midValue = curve.value(at: 0.5) - #expect(midValue > 0.5) - #expect(midValue < 1.0) - } - - @Test - func easeInOutCurve() { - let curve = UnitCurve.easeInOut - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) - #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - - let quarterValue = curve.value(at: 0.25) - #expect(quarterValue > 0.0) - #expect(quarterValue < 0.25) - - let threeQuarterValue = curve.value(at: 0.75) - #expect(threeQuarterValue > 0.75) - #expect(threeQuarterValue < 1.0) - } - - @Test - func easeInEaseOutDeprecated() { - let curve = UnitCurve.easeInEaseOut - let easeInOut = UnitCurve.easeInOut - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: easeInOut.value(at: 0.0))) - #expect(curve.value(at: 0.5).isApproximatelyEqual(to: easeInOut.value(at: 0.5))) - #expect(curve.value(at: 1.0).isApproximatelyEqual(to: easeInOut.value(at: 1.0))) - } - - @Test - func circularEaseInCurve() { - let curve = UnitCurve.circularEaseIn - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - - let midValue = curve.value(at: 0.5) - #expect(midValue > 0.0) - #expect(midValue < 0.5) - } - - @Test - func circularEaseOutCurve() { - let curve = UnitCurve.circularEaseOut - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - - let midValue = curve.value(at: 0.5) - #expect(midValue > 0.5) - #expect(midValue < 1.0) - } - - @Test - func circularEaseInOutCurve() { - let curve = UnitCurve.circularEaseInOut - - #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) - #expect(curve.value(at: 1.0).isApproximatelyEqual(to: 1.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)) - } - - @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)) - } - - // 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 - let easeInOut = UnitCurve.circularEaseInOut - - #expect(easeIn.inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(easeIn.inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - #expect(easeOut.inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(easeOut.inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - #expect(easeInOut.inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(easeInOut.inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - } - - @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 inverse = curve.inverse - - #expect(inverse.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(inverse.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - } - // MARK: - CubicSolver @Test @@ -234,10 +30,7 @@ struct UnitCurveTests { #expect(solver.value(at: 0.0).isApproximatelyEqual(to: 0.0)) #expect(solver.value(at: 1.0).isApproximatelyEqual(to: 1.0)) - - let midValue = solver.value(at: 0.5) - #expect(midValue >= 0.0) - #expect(midValue <= 1.0) + #expect(solver.value(at: 0.5).isApproximatelyEqual(to: 0.5)) } @Test @@ -245,31 +38,7 @@ struct UnitCurveTests { 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) - - let velocity = solver.velocity(at: 0.5) - #expect(velocity.isFinite) - } - - // MARK: - Hashable and Sendable - - @Test - func hashableConformance() { - let curve1 = UnitCurve.linear - let curve2 = UnitCurve.linear - let curve3 = UnitCurve.easeIn - - #expect(curve1.hashValue == curve2.hashValue) - #expect(curve1.hashValue != curve3.hashValue) - } - - @Test - func equatableConformance() { - let curve1 = UnitCurve.linear - let curve2 = UnitCurve.linear - let curve3 = UnitCurve.easeIn - - #expect(curve1 == curve2) - #expect(curve1 != curve3) + #expect(solver.velocity(at: 0.5).isApproximatelyEqual(to: 0.399, absoluteTolerance: 0.001)) } // MARK: - Edge Cases @@ -280,8 +49,8 @@ struct UnitCurveTests { #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) >= 0.0) - #expect(curve.velocity(at: 1000.0) >= 0.0) + #expect(curve.velocity(at: -1000.0).isApproximatelyEqual(to: 0.0)) + #expect(curve.velocity(at: 1000.0).isApproximatelyEqual(to: 0.0)) } @Test @@ -290,8 +59,9 @@ struct UnitCurveTests { let curve = UnitCurve.bezier(startControlPoint: point, endControlPoint: point) #expect(curve.value(at: 0.0).isApproximatelyEqual(to: 0.0)) - #expect(curve.value(at: 0.5).isApproximatelyEqual(to: 0.5)) #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 From e99245e3a66cd4bb67583e0c7e4cb7f49e3df65e Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 14 Jul 2025 03:53:51 +0800 Subject: [PATCH 4/4] Fix Curve velocity bug --- Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift | 4 ++-- .../Animation/Animation/UnitCurveTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift b/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift index 860bcd96c..53253012d 100644 --- a/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift +++ b/Sources/OpenSwiftUICore/Animation/Animation/UnitCurve.swift @@ -285,8 +285,8 @@ extension UnitCurve { package func velocity(at time: Double) -> Double { let t = solveX(time, epsilon: pow(2, -20)) - let x = cx + (bx + bx) * t + (ax * 3 * t) - let y = cy + (by + by) * t + (ay * 3 * t) + 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 } diff --git a/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift index bef5bf669..10edebd0a 100644 --- a/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift +++ b/Tests/OpenSwiftUICoreTests/Animation/Animation/UnitCurveTests.swift @@ -38,7 +38,7 @@ struct UnitCurveTests { 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: 0.399, absoluteTolerance: 0.001)) + #expect(solver.velocity(at: 0.5).isApproximatelyEqual(to: 1.199, absoluteTolerance: 0.001)) } // MARK: - Edge Cases