Skip to content

Instantly share code, notes, and snippets.

@YusukeHosonuma
Created May 29, 2022 05:34
Show Gist options
  • Save YusukeHosonuma/df4feacdd857f605e32fb8b5780726a2 to your computer and use it in GitHub Desktop.
Save YusukeHosonuma/df4feacdd857f605e32fb8b5780726a2 to your computer and use it in GitHub Desktop.
Canvas で Apple ロゴっぽいレインボーなレンダリングをするやつ + TimelineView でアニメーション
import SwiftUI
struct ContentView: View {
@State private var tick: Int = 0
@State private var opacity: Double = 0
@State private var inverse = true
var body: some View {
TimelineView(.animation) { timeline in
content(timeline.date)
}
}
private func content(_ date: Date) -> some View {
VStack(spacing: 12) {
ForEach(["applelogo", "swift"], id: \.self) { name in
HStack(spacing: 12) {
let logo = RainbowLogo(systemName: name, date: date, opacity: opacity, inverse: inverse).border(.red)
logo.frame(width: 140, height: 140)
logo.frame(width: 140, height: 120)
logo.frame(width: 120, height: 140)
}
.onChange(of: date) { _ in
tick += 1
opacity = CGFloat(tick % 200) / 200.0
if tick % 200 == 0 {
inverse.toggle()
}
}
}
}
.safeAreaInset(edge: .bottom) {
Text("\(opacity.formatted(.percent))")
.font(.largeTitle)
.fontWeight(.heavy)
}
}
}
private let colors: [Color] = [
.green,
.green,
.green,
.yellow,
.orange,
.red,
.purple,
.blue
]
struct RainbowLogo: View {
let systemName: String
let date: Date
let opacity: Double
let inverse: Bool
@State private var ratio: CGFloat = 1.0
var body: some View {
ZStack {
Canvas { context, size in
let canvasRatio = size.width / size.height
//
// Compute frame of image.
//
let w: CGFloat
let h: CGFloat
if ratio > canvasRatio {
w = size.width
h = size.width * (1 / ratio)
} else {
w = size.height * ratio
h = size.height
}
let x: CGFloat = (size.width - w) / 2
let y: CGFloat = (size.height - h) / 2
//
// Clip to image.
//
if inverse {
context.clipToLayer(opacity: 1.0, options: .inverse) { context in
let rect = CGRect(x: x, y: y, width: w, height: h)
context.draw(Image(systemName: systemName), in: rect)
}
} else {
context.clipToLayer(opacity: opacity) { context in
let rect = CGRect(x: x, y: y, width: w, height: h)
context.draw(Image(systemName: systemName), in: rect)
}
}
//
// Render borders.
//
var rect = CGRect(x: 0,
y: inverse ? 0 : y,
width: size.width,
height: (inverse ? size.height : h) / CGFloat(colors.count))
for color in colors {
if inverse {
context.fill(Path(rect), with: .color(color.opacity(opacity)))
} else {
context.fill(Path(rect), with: .color(color))
}
rect.origin.y += rect.height
}
} symbols: {
Color.clear.frame(width: ratio) // ☑️ Invalidates canvas by `ratio` was changed.
}
//
// Get ratio of image. (not rendered)
//
Image(systemName: systemName)
.hidden()
.readSize {
ratio = $0.width / $0.height
}
}
}
}
extension View {
func readSize(perform: @escaping (CGSize) -> ()) -> some View {
self.background(
GeometryReader { geometry in
Color.clear
.preference(key: SizeKey.self, value: geometry.size)
}
)
.onPreferenceChange(SizeKey.self, perform: perform)
}
}
struct SizeKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
@YusukeHosonuma
Copy link
Author

2022-05-29.14.28.37.mp4

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