Reputation: 679
I've created a UIView that draws a lot of UIBezierPaths. Now I want to fill them with the green texture Image. With the following code the pattern Image gets filled and repeated in the whole view and not in every single Path. (One Path is one of these rectangles with circle at the bottom). So I want the pattern Image to fit in every single path and rotate it when the path rotates.
var image = UIImage(named: "test.jpg")!
let color = UIColor.lightGray
let fillColor = UIColor.init(patternImage: image)
var lastPoint: CGPoint? = nil
for point in points {
if let lastPointUnwrapped = lastPoint{
let path = BiberBezierPath(leftPoint: lastPointUnwrapped, rightPoint: point, frame: frame, plusHeight: -140)
fillColor.setFill()
path.fill()
color.setStroke()
path.lineWidth = 1
path.lineCapStyle = .butt
path.stroke()
}
lastPoint = point
}
Upvotes: 0
Views: 760
Reputation: 16774
The task you are looking for may get complex for some cases. But it might be that something like this should be enough:
class RoofView: UIView {
var paths: [(path: UIBezierPath, rotationInRadians: CGFloat)]? { didSet { self.refresh() } }
var image: UIImage? { didSet { self.refresh() } }
var preserveImageAspect: Bool = true { didSet { self.refresh() } }
func refresh() {
self.setNeedsDisplay() // This will trigger redraw
}
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let paths = paths else { return }
guard let image = image else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
paths.forEach { item in
context.saveGState() // Save state. This includes transformations and clips
item.path.addClip()
let boundingRect: CGRect = {
let rotatedPath = (item.path.copy() as! UIBezierPath) // Create a duplicate path
rotatedPath.apply(CGAffineTransform(rotationAngle: -item.rotationInRadians)) // Rotate it backwards
if preserveImageAspect {
let minimumBounds = rotatedPath.bounds
let center = CGPoint(x: minimumBounds.midX, y: minimumBounds.midY)
let imageRatio: CGFloat = image.size.width / image.size.height
if minimumBounds.width / minimumBounds.height < imageRatio {
// Preserve height, scale width
let size = CGSize(width: minimumBounds.height*imageRatio, height: minimumBounds.height)
return CGRect(x: center.x - size.width*0.5, y: center.y - size.height*0.5, width: size.width, height: size.height)
} else {
// Preserve width, scale height
let size = CGSize(width: minimumBounds.width, height: minimumBounds.width/imageRatio)
return CGRect(x: center.x - size.width*0.5, y: center.y - size.height*0.5, width: size.width, height: size.height)
}
} else {
return rotatedPath.bounds
}
}()
context.rotate(by: item.rotationInRadians)
image.draw(in: boundingRect)
context.restoreGState() // Put back state. This includes transformations and clips
}
}
}
What this code does is takes an image and draws it on all paths with given rotations.
To do so it clips the context with path using addClip
which means every draw command that follows will only be drawn inside the clipped area.
Next the image is being drawn for which a position needs to be determined. We try to fit a minimum rectangle where the image should be drawn. It is important to rotate the path to determine correct bounds or they may be too small when rotated (for instance) 45 degrees. Optionally an aspect ratio is preserved as "fill" mode which needs a bit math...
Then the context is rotated so that the image appears rotated. The image is then drawn at given path.
What is left is only to cleanup which means restoring the state. This will clear both clipping mask and context rotation.
If interested in math to preserve aspect fill:
We are trying to "fill" size within a rectangle. The size comes from image while rectangle is where we want to draw it. We need to compare both ratios (width/height
) and depending on which is larger we either increase width or height to draw "out of bounds". In each of the cases one of the coordinates is preserved while the other one is determined with image aspect ratio. This is a general solution to determine a "fill" rectangle. As fun fact if you ever need "fit" all you do is change the inequality as if minimumBounds.width / minimumBounds.height > imageRatio {
.
Upvotes: 1