├── .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 |
--------------------------------------------------------------------------------