I’m engaged on a SwiftUI view the place the consumer can draw a customized path, and I wish to place letters of a string alongside that path. The letters needs to be aligned perpendicularly to the trail, guaranteeing that the textual content seems to be standing upright alongside the drawn floor (whether or not the trail is straight, sloped, or curvy). Nonetheless, the angle of the letters is usually not as anticipated, particularly when the trail is sloped or curved.
Right here’s the code I’ve written to this point to attain this:
import SwiftUI
struct DraggingLettersView: View {
@State non-public var letters: [DraggedLetter] = []
@State non-public var currentIndex = 0
@State non-public var dragPath: [CGPoint] = []
non-public let longString = "Let me know in case you face any additional points! 😊"
non-public let letterSpacing: CGFloat = 15
var physique: some View {
GeometryReader { geometry in
ZStack {
// Draw the drag path
Path { path in
for (index, level) in dragPath.enumerated() {
if index == 0 {
path.transfer(to: level)
} else {
path.addLine(to: level)
}
}
}
.stroke(Colour.blue, lineWidth: 2)
// Place letters alongside the trail
ForEach(letters) { letter in
Textual content(String(letter.character))
.font(.system(measurement: 24, weight: .daring))
.foregroundColor(.white)
.rotationEffect(.levels(letter.angle))
.place(letter.place)
}
// Clear Button
Button(motion: {
letters.removeAll()
currentIndex = 0
dragPath.removeAll()
}, label: {
Textual content("Clear")
.body(width: geometry.measurement.width * 0.2 , top: geometry.measurement.top * 0.05)
.background(Colour.white)
.cornerRadius(10)
.shadow(radius: 5)
})
.place(x: geometry.measurement.width - 60, y: 80)
}
.body(width: geometry.measurement.width, top: geometry.measurement.top)
.background(Colour.grey)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { worth in
handleDrag(worth.location)
}
.onEnded { worth in
placeTextAlongPath()
}
)
}
.background(Colour.white)
.edgesIgnoringSafeArea(.all)
}
// MARK: - Deal with Drag
non-public func handleDrag(_ place: CGPoint) {
dragPath.append(place)
}
// MARK: - Place Textual content Alongside Path
non-public func placeTextAlongPath() {
letters.removeAll()
let pathLength = calculatePathLength()
let numberOfLetters = Int(pathLength / letterSpacing)
for i in 0..<numberOfLetters {
guard currentIndex < longString.rely else { return }
let character = Array(longString)[currentIndex]
currentIndex += 1
let place = calculatePositionAlongPath(atIndex: i)
let angle = calculatePerpendicularAngle(from: i)
let draggedLetter = DraggedLetter(character: character, place: place, angle: angle)
letters.append(draggedLetter)
}
}
// MARK: - Calculate Path Size
non-public func calculatePathLength() -> CGFloat {
var size: CGFloat = 0
for i in 1..<dragPath.rely {
let begin = dragPath[i - 1]
let finish = dragPath[i]
size += hypot(finish.x - begin.x, finish.y - begin.y)
}
return size
}
// MARK: - Calculate Place Alongside Path
non-public func calculatePositionAlongPath(atIndex index: Int) -> CGPoint {
guard dragPath.rely > 1 else { return dragPath.first ?? .zero }
let pathLength = calculatePathLength()
let segmentLength = CGFloat(index) * letterSpacing
var accumulatedLength: CGFloat = 0
for i in 1..<dragPath.rely {
let begin = dragPath[i - 1]
let finish = dragPath[i]
let segmentDist = hypot(finish.x - begin.x, finish.y - begin.y)
accumulatedLength += segmentDist
if accumulatedLength >= segmentLength {
let ratio = (segmentLength - (accumulatedLength - segmentDist)) / segmentDist
let x = begin.x + ratio * (finish.x - begin.x)
let y = begin.y + ratio * (finish.y - begin.y)
return CGPoint(x: x, y: y)
}
}
return dragPath.final ?? .zero
}
// MARK: - Calculate Perpendicular Angle (Angle primarily based on drag course)
non-public func calculatePerpendicularAngle(from index: Int) -> Double {
guard dragPath.rely > 1 else { return 0 }
guard dragPath.rely > index else { return 0 }
let begin = dragPath[index]
let finish = dragPath[index + 1 < dragPath.count ? index + 1 : index]
let dx = finish.x - begin.x
let dy = finish.y - begin.y
let perpDx = -dy
let perpDy = dx
let angle = atan2(perpDy, perpDx) * 180 / .pi
return angle - 90.0
}
}
// MARK: - Dragged Letter Mannequin
struct DraggedLetter: Identifiable {
let id = UUID()
let character: Character
let place: CGPoint
let angle: Double
}
The problem:
The textual content is positioned alongside the trail appropriately, however the angle of the textual content doesn’t appear to match the floor of the trail, particularly on sloped or curved paths.
The letters needs to be standing “on the floor” of the trail, whatever the path’s course. The textual content ought to at all times face the “regular” or “perpendicular” course of the trail at every level.
What I’ve tried:
I calculate the angle primarily based on the course of the trail between two factors and use the perpendicular vector to set the angle for every letter.
The letters rotate typically, however on sure slopes or curves, the textual content nonetheless would not align as anticipated.
What I’m searching for:
Assist with guaranteeing the letters at all times face perpendicular to the trail at each level, particularly on sloped or curved paths.
A approach to make sure the letters at all times seem like standing upright, regardless of the form or slope of the drawn path.
what I need you possibly can see on this picture I need textual content keep hooked up at path floor
on plain floor it is little bit okay however whereas in slop or round path it isn’t correct