├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift ├── README.md └── Sources └── MorphingShapes ├── CGVectorOp.swift ├── MorphingBackground.swift ├── MorphingCircle.swift ├── ShapeExtensions.swift └── VectorAnimation.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/config/registries.json 8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 9 | .netrc 10 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MorphingShapes", 8 | platforms: [ 9 | .macOS(.v10_15), .iOS(.v14), .watchOS(.v7) 10 | ], products: [ 11 | // Products define the executables and libraries a package produces, and make them visible to other packages. 12 | .library( 13 | name: "MorphingShapes", 14 | targets: ["MorphingShapes"]), 15 | ], 16 | dependencies: [ 17 | // Dependencies declare other packages that this package depends on. 18 | // .package(url: /* package url */, from: "1.0.0"), 19 | ], 20 | targets: [ 21 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 22 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 23 | .target( 24 | name: "MorphingShapes", 25 | dependencies: [] 26 | ) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MorphingShapes 2 | 3 | Code for https://alexdremov.me/swiftui-advanced-animation/ 4 | 5 | 6 | 7 | https://user-images.githubusercontent.com/25539425/184550942-bb9cc1da-5916-42be-8342-883f200e2cbd.mov 8 | 9 | ```swift 10 | import SwiftUI 11 | import MorphingShapes 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | VStack { 16 | MorphingCircle(outlineColor: .orange, outlineWidth: 10.0) 17 | } 18 | } 19 | } 20 | 21 | struct ContentView_Previews: PreviewProvider { 22 | static var previews: some View { 23 | ContentView() 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /Sources/MorphingShapes/CGVectorOp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGVectorOp.swift 3 | // Spontanea 4 | // 5 | // Created by  Alex Dremov on 27.07.2021. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension CGPoint { 12 | public static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 13 | CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) 14 | } 15 | 16 | public static func +(lhs: CGPoint, rhs: CGVector) -> CGPoint { 17 | CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy) 18 | } 19 | 20 | public static func -(lhs: CGPoint, rhs: CGVector) -> CGPoint { 21 | CGPoint(x: lhs.x - rhs.dx, y: lhs.y - rhs.dy) 22 | } 23 | 24 | public static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint { 25 | CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) 26 | } 27 | 28 | init(_ vec: CGVector) { 29 | self = CGPoint(x: vec.dx, y: vec.dy) 30 | } 31 | } 32 | 33 | extension CGPoint: VectorArithmetic { 34 | mutating public func scale(by rhs: Double) { 35 | x = CGFloat(rhs) * x 36 | y = CGFloat(rhs) * y 37 | } 38 | 39 | public var magnitudeSquared: Double { 40 | Double(x * x + y * y) 41 | } 42 | } 43 | 44 | internal extension CGVector { 45 | init(_ point: CGPoint) { 46 | self = CGVector(dx: point.x, dy: point.y) 47 | } 48 | 49 | func scalar(_ vec: CGVector) -> CGFloat { 50 | dx * vec.dx + dy * vec.dy 51 | } 52 | 53 | func len() -> CGFloat { 54 | sqrt(dx * dx + dy * dy) 55 | } 56 | 57 | func perpendicular() -> CGVector { 58 | CGVector(dx: -dy, dy: dx) / len() 59 | } 60 | 61 | static func *(lhs: CGVector, rhs: CGFloat) -> CGVector { 62 | CGVector(dx: lhs.dx * rhs, dy: lhs.dy * rhs) 63 | } 64 | 65 | static func *(lhs: CGFloat, rhs: CGVector) -> CGVector { 66 | CGVector(dx: rhs.dx * lhs, dy: rhs.dy * lhs) 67 | } 68 | 69 | static func /(lhs: CGVector, rhs: CGFloat) -> CGVector { 70 | CGVector(dx: lhs.dx / rhs, dy: lhs.dy / rhs) 71 | } 72 | 73 | static func -(lhs: CGVector, rhs: CGVector) -> CGVector { 74 | CGVector(dx: lhs.dx - rhs.dx, dy: lhs.dy - rhs.dy) 75 | } 76 | 77 | static func +(lhs: CGVector, rhs: CGVector) -> CGVector { 78 | CGVector(dx: lhs.dx + rhs.dx, dy: lhs.dy + rhs.dy) 79 | } 80 | 81 | func angle(_ rhs: CGVector) -> CGFloat { 82 | return acos(scalar(rhs) / (rhs.len() * len())) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/MorphingShapes/MorphingBackground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MorphingBackground.swift 3 | // Spontanea 4 | // 5 | // Created by  Alex Dremov on 27.07.2021. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | import Combine 11 | 12 | @available(macOS 11.0, *) 13 | struct MorphingBackground: View { 14 | let number: Int 15 | let outlay:(x: (left: CGFloat, right: CGFloat), 16 | y: (up: CGFloat, bottom: CGFloat)) 17 | let duration: Double 18 | let range: ClosedRange 19 | @State var circles: [MorphingCircle] = [] 20 | @State var point: [CGPoint] = [] 21 | 22 | var timer: Publishers.Autoconnect 23 | 24 | var body: some View { 25 | GeometryReader { proxy in 26 | ZStack(alignment: .topLeading){ 27 | ForEach(Array(circles.enumerated()), id:\.1) { (i, circle) in 28 | circles[i] 29 | .position(point[i]) 30 | .animation(.easeInOut(duration: duration), value: point) 31 | } 32 | } 33 | .onReceive(timer, perform: { _ in 34 | update(proxy) 35 | }) 36 | .onAppear{ 37 | createCircles() 38 | update(proxy) 39 | withAnimation { 40 | update(proxy) 41 | } 42 | } 43 | }.ignoresSafeArea() 44 | } 45 | 46 | func update(_ proxy: GeometryProxy){ 47 | point = [] 48 | var newPoints = [CGPoint]() 49 | for i in 0.. i ? 76 | point[i] : CGPoint(x: CGFloat.random(in: minX...maxX), 77 | y: CGFloat.random(in: minY...maxY))) 78 | + delta 79 | 80 | newPoint.x = newPoint.x < minX ? minX: newPoint.x 81 | newPoint.y = newPoint.y < minY ? minY: newPoint.y 82 | 83 | newPoint.x = newPoint.x > maxX ? maxX: newPoint.x 84 | newPoint.y = newPoint.y > maxY ? maxY: newPoint.y 85 | 86 | newPoints.append(newPoint) 87 | } 88 | point = newPoints 89 | } 90 | 91 | func createCircles() { 92 | circles = [] 93 | point = [] 94 | for _ in 0.. = 100...300, 103 | duration: Double = 30, 104 | outlay:(x: (left: CGFloat, right: CGFloat), 105 | y: (up: CGFloat, bottom: CGFloat)) = (x:(0, 0), y:(0, 0))) { 106 | self.number = number 107 | self.range = shapeWidth 108 | self.duration = duration 109 | self.outlay = outlay 110 | self.timer = Timer.publish(every: duration / 2, on: .current, in: .common).autoconnect() 111 | } 112 | } 113 | 114 | struct MorphingBackground_Previews: PreviewProvider { 115 | static var previews: some View { 116 | MorphingBackground() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/MorphingShapes/MorphingCircle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MorphingCircle.swift 3 | // Spontanea 4 | // 5 | // Created by  Alex Dremov on 27.07.2021. 6 | // Updated by Michel Löhr on 06.10.22. 7 | // 8 | 9 | import SwiftUI 10 | import Foundation 11 | 12 | public struct MorphingCircleShape: Shape { 13 | var pointsNum: Int = 10 14 | var morphing: AnimatableVector 15 | var tangentCoeficient: CGFloat 16 | 17 | public var animatableData: AnimatableVector { 18 | get { morphing } 19 | set { morphing = newValue } 20 | } 21 | 22 | func getTwoTangent(center: CGPoint, point: CGPoint) -> (CGPoint, CGPoint) { 23 | let a = CGVector(center - point) 24 | let dir = a.perpendicular() * a.len() * tangentCoeficient 25 | return (point - dir, point + dir) 26 | } 27 | 28 | public func path(in rect: CGRect) -> Path { 29 | var path = Path() 30 | let radius = min(rect.width / 2, rect.height / 2) 31 | let center = CGPoint(x: rect.width / 2, y: rect.height / 2) 32 | var nextPoint = CGPoint.zero 33 | 34 | let ithPoint: (Int) -> CGPoint = { i in 35 | let point = center + CGPoint(x: radius * sin(CGFloat(i) * CGFloat.pi * CGFloat(2) / CGFloat(pointsNum)), 36 | y: radius * cos(CGFloat(i) * CGFloat.pi * CGFloat(2) / CGFloat(pointsNum))) 37 | var direction = CGVector(point - center) 38 | direction = direction / direction.len() 39 | return point + direction * CGFloat(morphing[i >= pointsNum ? 0 : i]) 40 | } 41 | var tangentLast = getTwoTangent(center: center, 42 | point: ithPoint(pointsNum - 1)) 43 | for i in (0...pointsNum){ 44 | nextPoint = ithPoint(i) 45 | let tangentNow = getTwoTangent(center: center, point: nextPoint) 46 | if i != 0 { 47 | path.addCurve(to: nextPoint, control1: tangentLast.1, control2: tangentNow.0) 48 | } else { 49 | path.move(to: nextPoint) 50 | } 51 | tangentLast = tangentNow 52 | } 53 | path.closeSubpath() 54 | return path 55 | } 56 | 57 | 58 | init(_ morph: AnimatableVector) { 59 | pointsNum = morph.count 60 | morphing = morph 61 | tangentCoeficient = (4 / 3) * tan(CGFloat.pi / CGFloat(2 * pointsNum)) 62 | } 63 | } 64 | 65 | public struct MorphingCircle: View & Identifiable & Hashable { 66 | public static func == (lhs: MorphingCircle, rhs: MorphingCircle) -> Bool { 67 | lhs.id == rhs.id 68 | } 69 | 70 | public func hash(into hasher: inout Hasher) { 71 | hasher.combine(id) 72 | } 73 | 74 | public let id = UUID() 75 | @State var morph: AnimatableVector = AnimatableVector.zero 76 | @State var timer: Timer? 77 | 78 | func morphCreator() -> AnimatableVector { 79 | let range = Float(-morphingRange)...Float(morphingRange) 80 | var morphing = Array.init(repeating: Float.zero, count: self.points) 81 | for i in 0.. MorphingCircle { 141 | var morphNew = self 142 | morphNew.color = newColor 143 | return morphNew 144 | } 145 | } 146 | 147 | struct MorphingCircle_Previews: PreviewProvider { 148 | static var previews: some View { 149 | MorphingCircle(outlineColor: .orange, outlineWidth: 10.0) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/MorphingShapes/ShapeExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Michel Löhr on 06.10.22. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension Shape { 12 | func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View { 13 | self 14 | .stroke(strokeStyle, lineWidth: lineWidth) 15 | .background(self.fill(fillStyle)) 16 | } 17 | } 18 | 19 | extension InsettableShape { 20 | func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View { 21 | self 22 | .strokeBorder(strokeStyle, lineWidth: lineWidth) 23 | .background(self.fill(fillStyle)) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/MorphingShapes/VectorAnimation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VectorAnimation.swift 3 | // Spontanea 4 | // 5 | // Created by  Alex Dremov on 27.07.2021. 6 | // 7 | 8 | 9 | // https://gist.github.com/mecid/18a80b18cc9670eef1d8667cf8c886bd 10 | import SwiftUI 11 | import enum Accelerate.vDSP 12 | 13 | public struct AnimatableVector: VectorArithmetic { 14 | public static var zero = AnimatableVector(values: [0.0]) 15 | 16 | public static func + (lhs: AnimatableVector, rhs: AnimatableVector) -> AnimatableVector { 17 | let count = min(lhs.values.count, rhs.values.count) 18 | return AnimatableVector(values: vDSP.add(lhs.values[0.. AnimatableVector { 27 | let count = min(lhs.values.count, rhs.values.count) 28 | return AnimatableVector(values: vDSP.subtract(lhs.values[0.. Float { 51 | get { 52 | values[i] 53 | } set { 54 | values[i] = newValue 55 | } 56 | } 57 | } 58 | --------------------------------------------------------------------------------