Fill a UIView with diagonally drawn lines?
Amazingly simple algorithm ...
Say you have these values:
let T: CGFloat = 15 // desired thickness of lines
let G: CGFloat = 30 // desired gap between lines
let W = rect.size.width
let H = rect.size.height
Remarkably, it is this simple ...
var p = -(W > H ? W : H) - T
while p <= W {
c.move( to: CGPoint(x: p-T, y: -T) )
c.addLine( to: CGPoint(x: p+T+H, y: T+H) )
c.strokePath()
p += G + T + T
}
Here is a complete UIView class:
class Ruled: UIView {
override func draw(_ rect: CGRect) {
let T: CGFloat = 15 // desired thickness of lines
let G: CGFloat = 30 // desired gap between lines
let W = rect.size.width
let H = rect.size.height
guard let c = UIGraphicsGetCurrentContext() else { return }
c.setStrokeColor(UIColor.orange.cgColor)
c.setLineWidth(T)
var p = -(W > H ? W : H) - T
while p <= W {
c.move( to: CGPoint(x: p-T, y: -T) )
c.addLine( to: CGPoint(x: p+T+H, y: T+H) )
c.strokePath()
p += G + T + T
}
}
}
That's it !
The entire fundamental algorithm:
1. Start at the top-left, minus the longest side
2. Draw diagonals until you come to the right
Nice and easy! :)
To clip to a rectangle:
The class above simply draws one "box" the size of the UIView.
Often, you want to draw many of the "boxes" within one view, at different positions. A typical example is for a calendar.
This example will draw one box. Call it for each of the boxes you need to draw:
Furthermore, this example explicitly draws both stripes, rather than drawing one stripe over the background color:
func simpleStripes(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) {
let stripeWidth: CGFloat = 20.0 // whatever you want
let m = stripeWidth / 2.0
guard let c = UIGraphicsGetCurrentContext() else { return }
c.setLineWidth(stripeWidth)
let r = CGRect(x: x, y: y, width: width, height: height)
let longerSide = width > height ? width : height
c.saveGState()
c.clip(to: r)
var p = x - longerSide
while p <= x + width {
c.setStrokeColor(pale blue)
c.move( to: CGPoint(x: p-m, y: y-m) )
c.addLine( to: CGPoint(x: p+m+height, y: y+m+height) )
c.strokePath()
p += stripeWidth
c.setStrokeColor(pale gray)
c.move( to: CGPoint(x: p-m, y: y-m) )
c.addLine( to: CGPoint(x: p+m+height, y: y+m+height) )
c.strokePath()
p += stripeWidth
}
c.restoreGState()
}
If you want to animate them moving...
1, To offset, simply subtract from the pointer when you start it. Amazingly, nothing else needs to be changed.
var p = x - longerSide - offset // animate offset from 0 to stripeWidth
2, Careful programmers would prefer an offset equal to the miter to avoid the "pointy top-left corner" problem:
var p = x - longerSide - offset - m // for better-looking top-left corner
3, You can use any number of stripes in various colors, and indeed you can use different stripe widths in any combination. Amazingly, the algorithm still works and is safe. (If you have more than one width, just set the miter m
as the maximum width.)
One way of achieving this would be to override the draw(_:)
method of UIView
and do your custom drawing there.
Drawing diagonal lines is fairly simple, you just need to:
Stride from 0 to width + height (along the horizontal edge of the rect, then up the vertical), by the gap + line width, converted from being a diagonal (at 45º) length to being parallel to the edge of the rect to draw in.
At each iteration, draw a line from the given point for that iteration to the point on the edge opposite (at 45º). We get this point by simply working up the vertical edge of the rect, then along the horizontal)
Something like this should achieve the desired result:
class StripeyView : UIView {
let lineGap: CGFloat = 7
let lineWidth: CGFloat = 3
let lineColor = UIColor.white
override func draw(_ rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()!
// flip y-axis of context, so (0,0) is the bottom left of the context
ctx.scaleBy(x: 1, y: -1)
ctx.translateBy(x: 0, y: -bounds.size.height)
// generate a slightly larger rect than the view,
// to allow the lines to appear seamless
let renderRect = bounds.insetBy(dx: -lineWidth * 0.5, dy: -lineWidth * 0.5)
// the total distance to travel when looping (each line starts at a point that
// starts at (0,0) and ends up at (width, height)).
let totalDistance = renderRect.size.width + renderRect.size.height
// loop through distances in the range 0 ... totalDistance
for distance in stride(from: 0, through: totalDistance,
// divide by cos(45º) to convert from diagonal length
by: (lineGap + lineWidth) / cos(.pi / 4)) {
// the start of one of the stripes
ctx.move(to: CGPoint(
// x-coordinate based on whether the distance is less than the width of the
// rect (it should be fixed if it is above, and moving if it is below)
x: distance < renderRect.width ?
renderRect.origin.x + distance :
renderRect.origin.x + renderRect.width,
// y-coordinate based on whether the distance is less than the width of the
// rect (it should be moving if it is above, and fixed if below)
y: distance < renderRect.width ?
renderRect.origin.y :
distance - (renderRect.width - renderRect.origin.x)
))
// the end of one of the stripes
ctx.addLine(to: CGPoint(
// x-coordinate based on whether the distance is less than the height of
// the rect (it should be moving if it is above, and fixed if it is below)
x: distance < renderRect.height ?
renderRect.origin.x :
distance - (renderRect.height - renderRect.origin.y),
// y-coordinate based on whether the distance is less than the height of
// the rect (it should be fixed if it is above, and moving if it is below)
y: distance < renderRect.height ?
renderRect.origin.y + distance :
renderRect.origin.y + renderRect.height
))
}
// stroke all of the lines added
ctx.setStrokeColor(lineColor.cgColor)
ctx.setLineWidth(lineWidth)
ctx.strokePath()
}
}
Output:
(Assuming the view has a red backgroundColor
)
You can adjust the lineGap
and lineWidth
properties to generate varying results.