Supporting SwiftUI with Core Graphics

Nov 22 2022 · Swift 5.5, iOS 15, Xcode 13

Part 1: Supporting SwiftUI with Core Graphics

06. Shading with Pencil

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 05. Drawing with Pencil Next episode: 07. Create a Thumbnail

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 06. Shading with Pencil

Azimuth & Altitude

As well as detecting force and giving us coalesced touches, Pencil is able to give us the tilt in three dimensions. Using this tilt information, we can shade instead of just draw.

When we’re drawing on a surface, we can rotate Pencil around in a circle. This is called the azimuth. We can get the tilt or altitude angle by raising and lowering the Pencil.

Altitude is measured in radians. When Pencil lies flat on the surface, the altitude is zero.

When Pencil stands straight up with the point on the screen, the angle is pi over 2 - that’s 90 degrees. Now that we can get the altitude, we can turn the drawing into shading when we hold Pencil at a particular threshold angle.

Assets

When we shade, instead of drawing a stroke, we’re going to draw a image that looks like a pencil shading.

Included in the starter project for this episode, in the Assets catalog is a pencil texture. This is a png with transparency.

Instead of drawing black, as this image shows, we can assign the drawing color to this image, by rendering it as a template.

Open the attributes inspector and change the rendering mode to Template image. This will allow us to set the color at the time of drawing.

To use this image, we’ll just set up a UIColor with an image instead of a color. When we stroke with that color, it will draw the image.

CanvasView.swift

In CanvasView, right under the color property, add a shading color property that sets up the pencil texture as a UImage with the correct color.

var shadingColor: UIColor = .black {
  didSet {
    let image = UIImage(named: "pencilTexture")!
  }
}

We’ll set up an image renderer and draw the image into the context, first setting the current drawing color:

let tintedImage = UIGraphicsImageRenderer(size: image.size).image { _ in
  color.set()
  image.draw(at: .zero)
}

Now we can set up the shading color with this image:

shadingColor = UIColor(patternImage: tintedImage)

We’ll change the color property to update this shading color every time the draw color changes.

var color: UIColor = .black {
  didSet {
    shadingColor = color
  }
}

Let’s test this out before checking out our tilt. In drawStroke, temporarily use the shading color instead of the draw color.

shadingColor.setStroke()

Build and run on the iPad.

That’s all working as expected. You can see that the grainy image is being used as the color.

Now we’ll set up an angle threshold up at the top of the class with the other default properties.

private let tiltThreshold: CGFloat = .pi / 6

When the user tilts Pencil below this threshold, we’ll shade instead of draw. pi over 6 is the same as 30 degrees. You can change this tilt threshold if you’d like. Now we can apply this down in drawStroke.

First, let’s remove this shadingColor.setStroke. Then, up under where we set up line width Depending on how much you tilt the pencil when drawing, we’ll either use the new shading color with the image, or the old draw color

if touch.altitudeAngle < tiltThreshold {
  shadingColor.setStroke()
} else {
  if touch.force > 0 {
    lineWidth = touch.force * forceSensitivity
  }
  color.setStroke()
}

Now, we should also adjust the width when we’re shading. We’ll add a new method at the bottom of the class to work that out:

private func lineWidthForShading(touch: UITouch) -> CGFloat {
  
}

Set up the minimum and maximum altitude angles and make sure that the altitude angle is greater than the minimum altitude, as we can’t physically achieve an altitude of zero.

let maxLineWidth: CGFloat = 60
let minAltitudeAngle: CGFloat = 0.25
let maxAltitudeAngle = tiltThreshold
let altitudeAngle = max(minAltitudeAngle, touch.altitudeAngle)

We also set that the maximum line width at full tilt will be 60. We want to refit the altitude angle to be between 0 and 1 instead of between 0.25 and the tilt threshold

let normalizedAltitude = (altitudeAngle - minAltitudeAngle) / (maxAltitudeAngle - minAltitudeAngle)

This formula allows us to refit a range of two values to a range of values between zero and 1. So here, if we input an altitude angle of 0.25, the resulting normalized altitude will be zero. Use this normalized altitude angle as a percentage of the maximum line width to calculate the final lineWidth.

return max(maxLineWidth * (1 - normalizedAltitude), minLineWidth)

… And I forgot to put this inside of the class, so I’ll just move the method up here.

We use the negative normalized altitude, as we want to make the line grow as we tilt more, rather than shrink Now back in drawStroke: Change the lineWidth to call our new method if the angle of Pencil is less than the threshold:

🟩var lineWidth: CGFloat

if touch.altitudeAngle < tiltThreshold {
  🟩lineWidth = lineWidthForShading(touch: touch)
  ...
} else {
  🟩lineWidth = defaultLineWidth
  ...
}

Build and run on the iPad to give it a try. I’ll start with Pencil low, and as I tilt it upwards, the shading width will get thinner until it goes above the threshold and becomes the drawing stroke.

All we have to do now is to take into account force when shading. We’ll make the context more transparent when the force is light and less transparent when we press hard. That way the UIColor image will be darker when we press hard.

In drawStroke, where we check the tilt angle, after setting the stroke to shadingColor, also check the force. And set the alpha accordingly

let minForce: CGFloat = 0
let maxForce: CGFloat = 4
let normalizedAlpha = (touch.force - minForce) / (maxForce - minForce)
context.setAlpha(normalizedAlpha)

We use the same normalize formula to set an alpha or transparency value between zero and one. Build and run on the iPad one last time

Now I can shade lightly or heavily. If I shade lightly, then I can build up the shading gradually, just as I can with a real pencil. When I don’t tilt Pencil, the stroke acts as it did before with the stroke width varying with pressure.