How to Convert SwiftUI Text to a Path and Apply Transformations?
Body: I’m trying to render a Text view as a path in SwiftUI so that I can apply transformations like scaling, skewing, and rotation. I've managed to extract the glyphs using Text.toPath(), but I’m facing issues with applying transformations and rendering the result properly.
Here’s my current approach:
//
// TextToPathView.swift
import SwiftUI
import CoreText
struct TextToPathView: View {
@State private var fontSize: CGFloat = 40
@State private var strokeWidth: CGFloat = 2
@State private var letterSpacing: CGFloat = 2
@State private var curveRadius: CGFloat = 0
@State private var isBold = false
@State private var isItalic = false
@State private var isUnderlined = false
@State private var isCurved = false
@State private var fontColor = Color.black
@State private var strokeColor = Color.red
@State private var alignment: NSTextAlignment = .center
var body: some View {
VStack {
Text("Text to Path Editor")
.font(.title)
.fontWeight(.bold)
.padding(.bottom, 10)
// Use the unified PathView that creates one CGPath for all effects.
PathView(
text: "Hello, SwiftUI!",
fontSize: fontSize,
strokeWidth: strokeWidth,
letterSpacing: letterSpacing,
curveRadius: curveRadius,
isBold: isBold,
isItalic: isItalic,
isUnderlined: isUnderlined,
isCurved: isCurved,
fontColor: UIColor(fontColor),
strokeColor: UIColor(strokeColor),
alignment: alignment
)
.frame(height: 200)
.padding()
Divider().padding()
// Sliders
VStack {
SliderView(value: $fontSize, label: "Font Size", range: 20...100)
SliderView(value: $strokeWidth, label: "Stroke Width", range: 0...5)
SliderView(value: $letterSpacing, label: "Letter Spacing", range: 0...10)
if isCurved {
SliderView(value: $curveRadius, label: "Curve Radius", range: 50...200)
}
}
// Toggles
HStack {
Toggle("Bold", isOn: $isBold)
Toggle("Italic", isOn: $isItalic)
Toggle("Underline", isOn: $isUnderlined)
}
.padding(.top, 10)
Toggle("Curved Text", isOn: $isCurved)
.padding(.top, 5)
// Alignment Picker
Picker("Alignment", selection: $alignment) {
Text("Left").tag(NSTextAlignment.left)
Text("Center").tag(NSTextAlignment.center)
Text("Right").tag(NSTextAlignment.right)
}
.pickerStyle(SegmentedPickerStyle())
.padding(.top, 5)
// Color Pickers
HStack {
VStack {
Text("Font Color")
ColorPicker("", selection: $fontColor)
.frame(width: 50)
}
VStack {
Text("Stroke Color")
ColorPicker("", selection: $strokeColor)
.frame(width: 50)
}
}
.padding(.top, 10)
}
.padding()
}
}
// MARK: - PathView (Creates one CGPath reflecting all effects)
struct PathView: View {
var text: String
var fontSize: CGFloat
var strokeWidth: CGFloat
var letterSpacing: CGFloat
var curveRadius: CGFloat
var isBold: Bool
var isItalic: Bool
var isUnderlined: Bool
var isCurved: Bool
var fontColor: UIColor
var strokeColor: UIColor
var alignment: NSTextAlignment
var body: some View {
GeometryReader { geometry in
if let path = styledTextToPath(
text: text,
font: UIFont.systemFont(ofSize: fontSize),
fontSize: fontSize,
color: fontColor,
strokeColor: strokeColor,
strokeWidth: strokeWidth,
alignment: alignment,
letterSpacing: letterSpacing,
isBold: isBold,
isItalic: isItalic,
isUnderlined: isUnderlined,
isCurved: isCurved,
curveRadius: curveRadius
) {
ZStack {
// Fill the complete text path with the chosen font color.
Path(path)
.fill(Color(fontColor))
// Then stroke the path with the chosen stroke color.
Path(path)
.stroke(Color(strokeColor), lineWidth: strokeWidth)
}
.frame(width: geometry.size.width, height: geometry.size.height)
// Adjust vertical offset as needed.
.offset(y: 50)
}
}
}
}
// MARK: - Create a CGPath that reflects all styling effects.
func styledTextToPath(
text: String,
font: UIFont,
fontSize: CGFloat,
color: UIColor,
strokeColor: UIColor,
strokeWidth: CGFloat,
alignment: NSTextAlignment,
letterSpacing: CGFloat,
isBold: Bool,
isItalic: Bool,
isUnderlined: Bool,
isCurved: Bool,
curveRadius: CGFloat
) -> CGPath? {
// Determine the font traits.
var traits: UIFontDescriptor.SymbolicTraits = []
if isBold { traits.insert(.traitBold) }
if isItalic { traits.insert(.traitItalic) }
var styledFont = font
if let descriptor = font.fontDescriptor.withSymbolicTraits(traits) {
styledFont = UIFont(descriptor: descriptor, size: fontSize)
}
// Build common attributes.
let attributes: [NSAttributedString.Key: Any] = [
.font: styledFont,
.foregroundColor: color,
.kern: letterSpacing,
.strokeColor: strokeColor,
.strokeWidth: strokeWidth
]
// If not curved, use a simple approach.
if !isCurved {
let attributedString = NSAttributedString(string: text, attributes: attributes)
let line = CTLineCreateWithAttributedString(attributedString)
let totalWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil))
let runArray = CTLineGetGlyphRuns(line) as NSArray
let path = CGMutablePath()
for run in runArray {
let run = run as! CTRun
let count = CTRunGetGlyphCount(run)
for index in 0..<count {
let range = CFRangeMake(index, 1)
var glyph: CGGlyph = 0
var position: CGPoint = .zero
CTRunGetGlyphs(run, range, &glyph)
CTRunGetPositions(run, range, &position)
if let glyphPath = CTFontCreatePathForGlyph(styledFont, glyph, nil) {
var transform = CGAffineTransform(translationX: position.x, y: position.y)
transform = transform.scaledBy(x: 1, y: -1)
path.addPath(glyphPath, transform: transform)
}
}
}
// Add a straight underline if needed.
if isUnderlined {
let underlineThickness = CTFontGetUnderlineThickness(styledFont)
let underlinePosition = CTFontGetUnderlinePosition(styledFont)
let underlinePath = CGMutablePath()
underlinePath.move(to: CGPoint(x: 0, y: -underlinePosition))
underlinePath.addLine(to: CGPoint(x: totalWidth, y: -underlinePosition))
path.addPath(underlinePath)
}
return path
}
else {
// --- Curved Text Branch ---
// First, compute total width by summing each character’s width.
var totalWidth: CGFloat = 0
var charWidths: [CGFloat] = []
for character in text {
let charStr = String(character)
let charAttr = NSAttributedString(string: charStr, attributes: attributes)
let charLine = CTLineCreateWithAttributedString(charAttr)
let charWidth = CGFloat(CTLineGetTypographicBounds(charLine, nil, nil, nil))
charWidths.append(charWidth)
totalWidth += charWidth + letterSpacing
}
// Remove extra spacing after the last character.
if !charWidths.isEmpty {
totalWidth -= letterSpacing
}
// Determine the arc span based on total width and curve radius.
let arcAngle = totalWidth / curveRadius
let startAngle = -arcAngle / 2
var currentX: CGFloat = 0
let path = CGMutablePath()
// Process each character individually.
for (i, character) in text.enumerated() {
let charStr = String(character)
let charAttr = NSAttributedString(string: charStr, attributes: attributes)
let charLine = CTLineCreateWithAttributedString(charAttr)
let charWidth = CGFloat(CTLineGetTypographicBounds(charLine, nil, nil, nil))
// Determine the center of this character relative to the whole text.
let charCenter = currentX + charWidth / 2
let fraction = charCenter / totalWidth
let angle = startAngle + fraction * arcAngle
let runArray = CTLineGetGlyphRuns(charLine) as NSArray
for run in runArray as! [CTRun] {
let count = CTRunGetGlyphCount(run)
for index in 0..<count {
let range = CFRangeMake(index, 1)
var glyph: CGGlyph = 0
var position: CGPoint = .zero
CTRunGetGlyphs(run, range, &glyph)
CTRunGetPositions(run, range, &position)
if let glyphPath = CTFontCreatePathForGlyph(styledFont, glyph, nil) {
// Start by centering the glyph in its character box.
var transform = CGAffineTransform(translationX: position.x - charWidth/2, y: position.y)
transform = transform.scaledBy(x: 1, y: -1)
// Compute the position along the circular arc.
let xPos = curveRadius * sin(angle)
let yPos = curveRadius * (1 - cos(angle))
// Apply rotation so the glyph follows the arc.
transform = transform.concatenating(CGAffineTransform(rotationAngle: angle))
// Finally, translate the glyph onto the arc.
transform = transform.concatenating(CGAffineTransform(translationX: xPos, y: yPos))
path.addPath(glyphPath, transform: transform)
}
}
}
currentX += charWidth + letterSpacing
}
// Add an underline that follows the curve.
if isUnderlined {
let underlineOffset: CGFloat = 5 // Adjust as needed.
let underlineRadius = curveRadius - underlineOffset
let underlinePath = CGMutablePath()
// For a curved underline, draw an arc spanning the same angles as the text.
// Here, we assume the circle’s center is at (0, curveRadius)
let arcCenter = CGPoint(x: 0, y: curveRadius)
underlinePath.addArc(center: arcCenter, radius: underlineRadius, startAngle: startAngle, endAngle: startAngle + arcAngle, clockwise: false)
path.addPath(underlinePath)
}
return path
}
}
// MARK: - Slider View
struct SliderView: View {
@Binding var value: CGFloat
var label: String
var range: ClosedRange<CGFloat>
var body: some View {
VStack {
Text("\(label): \(Int(value))")
Slider(value: $value, in: range)
}
.padding(.vertical, 5)
}
}
// MARK: - Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
TextToPathView()
}
}
Issues:
The text path doesn’t align properly in the frame.
Transformations like scaling and skewing don’t seem to work as expected.
The glyphs appear shifted when using CGAffineTransform.
What’s the best way to align and transform the generated text path correctly in SwiftUI?
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744797432a4594289.html
评论列表(0条)