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

294 lines
10 KiB
Swift

import SwiftUI
#if os(iOS) && EMG_PREVIEWS
import SnapshotPreferences
#endif
public extension AnyTransition.MovingParts {
/// The direction from which to animate in during a `skid` transition's insertion.
enum SkidDirection {
case leading
case trailing
}
/// A transition that moves the view in from its leading edge with any
/// overshoot resulting in an elastic deformation of the view.
static var skid: AnyTransition {
skid(direction: .leading)
}
/// A transition that moves the view in from the specified edge during
/// insertion and towards it during removal with any overshoot resulting
/// in an elastic deformation of the view.
///
/// - Parameter direction: The direction of the transition.
static func skid(direction: SkidDirection) -> AnyTransition {
.modifier(
active: Scaled(Skid(direction, animatableData: 0)),
identity: Scaled(Skid(direction, animatableData: 1))
)
}
}
internal struct Skid: DebugProgressableAnimation, GeometryEffect {
var direction: AnyTransition.MovingParts.SkidDirection
var animatableData: CGFloat = 0
internal init(_ direction: AnyTransition.MovingParts.SkidDirection, animatableData: CGFloat = 0) {
self.animatableData = animatableData
self.direction = direction
}
func effectValue(size: CGSize) -> ProjectionTransform {
let deltaX = -size.width * 2 * (1 - animatableData)
var t = CGAffineTransform.identity
t = t.translatedBy(x: size.width / 2, y: size.height / 2)
let clampedDeltaX = deltaX
switch direction {
case .leading:
t = t.translatedBy(x: clampedDeltaX, y: 0)
case .trailing:
t = t.translatedBy(x: -clampedDeltaX, y: 0)
}
let newMainAxisSize = clamp(size.width / 2, size.width - deltaX, size.width * 1.5)
switch direction {
case .leading:
t = t.translatedBy(x: -size.width * (-1 + (newMainAxisSize / size.width)), y: 0)
t = CGAffineTransformShear(t, -1 + (newMainAxisSize / size.width), 0)
case .trailing:
t = t.translatedBy(x: -size.width * (1 - (newMainAxisSize / size.width)), y: 0)
t = CGAffineTransformShear(t, 1 - (newMainAxisSize / size.width), 0)
}
t = t.translatedBy(x: -size.width / 2, y: -size.height / 2)
return ProjectionTransform(t)
}
}
#if os(iOS) && DEBUG
struct Skid_Preview: PreviewableAnimation, PreviewProvider {
static var animation: Skid {
Skid(.leading)
}
static var content: some View {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(Color.orange)
.overlay {
Text("Jell-O\nWorld")
.blendMode(.difference)
.offset(x: 2, y: 2)
}
.compositingGroup()
.overlay {
Text("Jell-O\nWorld")
}
.font(.system(.headline, design: .rounded).weight(.black))
.multilineTextAlignment(.center)
.frame(width: 150, height: 150)
}
}
@available(iOS 15.0, *)
struct Skid_Previews: PreviewProvider {
struct Item: Identifiable {
var color: Color
let id: UUID = UUID()
init() {
color = [Color.red, .orange, .yellow, .green, .indigo, .teal].randomElement()!
}
}
struct Preview: View {
@State
var items: [Item] = [Item()]
@State
var damping: Double = 0.66
@State
var direction: AnyTransition.MovingParts.SkidDirection = .leading
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
Text("Skid")
.bold()
Text("myView.transition(**.movingParts.skid**)\n .animation(.interactiveSpring(\n dampingFraction: \(damping.formatted(.number.precision(.fractionLength(2))))\n )\n)")
}
.font(.footnote.monospaced())
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
.padding()
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(.thickMaterial)
)
Slider(value: $damping, in: 0.2 ... 0.8)
Stepper("Count") {
withAnimation {
items.append(Item())
}
} onDecrement: {
withAnimation {
if !items.isEmpty {
items.removeLast()
}
}
}
if #available(iOS 16.0, *) {
LabeledContent("Direction") {
Picker("Direction", selection: $direction) {
Group {
Label("Leading", systemImage: "arrow.forward").tag(AnyTransition.MovingParts.SkidDirection.leading)
Label("Trailing", systemImage: "arrow.backward").tag(AnyTransition.MovingParts.SkidDirection.trailing)
}
}
}
.pickerStyle(.menu)
}
let columns: [GridItem] = [
.init(.flexible()),
.init(.flexible()),
.init(.flexible())
]
LazyVGrid(columns: columns) {
ForEach(items) { item in
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(item.color)
.overlay {
Text("Jell-O\nWorld")
.blendMode(.difference)
.offset(x: 2, y: 2)
}
.compositingGroup()
.overlay {
Text("Jell-O\nWorld")
}
.font(.system(.headline, design: .rounded).weight(.black))
.multilineTextAlignment(.center)
.transition(
.asymmetric(
insertion: .movingParts.skid(direction: direction)
.animation(.spring(dampingFraction: damping).speed(0.6))
.combined(with: .opacity.animation(.easeOut(duration: 0.01))),
removal: .opacity
)
)
.aspectRatio(1, contentMode: .fit)
.id(item.id)
}
}
Spacer()
}
.padding(.horizontal)
}
}
}
static var previews: some View {
NavigationView {
Preview()
.navigationBarHidden(true)
}
.environment(\.colorScheme, .dark)
#if os(iOS) && EMG_PREVIEWS
.emergeSnapshotPrecision(0)
#endif
}
}
@available(iOS 15.0, *)
struct Skid_2_Previews: PreviewProvider {
struct Preview: View {
@State
var isVisible: Bool = false
@State
var isRightToLeft: Bool = true
var body: some View {
VStack {
Toggle("Visible", isOn: $isVisible.animation())
Toggle("Right To Left", isOn: $isRightToLeft)
if #available(iOS 16.0, *) {
LabeledContent("Reference") {
Image(systemName: "arrow.forward.circle")
.imageScale(.large)
}
} else {
HStack {
Text("Reference")
Spacer()
Image(systemName: "arrow.forward.circle")
.imageScale(.large)
}
}
Spacer()
let overshoot = Animation.movingParts.overshoot(duration: 0.3)
let mediumSpring = Animation.interactiveSpring(dampingFraction: 0.5)
let looseSpring = Animation.interpolatingSpring(stiffness: 100, damping: 8)
Group {
if isVisible {
Color.blue
.frame(width: 120, height: 120)
.transition(.movingParts.skid(direction: .leading).animation(overshoot))
Color.blue
.frame(width: 120, height: 120)
.transition(.movingParts.skid(direction: .leading).animation(mediumSpring))
Color.blue
.frame(width: 120, height: 120)
.transition(.movingParts.skid(direction: .trailing).animation(looseSpring))
Color.blue
.frame(width: 120, height: 120)
.transition(.movingParts.move(edge: .leading).animation(looseSpring))
}
}
Spacer()
}
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
.padding()
.background {
Color.white.ignoresSafeArea()
}
}
}
static var previews: some View {
NavigationView {
Preview()
}
}
}
#endif
private extension CGAffineTransform {
init(skewX x: CGFloat, y: CGFloat) {
self.init(a: 1, b: x, c: y, d: 1, tx: 0, ty: 0)
}
}