1

User drawing a line and dotted/dashed lines are pretty straight forward. I am able to do that with something like:

            context.addPath(path)
            context.setLineCap(.round)
            context.setLineWidth(action.strokeWidth)
            context.setLineDash(phase: 0, lengths: [6, 9])
            context.setStrokeColor(action.color.cgColor)
            context.setBlendMode(.normal)

What I am trying to accomplish is to have a user generated line / curve drawing with a custom brush tip or a pattern. I tried doing something like colorWithPatternImage but that doesn't solve my issue because what it does it just fill the view's background with a textured image pattern and not the line itself. Something like:

Example image of a brush stroke I'd like to accomplish:

enter image description here

How can I accomplish this?

Nimantha
  • 6,405
  • 6
  • 28
  • 69

2 Answers2

1

I was working on a project that include free hand draw feature and I saw there is no source or example to learn how to do custom brushes, then after studying a lot of topics on core graphics and spending days for tests I found a solution for creating custom brushes like spray or any pattern that you want.

All these brushes can be draw with CGLayer. Logic of this operation is instead of doing hard math and calculating pixels just draw your pattern image on this type of layer and pass this guy to your drawing context and you will see the what you want. Before giving and example code to let you understand better let me share this info with you...

Apple says :

CGLayer no longer recommended (because of it's not stable)

Thats why think twice before using it on commercial project.

FYI: This code not tested so please correct it in your side if there is a problem because my aim is only sharing my knowledge on this topic.

private func drawWithCustomBrush(brushSize size: CGSize, brushPattern pattern: UIImage, drawAtPoint atPoint: CGPoint) -> UIImage? {
    
    // Create drawing context and adjust it's settings
    let canvasSize = view.bounds.size
    UIGraphicsBeginImageContext(canvasSize) // Preferred options is much more faster than custom options
    guard let context = UIGraphicsGetCurrentContext() else { return nil }
    context.interpolationQuality = .high
    context.setAlpha(1.0)
    
    // Optionally you can draw background image
    // YOUR_BG_IMAGE.draw(in: CGRect(origin: .zero, size: canvasSize))
    
    // Create CGLayer with drawing context and draw your pattern with context that CGLayer gives us
    let layer = CGLayer(context, size: size, auxiliaryInfo: nil)
    // Get drawing context from the layer for drawing pattern on this layer
    guard let layerContext = layer?.context else { return nil }
    layerContext.interpolationQuality = .high
    // Draw pattern (as cgImage) on layer with layer context
    layerContext.draw(pattern.cgImage!, in: CGRect(origin: .zero, size: size))
    // Calculate pattern layer position or rect
    let layerRect = CGRect(origin: CGPoint(x: atPoint.x - size.width / 2,
                                           y: atPoint.y - size.height / 2),
                           size: size)
    // Now draw the layer our canvas by using drawing context [Not Layer Context!]
    context.draw(layer!, in: layerRect)
    
    // Create result image from current drawing context
    guard let finalImage = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
    // Clean up the drawing environment
    UIGraphicsEndImageContext()
    // Return your new image
    return finalImage
}

BONUS:-

For get rid of unwanted copy of pattern for different colors use this UIImage extension function to re-color your pattern.

func maskWithColor(color: UIColor) -> UIImage? {
    let maskImage = cgImage!
    
    let width = size.width
    let height = size.height
    let bounds = CGRect(x: 0, y: 0, width: width, height: height)
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
    let context = CGContext(data: nil, width: Int(width), height: Int(height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)!
    
    context.clip(to: bounds, mask: maskImage)
    context.setFillColor(color.cgColor)
    context.fill(bounds)
    
    if let cgImage = context.makeImage() {
        let coloredImage = UIImage(cgImage: cgImage)
        return coloredImage
    } else {
        return nil
    }
}

As you can see how it's easy with CGLayer. Now you can create your custom brushes as you like only with passing a single UIImage [it should be png].

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Coder ACJHP
  • 1,940
  • 1
  • 20
  • 34
0

I strongly recommend reading getlooseleaf as there are several solutions. some involve shaders, some would go for CGPatternDrawPatternCallback functions where here is a Stack Overflow discussion how to do that in Swift.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
Ol Sen
  • 3,163
  • 2
  • 21
  • 30