Hi, I a have some code but doesn’t do what I expect it to do. The code is supposed to animate different shapes into each other. Here is my code:
(FYI: I describe each shape with curves going from the leading to the trailing of the frame. Then the code automatically closes the path.)
struct Curve {
var destination: CGPoint
var control1: CGPoint
var control2: CGPoint
}
/// A landscape shape defined by a start y-coordinate and an array of curves describing the surface of the landscape
struct AnimatableLandscape: View {
var startY: Double
var curves: [Curve]
init(startY: Double, curves: [Curve]) {
self.startY = startY
// Add lines to close the path of the shape
var newCurves = curves
// If the last destination isn't at the bottom of the frame, draw a line straight down
if let last = newCurves.last, last.destination.y != 1 {
newCurves.append(Curve(destination: CGPoint(x: last.destination.x, y: 1), control1: CGPoint(x: last.destination.x, y: (1+last.destination.y)/2), control2: CGPoint(x: last.destination.x, y: (1+last.destination.y)/2)))
}
// Draw a line to the bottom-leftcorner
newCurves.append(Curve(destination: CGPoint(x: 0, y: 1), control1: CGPoint(x: 0.5, y: 1), control2: CGPoint(x: 0.5, y: 1)))
// If needed, draw a straight line to the start y-coordinate of the first curve
if startY != 1 {
newCurves.append(Curve(destination: CGPoint(x: 0, y: startY), control1: CGPoint(x: 0, y: (1+startY)/2), control2: CGPoint(x: 0, y: (1+startY)/2)))
}
self.curves = newCurves
}
var body: some View {
AnimatableBezier(
yStartPoint: startY,
xDestinations: curves.map {$0.destination.x},
yDestinations: curves.map {$0.destination.y},
xControl1: curves.map {$0.control1.x},
yControl1: curves.map {$0.control1.y},
xControl2: curves.map {$0.control2.x},
yControl2: curves.map {$0.control2.y}
)
}
}
/// An animatable bezier shape which is defined by a list of destinations and their control points with their x- and y-coordinates passed through in seperate arrays
struct AnimatableBezier: Shape {
// Properties
// Define only the y-coordinate of the start point as the bezier should always start at x zero
var yStartPoint: Double
// Define the x- and y-coordinates as different properties to calculate these values seperately using AnimatableValues
var xDestinations: AnimatableValues
var yDestinations: AnimatableValues
var xControl1: AnimatableValues
var yControl1: AnimatableValues
var xControl2: AnimatableValues
var yControl2: AnimatableValues
init(yStartPoint: Double, xDestinations: [Double], yDestinations: [Double], xControl1: [Double], yControl1: [Double], xControl2: [Double], yControl2: [Double]) {
self.yStartPoint = yStartPoint
self.xDestinations = AnimatableValues(xDestinations)
self.yDestinations = AnimatableValues(yDestinations)
self.xControl1 = AnimatableValues(xControl1)
self.yControl1 = AnimatableValues(yControl1)
self.xControl2 = AnimatableValues(xControl2)
self.yControl2 = AnimatableValues(yControl2)
}
// Declare the animatable data
typealias Points = AnimatablePair<AnimatableValues, AnimatableValues>
typealias Curves = AnimatablePair<Points, AnimatablePair<Points, Points>>
var animatableData: AnimatablePair<Double, Curves> {
get {
let destinationPoints = Points(xDestinations, yDestinations)
let control1Points = Points(xControl1, yControl1)
let control2Points = Points(xControl2, yControl2)
let curves = Curves(destinationPoints, AnimatablePair(control1Points, control2Points))
return AnimatablePair(yStartPoint, curves)
}
set {
yStartPoint = newValue.first
let curves: Curves = newValue.second
let destinationPoints: Points = curves.first
xDestinations = destinationPoints.first
yDestinations = destinationPoints.second
let control1Points: Points = curves.second.first
xControl1 = control1Points.first
yControl1 = control1Points.second
let control2Points: Points = curves.second.second
xControl2 = control2Points.first
yControl2 = control2Points.second
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.size.width
let height = rect.size.height
// Move the the start point
let startPoint = CGPoint(x: 0, y: yStartPoint*height)
path.move(to: startPoint)
// Draw the curves
let count = min(xDestinations.values.count, yDestinations.values.count, xControl1.values.count, yControl1.values.count, xControl2.values.count, yControl2.values.count)
for i in 0..<count {
let destination = CGPoint(x: xDestinations.values[i], y: yDestinations.values[i])
let control1 = CGPoint(x: xControl1.values[i], y: yControl1.values[i])
let control2 = CGPoint(x: xControl2.values[i], y: yControl2.values[i])
path.addCurve(to: CGPoint(x: destination.x*width, y: destination.y*height),
control1: CGPoint(x: control1.x*width, y: control1.y*height),
control2: CGPoint(x: control2.x*width, y: control2.y*height))
}
// Close the path
path.closeSubpath()
return path
}
}
/// Uses the Accelerate framework to utilize its high-performance vector-based arithmetic for animating large arrays.
import enum Accelerate.vDSP
/// Holds an array of Doubles anc conforms to VectorArithmetic allowing it to be used as AnimatableValue
struct AnimatableValues: VectorArithmetic {
init(_ values: [Double]) {
self.values = values
}
var values: [Double]
static var zero: AnimatableValues = AnimatableValues([0.0])
var magnitudeSquared: Double {
vDSP.sum(vDSP.multiply(values, values))
}
static func + (lhs: AnimatableValues, rhs: AnimatableValues) -> AnimatableValues {
let count: Int = min(lhs.values.count, rhs.values.count)
return AnimatableValues(vDSP.add(lhs.values[0..<count], rhs.values[0..<count]))
}
static func += (lhs: inout AnimatableValues, rhs: AnimatableValues) {
let count: Int = min(lhs.values.count, rhs.values.count)
vDSP.add(lhs.values[0..<count], rhs.values[0..<count], result: &lhs.values[0..<count])
}
static func - (lhs: AnimatableValues, rhs: AnimatableValues) -> AnimatableValues {
let count: Int = min(lhs.values.count, rhs.values.count)
return AnimatableValues(vDSP.subtract(lhs.values[0..<count], rhs.values[0..<count]))
}
static func -= (lhs: inout AnimatableValues, rhs: AnimatableValues) {
let count: Int = min(lhs.values.count, rhs.values.count)
vDSP.subtract(lhs.values[0..<count], rhs.values[0..<count], result: &lhs.values[0..<count])
}
mutating func scale(by rhs: Double) {
values = vDSP.multiply(rhs, values)
}
}
When I use this code to show a shape without animating it, it shows as expected, but when I change a shape into another shape, the other shape doesn’t look like it normally does when not animated.
Shape 1:
let firstStartY = 0.90164
let firstCurves = [
Curve(destination: CGPoint(x: 0.48065, y: 0.53167),
control1: CGPoint(x: 0.09511, y: 0.67288),
control2: CGPoint(x: 0.25329, y: 0.48162)),
Curve(destination: CGPoint(x: 0.92773, y: 0.13855),
control1: CGPoint(x: 0.79214, y: 0.60023),
control2: CGPoint(x: 0.87503, y: 0.318)),
Curve(destination: CGPoint(x: 1, y: 0.0002),
control1: CGPoint(x: 0.95181, y: 0.05655),
control2: CGPoint(x: 0.9696, y: -0.00398))
]
Shape 2:
let secondStartY = 0.03728
let secondCurves = [
Curve(destination: CGPoint(x: 0.69054, y: 0.20178),
control1: CGPoint(x: 0, y: 0.03728),
control2: CGPoint(x: 0.45736, y: -0.11688)),
Curve(destination: CGPoint(x: 1, y: 1),
control1: CGPoint(x: 0.82381, y: 0.38388),
control2: CGPoint(x: 0.94161, y: 0.78003))
]
Animating Shape 1 into Shape 2:
(See file”Animating Shape1 into Shape2.mov” for visual):
https://drive.google.com/file/d/12dR4U8JQZl-fySI7Vtk7H_Q-u6msXLFc/view?usp=share_link
struct ContentView: View {
@State var startPointY = 0.0
@State var curves = [Curve]()
var body: some View {
GeometryReader { geo in
AnimatableLandscape(startY: startPointY, curves: curves)
.frame(height: geo.size.width * 0.5)
}
.onAppear {
self.startPointY = firstStartY
self.curves = firstCurves
}
.onTapGesture {
withAnimation {
if startPointY == firstStartY {
self.startPointY = secondStartY
self.curves = secondCurves
} else {
self.startPointY = firstStartY
self.curves = firstCurves
}
}
}
}
}
As you can see, when I animate shape 1 into shape 2, the result doesn’t look like the original shape 2 at all. I can’t figure out why this happens, if anyone could help me with this that’d be great!