ios - How to convert Text to a path and apply transformations? - Stack Overflow

How to Convert SwiftUI Text to a Path and Apply Transformations?Body:I’m trying to render a Text view

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条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信