gh-EmergeTools-Pow/Sources/Pow/Transitions/Move.swift
Joe Fabisevich 5b95fe95b0
Moving Pow to @emergetools (#36)
Co-authored-by: Robert Böhnke <robb@robb.is>
Co-authored-by: Kasper Lahti <kasper@lahti.email>
2023-11-29 12:08:53 -03:00

263 lines
8.9 KiB
Swift

import SwiftUI
import simd
public extension AnyTransition.MovingParts {
/// A transition that moves the view from the specified edge of the on
/// insertion and towards it on removal.
static func move(edge: Edge) -> AnyTransition {
return .modifier(
active: Scaled(Move(edge: edge)),
identity: Scaled(Move())
)
}
/// A transition that moves the view at the specified angle.
///
/// The angle is relative to the current `layoutDirection`, such that 0° represents animating towards the trailing edge on insertion and 90° represents inserting towards the bottom edge.
///
/// In this example, the view insertion is animated by moving it towards the top trailing corner and the removal is animated by moving it towards the bottom edge.
///
/// ```swift
/// Text("Hello")
/// .transition(
/// .asymmetric(
/// insertion: .movingParts.move(angle: .degrees(45)),
/// removal: .movingParts.move(angle: .degrees(90))
/// )
/// )
/// ```
///
/// - Parameter angle: The direction of the animation.
static func move(angle: Angle) -> AnyTransition {
return .modifier(
active: Scaled(Move(angle: angle)),
identity: Scaled(Move())
)
}
}
internal struct Move: GeometryEffect, Animatable {
/// Translation is relative, depth is ignored, anchor is always
/// `UnitPoint(0.5, 0.5)`.
var animatableData: TRS = .identity
init(edge: Edge) {
switch edge {
case .top:
animatableData.translation.y = -1
case .leading:
animatableData.translation.x = -1
case .bottom:
animatableData.translation.y = 1
case .trailing:
animatableData.translation.x = 1
}
}
init() {}
init(angle: Angle) {
let u = cos(angle.radians)
let v = sin(angle.radians)
let u_2: Double = pow(u, 2)
let v_2: Double = pow(v, 2)
let sq2: Double = sqrt(2.0)
let x: Double = 0.5 * sqrt(abs(2.0 + u_2 - v_2 + 2.0 * u * sq2)) - 0.5 * sqrt(abs(2.0 + u_2 - v_2 - 2.0 * u * sq2))
let y: Double = 0.5 * sqrt(abs(2.0 - u_2 + v_2 + 2.0 * v * sq2)) - 0.5 * sqrt(abs(2.0 - u_2 + v_2 - 2.0 * v * sq2))
animatableData.translation.x = -x
animatableData.translation.y = -y
}
private var trs: TRS {
get { animatableData }
set { animatableData = newValue }
}
func effectValue(size: CGSize) -> ProjectionTransform {
let anchor = UnitPoint.center
let offset = simd_double4x4(translationX: size.width * anchor.x, y: size.height * anchor.y)
let translation = simd_double4x4(translationX: trs.translation.x * size.width, y: trs.translation.y * size.height, z: 0)
let rotation = simd_double4x4(trs.rotation.normalized)
let scale = simd_double4x4(scaleX: trs.scale.x, y: trs.scale.y, z: 1)
return ProjectionTransform((((offset * translation) * rotation) * scale) * offset.inverse)
}
}
#if os(iOS) && DEBUG
@available(iOS 15.0, *)
struct Move_Previews: PreviewProvider {
struct Preview: View {
@State
var indices: [UUID] = [UUID()]
enum DirectionType: String, Hashable, Identifiable, CaseIterable {
case edge = "Edge"
case angle = "Angle"
var name: String {
return rawValue
}
var id: Self {
return self
}
}
@State
var directionType: DirectionType = .edge
@State
var edge: Edge = .leading
@State
var angle: Angle = .degrees(0)
@State
var isRightToLeft: Bool = false
func makeTransition() -> AnyTransition {
switch directionType {
case .edge:
return .movingParts.move(edge: edge)
case .angle:
return .movingParts.move(angle: angle)
}
}
var resolvedAngle: Angle {
switch directionType {
case .edge:
switch edge {
case .top:
return .degrees(90)
case .leading:
return .degrees(0)
case .bottom:
return .degrees(270)
case .trailing:
return .degrees(180)
}
case .angle:
return angle
}
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading, spacing: 12) {
Text("Swoosh")
.bold()
Text("myView.transition(**.movingParts.move**)")
}
.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(.spring()) {
indices.append(UUID())
}
} onDecrement: {
if !indices.isEmpty {
let _ = withAnimation {
indices.removeLast()
}
}
}
Toggle("Right To Left", isOn: $isRightToLeft)
if #available(iOS 16.0, *) {
Picker("Type", selection: $directionType) {
ForEach(DirectionType.allCases) { type in
Text(type.name).tag(type)
}
}
.pickerStyle(.segmented)
switch directionType {
case .edge:
LabeledContent("Edge") {
Picker("Edge", selection: $edge) {
Group {
Text("Leading").tag(Edge.leading)
Text("Trailing").tag(Edge.trailing)
Text("Top").tag(Edge.top)
Text("Bottom").tag(Edge.bottom)
}
}
}
.pickerStyle(.menu)
.frame(height: 44)
case .angle:
LabeledContent("Angle") {
AngleControl(angle: $angle)
}
.frame(height: 44)
}
LabeledContent("Reference") {
Image(systemName: "arrow.forward.circle")
.imageScale(.large)
.rotationEffect(resolvedAngle)
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
}
}
let columns: [GridItem] = [
.init(.flexible()),
.init(.flexible())
]
LazyVGrid(columns: columns) {
ForEach(indices, id: \.self) { uuid in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.accentColor)
Text("Hello\nWorld!")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.font(.system(.title, design: .rounded))
}
.transition(
makeTransition().combined(with: .opacity)
)
.aspectRatio(1.1, contentMode: .fit)
.id(uuid)
}
}
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
Spacer()
}
.padding()
}
}
}
static var previews: some View {
NavigationView {
Preview()
.navigationBarHidden(true)
}
}
}
#endif