Skip to content

Instantly share code, notes, and snippets.

@kieranb662
Created July 23, 2021 23:38
Show Gist options
  • Save kieranb662/985c5274519d36e3d000634e046c7425 to your computer and use it in GitHub Desktop.
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.
// 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)
}
}
@kieranb662
Copy link
Author

Rain Cloud Canvas Animation

@Nafiz1982
Copy link

Good

@Nafiz1982
Copy link

Good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment