Core Graphics CGImage mask not affecting - ios

What I'm essentially trying to do is have a text label 'cut' a text-shaped hole through the view. I've tried using self.mask = uiLabel but those refused to position the text correctly so I'm approaching this through Core Graphics.
Here's the code that isn't working (in the draw(_ rect: CGRect)):
let context = (UIGraphicsGetCurrentContext())!
// Set mask background color
context.setFillColor(UIColor.black.cgColor)
context.fill(rect)
context.saveGState()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes = [
NSParagraphStyleAttributeName: paragraphStyle,
NSFontAttributeName: UIFont.systemFont(ofSize: 16, weight: UIFontWeightMedium),
NSForegroundColorAttributeName: UIColor.white
]
let string = NSString(string: "LOGIN")
// This wouldn't vertically align so we calculate the string size and create a new rect in which it is vertically aligned
let size = string.size(attributes: attributes)
let position = CGRect(
x: rect.origin.x,
y: rect.origin.y + (rect.size.height - size.height) / 2,
width: rect.size.width,
height: size.height
)
context.translateBy(x: 0, y: rect.size.height)
context.scaleBy(x: 1, y: -1)
string.draw(
in: position,
withAttributes: attributes
)
let mask = (context.makeImage())!
context.restoreGState()
// Redraw with created mask
context.clear(rect)
context.saveGState()
// !!!! Below line is the problem
context.clip(to: rect, mask: mask)
context.restoreGState()
Essentially I've successfully created the code to create a CGImage (the mask variable) which is the mask I want to apply to the whole image.
The marked line when replaced with context.draw(mask, in: rect) (to view the mask) correctly displays. The mask shows (correctly) as:
:
However once I try to apply this mask (using the context.clip(to: rect, mask: mask)) nothing happens!. Actual result:
Desired result is:
but for some reason the mask is not being correctly applied.
This code seems like it should work as I've read the docs over and over again. I've additionally tried to create the mask in a separate CGContext which didn't work. Also when I tried to convert the CGImage (mask) to CGColorSpaceCreateDeviceGray() using .copy(colorSpace:), it returned nil. I've been at this for two days so any help is appreciated

If you want the label to have fully translucent text, you can use blend modes instead of masks.
public class KnockoutLabel: UILabel {
public override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.setBlendMode(.clear)
self.drawText(in: rect)
}
}
Make sure to set isOpaque to false though; by default the view assumes it is opaque, since you use an opaque background color.

I'm not actually sure that you will enjoy the code below. It was made by using the PaintCode. Text will be always at the center of rounded rectangle. Hope it will help you.
Code, put it inside draw(_ rect: CGRect):
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// Color Declarations - just to make a gradient same as your button have
let gradientColor = UIColor(red: 0.220, green: 0.220, blue: 0.223, alpha: 1.000)
let gradientColor2 = UIColor(red: 0.718, green: 0.666, blue: 0.681, alpha: 1.000)
let gradientColor3 = UIColor(red: 0.401, green: 0.365, blue: 0.365, alpha: 1.000)
//// Gradient Declarations
let gradient = CGGradient(colorsSpace: nil, colors: [gradientColor.cgColor, gradientColor3.cgColor, gradientColor2.cgColor] as CFArray, locations: [0, 0.55, 1])!
//// Subframes
let group: CGRect = CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height)
//// Group
context.saveGState()
context.beginTransparencyLayer(auxiliaryInfo: nil)
//// Rectangle Drawing
let rectanglePath = UIBezierPath(roundedRect: group, cornerRadius: 5)
context.saveGState()
rectanglePath.addClip()
let rectangleRotatedPath = UIBezierPath()
rectangleRotatedPath.append(rectanglePath)
var rectangleTransform = CGAffineTransform(rotationAngle: -99 * -CGFloat.pi/180)
rectangleRotatedPath.apply(rectangleTransform)
let rectangleBounds = rectangleRotatedPath.cgPath.boundingBoxOfPath
rectangleTransform = rectangleTransform.inverted()
context.drawLinearGradient(gradient,
start: CGPoint(x: rectangleBounds.minX, y: rectangleBounds.midY).applying(rectangleTransform),
end: CGPoint(x: rectangleBounds.maxX, y: rectangleBounds.midY).applying(rectangleTransform),
options: [])
context.restoreGState()
//// Text Drawing
context.saveGState()
context.setBlendMode(.destinationOut)
let textRect = group
let textTextContent = "LOGIN"
let textStyle = NSMutableParagraphStyle()
textStyle.alignment = .center
let textFontAttributes = [
NSFontAttributeName: UIFont.systemFont(ofSize: 16, weight: UIFontWeightBold),
NSForegroundColorAttributeName: UIColor.black,
NSParagraphStyleAttributeName: textStyle,
]
let textTextHeight: CGFloat = textTextContent.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
context.saveGState()
context.clip(to: textRect)
textTextContent.draw(in: CGRect(x: textRect.minX, y: textRect.minY + (textRect.height - textTextHeight) / 2, width: textRect.width, height: textTextHeight), withAttributes: textFontAttributes)
context.restoreGState()
context.restoreGState()
context.endTransparencyLayer()
context.restoreGState()
Results:

Related

Why is CILinearGradient resulting in a very NON-linear gradient?

I'm a relatively new Swift developer and I am using the CILinearGradient CIFilter to generate gradients that I can then use as backgrounds and textures. I was pretty happy with the way it was working, until I realized that the gradients coming out of it seem to be heavily skewed towards away from the black end of the spectrum.
At first I thought I was nuts, but then I created pure black-to-white and white-to-black gradients and put them on screen next to each other. I took a screenshot and brought it into Photoshop. then I looked at the color values. You can see that the ends of each gradient line up (pure black over pure white on one end, and the opposite on the other), but the halfway point of each gradient is significantly skewed towards the black end.
Is this an issue with the CIFilter or am I doing something wrong? Thanks to anyone with any insight on this!
Here's my code:
func gradient2colorIMG(UIcolor1: UIColor, UIcolor2: UIColor, width: CGFloat, height: CGFloat) -> CGImage? {
if let gradientFilter = CIFilter(name: "CILinearGradient") {
let startVector:CIVector = CIVector(x: 0 + 10, y: 0)
let endVector:CIVector = CIVector(x: width - 10, y: 0)
let color1 = CIColor(color: UIcolor1)
let color2 = CIColor(color: UIcolor2)
let context = CIContext(options: nil)
if let currentFilter = CIFilter(name: "CILinearGradient") {
currentFilter.setValue(startVector, forKey: "inputPoint0")
currentFilter.setValue(endVector, forKey: "inputPoint1")
currentFilter.setValue(color1, forKey: "inputColor0")
currentFilter.setValue(color2, forKey: "inputColor1")
if let output = currentFilter.outputImage {
if let cgimg = context.createCGImage(output, from: CGRect(x: 0, y: 0, width: width, height: height)) {
let gradImage = cgimg
return gradImage
}
}
}
}
return nil
}
and then I call it in SpriteKit using this code (but this is just so I can see them on the screen to compare the CGImages that are output by the function) ...
if let gradImage = gradient2colorIMG(UIcolor1: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), width: 250, height: 80) {
let sampleback = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
sampleback.fillColor = .white
sampleback.fillTexture = SKTexture(cgImage: gradImage)
sampleback.zPosition = 200
sampleback.position = CGPoint(x: 150, y: 50)
self.addChild(sampleback)
}
if let gradImage2 = gradient2colorIMG(UIcolor1: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), width: 250, height: 80) {
let sampleback2 = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
sampleback2.fillColor = .white
sampleback2.fillTexture = SKTexture(cgImage: gradImage2)
sampleback2.zPosition = 200
sampleback2.position = CGPoint(x: 150, y: 150)
self.addChild(sampleback2)
}
As another follow-up, I tried doing a red-blue gradient (so purely a change in hue) and it is perfectly linear (see below). The issue seems to be around the brightness.
A red-blue gradient DOES ramp its hue in a perfectly linear fashion
Imagine that black is 0 and white is 1. Then the problem here is that we intuitively think that 50% of black "is" a grayscale value of 0.5 — and that is not true.
To see this, consider the following core image experiment:
let con = CIContext(options: nil)
let white = CIFilter(name:"CIConstantColorGenerator")!
white.setValue(CIColor(color:.white), forKey:"inputColor")
let black = CIFilter(name:"CIConstantColorGenerator")!
black.setValue(CIColor(color:UIColor.black.withAlphaComponent(0.5)),
forKey:"inputColor")
let atop = CIFilter(name:"CISourceAtopCompositing")!
atop.setValue(white.outputImage!, forKey:"inputBackgroundImage")
atop.setValue(black.outputImage!, forKey:"inputImage")
let cgim = con.createCGImage(atop.outputImage!,
from: CGRect(x: 0, y: 0, width: 201, height: 50))!
let image = UIImage(cgImage: cgim)
let iv = UIImageView(image:image)
self.view.addSubview(iv)
iv.frame.origin = CGPoint(x: 100, y: 150)
What I've done here is to lay a 50% transparency black swatch on top of a white swatch. We intuitively imagine that the result will be a swatch that will read as 0.5. But it isn't; it's 0.737, the very same shade that is appearing at the midpoint of your gradients:
The reason is that everything here is happening, not in some mathematical vacuum, but in a color space adjusted for a specific gamma.
Now, you may justly ask: "But where did I specify this color space? This is not what I want!" Aha. You specified it in the first line, when you created a CIContext without overriding the default working color space.
Let's fix that. Change the first line to this:
let con = CIContext(options: [.workingColorSpace : NSNull()])
Now the output is this:
Presto, that's your 0.5 gray!
So what I'm saying is, if you create your CIContext like that, you will get the gradient you are after, with 0.5 gray at the midpoint. I'm not saying that that is any more "right" than the result you are getting, but at least it shows how to get that particular result with the code you already have.
(In fact, I think what you were getting originally is more "right", as it is adjusted for human perception.)
The midpoint of the CILinearGradient appears to correspond to 188, 188, 188, which looks like the “absolute whiteness” rendition of middle gray, which is not entirely unreasonable. (The CISmoothLinearGradient offers a smoother transition, but it doesn’t have the midpoint at 0.5, 0.5, 0.5, either.) As an aside, the “linear” in CILinearGradient and CISmoothLinearGradient refer to the shape of the gradient (to differentiate it from a “radial” gradient), not the nature of the color transitions within the gradient.
However if you want a gradient whose midpoint is 0.5, 0.5, 0.5, you can use CGGradient:
func simpleGradient(in rect: CGRect) -> UIImage {
return UIGraphicsImageRenderer(bounds: rect).image { context in
let colors = [UIColor.white.cgColor, UIColor.black.cgColor]
let colorSpace = CGColorSpaceCreateDeviceGray() // or RGB works, too
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil) else { return }
context.cgContext.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: rect.maxX, y: 0), options: [])
}
}
Alternatively, if you want a gradient background, you might define a UIView subclass that uses a CAGradientLayer as its backing layer:
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
gradientLayer.colors = [UIColor.white.cgColor, UIColor.black.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
}
}

How to draw vertical text on CGContext?

I want to draw a vertical text. I had tried different way to rotate string using rotate by but its not working
Here is my code
override func draw(with box: PDFDisplayBox, to context: CGContext) {
let name = "Sunny"
// Draw original content
super.draw(with: box, to: context)
// Draw rotated overlay string
UIGraphicsPushContext(context)
context.saveGState()
let pageBounds = self.bounds(for: box)
context.translateBy(x: pageBounds.size.width, y: 0)
context.scaleBy(x: -1.0, y: -1.0)
context.rotate(by: CGFloat.pi / 4.0)
let string: NSString = name as! NSString
let attributes = [
NSAttributedString.Key.foregroundColor: UIColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5),
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 64)
]
string.draw(at: CGPoint(x:250, y:40), withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
How can i draw?
We can draw the vertical string using the below code, in which I have converted given string by adding line separators between the characters.
override func draw(with box: PDFDisplayBox, to context: CGContext) {
// Draw original content
super.draw(with: box, to: context)
// Draw rotated overlay string
UIGraphicsPushContext(context)
context.saveGState()
let pageBounds = self.bounds(for: box)
context.translateBy(x: 0.0, y: pageBounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
context.rotate(by: CGFloat.pi / -60.0)
// Convert string by adding line separators.
let string = "Sunny".trimmingCharacters(in: .whitespaces)
let asArray = Array(string)
let verticalString = asArray.map { "\($0)" }.joined(separator: "\n")
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.foregroundColor: #colorLiteral(red: 0.4980392157, green: 0.4980392157, blue: 0.4980392157, alpha: 0.5),
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 64)
]
verticalString.draw(at: CGPoint(x: 250, y: 40), withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
Output of vertical string in PDF

UIProgressBar: How to Change UILabel Color according to Background Color

I'm new to iOS programming. I want to create a progress bar with a UIlabel which also change text color according to background color like this:
Please don't mark the question as duplicate. Because the solution I found are in Obj C or some Pods. But I wanted the solution in Swift because I didn't have any knowledge about Obj-C.
Thanks!
If you want an answer in swift, here is the code(changed from objc to swift) from this post, which should be exactly what you need:
This is swift 4
class MyProgressView: UIView {
var progress: CGFloat = 0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
// Set up environment.
let size = bounds.size
let backgroundColor = UIColor(red: 108/255, green: 200/255, blue: 226/255, alpha: 1)
let foregroundColor = UIColor.white
let font = UIFont.boldSystemFont(ofSize: 42)
// Prepare progress as a string.
let progress = NSString(format: "%d%%", Int(round(self.progress * 100))) // this cannot be a String because there are many subsequent calls to NSString-only methods such as `size` and `draw`
var attributes: [NSAttributedString.Key: Any] = [.font: font]
let textSize = progress.size(withAttributes: attributes)
let progressX = ceil(self.progress * size.width)
let textPoint = CGPoint(x: ceil((size.width - textSize.width) / 2), y: ceil((size.height - textSize.height) / 2))
// Draw background + foreground text
backgroundColor.setFill()
context?.fill(bounds)
attributes[.foregroundColor] = foregroundColor
progress.draw(at: textPoint, withAttributes: attributes)
// Clip the drawing that follows to the remaining progress's frame
context?.saveGState()
let remainingProgressRect = CGRect(x: progressX, y: 0, width: size.width - progressX, height: size.height)
context?.addRect(remainingProgressRect)
context?.clip()
// Draw again with inverted colors
foregroundColor.setFill()
context?.fill(bounds)
attributes[.foregroundColor] = backgroundColor
progress.draw(at: textPoint, withAttributes: attributes)
context?.restoreGState()
}
}
Tested with playground

CGContextSetShadowWithColor puts shadows on top of the text casting the shadow

I am using CGContextSetShadowWithColor and core graphics to draw shadowed text. The shadow appears, but it also seems to "muddy" the actual text itself which is casting the shadow (which should be pure white). It is as if it is casting the shadow on top of the text (but not quite).
Like this:
If I redraw the text in the same position with shadowing off, I can overwrite the muddy text with clean white text, so it is a work-around, but I am wondering:
Am I am doing something wrong, or is this a bug?
let shadowOffset : CGSize = CGSize (width: 4, height: 4)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 800, height: 200), false, 1.0)
let ctx = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(ctx, 0, 200);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextSetAlpha(ctx, 1.0)
CGContextSetShadowWithColor(ctx, shadowOffset, 5, UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).CGColor)
CGContextSetAllowsAntialiasing (ctx, true)
let attr:CFDictionaryRef = [
NSFontAttributeName : UIFont(name: fontName, size: fontSize)!,
NSForegroundColorAttributeName:UIColor.whiteColor()]
let line = CTLineCreateWithAttributedString(CFAttributedStringCreate(nil, "1234567890", attr))
let bounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.UseOpticalBounds)
CGContextSetLineWidth(ctx, 1)
CGContextSetTextDrawingMode(ctx, kCGTextFillStroke)
CGContextSetTextPosition(ctx, 100.0, 100.0)
CTLineDraw(line, ctx)
//Uncomment to clean-up text
//CGContextSetShadowWithColor(ctx, shadowOffset, 0, nil)
//CGContextSetTextPosition(ctx, 100.0, 100.0)
//CTLineDraw(line, ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
When calling to CGContextSetTextDrawingMode, set the drawing mode to kCGTextFill. To my understanding, the shadow you see is casted by the stroke of the text.

iOS: How to draw a speical shape

How to draw the shape shown below using UIBezierPath or CG? I can think of several ways, but none are very good. It contains a small arrow/triangle and a circle. Also I need to be able to rotate this shape, so the angle of the arrow can not be hard coded. You can ignore the background light grey circle. Just the red shape (plus a UILabel is better)
First, subclass UIView and override drawRect:. This code is really close and should get you there with some tweaking:
//Get the graphics context
let context = UIGraphicsGetCurrentContext()
//Colors, I was a little off here
let color = UIColor(red: 0.880, green: 0.653, blue: 0.653, alpha: 1.000)
//The circle
var ovalPath = UIBezierPath(ovalInRect: CGRectMake(86, 31, 58, 56))
color.setFill()
ovalPath.fill()
//The pointed arrow on the left of the circle
var bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(87, 54))
bezierPath.addLineToPoint(CGPointMake(76, 60))
bezierPath.addLineToPoint(CGPointMake(87, 66))
bezierPath.addLineToPoint(CGPointMake(87, 54))
color.setFill()
bezierPath.fill()
//Now, draw the 17%
let textRect = CGRectMake(95, 45, 40, 29)
var textTextContent = NSString(string: "17%")
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as NSMutableParagraphStyle
textStyle.alignment = NSTextAlignment.Center
//Text components
let textFontAttributes = [NSFontAttributeName: UIFont.systemFontOfSize(19), NSForegroundColorAttributeName: UIColor.whiteColor(), NSParagraphStyleAttributeName: textStyle]
let textTextHeight: CGFloat = textTextContent.boundingRectWithSize(CGSizeMake(textRect.width, CGFloat.infinity), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: textFontAttributes, context: nil).size.height
CGContextSaveGState(context)
CGContextClipToRect(context, textRect);
textTextContent.drawInRect(CGRectMake(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight), withAttributes: textFontAttributes)
CGContextRestoreGState(context)
All of that gives you this:

Resources