mirror of
https://github.com/EmergeTools/Pow.git
synced 2026-03-25 08:55:50 +00:00
316 lines
11 KiB
Swift
316 lines
11 KiB
Swift
import SwiftUI
|
|
#if os(iOS) && EMG_PREVIEWS
|
|
import SnapshotPreferences
|
|
#endif
|
|
|
|
public extension AnyTransition.MovingParts {
|
|
/// A transition that moves the view down with any overshoot resulting in an
|
|
/// elastic deformation of the view.
|
|
static var boing: AnyTransition {
|
|
boing(edge: .top)
|
|
}
|
|
|
|
/// A transition that moves the view from the specified edge on insertion,
|
|
/// and towards it on removal, with any overshoot resulting in an elastic
|
|
/// deformation of the view.
|
|
static func boing(edge: Edge) -> AnyTransition {
|
|
.modifier(
|
|
active: Scaled(Boing(edge, animatableData: 0)),
|
|
identity: Scaled(Boing(edge, animatableData: 1))
|
|
)
|
|
}
|
|
}
|
|
|
|
internal struct Boing: DebugProgressableAnimation, GeometryEffect {
|
|
var edge: Edge
|
|
|
|
var animatableData: CGFloat = 0
|
|
|
|
internal init(_ edge: Edge, animatableData: CGFloat = 0) {
|
|
self.animatableData = animatableData
|
|
self.edge = edge
|
|
}
|
|
|
|
func effectValue(size: CGSize) -> ProjectionTransform {
|
|
let area = size.width * size.height
|
|
|
|
var mainAxisSize: CGFloat {
|
|
edge == .leading || edge == .trailing ? size.width : size.height
|
|
}
|
|
|
|
var crossAxisSize: CGFloat {
|
|
edge == .leading || edge == .trailing ? size.height : size.width
|
|
}
|
|
|
|
let deltaP = -mainAxisSize * 2 * (1 - animatableData)
|
|
|
|
var t = CGAffineTransform.identity
|
|
|
|
if deltaP < 1 {
|
|
let newMainAxisSize = rubberClamp(mainAxisSize / 2, mainAxisSize - deltaP / 3, mainAxisSize * 1.5)
|
|
let newCrossAxisSize = area / newMainAxisSize
|
|
|
|
t = t.translatedBy(x: size.width / 2, y: size.height / 2)
|
|
|
|
switch edge {
|
|
case .top:
|
|
t = t.translatedBy(x: 0, y: deltaP)
|
|
case .bottom:
|
|
t = t.translatedBy(x: 0, y: -deltaP)
|
|
case .leading:
|
|
t = t.translatedBy(x: deltaP, y: 0)
|
|
case .trailing:
|
|
t = t.translatedBy(x: -deltaP, y: 0)
|
|
}
|
|
|
|
if edge == .leading || edge == .trailing {
|
|
t = t.scaledBy(x: newMainAxisSize / mainAxisSize, y: newCrossAxisSize / crossAxisSize)
|
|
} else {
|
|
t = t.scaledBy(x: newCrossAxisSize / crossAxisSize, y: newMainAxisSize / mainAxisSize)
|
|
}
|
|
|
|
t = t.translatedBy(x: -size.width / 2, y: -size.height / 2)
|
|
}
|
|
|
|
if deltaP >= 5 {
|
|
let deltaY = deltaP - 5
|
|
|
|
let newMainAxisSize = rubberClamp(mainAxisSize * 0.75, mainAxisSize - deltaY / 3, mainAxisSize * 1)
|
|
let newCrossAxisSize = area / newMainAxisSize
|
|
|
|
let translation: CGAffineTransform
|
|
|
|
switch edge {
|
|
case .top:
|
|
translation = CGAffineTransformMakeTranslation(size.width / 2, size.height)
|
|
case .leading:
|
|
translation = CGAffineTransformMakeTranslation(size.width, size.height / 2)
|
|
case .bottom:
|
|
translation = CGAffineTransformMakeTranslation(size.width / 2, 0)
|
|
case .trailing:
|
|
translation = CGAffineTransformMakeTranslation(0, size.height / 2)
|
|
}
|
|
|
|
t = translation.concatenating(t)
|
|
|
|
if edge == .leading || edge == .trailing {
|
|
t = t.scaledBy(x: newMainAxisSize / mainAxisSize, y: newCrossAxisSize / crossAxisSize)
|
|
} else {
|
|
t = t.scaledBy(x: newCrossAxisSize / crossAxisSize, y: newMainAxisSize / mainAxisSize)
|
|
}
|
|
|
|
t = translation.inverted().concatenating(t)
|
|
}
|
|
|
|
return ProjectionTransform(t)
|
|
}
|
|
}
|
|
|
|
#if os(iOS) && DEBUG
|
|
struct Boing_Preview: PreviewableAnimation, PreviewProvider {
|
|
static var animation: Scaled<Boing> {
|
|
Scaled(Boing(.top, animatableData: 0))
|
|
}
|
|
}
|
|
|
|
@available(iOS 15.0, *)
|
|
struct Bounce_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.5
|
|
|
|
@State
|
|
var edge: Edge = .top
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Boing")
|
|
.bold()
|
|
|
|
Text("myView.transition(**.movingParts.boing**)\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 {
|
|
var item = Item()
|
|
item.color = [Color.red, .orange, .yellow, .green, .indigo, .teal].shuffled().first { color in
|
|
!items.contains { $0.color == color }
|
|
} ?? .blue
|
|
|
|
items.append(item)
|
|
}
|
|
} onDecrement: {
|
|
if !items.isEmpty {
|
|
items.removeLast()
|
|
}
|
|
}
|
|
|
|
if #available(iOS 16.0, *) {
|
|
LabeledContent("Edge") {
|
|
Picker("Edge", selection: $edge) {
|
|
Group {
|
|
Label("Leading", systemImage: "arrow.forward").tag(Edge.leading)
|
|
Label("Trailing", systemImage: "arrow.backward").tag(Edge.trailing)
|
|
Label("Top", systemImage: "arrow.down").tag(Edge.top)
|
|
Label("Bottom", systemImage: "arrow.up").tag(Edge.bottom)
|
|
}
|
|
}
|
|
}
|
|
.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(
|
|
.movingParts.boing(edge: edge)
|
|
.animation(.spring(dampingFraction: damping))
|
|
.combined(with: .opacity.animation(.easeOut(duration: 0.01)))
|
|
)
|
|
.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 Boing_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.boing(edge: .leading).animation(overshoot))
|
|
|
|
Color.blue
|
|
.frame(width: 120, height: 120)
|
|
.transition(.movingParts.boing(edge: .leading).animation(mediumSpring))
|
|
|
|
Color.blue
|
|
.frame(width: 120, height: 120)
|
|
.transition(.movingParts.boing(edge: .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()
|
|
}
|
|
#if os(iOS) && EMG_PREVIEWS
|
|
.emergeSnapshotPrecision(0)
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
private extension CGAffineTransform {
|
|
init(skewX x: CGFloat, y: CGFloat) {
|
|
self.init(a: 1, b: x, c: y, d: 1, tx: 0, ty: 0)
|
|
}
|
|
}
|