Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Created March 26, 2025 10:19
Show Gist options
  • Save Codelaby/e16f46ab4387d0092db16b3d3bf34435 to your computer and use it in GitHub Desktop.
Save Codelaby/e16f46ab4387d0092db16b3d3bf34435 to your computer and use it in GitHub Desktop.
Curve experiments
//
// CraterShape.swift
// IOS18Playground
//
// Created by Codelay on 26/3/25.
//
import SwiftUI
// Helper for animating three values
struct AnimatableTriplet<First: VectorArithmetic, Second: VectorArithmetic, Third: VectorArithmetic>: VectorArithmetic {
var first: First
var second: Second
var third: Third
init(_ first: First, _ second: Second, _ third: Third) {
self.first = first
self.second = second
self.third = third
}
static var zero: AnimatableTriplet<First, Second, Third> {
return AnimatableTriplet(First.zero, Second.zero, Third.zero)
}
static func + (lhs: AnimatableTriplet<First, Second, Third>, rhs: AnimatableTriplet<First, Second, Third>) -> AnimatableTriplet<First, Second, Third> {
return AnimatableTriplet(
lhs.first + rhs.first,
lhs.second + rhs.second,
lhs.third + rhs.third
)
}
static func - (lhs: AnimatableTriplet<First, Second, Third>, rhs: AnimatableTriplet<First, Second, Third>) -> AnimatableTriplet<First, Second, Third> {
return AnimatableTriplet(
lhs.first - rhs.first,
lhs.second - rhs.second,
lhs.third - rhs.third
)
}
mutating func scale(by rhs: Double) {
first.scale(by: rhs)
second.scale(by: rhs)
third.scale(by: rhs)
}
var magnitudeSquared: Double {
return first.magnitudeSquared + second.magnitudeSquared + third.magnitudeSquared
}
}
struct AnimatableQuad<First: VectorArithmetic, Second: VectorArithmetic, Third: VectorArithmetic, Fourth: VectorArithmetic>: VectorArithmetic {
var first: First
var second: Second
var third: Third
var fourth: Fourth
init(_ first: First, _ second: Second, _ third: Third, _ fourth: Fourth) {
self.first = first
self.second = second
self.third = third
self.fourth = fourth
}
static var zero: AnimatableQuad<First, Second, Third, Fourth> {
return AnimatableQuad(First.zero, Second.zero, Third.zero, Fourth.zero)
}
static func + (lhs: AnimatableQuad<First, Second, Third, Fourth>, rhs: AnimatableQuad<First, Second, Third, Fourth>) -> AnimatableQuad<First, Second, Third, Fourth> {
return AnimatableQuad(
lhs.first + rhs.first,
lhs.second + rhs.second,
lhs.third + rhs.third,
lhs.fourth + rhs.fourth
)
}
static func - (lhs: AnimatableQuad<First, Second, Third, Fourth>, rhs: AnimatableQuad<First, Second, Third, Fourth>) -> AnimatableQuad<First, Second, Third, Fourth> {
return AnimatableQuad(
lhs.first - rhs.first,
lhs.second - rhs.second,
lhs.third - rhs.third,
lhs.fourth - rhs.fourth
)
}
mutating func scale(by rhs: Double) {
first.scale(by: rhs)
second.scale(by: rhs)
third.scale(by: rhs)
fourth.scale(by: rhs)
}
var magnitudeSquared: Double {
return first.magnitudeSquared + second.magnitudeSquared + third.magnitudeSquared + fourth.magnitudeSquared
}
}
struct AnimatableQuintuple<First: VectorArithmetic, Second: VectorArithmetic, Third: VectorArithmetic, Fourth: VectorArithmetic, Fifth: VectorArithmetic>: VectorArithmetic {
var first: First
var second: Second
var third: Third
var fourth: Fourth
var fifth: Fifth
init(_ first: First, _ second: Second, _ third: Third, _ fourth: Fourth, _ fifth: Fifth) {
self.first = first
self.second = second
self.third = third
self.fourth = fourth
self.fifth = fifth
}
static var zero: AnimatableQuintuple<First, Second, Third, Fourth, Fifth> {
return AnimatableQuintuple(First.zero, Second.zero, Third.zero, Fourth.zero, Fifth.zero)
}
static func + (lhs: AnimatableQuintuple, rhs: AnimatableQuintuple) -> AnimatableQuintuple {
return AnimatableQuintuple(
lhs.first + rhs.first,
lhs.second + rhs.second,
lhs.third + rhs.third,
lhs.fourth + rhs.fourth,
lhs.fifth + rhs.fifth
)
}
static func - (lhs: AnimatableQuintuple, rhs: AnimatableQuintuple) -> AnimatableQuintuple {
return AnimatableQuintuple(
lhs.first - rhs.first,
lhs.second - rhs.second,
lhs.third - rhs.third,
lhs.fourth - rhs.fourth,
lhs.fifth - rhs.fifth
)
}
mutating func scale(by rhs: Double) {
first.scale(by: rhs)
second.scale(by: rhs)
third.scale(by: rhs)
fourth.scale(by: rhs)
fifth.scale(by: rhs)
}
var magnitudeSquared: Double {
return first.magnitudeSquared + second.magnitudeSquared + third.magnitudeSquared + fourth.magnitudeSquared + fifth.magnitudeSquared
}
}
struct CraterShape: Shape {
var craterWidth: CGFloat = 60
var craterDepth: CGFloat = 20
var offsetX: CGFloat = 0 // New parameter for horizontal offset
// Make the shape animatable
var animatableData: AnimatableTriplet<CGFloat, CGFloat, CGFloat> {
get { AnimatableTriplet(craterWidth, craterDepth, offsetX) }
set {
craterWidth = newValue.first
craterDepth = newValue.second
offsetX = newValue.third
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
let craterCenterX = rect.midX + offsetX
// Start from top-left
path.move(to: CGPoint(x: 0, y: 0))
// Line to left side of crater
path.addLine(to: CGPoint(x: craterCenterX - craterWidth/2, y: 0))
// Create the crater indentation
path.addCurve(
to: CGPoint(x: craterCenterX + craterWidth/2, y: 0),
control1: CGPoint(x: craterCenterX - craterWidth/2, y: craterDepth),
control2: CGPoint(x: craterCenterX + craterWidth/2, y: craterDepth)
)
// Complete the rectangle
path.addLine(to: CGPoint(x: rect.maxX, y: 0))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
return path
}
}
//struct CraterShape: Shape {
// var craterWidth: CGFloat = 60
// var craterDepth: CGFloat = 20
// var offsetX: CGFloat = 0
// var smoothness: CGFloat = 1.0 // Controla la curvatura y fluidez del cráter
//
// // Hacer la forma animable
// var animatableData: AnimatableQuad<CGFloat, CGFloat, CGFloat, CGFloat> {
// get { AnimatableQuad(craterWidth, craterDepth, offsetX, smoothness) }
// set {
// craterWidth = newValue.first
// craterDepth = newValue.second
// offsetX = newValue.third
// smoothness = newValue.fourth
// }
// }
//
// func path(in rect: CGRect) -> Path {
// var path = Path()
//
// let baseY = rect.midY
// let peakX = rect.midX + offsetX
// let peakY = baseY + craterDepth
//
// let leftBaseX = peakX - craterWidth / 2
// let rightBaseX = peakX + craterWidth / 2
//
// let leftCtrlX = leftBaseX + craterWidth * smoothness * 0.3
// let rightCtrlX = rightBaseX - craterWidth * smoothness * 0.3
//
// let curveDepthY = peakY - craterDepth * smoothness * 0.7
//
// path.move(to: CGPoint(x: 0, y: baseY))
// path.addLine(to: CGPoint(x: leftBaseX, y: baseY))
//
// // 🔹 Borde izquierdo del cráter
// path.addQuadCurve(to: CGPoint(x: peakX - craterWidth / 4, y: curveDepthY),
// control: CGPoint(x: leftCtrlX, y: baseY))
//
// // 🔹 Fondo redondeado del cráter con Bezier cúbica
// path.addCurve(to: CGPoint(x: peakX + craterWidth / 4, y: curveDepthY),
// control1: CGPoint(x: peakX - craterWidth / 8, y: peakY),
// control2: CGPoint(x: peakX + craterWidth / 8, y: peakY))
//
// // 🔹 Borde derecho del cráter
// path.addQuadCurve(to: CGPoint(x: rightBaseX, y: baseY),
// control: CGPoint(x: rightCtrlX, y: baseY))
//
// path.addLine(to: CGPoint(x: rect.maxX, y: baseY))
// path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
// path.addLine(to: CGPoint(x: 0, y: rect.maxY))
// path.closeSubpath()
//
// return path
// }
//}
#Preview {
struct InteractivePreview: View {
@State private var width: CGFloat = 60
@State private var depth: CGFloat = 20
@State private var offset: CGFloat = 0
@State private var smoothness: CGFloat = 0.5
@State private var isAnimating = false
var body: some View {
VStack {
Spacer()
CraterShape(
craterWidth: width,
craterDepth: depth,
offsetX: offset//,
// smoothness: smoothness
)
.fill(Color.blue)
.frame(height: 100)
.animation(.easeInOut(duration: 1), value: isAnimating)
Spacer()
VStack {
Slider(value: $width, in: 30...120) {
Text("Width: \(Int(width))")
}
Slider(value: $depth, in: 0...40) {
Text("Depth: \(Int(depth))")
}
Slider(value: $offset, in: -100...100) {
Text("Offset: \(Int(offset))")
}
Slider(value: $smoothness, in: 0...1) {
Text("Smoothness: \(smoothness, specifier: "%.1f")")
}
Button(isAnimating ? "Stop Animation" : "Start Animation") {
isAnimating.toggle()
if isAnimating {
withAnimation(.easeInOut.repeatForever(autoreverses: true)) {
width = 100
depth = 30
offset = 50
smoothness = 0.8
}
} else {
withAnimation {
width = 60
depth = 20
offset = 0
smoothness = 0.5
}
}
}
.padding()
}
.padding()
}
}
}
return InteractivePreview()
}
struct SmoothNotchShape: Shape {
var cornerRadius: CGFloat
var notchWidth: CGFloat = 80
var notchDepth: CGFloat = 12
var notchEdges: Set<Edge> = [.top]
var animatableData: CGFloat {
get { cornerRadius }
set { cornerRadius = newValue }
}
func path(in rect: CGRect) -> Path {
// Asegura que el radio de las esquinas no exceda la mitad del ancho o altura del rectángulo
let boundedCornerRadius = min(cornerRadius, min(rect.width, rect.height) / 2)
return Path { p in
// Mover a la posición inicial
p.move(to: CGPoint(x: rect.minX + boundedCornerRadius, y: rect.minY))
// Línea superior izquierda
if notchEdges.contains(.top) {
p.addLine(to: CGPoint(x: rect.midX - notchWidth / 2, y: rect.minY))
// Muesca superior
p.addQuadCurve(
to: CGPoint(x: rect.midX + notchWidth / 2, y: rect.minY),
control: CGPoint(x: rect.midX, y: rect.minY - notchDepth)
)
} else {
// Línea superior derecha
p.addLine(to: CGPoint(x: rect.maxX - boundedCornerRadius, y: rect.minY))
}
// Esquina superior derecha
p.addArc(
center: CGPoint(x: rect.maxX - boundedCornerRadius, y: rect.minY + boundedCornerRadius),
radius: boundedCornerRadius,
startAngle: Angle(degrees: 270),
endAngle: Angle(degrees: 360),
clockwise: false
)
// Línea derecha
if notchEdges.contains(.trailing) {
p.addLine(to: CGPoint(x: rect.maxX, y: rect.midY - notchWidth / 2))
// Muesca derecha
p.addQuadCurve(
to: CGPoint(x: rect.maxX, y: rect.midY + notchWidth / 2),
control: CGPoint(x: rect.maxX + notchDepth, y: rect.midY)
)
} else {
p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - boundedCornerRadius))
}
// Esquina inferior derecha
p.addArc(
center: CGPoint(x: rect.maxX - boundedCornerRadius, y: rect.maxY - boundedCornerRadius),
radius: boundedCornerRadius,
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 90),
clockwise: false
)
// Línea inferior derecha
if notchEdges.contains(.bottom) {
p.addLine(to: CGPoint(x: rect.midX + notchWidth / 2, y: rect.maxY))
// Muesca inferior
p.addQuadCurve(
to: CGPoint(x: rect.midX - notchWidth / 2, y: rect.maxY),
control: CGPoint(x: rect.midX, y: rect.maxY + notchDepth)
)
} else {
p.addLine(to: CGPoint(x: rect.minX + boundedCornerRadius, y: rect.maxY))
}
// Esquina inferior izquierda
p.addArc(
center: CGPoint(x: rect.minX + boundedCornerRadius, y: rect.maxY - boundedCornerRadius),
radius: boundedCornerRadius,
startAngle: Angle(degrees: 90),
endAngle: Angle(degrees: 180),
clockwise: false
)
// Línea izquierda
if notchEdges.contains(.leading) {
p.addLine(to: CGPoint(x: rect.minX, y: rect.midY + notchWidth / 2))
// Muesca izquierda
p.addQuadCurve(
to: CGPoint(x: rect.minX, y: rect.midY - notchWidth / 2),
control: CGPoint(x: rect.minX - notchDepth, y: rect.midY)
)
} else {
p.addLine(to: CGPoint(x: rect.minX, y: rect.minY + boundedCornerRadius))
}
// Esquina superior izquierda
p.addArc(
center: CGPoint(x: rect.minX + boundedCornerRadius, y: rect.minY + boundedCornerRadius),
radius: boundedCornerRadius,
startAngle: Angle(degrees: 180),
endAngle: Angle(degrees: 270),
clockwise: false
)
p.closeSubpath()
}
}
}
#Preview("Smooth Notch") {
struct PreviewWrapper: View {
var body: some View {
VStack {
Text("Smooth Notch Example")
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
Spacer()
// Utiliza HalfCircleNotchTop para dibujar la forma
SmoothNotchShape(
cornerRadius: 16,
notchWidth: 60,
notchDepth: -32,
notchEdges: [.top, .bottom]
)
.fill(Color.blue)
//.stroke(.red, lineWidth: 8)
.frame(width: 280, height: 340)
Spacer()
Text("bento.me/codelaby")
.foregroundColor(.blue)
}
.padding()
}
}
return PreviewWrapper()
}
struct SmoothArcExample: View {
var body: some View {
Path { path in
let midX = UIScreen.main.bounds.midX
let base1 = CGPoint(x: midX - 10, y: .zero)
let base2 = CGPoint(x: midX + 30, y: .zero)
let ctrl1 = CGPoint(x: midX, y: .zero)
let ctrl2 = CGPoint(x: midX+20, y: .zero)
let curv = CGPoint(x: midX, y: 10)
let peak = CGPoint(x: midX+10, y: 10)
path.move(to: base1)
path.addQuadCurve(to: curv, control: ctrl1)
path.addArc(center: peak, radius: 10, startAngle: .degrees(180), endAngle: .degrees(0), clockwise: true)
path.addQuadCurve(to: base2, control: ctrl2)
}
.stroke()
}
}
#Preview {
SmoothArcExample()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment