Created
July 23, 2021 23:38
-
-
Save kieranb662/985c5274519d36e3d000634e046c7425 to your computer and use it in GitHub Desktop.
A raining cloud animation made with SwiftUI as a demo to try out the new Canvas and Timeline Views.
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 characters
// Swift toolchain version 5.0 | |
// Running macOS version 12.0 | |
// Created on 6/24/21. | |
// | |
// Author: Kieran Brown | |
// | |
import SwiftUI | |
fileprivate func makeDroplets(count: Int = 10, xRange: Range<Int> = 20..<350, yRange: Range<Int>) -> [CGPoint] { | |
var droplets = [CGPoint]() | |
for _ in 0..<count { | |
droplets.append(CGPoint(x: .random(in: xRange), y: .random(in: yRange))) | |
} | |
return droplets | |
} | |
final class RainModel: ObservableObject { | |
private var lastTime = 0.0 | |
private var dropletHeight = 25.0 | |
private var dropletThickness = 10.0 | |
var cloudImage = Image(systemName: "cloud.fill") | |
var drops = makeDroplets(yRange: 20..<200) | |
+ makeDroplets(yRange: 220..<400) | |
+ makeDroplets(count: 15, yRange: -200..<0) | |
func update(time: Double, size: CGSize) { | |
let delta = min(time - lastTime, 1.0 / 30.0) | |
lastTime = time | |
if delta > 0 { | |
for i in drops.indices { | |
drops[i].y = (drops[i].y + delta * size.height).remainder(dividingBy: size.height) | |
} | |
} | |
} | |
func drawLightningBolt(_ context: GraphicsContext, size: CGSize, now: Double) { | |
LightningBolt(context: context, size: size, now: now) | |
.draw() | |
} | |
struct LightningBolt { | |
var context: GraphicsContext | |
var size: CGSize | |
var now: Double | |
var path: Path { | |
Path { path in | |
path.addLines([ | |
CGPoint(x: 0.2 * size.width, y: 0.2 * size.height), | |
CGPoint(x: 0.8 * size.width, y: 0.4 * size.height), | |
CGPoint(x: 0.2 * size.width, y: 0.7 * size.height), | |
CGPoint(x: 0.7 * size.width, y: 1.1 * size.height), | |
]) | |
} | |
} | |
var shading: GraphicsContext.Shading { | |
.linearGradient(Gradient(colors: [.yellow, .white]), | |
startPoint: CGPoint(x: 0, y: 0), | |
endPoint: CGPoint(x: size.width, y: size.height)) | |
} | |
var strokeStyle: StrokeStyle { | |
StrokeStyle(lineWidth: 20, | |
lineCap: .round, | |
lineJoin: .round, | |
dash: [ 1000 ], | |
dashPhase: -(now * 3750).remainder(dividingBy: 2000)) | |
} | |
func draw() { | |
context.stroke(path, with: shading, style: strokeStyle) | |
} | |
} | |
func drawDroplet(position: CGPoint, context: GraphicsContext, size: CGSize, imageSize: CGSize) { | |
Droplet(position: position, | |
context: context, | |
size: size, | |
imageSize: imageSize, | |
dropletThickness: dropletThickness, | |
dropletHeight: dropletHeight) | |
.draw() | |
} | |
struct Droplet { | |
var context: GraphicsContext | |
var dropFrame: CGRect | |
private var dropletHeight = 25.0 | |
private var dropletThickness = 10.0 | |
init(position: CGPoint, | |
context: GraphicsContext, | |
size: CGSize, | |
imageSize: CGSize, | |
dropletThickness: CGFloat, | |
dropletHeight: CGFloat) { | |
self.dropFrame = CGRect(x: position.x, | |
y: position.y + imageSize.height + 0.5 * size.height, | |
width: dropletThickness, | |
height: dropletHeight) | |
self.dropletHeight = dropletHeight | |
self.dropletThickness = dropletThickness | |
self.context = context | |
} | |
func draw() { | |
drawBottomDrop() | |
drawMiddleDrop() | |
drawTopDrop() | |
} | |
func drawBottomDrop() { | |
context.fill(Capsule().path(in: dropFrame), with: .color(.blue)) | |
} | |
func drawMiddleDrop() { | |
let frame = dropFrame | |
.insetBy(dx: 0, dy: -dropletHeight*0.5) | |
.offsetBy(dx: 0, dy: -dropletHeight*0.5) | |
context.fill(Capsule().path(in: frame), with: .color(.blue.opacity(0.5))) | |
} | |
func drawTopDrop() { | |
let frame = dropFrame | |
.insetBy(dx: 0, dy: -dropletHeight) | |
.offsetBy(dx: 0, dy: -2*dropletHeight) | |
context.fill(Capsule().path(in: frame), with: .color(.blue.opacity(0.2))) | |
} | |
} | |
func blockDropletsFromShowingAboveCloud(_ context: GraphicsContext, frame: CGRect) { | |
context.fill(Rectangle().path(in: frame.offsetBy(dx: 0, dy: -30)), with: .color(.black)) | |
} | |
func drawCloud(_ context: GraphicsContext, frame: CGRect, image: GraphicsContext.ResolvedImage, now: Double) { | |
Cloud(context, frame: frame, frameInset: 10, image: image, now: now) | |
.draw() | |
} | |
struct Cloud { | |
var context: GraphicsContext | |
var frame: CGRect | |
var image: GraphicsContext.ResolvedImage | |
let oscillation: Double | |
let gradientColors = [Color(white: 0.8), Color(white: 0.4)] | |
let radius = 200.0 | |
init(_ context: GraphicsContext, | |
frame: CGRect, frameInset: CGFloat = 10, | |
image: GraphicsContext.ResolvedImage, | |
now: Double) { | |
self.context = context | |
self.oscillation = cos(now * 2) * 7 | |
self.image = image | |
self.frame = frame.insetBy(dx: frameInset, dy: frameInset) | |
} | |
func draw() { | |
drawBackgroundCloud() | |
drawLeftGradientCloud() | |
drawMiddleCloudGradient() | |
} | |
func drawBackgroundCloud() { | |
var image = self.image | |
image.shading = .color(Color(white: 0.7).opacity(0.5)) | |
context.draw(image, in: frame.offsetBy(dx: oscillation, dy: 10)) | |
} | |
func drawLeftGradientCloud() { | |
var image = self.image | |
image.shading = .radialGradient( | |
Gradient(colors: gradientColors), | |
center: CGPoint(x: frame.midX, y: frame.maxY + oscillation), | |
startRadius: 0, | |
endRadius: radius) | |
context.draw(image, in: frame) | |
} | |
func drawMiddleCloudGradient() { | |
var image = self.image | |
image.shading = .radialGradient( | |
Gradient(colors: gradientColors.map({ $0.opacity(0.5) })), | |
center: CGPoint(x: 0.3*frame.width + oscillation, y: frame.midY + oscillation), | |
startRadius: 0, | |
endRadius: radius) | |
context.draw(image, in: frame) | |
} | |
} | |
} | |
struct RainingCloud: View { | |
@StateObject var model = RainModel() | |
var body: some View { | |
TimelineView(.animation) { timeline in | |
Canvas { context, size in | |
let now = timeline.date.timeIntervalSinceReferenceDate | |
model.update(time: now, size: size) | |
let image = context.resolve(model.cloudImage) | |
model.drawLightningBolt(context, size: size, now: now) | |
for droplet in model.drops { | |
model.drawDroplet(position: droplet, | |
context: context, | |
size: size, | |
imageSize: image.size) | |
} | |
let aspectRatio = image.size.width / image.size.height | |
let frame = CGRect(x:0, y: 0, width: size.width, height: size.width / aspectRatio) | |
model.blockDropletsFromShowingAboveCloud(context, frame: frame) | |
model.drawCloud(context, frame: frame, image: image, now: now) | |
} | |
} | |
} | |
} | |
struct RainingCloud_Previews: PreviewProvider { | |
static var previews: some View { | |
RainingCloud() | |
.edgesIgnoringSafeArea(.bottom) | |
.padding(.horizontal) | |
.preferredColorScheme(.dark) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Good