Creating transparent gradient and use it as an alpha-mask in SpriteKit - swift

I am trying to make a gradient and use it as an alpha mask. Right now, I am able to make an image similar to this (from black to transparent):
This is the code which I use to make all this:
private func createImage(width: CGFloat, height: CGFloat) -> CGImageRef?{
if let ciFilter = CIFilter(name: "CILinearGradient"){
let ciContext = CIContext()
ciFilter.setDefaults()
let startColor = CIColor(red: 0, green: 0, blue: 0, alpha: 0)
let endColor = CIColor(red: 0, green: 0, blue: 0, alpha: 1)
let startVector = CIVector(x: 0, y: height-10)
let endVector = CIVector(x: 0, y: height-22)
ciFilter.setValue(startColor, forKey: "inputColor0")
ciFilter.setValue(endColor, forKey: "inputColor1")
ciFilter.setValue(startVector, forKey: "inputPoint0")
ciFilter.setValue(endVector, forKey: "inputPoint1")
if let outputImage = ciFilter.outputImage {
let cgImage:CGImageRef = ciContext.createCGImage(outputImage, fromRect: CGRect(x: 0, y: 0, width: width, height: height))
return cgImage
}
}
return nil
}
For me, this way works pretty much perfect because I create a mask only once in my code (when GUI element is created) so performance is not affected at all.
The thing is that I actually need a resulting image (gradient texture) to look like this (gradients should have fixed height, but size of black area may vary):
Because CILinearGradient filter doesn't have an inputImage parameter, I can't chain two filters one after another. But rather I have to create an image, and apply a filter, then to create another image, and apply a new filter.
I solved this just like described above. Also I had to extend a process in three steps (create upper gradient, create middle, black area and create lower gradient) and then to combine all that into one image.
I wonder is there anything about CILinearGradient filter I am not aware of ? Maybe there is an easier way to attack this problem and make a complex gradient?
Note that I am using SpriteKit and solving this with CAGradientLayer is not a way I want to go.

I'm not sure Core Image is the right tool for the job here — you'd probably be better off using CGGradient functions to draw your image.
Aside: What you're asking would be possible but cumbersome in CoreImage. Think of it by analogy to what you'd do to create these images as a user of Photoshop or other common graphics software... the CoreImage way would be something like this:
Create a layer for the upper gradient, fill the entire canvas with your gradient, and transform the layer to the proper position and size
Create a layer for the middle black portion, fill the entire canvas with black, and transform the layer to the proper position and size
Ditto #1, but for the bottom gradient.
You could do this with a combination of generator filters, transform filters, and compositing filters...
gradient -> transform -\
}-> composite -\
solid color -> transform -/ \
}-> composite
gradient -> transform ————————————————--/
But setting that up would be ugly.
Likewise, in Photoshop you could use selection tools and fill/gradient tools to create your gradient-fill-gradient design completely within a single layer... that's what drawing a single image using CoreGraphics is analogous to.
So, how to do that single-pass CG drawing? Something like this...
func createImage(width: CGFloat, _ height: CGFloat) -> CGImage {
let gradientOffset: CGFloat = 10 // white at bottom and top before/after gradient
let gradientLength: CGFloat = 12 // height of gradient region
// create colors and gradient for drawing with
let space = CGColorSpaceCreateDeviceRGB()
let startColor = UIColor.whiteColor().CGColor
let endColor = UIColor.blackColor().CGColor
let colors: CFArray = [startColor, endColor]
let gradient = CGGradientCreateWithColors(space, colors, [0,1])
// start an image context
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext()
// fill the whole thing with white (that'll show through at the ends when done)
CGContextSetFillColorWithColor(context, startColor)
CGContextFillRect(context, CGRect(x: 0, y: 0, width: width, height: height))
// draw top gradient
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0, y: gradientOffset), CGPoint(x: 0, y: gradientOffset + gradientLength), [])
// fill solid black middle
CGContextSetFillColorWithColor(context, endColor)
CGContextFillRect(context, CGRect(x: 0, y: gradientOffset + gradientLength, width: width, height: height - 2 * gradientOffset - 2 * gradientLength))
// draw bottom gradient
CGContextDrawLinearGradient(context, gradient, CGPoint(x: 0, y: height - gradientOffset), CGPoint(x: 0, y: height - gradientOffset - gradientLength), [])
return CGBitmapContextCreateImage(context)!
}

Related

Erasing pixels from image and then reverting them back

I'm creating an eraser app where i have a picture and the user can erase the background with the following func :
func eraseImage( image: UIImage ,line: [CGPoint] ,brushSize: CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(proxy.size, false, 0)
let context = UIGraphicsGetCurrentContext()
let rect = AVMakeRect(aspectRatio: image.size, insideRect: CGRect(x: 0, y:0, width: proxy.size.width, height: proxy!.size.height))
//lassoImageView.image?.draw(in: calculateRectOfImageInImageView(imageView: lassoImageView))
image.draw(in: rect, blendMode: .normal, alpha: 1)
context?.move(to: CGPoint(x: line.first!.x - 50, y: line.first!.y - 50))
for pointIndex in 1..<line.count {
context?.addLine(to: CGPoint(x: line[pointIndex].x - 50, y: line[pointIndex].y - 50))
}
context?.setBlendMode(.clear)
context?.setLineCap(.round)
context?.setLineWidth(brushSize)
context?.setShadow(offset: CGSize(width: 0, height: 0), blur: 8)
context?.strokePath()
if let img = UIGraphicsGetImageFromCurrentImageContext() {
return img
}
UIGraphicsEndImageContext()
return nil
}
I'm struggling with figuring out how can i add a drawing function where the user can correct it's earaser mistake and draw back some parts.
I'm thinking of drawing on the edited image the original image with a mask of the path which tracks the cgpoints of the users location. is that possible?
I would suggest setting up your image with a second CALayer installed as a mask. Install an image view into the mask layer's contents, and draw with clear/opaque colors into the mask to mask/expose pixels from the image layer.

Filling animation + permanent mask in swift?

Something like this:
Or follow this link if you can:
https://www.lottiefiles.com/450-play-fill-loader
My case is simpler - I have a view with mask which consists of multiple rects and a linear fill. Mask is created in runtime (so I can't use lottie) but remains permanent, fill is animated (filling from right to left). But how to draw it?
Note: I tried to find a similar animation implementation but in most cases they just try to change the mask params while in my case mask is constant.
If i understand you correct, you can add subview under your mask and change its width, something like that:
let fillingStartFrame = CGRect.init(x: 0, y: 0, width: 0, height: view.frame.height)
let fillingEndFrame = CGRect.init(x: 0, y: 0, width: view.frame.width, height: view.frame.width)
let fillingView = UIView(frame: fillingStartFrame)
view.insertSubview(fillingView, belowSubview: YOUR_MASK)
UIView.animate(withDuration: 0.3) {
fillingView.frame = fillingEndFrame
}

Swift: draw image and border using UIGraphicsBeginImageContextWithOptions

I need to draw an image like this
What provided is: green bubble with down arrow image(green bubble image already have border so I don't need to draw it) and center photo. And I need to draw white border around the photo + rounded corner it
This is the code I have so far:
let rect = CGRect(origin: .zero, size: CGSize(width: 60, height: 67))
let width = CGFloat(50)
let borderWidth: CGFloat = 1.0
let imageRect = CGRect(x: 5, y: 5, width: width, height: width)
let bubbleImg = #imageLiteral(resourceName: "pinGreen")
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
bubbleImg.draw(in: rect)
let path = UIBezierPath(roundedRect: imageRect.insetBy(dx: borderWidth / 2, dy: borderWidth / 2), cornerRadius: width/2)
context!.saveGState()
path.addClip()
image.draw(in: imageRect)
context!.restoreGState()
UIColor.purple.setStroke()
path.lineWidth = borderWidth
path.stroke()
let roundedImage = UIGraphicsGetImageFromCurrentImageContext();
let img = roundedImage?.cgImage!
UIGraphicsEndImageContext();
and this is the result
Can anyone help me with this?
Is there anyway to design and get this type of image from xib file?
I will tell you the simplest way:
You need imageView1 for green bubble image, imageView2 for the photo (width=height). Their centers are the same.
The imageView2 has corner radius equal to its frame width/2.
Set border color of imageView2's layer to White, and border width is about 5 px. Dont forget to set maskToBounds=true.
Please try.
DaijDjan: this answer in one image (showing IB + code + app):

how to change specific color in image to a different color

I have an UIView that its layer contents is an image.
let image = UIImage(names: "myImage")
layer.contents = image.CGImage
This image has a few colors.
Is there a way to change a specific color to any other color of my choice?
I found answers for changing the all of the colors in the image but not a specific one.
answer
You can't change specific color in PNG with transparent background, but.. i found the solution.
extension UIImage {
func maskWithColors(color: UIColor) -> UIImage? {
let maskingColors: [CGFloat] = [100, 255, 100, 255, 100, 255] // We should replace white color.
let maskImage = cgImage! //
let bounds = CGRect(x: 0, y: 0, width: size.width * 3, height: size.height * 3) // * 3, for best resolution.
let sz = CGSize(width: size.width * 3, height: size.height * 3) // Size.
var returnImage: UIImage? // Image, to return
/* Firstly we will remove transparent background, because
maskingColorComponents don't work with transparent images. */
UIGraphicsBeginImageContextWithOptions(sz, true, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
context.scaleBy(x: 1.0, y: -1.0) // iOS flips images upside down, this fix it.
context.translateBy(x: 0, y: -sz.height) // and this :)
context.draw(maskImage, in: bounds)
context.restoreGState()
let noAlphaImage = UIGraphicsGetImageFromCurrentImageContext() // new image, without transparent elements.
UIGraphicsEndImageContext()
let noAlphaCGRef = noAlphaImage?.cgImage // get CGImage.
if let imgRefCopy = noAlphaCGRef?.copy(maskingColorComponents: maskingColors) { // Magic.
UIGraphicsBeginImageContextWithOptions(sz, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: 0, y: -sz.height)
context.clip(to: bounds, mask: maskImage) // Remove background from image with mask.
context.setFillColor(color.cgColor) // set new color. We remove white color, and set red.
context.fill(bounds)
context.draw(imgRefCopy, in: bounds) // draw new image
let finalImage = UIGraphicsGetImageFromCurrentImageContext()
returnImage = finalImage! // YEAH!
UIGraphicsEndImageContext()
}
return returnImage
}
}
For call this function use code like this...
let image = UIImage(named: "Brush").maskWithColor(color: UIColor.red)
Result:
You can not change the image color... The only way is to change the image on any event or something...
Another variant is to create one image with transparent color, and set the background color of the view or something where you put the image...

How to draw a gradient line (fading in/out) with Core Graphics/iPhone?

I know how to draw a simple line:
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);
And I know how to do a gradient rectangle, i.g.:
CGColorSpaceRef myColorspace=CGColorSpaceCreateDeviceRGB();
size_t num_locations = 2;
CGFloat locations[2] = { 1.0, 0.0 };
CGFloat components[8] = { 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0 };
CGGradientRef myGradient = CGGradientCreateWithColorComponents(myColorspace, components, locations, num_locations);
CGPoint myStartPoint, myEndPoint;
myStartPoint.x = 0.0;
myStartPoint.y = 0.0;
myEndPoint.x = 0.0;
myEndPoint.y = 10.0;
CGContextDrawLinearGradient (context, myGradient, myStartPoint, myEndPoint, 0);
But how could I draw a line with a gradient, i.g. fading in from black to white (and maybe fading out to black on the other side as well) ?
It is possible to stroke arbitrary paths with a gradient, or any other fill effect, such as a pattern.
As you have found, stroked paths are not rendered with the current gradient. Only filled paths use the gradient (when you turn them in to a clip and then draw the gradient).
However, Core Graphics has an amazingly cool procedure CGContextReplacePathWithStrokedPath that will transform the path you intend to stroke in to a path that is equivalent when filled.
Behind the scenes, CGContextReplacePathWithStrokedPath builds up an edge polygon around your stroke path and switches that for the path you have defined. I'd speculate that the Core Graphics rendering engine probably does this anyway in calls to CGContextStrokePath.
Here's Apple's documentation on this:
Quartz creates a stroked path using the parameters of the current graphics context. The new path is created so that filling it draws the same pixels as stroking the original path. You can use this path in the same way you use the path of any context. For example, you can clip to the stroked version of a path by calling this function followed by a call to the function CGContextClip.
So, convert your path in to something you can fill, turn that in to a clip, and then draw your gradient. The effect will be as if you had stroked the path with the gradient.
Code
It'll look something like this…
// Get the current graphics context.
//
const CGContextRef context = UIGraphicsGetCurrentContext();
// Define your stroked path.
//
// You can set up **anything** you like here.
//
CGContextAddRect(context, yourRectToStrokeWithAGradient);
// Set up any stroking parameters like line.
//
// I'm setting width. You could also set up a dashed stroke
// pattern, or whatever you like.
//
CGContextSetLineWidth(context, 1);
// Use the magical call.
//
// It turns your _stroked_ path in to a **fillable** one.
//
CGContextReplacePathWithStrokedPath(context);
// Use the current _fillable_ path in to define a clipping region.
//
CGContextClip(context);
// Draw the gradient.
//
// The gradient will be clipped to your original path.
// You could use other fill effects like patterns here.
//
CGContextDrawLinearGradient(
context,
yourGradient,
gradientTop,
gradientBottom,
0
);
Further notes
It's worth emphasising part of the documentation above:
Quartz creates a stroked path using the parameters of the current graphics context.
The obvious parameter is the line width. However, all line drawing state is used, such as stroke pattern, mitre limit, line joins, caps, dash patterns, etc. This makes the approach extremely powerful.
For additional details see this answer of this S.O. question.
After several tries I'm now sure that gradients doesn't affect strokes, so I think it's impossible to draw gradient lines with CGContextStrokePath(). For horizontal and vertical lines the solution is to use CGContextAddRect() instead, which fortunately is what I need. I replaced
CGContextMoveToPoint(context, x, y);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);
with
CGContextSaveGState(context);
CGContextAddRect(context, CGRectMake(x, y, width, height));
CGContextClip(context);
CGContextDrawLinearGradient (context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);
and everything works fine. Thanks to Brad Larson for the crucial hint.
After you draw the line, you can call
CGContextClip(context);
to clip further drawing to your line area. If you draw the gradient, it should now be contained within the line area. Note that you will need to use a clear color for your line if you just want the gradient to show, and not the line underneath it.
There is the possibility that a line will be too thin for your gradient to show up, in which case you can use CGContextAddRect() to define a thicker area.
I present a more elaborate example of using this context clipping in my answer here.
You can use Core Animation layers. You can use a CAShaperLayer for your line by settings its path property and then you can use a CAGradientLayer as a layer mask to your shape layer that will cause the line to fade.
Replace your CGContext... calls with calls to CGPath... calls to create the line path. Set the path field on the layer using that path. Then in your gradient layer, specify the colors you want to use (probably black to white) and then set the mask to be the line layer like this:
[gradientLayer setMask:lineLayer];
What's cool about the gradient layer is that is allows you to specify a list of locations where the gradient will stop, so you can fade in and fade out. It only supports linear gradients, but it sounds like that may fit your needs.
Let me know if you need clarification.
EDIT: Now that I think of it, just create a single CAGradientLayer that is the width/height of the line you desire. Specify the gradient colors (black to white or black to clear color) and the startPoint and endtPoints and it should give you what you need.
I created a Swift version of Benjohn's answer.
class MyView: UIView {
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
context.addPath(createPath().cgPath)
context.setLineWidth(15)
context.replacePathWithStrokedPath()
context.clip()
let gradient = CGGradient(colorsSpace: nil, colors: [UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor] as CFArray, locations: [0, 0.4, 1.0])!
let radius:CGFloat = 200
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
context.drawRadialGradient(gradient, startCenter: center, startRadius: 10, endCenter: center, endRadius: radius, options: .drawsBeforeStartLocation)
}
}
If the createPath() method creates a triangle, you get something like this:
Hope this helps anyone!
Swift 5 Version
This code worked for me.
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
drawGradientBorder(rect, context: context)
}
fileprivate
func drawGradientBorder(_ rect: CGRect, context: CGContext) {
context.saveGState()
let path = ribbonPath()
context.setLineWidth(5.0)
context.addPath(path.cgPath)
context.replacePathWithStrokedPath()
context.clip()
let baseSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: baseSpace, colors: [UIColor.red.cgColor, UIColor.blue.cgColor] as CFArray, locations: [0.0 ,1.0])!
context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: rect.height/2), end: CGPoint(x: rect.width, y: rect.height/2), options: [])
context.restoreGState()
}
fileprivate
func ribbonPath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: bounds.maxX - gradientWidth, y: 0))
path.addLine(to: CGPoint(x: bounds.maxX - gradientWidth - ribbonGap, y: bounds.maxY))
path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
path.close()
return path
}
CGContextMoveToPoint(context, frame.size.width-200, frame.origin.y+10);
CGContextAddLineToPoint(context, frame.size.width-200, 100-10);
CGFloat colors[16] = { 0,0, 0, 0,
0, 0, 0, .8,
0, 0, 0, .8,
0, 0,0 ,0 };
CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 4);
CGContextSaveGState(context);
CGContextAddRect(context, CGRectMake(frame.size.width-200,10, 1, 80));
CGContextClip(context);
CGContextDrawLinearGradient (context, gradient, CGPointMake(frame.size.width-200, 10), CGPointMake(frame.size.width-200,80), 0);
CGContextRestoreGState(context);
its work for me.
Based on #LinJin's answer, here's something that draws a linear rainbow gradient border around a rectangle... I guess the start of a rectangular UIColorWell sort of thing.
Swift 5
import UIKit
import QuartzCore
class RectangularColorWell : UIControl {
var lineWidth : CGFloat = 3.0 {
didSet {
self.setNeedsDisplay()
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(CustomToggleControl.controlTapped(_:)))
tapGestureRecognizer.numberOfTapsRequired = 1;
tapGestureRecognizer.numberOfTouchesRequired = 1;
self.addGestureRecognizer(tapGestureRecognizer)
self.setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.setLineWidth(lineWidth)
ctx.setFillColor(backgroundColor!.cgColor)
ctx.fill(rect)
drawGradientBorder(rect, context: ctx)
}
func drawGradientBorder(_ rect: CGRect, context: CGContext) {
context.saveGState()
let path = UIBezierPath(rect: rect)
context.addPath(path.cgPath)
context.replacePathWithStrokedPath()
context.clip()
let rainbowColors : [UIColor] = [ .red, .orange, .yellow, .green, .blue, .purple ]
let colorDistribution: [CGFloat] = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0 ]
let gradient = CGGradient(
colorsSpace: CGColorSpaceCreateDeviceRGB(),
colors: rainbowColors.map { $0.cgColor } as CFArray,
locations: colorDistribution)!
context.drawLinearGradient(gradient,
start: CGPoint(x: 0, y: 0),
end: CGPoint(x: rect.width, y: rect.height), options: [])
context.restoreGState()
}
#objc func controlTapped(_ gestureRecognizer :UIGestureRecognizer) {
UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseIn, animations: {
self.alpha = 0.5
}, completion: { (finished: Bool) -> Void in
UIView.animate(withDuration: 0.2, delay: 0.0, options: UIView.AnimationOptions.curveEaseOut, animations: {
self.alpha = 1.0
}, completion: { (finished: Bool) -> Void in
})
})
setNeedsDisplay()
sendActions(for: .touchDown)
}
}