gh-EmergeTools-Pow/Sources/Pow/Transitions/Glare.swift
2023-12-26 17:31:16 -08:00

265 lines
10 KiB
Swift

import SwiftUI
#if os(iOS) && EMG_PREVIEWS
import SnapshotPreferences
#endif
public extension AnyTransition.MovingParts {
/// A transitions that shows the view by combining a diagonal wipe with a
/// white streak.
static var glare: AnyTransition {
glare(angle: .degrees(45))
}
/// A transitions that shows the view by combining a wipe with a colored
/// streak.
///
/// The angle is relative to the current `layoutDirection`, such that 0°
/// represents sweeping towards the trailing edge on insertion and 90°
/// represents sweeping towards the bottom edge.
///
/// In this example, the removal of the view is using a glare with an
/// exponential ease-in curve, combined with a anticipating scale animation,
/// making for a more dramatic exit.
///
/// ```swift
/// infoBox
/// .transition(
/// .asymmetric(
/// insertion: .movingParts.glare(angle: .degrees(225)),
/// removal: .movingParts.glare(angle: .degrees(45))
/// .animation(.movingParts.easeInExponential(duration: 0.9))
/// .combined(with:
/// .scale(scale: 1.4).animation(.movingParts.anticipate(duration: 0.9).delay(0.1))
/// )
/// )
/// )
/// ```
///
/// - Parameters:
/// - direction: The angle of the wipe.
/// - color: The color of the glare effect.
/// - increasedBrightness: A Boolean that indicates whether the glare is displayed with increased brightness. Defaults to `true`.
static func glare(angle: Angle, color: Color = .white, increasedBrightness: Bool = true) -> AnyTransition {
.modifier(
active: Glare(angle, color: color, increasedBrightness: increasedBrightness, animatableData: 0),
identity: Glare(angle, color: color, increasedBrightness: increasedBrightness, animatableData: 1)
)
}
}
internal struct Glare: ViewModifier, Animatable, AnimatableModifier {
var animatableData: CGFloat = 0
var angle: Angle
var color: Color
var increasedBrightness: Bool
@Environment(\.layoutDirection)
var layoutDirection
internal init(_ angle: Angle, color: Color, increasedBrightness: Bool = true, animatableData: CGFloat = 0) {
self.animatableData = animatableData
self.angle = angle
self.color = color
self.increasedBrightness = increasedBrightness
}
func body(content: Content) -> some View {
let l = animatableData * 1.6
let t = animatableData * 1
let full = color
let empty = color.opacity(0)
content
.mask {
GeometryReader { p in
let bounds = CGRect(origin: .zero, size: p.size).boundingBox(at: angle)
Rectangle()
.fill(
LinearGradient(
stops: [
Gradient.Stop(color: .black, location: -1),
Gradient.Stop(color: .black, location: l),
Gradient.Stop(color: .clear, location: l),
Gradient.Stop(color: .clear, location: 2),
],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: bounds.width, height: bounds.height)
.position(x: bounds.midX, y: bounds.midY)
.rotationEffect(angle)
.animation(nil, value: animatableData)
.animation(nil, value: angle)
}
}
.overlay {
GeometryReader { p in
let bounds = CGRect(origin: .zero, size: p.size).boundingBox(at: angle)
Rectangle()
.fill(
LinearGradient(
stops: [
Gradient.Stop(color: empty, location: -1),
Gradient.Stop(color: empty, location: t),
Gradient.Stop(color: full, location: t + 0.01),
Gradient.Stop(color: full, location: l + 0.01),
Gradient.Stop(color: empty, location: l + 0.02),
Gradient.Stop(color: empty, location: 2),
],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: bounds.width, height: bounds.height)
.position(x: bounds.midX, y: bounds.midY)
.rotationEffect(angle)
.brightness(increasedBrightness ? 4 * easeInCubic(clamp(1.0 - animatableData)) : 0)
.blendMode(.sourceAtop)
.allowsHitTesting(false)
.animation(nil, value: animatableData)
.animation(nil, value: angle)
}
}
.compositingGroup()
}
}
#if os(iOS) && DEBUG
@available(iOS 16.0, *)
struct Glare_Previews: PreviewProvider {
struct Item: Identifiable {
var color1: Color
var color2: Color
let id: UUID = UUID()
init() {
let color1: Color = [.indigo, .purple, .pink].randomElement()!
self.color1 = color1
self.color2 = [.indigo, .purple, .pink].filter {
$0 != color1
}.randomElement()!
}
}
struct Preview: View {
@State
var items: [Item] = [Item()]
@State
var angle: Angle = .degrees(45)
@State
var isRightToLeft: Bool = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
Text("Glare")
.bold()
Text("myView.transition(**.movingParts.glare**)")
}
.font(.footnote.monospaced())
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding()
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(.thickMaterial)
)
Stepper {
(Text("View Count ") + Text("(\(items.count))").foregroundColor(.secondary))
.animation(nil, value: items.count)
} onIncrement: {
withAnimation {
items.append(Item())
}
} onDecrement: {
withAnimation {
if !items.isEmpty {
items.removeLast()
}
}
}
Toggle("Right To Left", isOn: $isRightToLeft)
LabeledContent("Angle") {
AngleControl(angle: $angle)
}
LabeledContent("Reference") {
Image(systemName: "arrow.forward.circle")
.imageScale(.large)
.rotationEffect(angle)
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
}
let columns: [GridItem] = [
.init(.flexible()),
.init(.flexible()),
]
LazyVGrid(columns: columns) {
ForEach(items.indices, id: \.self) { index in
let item = items[index]
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(LinearGradient(
colors: [item.color1, item.color2],
startPoint: .topLeading,
endPoint: .bottom
))
.compositingGroup()
.overlay {
Text("Hello\nWorld")
.foregroundStyle(.white.shadow(.inner(radius: 0.5)))
}
.font(.system(.largeTitle, design: .rounded).weight(.medium))
.multilineTextAlignment(.center)
.transition(
.asymmetric(
insertion: .movingParts.glare(angle: angle),
removal: .movingParts.glare(angle: angle)
.animation(.movingParts.easeInExponential(duration: 0.9))
.combined(with:
.scale(scale: 1.4).animation(.movingParts.anticipate(duration: 0.9).delay(0.1))
)
)
)
.aspectRatio(1, contentMode: .fit)
.id(item.id)
.zIndex(Double(index))
}
}
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
Spacer()
}
.padding(.horizontal)
}
}
}
static var previews: some View {
NavigationView {
Preview()
.navigationBarHidden(true)
}
.environment(\.colorScheme, .dark)
#if os(iOS) && EMG_PREVIEWS
.emergeSnapshotPrecision(0)
#endif
}
}
#endif