Created
May 29, 2022 05:34
-
-
Save YusukeHosonuma/df4feacdd857f605e32fb8b5780726a2 to your computer and use it in GitHub Desktop.
Canvas で Apple ロゴっぽいレインボーなレンダリングをするやつ + TimelineView でアニメーション
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
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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
2022-05-29.14.28.37.mp4