2

I am developing an iOS board game. I am trying to give the board a kind of "texture".

What I did was I created this very small image (really small, be sure to look carefully):

enter image description here

And I passed this image to the UIColor.init(patternImage:) initializer to create a UIColor that is this image. I used this UIColor to fill some square UIBezierPaths, and the result looks like this:

enter image description here

All copies of that image lines up perfectly and they form many diagonal straight lines. So far so good.

Now on the iPad, the squares that I draw will be larger, and the borders of those squares will be larger too. I have successfully calculated what the stroke width and size of the squares should be, so that is not a problem.

However, since the squares are larger on an iPad, there will be more diagonal lines per square. I do not want that. I need to resize the very small image to a bigger one, and that the size depends on the stroke width of the squares. Specifically, the width of the resized image should be twice as much as the stroke width.

I wrote this extension to resize the image, adapted from this post:

extension UIImage {
    func resized(toWidth newWidth: CGFloat) -> UIImage {

        let scale = newWidth / size.width
        let newHeight = size.height * scale
        UIGraphicsBeginImageContextWithOptions(CGSize(width: newWidth, height: newHeight), false, 0)
        self.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage!
    }
}

And called it like this:

// this is the code I used to draw a single square
let path = UIBezierPath(rect: CGRect(origin: point(for: Position(x, y)), size: CGSize(width: squareLength, height: squareLength)))
UIColor.black.setStroke()
path.lineWidth = strokeWidth
// this is the line that's important!
UIColor(patternImage: #imageLiteral(resourceName: 

"texture").resized(toWidth: strokeWidth * 2)).setFill() path.fill() path.stroke()

Now the game board looks like this on an iPhone:

enter image description here

You might need to zoom in the webpage a bit to see what I mean. The board now looks extremely ugly. You can see the "borders" of each copy of the image. I don't want this. On an iPad though, the board looks fine. I suspect that this only happens when I downsize the image.

I figured that this might be due to the antialiasing that happens when I use the extension. I found this post and this post about removing antialiasing, but the former seems to be doing this in a image view while I am doing this in the draw(_:) method of my custom GameBoardView. The latter's solution seems to be exactly the same as what I am using.

How can I resize without antialiasing? Or on a higher level of abstraction, How can I make my board look pretty?

Sweeper
  • 213,210
  • 22
  • 193
  • 313

2 Answers2

2
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
        }
    }
}

Enjoy.

Note that you would, obviously, clip that view.

If you want to have a number of them on the screen or in a pattern, just do that.


To clip to a given rectangle:

The class above simply draws it the "size of the UIView".

However, often, you want to draw a number of the "boxes" actually within the view, at different coordinates. (A good example is for a calendar).

enter image description here

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()
}
Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • This does not work for my specific case. All the squares are not `UIView`s themselves. I drew the squares with paths in a big `UIView` that acts as the "parent". The squares does not fill the big view and there are some surrounding space. Your code will draw outside the bounds of each square. This results in the whitespace surrounding the squares to be covered with diagonals as well. Can you modify it to just draw diagonals inside the bounds of each square? – Sweeper Aug 07 '18 at 08:39
  • Heh, you just set clipping on the small views, @sweeper. But I have another version I will include. However, you need to vote "up" any answer with as much work as this or the other answer. – Fattie Aug 07 '18 at 09:28
  • The code in your edit worked. Thank you. I will give you the bounty unless someone else can get the image resizing working without antialiasing, since that was the original question. – Sweeper Aug 07 '18 at 10:35
  • heh good one @Sweeper , hope it helps. (Unfortunately there's no way to antialias like that.) BTW you would really enjoy: https://www.paintcodeapp.com it's the more modern way to do this sort of thing. Cheers! – Fattie Aug 07 '18 at 13:37
1
extension UIImage {

    func ResizeImage(targetSize: CGSize) -> UIImage
    {
        let size = self.size

        let widthRatio  = targetSize.width  / self.size.width
        let heightRatio = targetSize.height / self.size.height

        // Figure out what our orientation is, and use that to form the rectangle
        var newSize: CGSize
        if(widthRatio > heightRatio) {
            newSize = CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
        } else {
            newSize = CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
        }
        // This is the rect that we've calculated out and this is what is actually used below
        let rect = CGRect(x: 0, y: 0, width: newSize.width,height: newSize.height)

        // Actually do the resizing to the rect using the ImageContext stuff
        UIGraphicsBeginImageContextWithOptions(newSize, false, 1.0)
        self.draw(in: rect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage!
    }
}
Tm Goyani
  • 406
  • 2
  • 9