Created
March 26, 2025 10:19
-
-
Save Codelaby/e16f46ab4387d0092db16b3d3bf34435 to your computer and use it in GitHub Desktop.
Curve experiments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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