gh-EmergeTools-Pow/Sources/Pow/Transitions/Iris.swift
2024-01-03 09:01:34 -08:00

219 lines
7.4 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
public extension AnyTransition.MovingParts {
/// A transition that takes the shape of a growing circle when inserting,
/// and a shrinking circle when removing.
///
/// - Parameters:
/// - origin: The center point of the circle as it grows or shrinks.
/// - blurRadius: The radius of the blur applied to the mask.
static func iris(origin: UnitPoint = .center, blurRadius: CGFloat = 0) -> AnyTransition {
.modifier(
active: Iris(origin: origin, blurRadius: blurRadius, animatableData: 0),
identity: Iris(origin: origin, blurRadius: blurRadius, animatableData: 1)
)
}
}
struct Iris: ViewModifier, DebugProgressableAnimation, AnimatableModifier {
var origin: UnitPoint
var blurRadius: CGFloat
var animatableData: CGFloat = 0
internal init(origin: UnitPoint, blurRadius: CGFloat = 0, animatableData: CGFloat) {
self.origin = origin
self.blurRadius = clamp(0, blurRadius, 30)
self.animatableData = animatableData
}
var progress: CGFloat {
get { animatableData }
set { animatableData = newValue }
}
func body(content: Content) -> some View {
content
.mask(
GeometryReader { proxy in
let width = proxy.size.width
let height = proxy.size.height
let scaledWidth = width * 2 * max(origin.x, 1 - origin.x)
let scaledHeight = height * 2 * max(origin.y, 1 - origin.y)
let diagonal = progress * sqrt(scaledWidth * scaledWidth + scaledHeight * scaledHeight)
Circle()
.frame(width: diagonal, height: diagonal)
.position(
x: origin.x * width,
y: origin.y * height
)
.blur(radius: (1 - progress) * blurRadius)
}
)
}
}
#if os(iOS) && DEBUG
struct Iris_Preview: PreviewableAnimation, PreviewProvider {
static var animation: Iris {
Iris(origin: .center, animatableData: 0)
}
}
@available(iOS 15.0, *)
struct Mask_Previews: PreviewProvider {
struct Preview: View {
@State
var indices: [UUID] = [UUID()]
enum ShapeType: String, Hashable, Identifiable, CaseIterable {
case rectangle = "Rectangle"
case roundedRectangle = "Rounded Rectangle"
case capsule = "Capsule"
case circle = "Circle"
var name: String {
return rawValue
}
var id: Self {
return self
}
var symbolName: String {
switch self {
case .rectangle:
return "rectangle.fill"
case .roundedRectangle:
return "rectangle.roundedtop.fill"
case .capsule:
return "capsule.fill"
case .circle:
return "circle.fill"
}
}
}
@State
var selectedShape: ShapeType = .roundedRectangle
@ViewBuilder
func filledShape(color: Color) -> some View {
switch selectedShape {
case .rectangle:
Rectangle().fill(color)
case .roundedRectangle:
RoundedRectangle(cornerRadius: 8, style: .continuous).fill(color)
case .capsule:
Capsule().fill(color)
case .circle:
Circle().fill(color)
}
}
@State
var originX: CGFloat = 0.5
@State
var originY: CGFloat = 0.5
@State
var blurRadius: CGFloat = 0
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
Text("Flip")
.bold()
Text("myView.transition(.movingParts.iris())")
}
.font(.footnote.monospaced())
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding()
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(.thickMaterial)
)
Stepper {
Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
} onIncrement: {
withAnimation {
indices.append(UUID())
}
} onDecrement: {
if !indices.isEmpty {
let _ = withAnimation {
indices.removeLast()
}
}
}
if #available(iOS 16.0, *) {
LabeledContent("Shape") {
Picker("Shape", selection: $selectedShape) {
ForEach(ShapeType.allCases) { shapeType in
Label(shapeType.name, systemImage: shapeType.symbolName).tag(shapeType)
}
}
}
.pickerStyle(.menu)
LabeledContent("Origin") {
Text(originX, format: .number.precision(.fractionLength(2))) +
Text("×") +
Text(originY, format: .number.precision(.fractionLength(2)))
}
Slider(value: $originX, in: -0.5...1.5)
Slider(value: $originY, in: -0.5...1.5)
}
let columns: [GridItem] = [
.init(.flexible()),
.init(.flexible())
]
LazyVGrid(columns: columns) {
ForEach(indices, id: \.self) { uuid in
ZStack {
RoundedRectangle(cornerRadius: 32, style: .continuous)
.fill(Color.accentColor)
Text("Hello\nWorld!")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.font(.system(.title, design: .rounded))
}
.transition(
.asymmetric(
insertion: .movingParts.iris(origin: .init(x: originX, y: originY), blurRadius: blurRadius),
removal: .movingParts.iris(origin: .init(x: originX, y: originY), blurRadius: blurRadius)
)
)
.aspectRatio(2, contentMode: .fit)
.id(uuid)
}
}
Spacer()
}
.padding()
}
}
}
static var previews: some View {
NavigationView {
Preview()
.navigationBarHidden(true)
}
}
}
#endif