-
-
Save jeffbailey/94d6e2c8ed4c68070b99017bc8c43eb3 to your computer and use it in GitHub Desktop.
Sparkle Effect in SwiftUI
This file contains 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
@available(iOS 15.0, *) | |
struct TwinkleView:View { | |
private func position(in proxy: GeometryProxy, sparkle:Sparkle) -> CGPoint { | |
let radius = min(proxy.size.width, proxy.size.height) / 2.0 | |
let drawnRadius = (radius - 5) * sparkle.position.x | |
let angle = Double.pi * 2.0 * sparkle.position.y | |
let x = proxy.size.width * 0.5 + drawnRadius * cos(angle) | |
let y = proxy.size.height * 0.5 + drawnRadius * sin(angle) | |
return CGPoint(x: x, y: y) | |
} | |
private func scaleFor(date:Date, sparkle:Sparkle) -> CGFloat { | |
var offset = date.timeIntervalSince(sparkle.startDate) | |
offset = max(offset, 0) | |
offset = min(offset, SparkleMagic.sparkleDuration) | |
let halfDuration = SparkleMagic.sparkleDuration * 0.5 | |
let value:CGFloat | |
if offset < halfDuration { | |
value = offset / halfDuration | |
} else { | |
value = 1.0 - ((offset - halfDuration) / halfDuration) | |
} | |
if value == 0 { | |
return 0.1 | |
} else { | |
return value | |
} | |
} | |
let active:Bool | |
@StateObject var magic = SparkleMagic() | |
var body: some View { | |
if active { | |
GeometryReader { geo in | |
ZStack { | |
TimelineView(.animation) { context in | |
let _ = magic.update(date: context.date) | |
ForEach(magic.sparkles) { sparkle in | |
SparkleShape() | |
.fill(Color.white) | |
.frame(width:10, height:10) | |
.scaleEffect(scaleFor(date: context.date, sparkle: sparkle)) | |
.position(self.position(in: geo, sparkle:sparkle)) | |
} | |
} | |
} | |
} | |
} else { | |
EmptyView() | |
} | |
} | |
} | |
struct SparkleShape:Shape { | |
func path(in rect: CGRect) -> Path { | |
var path = Path() | |
let radius = 1.0 | |
path.move(to: CGPoint(x: rect.midX, y: rect.minY)) | |
path.addLine(to: CGPoint(x: rect.midX - radius, y: rect.midY - radius)) | |
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY)) | |
path.addLine(to: CGPoint(x: rect.midX - radius, y: rect.midY + radius)) | |
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) | |
path.addLine(to: CGPoint(x: rect.midX + radius, y: rect.midY + radius)) | |
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY)) | |
path.addLine(to: CGPoint(x: rect.midX + radius, y: rect.midY - radius)) | |
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY)) | |
return path | |
} | |
} | |
class Sparkle:Identifiable { | |
let id = UUID() | |
let position:CGPoint | |
let startDate:Date | |
init(position: CGPoint, startDate: Date) { | |
self.position = position | |
self.startDate = startDate | |
} | |
} | |
class SparkleMagic:ObservableObject { | |
static let sparkleDuration:Double = 2.0 | |
var sparkles:[Sparkle] | |
init() { | |
let anchor = Date() | |
var result:[Sparkle] = [] | |
for _ in 0..<20 { | |
result.append(Sparkle(position: CGPoint(x: CGFloat.random(in: 0...1), | |
y: CGFloat.random(in: 0...1)), | |
startDate: anchor.addingTimeInterval(Double.random(in: 0...(SparkleMagic.sparkleDuration))))) | |
} | |
self.sparkles = result | |
} | |
func update(date:Date) { | |
let anchor = Date() | |
var result:[Sparkle] = [] | |
for sparkle in sparkles { | |
if anchor.timeIntervalSince(sparkle.startDate) > SparkleMagic.sparkleDuration { | |
result.append(Sparkle(position: CGPoint(x: CGFloat.random(in: 0...1), | |
y: CGFloat.random(in: 0...1)), | |
startDate: anchor.addingTimeInterval(Double.random(in: 0...(SparkleMagic.sparkleDuration))))) | |
} else { | |
result.append(sparkle) | |
} | |
} | |
self.sparkles = result | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment