Forked from swiftui-lab/advanced-swiftui-animations.swift
Created
August 31, 2019 01:42
-
-
Save bigsan/7378a1682a300fadff4f7d93efc84510 to your computer and use it in GitHub Desktop.
Revisions
-
swiftui-lab revised this gist
Aug 28, 2019 . 1 changed file with 116 additions and 8 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,7 +2,6 @@ // The SwiftUI Lab: Advanced SwiftUI Animations // https://swiftui-lab.com/swiftui-animations-part1 //-------------------------------------------------- import SwiftUI struct ContentView: View { @@ -31,33 +30,37 @@ struct ContentView: View { NavigationLink(destination: Example5(), label: { Text("Example 5 (clock)") }) NavigationLink(destination: Example6(), label: { Text("Example 6 (metal)") }) } Section(header: Text("Part 2: Geometry Effect (coming soon)")) { NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 7 (skew)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 8 (follow path)") }) } Section(header: Text("Part 3: Animatable Modifier (coming soon)")) { NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 9 (wave text)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 10 (counter)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 11 (gradient)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 12 (progress circle)") }) } @@ -637,4 +640,109 @@ extension ClockTime: VectorArithmetic { return ClockTime(0, 0, 0) } } // MARK: - Example 6: Clock Shape struct Example6: View { var body: some View { VStack { FlowerView().drawingGroup() }.padding(20) } } struct FlowerView: View { @State private var animate = false let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple, .pink] var body: some View { ZStack { ForEach(0..<7) { i in FlowerColor(petals: self.getPetals(i), length: self.getLength(i), color: self.colors[i]) } .rotationEffect(Angle(degrees: animate ? 360 : 0)) .onAppear { withAnimation(Animation.easeInOut(duration: 25.0).repeatForever()) { self.animate = true } } } } func getLength(_ i: Int) -> Double { return 1 - (Double(i) * 1 / 7) } func getPetals(_ i: Int) -> Int { return i * 2 + 15 } } struct FlowerColor: View { let petals: Int let length: Double let color: Color @State private var animate = false var body: some View { let petalWidth1 = Angle(degrees: 2) let petalWidth2 = Angle(degrees: 360 / Double(self.petals)) * 2 return GeometryReader { proxy in ForEach(0..<self.petals) { i in PetalShape(angle: Angle(degrees: Double(i) * 360 / Double(self.petals)), arc: self.animate ? petalWidth1 : petalWidth2, length: self.animate ? self.length : self.length * 0.9) .fill(RadialGradient(gradient: Gradient(colors: [self.color.opacity(0.2), self.color]), center: UnitPoint(x: 0.5, y: 0.5), startRadius: 0.1 * min(proxy.size.width, proxy.size.height) / 2.0, endRadius: min(proxy.size.width, proxy.size.height) / 2.0)) } }.onAppear { withAnimation(Animation.easeInOut(duration: 1.5).repeatForever()) { self.animate = true } } } } struct PetalShape: Shape { let angle: Angle var arc: Angle var length: Double var animatableData: AnimatablePair<Double, Double> { get { AnimatablePair(arc.degrees, length) } set { arc = Angle(degrees: newValue.first) length = newValue.second } } func path(in rect: CGRect) -> Path { let center = CGPoint(x: rect.midX, y: rect.midY) let hypotenuse = Double(min(rect.width, rect.height)) / 2.0 * length let sep = arc / 2 let to = CGPoint(x: CGFloat(cos(angle.radians) * Double(hypotenuse)) + center.x, y: CGFloat(sin(angle.radians) * Double(hypotenuse)) + center.y) let ctrl1 = CGPoint(x: CGFloat(cos((angle + sep).radians) * Double(hypotenuse)) + center.x, y: CGFloat(sin((angle + sep).radians) * Double(hypotenuse)) + center.y) let ctrl2 = CGPoint(x: CGFloat(cos((angle - sep).radians) * Double(hypotenuse)) + center.x, y: CGFloat(sin((angle - sep).radians) * Double(hypotenuse)) + center.y) var path = Path() path.move(to: center) path.addQuadCurve(to: to, control: ctrl1) path.addQuadCurve(to: center, control: ctrl2) return path } } -
swiftui-lab created this gist
Aug 26, 2019 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,640 @@ //-------------------------------------------------- // The SwiftUI Lab: Advanced SwiftUI Animations // https://swiftui-lab.com/swiftui-animations-part1 //-------------------------------------------------- import SwiftUI struct ContentView: View { var body: some View { NavigationView { List { Section(header: Text("Part 1: Path Animations")) { NavigationLink(destination: Example1(), label: { Text("Example 1 (sides: Double)") }) NavigationLink(destination: Example2(), label: { Text("Example 2 (sides: Int)") }) NavigationLink(destination: Example3(), label: { Text("Example 3 (sides & scale)") }) NavigationLink(destination: Example4(), label: { Text("Example 4 (vertex to vertex)") }) NavigationLink(destination: Example5(), label: { Text("Example 5 (clock)") }) } Section(header: Text("Part 2: Geometry Effect (coming soon)")) { NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 6 (skew)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 7 (follow path)") }) } Section(header: Text("Part 3: Animatable Modifier (coming soon)")) { NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 8 (wave text)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 9 (counter)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 10 (gradient)") }) NavigationLink(destination: Text("COMING SOON"), label: { Text("Example 11 (progress circle)") }) } }.navigationBarTitle("SwiftUI Lab") } } } struct MyButton: View { let label: String var font: Font = .title var textColor: Color = .white let action: () -> () var body: some View { Button(action: { self.action() }, label: { Text(label) .font(font) .padding(10) .frame(width: 70) .background(RoundedRectangle(cornerRadius: 10).foregroundColor(Color.green).shadow(radius: 2)) .foregroundColor(textColor) }) } } // MARK: - Part 1: Path Animations // MARK: Example 1: Polygon animatable struct Example1: View { @State private var sides: Double = 4 @State private var duration: Double = 1.0 var body: some View { VStack { Example1PolygonShape(sides: sides) .stroke(Color.blue, lineWidth: 3) .padding(20) .animation(.easeInOut(duration: duration)) .layoutPriority(1) Text("\(Int(sides)) sides").font(.headline) HStack(spacing: 20) { MyButton(label: "1") { self.duration = self.animationTime(before: self.sides, after: 1) self.sides = 1.0 } MyButton(label: "3") { self.duration = self.animationTime(before: self.sides, after: 3) self.sides = 3.0 } MyButton(label: "7") { self.duration = self.animationTime(before: self.sides, after: 7) self.sides = 7.0 } MyButton(label: "30") { self.duration = self.animationTime(before: self.sides, after: 30) self.sides = 30.0 } }.navigationBarTitle("Example 1").padding(.bottom, 50) } } func animationTime(before: Double, after: Double) -> Double { // Calculate an animation time that is // adequate to the number of sides to add/remove. return abs(before - after) * (1 / abs(before - after)) } } struct Example1PolygonShape: Shape { var sides: Double var animatableData: Double { get { return sides } set { sides = newValue } } func path(in rect: CGRect) -> Path { // hypotenuse let h = Double(min(rect.size.width, rect.size.height)) / 2.0 // center let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) var path = Path() let extra: Int = Double(sides) != Double(Int(sides)) ? 1 : 0 for i in 0..<Int(sides) + extra { let angle = (Double(i) * (360.0 / Double(sides))) * Double.pi / 180 // Calculate vertex let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) if i == 0 { path.move(to: pt) // move to first vertex } else { path.addLine(to: pt) // draw line to next vertex } } path.closeSubpath() return path } } // MARK: - Example 2: Polygon with sides as Integer struct Example2: View { @State private var sides: Int = 4 @State private var duration: Double = 1.0 var body: some View { VStack { Example2PolygonShape(sides: sides) .stroke(Color.red, lineWidth: 3) .padding(20) .animation(.easeInOut(duration: duration)) .layoutPriority(1) Text("\(Int(sides)) sides").font(.headline) HStack(spacing: 20) { MyButton(label: "1") { self.duration = self.animationTime(before: self.sides, after: 1) self.sides = 1 } MyButton(label: "3") { self.duration = self.animationTime(before: self.sides, after: 3) self.sides = 3 } MyButton(label: "7") { self.duration = self.animationTime(before: self.sides, after: 7) self.sides = 7 } MyButton(label: "30") { self.duration = self.animationTime(before: self.sides, after: 30) self.sides = 30 } }.navigationBarTitle("Example 2").padding(.bottom, 50) } } func animationTime(before: Int, after: Int) -> Double { // Calculate an animation time that is // adequate to the number of sides to add/remove. return Double(abs(before - after)) * (1 / Double(abs(before - after))) } } struct Example2PolygonShape: Shape { var sides: Int private var sidesAsDouble: Double var animatableData: Double { get { return sidesAsDouble } set { sidesAsDouble = newValue } } init(sides: Int) { self.sides = sides self.sidesAsDouble = Double(sides) } func path(in rect: CGRect) -> Path { // hypotenuse let h = Double(min(rect.size.width, rect.size.height)) / 2.0 // center let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) var path = Path() let extra: Int = sidesAsDouble != Double(Int(sidesAsDouble)) ? 1 : 0 for i in 0..<Int(sidesAsDouble) + extra { let angle = (Double(i) * (360.0 / sidesAsDouble)) * Double.pi / 180 // Calculate vertex let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) if i == 0 { path.move(to: pt) // move to first vertex } else { path.addLine(to: pt) // draw line to next vertex } } path.closeSubpath() return path } } // MARK: - Example 3: Polygon with multiple animatable paramters struct Example3: View { @State private var sides: Double = 4 @State private var duration: Double = 1.0 @State private var scale: Double = 1.0 var body: some View { VStack { Example3PolygonShape(sides: sides, scale: scale) .stroke(Color.purple, lineWidth: 5) .padding(20) .animation(.easeInOut(duration: duration)) .layoutPriority(1) Text("\(Int(sides)) sides, \(String(format: "%.2f", scale as Double)) scale") HStack(spacing: 20) { MyButton(label: "1") { self.duration = self.animationTime(before: self.sides, after: 1) self.sides = 1.0 self.scale = 1.0 } MyButton(label: "3") { self.duration = self.animationTime(before: self.sides, after: 3) self.sides = 3.0 self.scale = 0.7 } MyButton(label: "7") { self.duration = self.animationTime(before: self.sides, after: 7) self.sides = 7.0 self.scale = 0.4 } MyButton(label: "30") { self.duration = self.animationTime(before: self.sides, after: 30) self.sides = 30.0 self.scale = 1.0 } } }.navigationBarTitle("Example 3").padding(.bottom, 50) } func animationTime(before: Double, after: Double) -> Double { // Calculate an animation time that is // adequate to the number of sides to add/remove. return abs(before - after) * (1 / abs(before - after)) } } struct Example3PolygonShape: Shape { var sides: Double var scale: Double var animatableData: AnimatablePair<Double, Double> { get { AnimatablePair(sides, scale) } set { sides = newValue.first scale = newValue.second } } func path(in rect: CGRect) -> Path { // hypotenuse let h = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale // center let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) var path = Path() let extra: Int = sides != Double(Int(sides)) ? 1 : 0 for i in 0..<Int(sides) + extra { let angle = (Double(i) * (360.0 / sides)) * (Double.pi / 180) // Calculate vertex let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) if i == 0 { path.move(to: pt) // move to first vertex } else { path.addLine(to: pt) // draw line to next vertex } } path.closeSubpath() return path } } // MARK: - Example 4: Polygon with lines vertex-to-vertex struct Example4: View { @State private var sides: Double = 4 @State private var duration: Double = 1.0 @State private var scale: Double = 1.0 var body: some View { VStack { Example4PolygonShape(sides: sides, scale: scale) .stroke(Color.pink, lineWidth: (sides < 3) ? 10 : ( sides < 7 ? 5 : 2)) .padding(20) .animation(.easeInOut(duration: duration)) .layoutPriority(1) Text("\(Int(sides)) sides, \(String(format: "%.2f", scale as Double)) scale") Slider(value: $sides, in: 0...30) HStack(spacing: 20) { MyButton(label: "1") { self.duration = self.animationTime(before: self.sides, after: 1) self.sides = 1.0 self.scale = 1.0 } MyButton(label: "3") { self.duration = self.animationTime(before: self.sides, after: 3) self.sides = 3.0 self.scale = 1.0 } MyButton(label: "7") { self.duration = self.animationTime(before: self.sides, after: 7) self.sides = 7.0 self.scale = 1.0 } MyButton(label: "30") { self.duration = self.animationTime(before: self.sides, after: 30) self.sides = 30.0 self.scale = 1.0 } } }.navigationBarTitle("Example 4").padding(.bottom, 50) } func animationTime(before: Double, after: Double) -> Double { // Calculate an animation time that is // adequate to the number of sides to add/remove. return abs(before - after) * (1 / abs(before - after)) + 3 } } struct Example4PolygonShape: Shape { var sides: Double var scale: Double var animatableData: AnimatablePair<Double, Double> { get { AnimatablePair(sides, scale) } set { sides = newValue.first scale = newValue.second } } func path(in rect: CGRect) -> Path { // hypotenuse let h = Double(min(rect.size.width, rect.size.height)) / 2.0 * scale // center let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) var path = Path() let extra: Int = sides != Double(Int(sides)) ? 1 : 0 var vertex: [CGPoint] = [] for i in 0..<Int(sides) + extra { let angle = (Double(i) * (360.0 / sides)) * (Double.pi / 180) // Calculate vertex let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h)) vertex.append(pt) if i == 0 { path.move(to: pt) // move to first vertex } else { path.addLine(to: pt) // draw line to next vertex } } path.closeSubpath() // Draw vertex-to-vertex lines drawVertexLines(path: &path, vertex: vertex, n: 0) return path } func drawVertexLines(path: inout Path, vertex: [CGPoint], n: Int) { if (vertex.count - n) < 3 { return } for i in (n+2)..<min(n + (vertex.count-1), vertex.count) { path.move(to: vertex[n]) path.addLine(to: vertex[i]) } drawVertexLines(path: &path, vertex: vertex, n: n+1) } } // MARK: - Example 5: Clock Shape struct Example5: View { @State private var time: ClockTime = ClockTime(9, 50, 5) @State private var duration: Double = 1.0 var body: some View { VStack { ClockShape(clockTime: time) .stroke(Color.blue, lineWidth: 3) .padding(20) .animation(.easeInOut(duration: duration)) .layoutPriority(1) Text("\(time.asString())") HStack(spacing: 20) { MyButton(label: "9:51:45", font: .footnote, textColor: .black) { self.duration = 2.0 self.time = ClockTime(9, 51, 45) } MyButton(label: "9:51:15", font: .footnote, textColor: .black) { self.duration = 2.0 self.time = ClockTime(9, 51, 15) } MyButton(label: "9:52:15", font: .footnote, textColor: .black) { self.duration = 2.0 self.time = ClockTime(9, 52, 15) } MyButton(label: "10:01:45", font: .caption, textColor: .black) { self.duration = 10.0 self.time = ClockTime(10, 01, 45) } } }.navigationBarTitle("Example 5").padding(.bottom, 50) } } struct ClockShape: Shape { var clockTime: ClockTime var animatableData: ClockTime { get { clockTime } set { clockTime = newValue } } func path(in rect: CGRect) -> Path { var path = Path() let radius = min(rect.size.width / 2.0, rect.size.height / 2.0) let center = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0) let hHypotenuse = Double(radius) * 0.5 // hour needle length let mHypotenuse = Double(radius) * 0.7 // minute needle length let sHypotenuse = Double(radius) * 0.9 // second needle length let hAngle: Angle = .degrees(Double(clockTime.hours) / 12 * 360 - 90) let mAngle: Angle = .degrees(Double(clockTime.minutes) / 60 * 360 - 90) let sAngle: Angle = .degrees(Double(clockTime.seconds) / 60 * 360 - 90) let hourNeedle = CGPoint(x: center.x + CGFloat(cos(hAngle.radians) * hHypotenuse), y: center.y + CGFloat(sin(hAngle.radians) * hHypotenuse)) let minuteNeedle = CGPoint(x: center.x + CGFloat(cos(mAngle.radians) * mHypotenuse), y: center.y + CGFloat(sin(mAngle.radians) * mHypotenuse)) let secondNeedle = CGPoint(x: center.x + CGFloat(cos(sAngle.radians) * sHypotenuse), y: center.y + CGFloat(sin(sAngle.radians) * sHypotenuse)) path.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) path.move(to: center) path.addLine(to: hourNeedle) path = path.strokedPath(StrokeStyle(lineWidth: 3.0)) path.move(to: center) path.addLine(to: minuteNeedle) path = path.strokedPath(StrokeStyle(lineWidth: 3.0)) path.move(to: center) path.addLine(to: secondNeedle) path = path.strokedPath(StrokeStyle(lineWidth: 1.0)) return path } } struct ClockTime { var hours: Int // Hour needle should jump by integer numbers var minutes: Int // Minute needle should jump by integer numbers var seconds: Double // Second needle should move smoothly // Initializer with hour, minute and seconds init(_ h: Int, _ m: Int, _ s: Double) { self.hours = h self.minutes = m self.seconds = s } // Initializer with total of seconds init(_ seconds: Double) { let h = Int(seconds) / 3600 let m = (Int(seconds) - (h * 3600)) / 60 let s = seconds - Double((h * 3600) + (m * 60)) self.hours = h self.minutes = m self.seconds = s } // compute number of seconds var asSeconds: Double { return Double(self.hours * 3600 + self.minutes * 60) + self.seconds } // show as string func asString() -> String { return String(format: "%2i", self.hours) + ":" + String(format: "%02i", self.minutes) + ":" + String(format: "%02.0f", self.seconds) } } extension ClockTime: VectorArithmetic { static func -= (lhs: inout ClockTime, rhs: ClockTime) { lhs = lhs - rhs } static func - (lhs: ClockTime, rhs: ClockTime) -> ClockTime { return ClockTime(lhs.asSeconds - rhs.asSeconds) } static func += (lhs: inout ClockTime, rhs: ClockTime) { lhs = lhs + rhs } static func + (lhs: ClockTime, rhs: ClockTime) -> ClockTime { return ClockTime(lhs.asSeconds + rhs.asSeconds) } mutating func scale(by rhs: Double) { var s = Double(self.asSeconds) s.scale(by: rhs) let ct = ClockTime(s) self.hours = ct.hours self.minutes = ct.minutes self.seconds = ct.seconds } var magnitudeSquared: Double { 1 } static var zero: ClockTime { return ClockTime(0, 0, 0) } }