mirror of
https://github.com/EmergeTools/Pow.git
synced 2026-03-25 08:55:50 +00:00
265 lines
10 KiB
Swift
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
|