grayscale chromakey swift - swift

I wanted to change all the white background of a grayscale line art to transparent, using swift. I implemented RGB chromakey document for swift: https://developer.apple.com/documentation/coreimage/applying_a_chroma_key_effect
The hue range was initially for green color and there were R,G,B and alpha parameters
self.ChromaKeyFilter(fromHue: 0.3, toHue: 0.4)
, but for grayscale there were only 2 according to apple documentation: white, alpha. I wanted to change the extreme white color to transparent, so I just set hue range as 1.
class ChromaKey{
//Hue range
let whiteRange: CGFloat = 1
func getHue(white: CGFloat) -> CGFloat{
let color = UIColor(white: white, alpha: 1)
var hue: CGFloat = 0
color.getHue(&hue, saturation: nil, brightness: nil, alpha: nil)
return hue
}
func chromaKeyFilter(ridHue:CGFloat) -> CIFilter? {
let size = 64
var cubeGrey = [Float]()
for z in 0 ..< size{
let white = CGFloat(z)/CGFloat(size-1)
let hue = getHue(white: white)
let alpha: CGFloat = (hue == ridHue) ? 0:1
cubeGrey.append(Float(white * alpha))
cubeGrey.append(Float(alpha))
}
let data = Data(buffer: UnsafeBufferPointer(start: &cubeGrey, count: cubeGrey.count))
let colorCubeFilter = CIFilter(name: "CIColorCube", parameters: ["inputCubeDimension": size, "inputCubeData":data])
return colorCubeFilter
}
func filterPixels(foregroundCIImage: CIImage) -> CIImage {
let chromaCIFilter = self.chromaKeyFilter(ridHue: whiteRange)
chromaCIFilter?.setValue(foregroundCIImage, forKey: kCIInputImageKey)
let sourceCIImageWithoutBackground = chromaCIFilter?.outputImage
var image = CIImage()
if let filteredImage = sourceCIImageWithoutBackground{
image = filteredImage
}
return image
}
the func for chromakey. when I apply the func, nothing shows up.

Related

How do I extract gradient color of minimum track image at the thumb position?

I want to change the thumb color same as the minimum track color. I need to extract color at the thumb location as it moves along the slider. I want my slider thumb to look something like this, and change color in accordance with the minimum track gradient color as it moves along the slider.
Following is my code for the gradient I created
func setSlider(slider:UISlider) {
let tgl = CAGradientLayer()
let frame = CGRect(x: 0.0, y: 0.0, width: slider.bounds.width, height: 10.0)
tgl.frame = frame
tgl.colors = [UIColor.black.cgColor, UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor]
tgl.borderWidth = 1.0
tgl.borderColor = UIColor.gray.cgColor
tgl.cornerRadius = 5.0
tgl.endPoint = CGPoint(x: 1.0, y: 1.0)
tgl.startPoint = CGPoint(x: 0.0, y: 1.0)
UIGraphicsBeginImageContextWithOptions(tgl.frame.size, false, 10.0)
tgl.render(in: UIGraphicsGetCurrentContext()!)
let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
slider.setMaximumTrackImage(getBgImage(width: slider.bounds.width), for: .normal)
slider.setMinimumTrackImage(backgroundImage, for: .normal)
}
I tried to fetch color using the following code:
let color = sliderRating.minimumTrackImage(for: .normal)?.getPixelColor(point: CGPoint(x: Int(sender.value), y: 1))
func getPixelColor(point: CGPoint) -> UIColor? {
guard let cgImage = cgImage else { return nil }
let width = Int(size.width)
let height = Int(size.height)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: colorSpace,
bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
else {
return nil
}
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
guard let pixelBuffer = context.data else { return nil }
let pointer = pixelBuffer.bindMemory(to: UInt32.self, capacity: width * height)
let pixel = pointer[Int(point.y) * width + Int(point.x)]
let r: CGFloat = CGFloat(red(for: pixel)) / 255
let g: CGFloat = CGFloat(green(for: pixel)) / 255
let b: CGFloat = CGFloat(blue(for: pixel)) / 255
let a: CGFloat = CGFloat(alpha(for: pixel)) / 255
return UIColor(red: r, green: g, blue: b, alpha: a)
}
private func alpha(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 24) & 255)
}
private func red(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 16) & 255)
}
private func green(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 8) & 255)
}
private func blue(for pixelData: UInt32) -> UInt8 {
return UInt8((pixelData >> 0) & 255)
}
private func rgba(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) -> UInt32 {
return (UInt32(alpha) << 24) | (UInt32(red) << 16) | (UInt32(green) << 8) | (UInt32(blue) << 0)
}
Here is a similar question I found on stack overflow for more reference:
How can I extract the uislider gradient color at the thumb position?
I tried extracting pixel color from the image but it gives me only white, gray, and darker gray shades but my track has colors ranging from black to green.
I'm getting output like this:
The issue is how you're determining the point for your color.
The code you show:
let color = sliderRating.minimumTrackImage(for: .normal)?.getPixelColor(point: CGPoint(x: Int(sender.value), y: 1))
is difficult to debug.
Let's split that up:
// safely unwrap the optional to make sure we get a valid image
if let minImg = sender.minimumTrackImage(for: .normal) {
let x = Int(sender.value)
let y = 1
let point = CGPoint(x: x, y: y)
// to debug this:
print("point:", point)
// safely unwrap the optional returned color
if let color = minImg.getPixelColor(pos: point) {
// do something with color
}
}
By default, a slider has values between 0.0 and 1.0. So as you drag the thumb, you'll see this output in the debug console:
// lots of these
point: (0.0, 1.0)
point: (0.0, 1.0)
point: (0.0, 1.0)
point: (0.0, 1.0)
point: (0.0, 1.0)
// then when you final get all the way to the right
point: (1.0, 1.0)
As you see, you're not getting the point that you want on your image.
You don't mention it, but if did something like this:
sliderRating.minimumValue = 100
sliderRating.maximumValue = 120
Your x will range from 100 to 120. Again, not the point you want.
Instead of using the .value, you want to get the horizontal center of the thumb rect for x, and the vertical center of the image size for y.
Try it like this:
#objc func sliderRatingValueChanged(_ sender: UISlider) {
// get the slider's trackRect
let trackR = sender.trackRect(forBounds: sender.bounds)
// get the slider's thumbRect
let thumbR = sender.thumbRect(forBounds: sender.bounds, trackRect: trackR, value: sender.value)
// get the slider's minimumTrackImage
if let minImg = sender.minimumTrackImage(for: .normal) {
// we want point.x to be thumb rect's midX
// we want point.y to be 1/2 the height of the min track image
let point = CGPoint(x: thumbR.midX, y: minImg.size.height * 0.5)
// for debugging:
print("point:", point)
// get the color at point
if let color = minImg.getPixelColor(point: point) {
// set tint color of thumb
sender.thumbTintColor = color
}
}
// now do something with the slider's value?
let value = sender.value
}

How to find current color in UIImageView and change repeatedly?

I have this UIImageView where I am only changing the white color of the image. When the white color changes it doesn't change again because the white color is no longer white anymore. I want to access the new color and change it to a different color every time I press a button. Im using this func I found on github.
var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
#IBAction func changeColors(_ sender: Any) {
let randomRGB = CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)
//randomly change color
var newColor = UIColor.init(red: randomRGB3, green: randomRGB2, blue: randomRGB, alpha: 1)
let changeColor = replaceColor(color: currentColor, withColor: newColor, image: mainImage.image!, tolerance: 0.5)
mainImage.image = changeColor
//change current color to new color
currentColor = newColor
}
extension ViewController {
func replaceColor(color: UIColor, withColor: UIColor, image: UIImage, tolerance: CGFloat) -> UIImage {
// This function expects to get source color(color which is supposed to be replaced)
// and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
assert(color.cgColor.numberOfComponents == 4 && withColor.cgColor.numberOfComponents == 4,
"Must be RGBA colorspace")
// Allocate bitmap in memory with the same width and size as source image
let imageRef = image.cgImage!
let width = imageRef.width
let height = imageRef.height
let bytesPerPixel = 4
let bytesPerRow = bytesPerPixel * width;
let bitsPerComponent = 8
let bitmapByteCount = bytesPerRow * height
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: bitmapByteCount)
let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: CGColorSpace(name: CGColorSpace.genericRGBLinear)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue)
let rc = CGRect(x: 0, y: 0, width: width, height: height)
// Draw source image on created context
context!.draw(imageRef, in: rc)
// Get color components from replacement color
let withColorComponents = withColor.cgColor.components
let r2 = UInt8(withColorComponents![0] * 255)
let g2 = UInt8(withColorComponents![1] * 255)
let b2 = UInt8(withColorComponents![2] * 255)
let a2 = UInt8(withColorComponents![3] * 255)
// Prepare to iterate over image pixels
var byteIndex = 0
while byteIndex < bitmapByteCount {
// Get color of current pixel
let red = CGFloat(rawData[byteIndex + 0]) / 255
let green = CGFloat(rawData[byteIndex + 1]) / 255
let blue = CGFloat(rawData[byteIndex + 2]) / 255
let alpha = CGFloat(rawData[byteIndex + 3]) / 255
let currentColor = UIColor(red: red, green: green, blue: blue, alpha: alpha)
// Compare two colors using given tolerance value
if compareColor(color: color, withColor: currentColor , withTolerance: tolerance) {
// If the're 'similar', then replace pixel color with given target color
rawData[byteIndex + 0] = r2
rawData[byteIndex + 1] = g2
rawData[byteIndex + 2] = b2
rawData[byteIndex + 3] = a2
}
byteIndex = byteIndex + 4;
}
// Retrieve image from memory context
let imgref = context!.makeImage()
let result = UIImage(cgImage: imgref!)
// Clean up a bit
rawData.deallocate()
return result
}
func compareColor(color: UIColor, withColor: UIColor, withTolerance: CGFloat) -> Bool
{
var r1: CGFloat = 0.0, g1: CGFloat = 0.0, b1: CGFloat = 0.0, a1: CGFloat = 0.0;
var r2: CGFloat = 0.0, g2: CGFloat = 0.0, b2: CGFloat = 0.0, a2: CGFloat = 0.0;
color.getRed(&r1, green: &g1, blue: &b1, alpha: &a1);
withColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2);
return abs(r1 - r2) <= withTolerance &&
abs(g1 - g2) <= withTolerance &&
abs(b1 - b2) <= withTolerance &&
abs(a1 - a2) <= withTolerance;
}
}
Here are a few observations that I made which might be impacting the results you see:
As we discussed in the comments, if you want to start from the color which was changed previously, you need to hold on the color after the image has been updated beyond the scope of your function (you did this)
The next issue about ending up with one color probably has a lot to do with the fault tolerance
When you try to change a color in an image with 0.5 (50%) fault tolerance of a given color, you are changing a huge number of colors in an image in the first pass
If there were 100 colors in a color system, you are going to look for 50 of those colors in the image and change them to 1 specific color
In the first pass, you start with white. Lets say that 75% of the image has colors that are similar to white with a 50% fault tolerance - 75% of the image is going to change to that color
With such a high fault tolerance, soon enough one color will appear that will be close to most of the colors in the image with a 50% fault tolerance and you will end up with colors with 1 image
Some ideas to improve the results
Set a lower fault tolerance - you will see smaller changes and the same result could occur with 1 color but it will happen over a longer period of time
If you really want to randomize and no get this 1 color results, I suggest to change how you use the currentColor and make changes to the original image, not the updated image (I have this example below)
This will not impact the solution but better to handle optionals more safely as I see a lot of ! so I would recommend changing that
Perform the image processing in a background thread (also in the example below)
Here is an update with an example
class ImageColorVC: UIViewController
{
// UI Related
private var loaderController: UIAlertController!
let mainImage = UIImageView(image: UIImage(named: "art"))
// Save the current color and the original image
var currentColor = UIColor.init(red: 1, green: 1, blue: 1, alpha: 1)
var originalImage: UIImage!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration, you can ignore
view.backgroundColor = .white
title = "Image Color"
configureBarButton()
configureImageView()
// Store the original image
originalImage = mainImage.image!
}
// MARK: AUTO LAYOUT
private func configureBarButton()
{
let barButton = UIBarButtonItem(barButtonSystemItem: .refresh,
target: self,
action: #selector(changeColors))
navigationItem.rightBarButtonItem = barButton
}
private func configureImageView()
{
mainImage.translatesAutoresizingMaskIntoConstraints = false
mainImage.contentMode = .scaleAspectFit
mainImage.clipsToBounds = true
view.addSubview(mainImage)
view.addConstraints([
mainImage.leadingAnchor
.constraint(equalTo: view.leadingAnchor),
mainImage.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
mainImage.trailingAnchor
.constraint(equalTo: view.trailingAnchor),
mainImage.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
}
// Configures a loader to show while image is processing
private func configureLoaderController()
{
loaderController = UIAlertController(title: nil,
message: "Processing",
preferredStyle: .alert)
let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10,
y: 5,
width: 50,
height: 50))
loadingIndicator.hidesWhenStopped = true
loadingIndicator.style = UIActivityIndicatorView.Style.medium
loadingIndicator.startAnimating();
loaderController.view.addSubview(loadingIndicator)
}
//MARK: FACTORY
// Similar to your function, only difference is that it uses
// the original image
private func performChangeColors()
{
let randomRGB = CGFloat.random(in: 0.0...1.0)
let randomRGB2 = CGFloat.random(in: 0.0...1.0)
let randomRGB3 = CGFloat.random(in: 0.0...1.0)
//randomly change color
let newColor = UIColor.init(red: randomRGB3,
green: randomRGB2,
blue: randomRGB,
alpha: 1)
// Do work in the back ground
DispatchQueue.global(qos: .background).async
{
let imageWithNewColor = self.replaceColor(color: self.currentColor,
withColor: newColor,
image: self.originalImage!,
tolerance: 0.5)
// Update the UI on the main thread
DispatchQueue.main.async
{
self.updateImageView(with: imageWithNewColor)
//change current color to new color
self.currentColor = newColor
}
}
}
#objc
private func changeColors()
{
// Configure a loader to show while image is processing
configureLoaderController()
present(loaderController, animated: true) { [weak self] in
self?.performChangeColors()
}
}
// Update the UI
private func updateImageView(with image: UIImage)
{
dismiss(animated: true) { [weak self] in
self?.mainImage.image = image
}
}
}
After starting with this:
About 50 tries later, it still seems to work well:
You can watch a longer video here to see a few more color changes that happen without leading to one single color
Hope this gives you enough to create the required workaround for your solution

How to not cause dark gray color to be transparent removing background from image

I'm having an issue where when I try to remove the green from an image (in this case the image background) but all the dark grays (within the part of the image I want to keep) become semi-transparent. I am unsure why, would like some advice on how to:
func chromaKeyFilter(fromHue: CGFloat, toHue: CGFloat) -> CIFilter? {
let size = 64
var cubeRGB = [Float]()
for z in 0 ..< size {
let blue = CGFloat(z) / CGFloat(size-1)
for y in 0 ..< size {
let green = CGFloat(y) / CGFloat(size-1)
for x in 0 ..< size {
let red = CGFloat(x) / CGFloat(size-1)
let color = UIColor(red: red, green: green, blue: blue, alpha: 1)
let hueColor = color.hsbColor
let alpha: CGFloat = (hueColor.hue >= fromHue && hueColor.hue <= toHue) ? 0 : 1
cubeRGB.append(Float(red * alpha))
cubeRGB.append(Float(green * alpha))
cubeRGB.append(Float(blue * alpha))
cubeRGB.append(Float(alpha))
}
}
}
let data = Data(bytes: cubeRGB, count: cubeRGB.count * MemoryLayout<Float>.size)
let params: [String: Any] = ["inputCubeDimension": size, "inputCubeData": data]
return CIFilter(name: "CIColorCube", parameters: params)
}
func filterPixels(foregroundCIImage: CIImage) -> CIImage {
let chromaCIFilter = self.chromaKeyFilter(fromHue: 0.33, toHue: 0.34)
chromaCIFilter?.setValue(foregroundCIImage, forKey: kCIInputImageKey)
let sourceCIImageWithoutBackground = chromaCIFilter?.outputImage
var image = CIImage()
if let filteredImage = sourceCIImageWithoutBackground {
image = filteredImage
}
return image
}
}
extension UIColor {
/// Decomposes UIColor to its HSBA components
var hsbColor: HSBColor {
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
return HSBColor(hue: h, saturation: s, brightness: b, alpha: a)
}
/// Holds the CGFloat values of HSBA components of a color
public struct HSBColor {
var hue: CGFloat
var saturation: CGFloat
var brightness: CGFloat
var alpha: CGFloat
}
}
Sample image:
Your code is correct, but remember that a dark gray could really be a very dark green.
On this line:
let alpha: CGFloat = (hueColor.hue >= fromHue && hueColor.hue <= toHue) ? 0 : 1
I would take brightness/saturation into account. For example
let alpha: CGFloat = (hueColor.saturation > 0.1 && hueColor.hue >= fromHue && hueColor.hue <= toHue) ? 0 : 1

Color animation

I've written simple animations for drawing rectangles in lines, we can treat them as a bars.
Each bar is one shape layer which has a path which animates ( size change and fill color change ).
#IBDesignable final class BarView: UIView {
lazy var pathAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
let red = UIColor(red: 249/255, green: 26/255, blue: 26/255, alpha: 1)
let orange = UIColor(red: 1, green: 167/255, blue: 463/255, alpha: 1)
let green = UIColor(red: 106/255, green: 239/255, blue: 47/255, alpha: 1)
lazy var backgroundColorAnimation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "fillColor")
animation.duration = 1
animation.fromValue = red.cgColor
animation.byValue = orange.cgColor
animation.toValue = green.cgColor
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fillMode = kCAFillModeBoth
animation.isRemovedOnCompletion = false
return animation
}()
#IBInspectable var spaceBetweenBars: CGFloat = 10
var numberOfBars: Int = 5
let data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
override func awakeFromNib() {
super.awakeFromNib()
initSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayers()
}
func setupLayers() {
let width = bounds.width - (spaceBetweenBars * CGFloat(numberOfBars + 1)) // There is n + 1 spaces between bars.
let barWidth: CGFloat = width / CGFloat(numberOfBars)
let scalePoint: CGFloat = bounds.height / 10.0 // 10.0 - 10 points is max
guard let sublayers = layer.sublayers as? [CAShapeLayer] else { return }
for i in 0...numberOfBars - 1 {
let barHeight: CGFloat = scalePoint * data[i]
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
var xPos: CGFloat!
if i == 0 {
xPos = spaceBetweenBars
} else if i == numberOfBars - 1 {
xPos = bounds.width - (barWidth + spaceBetweenBars)
} else {
xPos = barWidth * CGFloat(i) + spaceBetweenBars * CGFloat(i) + spaceBetweenBars
}
let startPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: 0)).cgPath
let endPath = UIBezierPath(rect: CGRect(x: xPos, y: bounds.height, width: barWidth, height: -barHeight)).cgPath
sublayers[i].path = startPath
pathAnimation.toValue = endPath
sublayers[i].removeAllAnimations()
sublayers[i].add(pathAnimation, forKey: "path")
sublayers[i].add(backgroundColorAnimation, forKey: "backgroundColor")
}
}
func initSublayers() {
for _ in 1...numberOfBars {
let shapeLayer = CAShapeLayer()
layer.addSublayer(shapeLayer)
}
}
}
The size ( height ) of bar depends of the data array, each sublayers has a different height. Based on this data I've crated a scale.
PathAnimation is changing height of the bars.
BackgroundColorAnimation is changing the collors of the path. It starts from red one, goes through the orange and finish at green.
My goal is to connect backgroundColorAnimation with data array as well as it's connected with pathAnimation.
Ex. When in data array is going to be value 1.0 then the bar going to be animate only to the red color which is a derivated from a base red color which is declared as a global variable. If the value in the data array going to be ex. 4.5 then the color animation will stop close to the delcared orange color, the 5.0 limit going to be this orange color or color close to this. Value closer to 10 going to be green.
How could I connect these conditions with animation properties fromValue, byValue, toValue. Is it an algorithm for that ? Any ideas ?
You have several problems.
You're setting fillMode and isRemovedOnCompletion. This tells me, to be blunt, that you don't understand Core Animation. You need to watch WWDC 2011 Session 421: Core Animation Essentials.
You're adding more layers every time layoutSubviews is called, but not doing anything with them.
You're adding animation every time layoutSubviews runs. Do you really want to re-animate the bars when the double-height “in-call” status bar appears or disappears, or on an interface rotation? It's probably better to have a separate animateBars() method, and call it from your view controller's viewDidAppear method.
You seem to think byValue means “go through this value on the way from fromValue to toValue”, but that's not what it means. byValue is ignored in your case, because you're setting fromValue and toValue. The effects of byValue are explained in Setting Interpolation Values.
If you want to interpolate between colors, it's best to use a hue-based color space, but I believe Core Animation uses an RGB color space. So you should use a keyframe animation to specify intermediate colors that you calculate by interpolating in a hue-based color space.
Here's a rewrite of BarView that fixes all these problems:
#IBDesignable final class BarView: UIView {
#IBInspectable var spaceBetweenBars: CGFloat = 10
var data: [CGFloat] = [5.5, 9.0, 9.5, 3.0, 8.0]
var maxDatum = CGFloat(10)
func animateBars() {
guard window != nil else { return }
let bounds = self.bounds
var flatteningTransform = CGAffineTransform.identity.translatedBy(x: 0, y: bounds.size.height).scaledBy(x: 1, y: 0.001)
let duration: CFTimeInterval = 1
let frames = Int((duration * 60.0).rounded(.awayFromZero))
for (datum, barLayer) in zip(data, barLayers) {
let t = datum / maxDatum
if let path = barLayer.path {
let path0 = path.copy(using: &flatteningTransform)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.duration = 1
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
pathAnimation.fromValue = path0
barLayer.add(pathAnimation, forKey: pathAnimation.keyPath)
let colors = gradient.colors(from: 0, to: t, count: frames).map({ $0.cgColor })
let colorAnimation = CAKeyframeAnimation(keyPath: "fillColor")
colorAnimation.timingFunction = pathAnimation.timingFunction
colorAnimation.duration = duration
colorAnimation.values = colors
barLayer.add(colorAnimation, forKey: colorAnimation.keyPath)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
createOrDestroyBarLayers()
let bounds = self.bounds
let barSpacing = (bounds.size.width - spaceBetweenBars) / CGFloat(data.count)
let barWidth = barSpacing - spaceBetweenBars
for ((offset: i, element: datum), barLayer) in zip(data.enumerated(), barLayers) {
let t = datum / maxDatum
let barHeight = t * bounds.size.height
barLayer.frame = bounds
let rect = CGRect(x: spaceBetweenBars + CGFloat(i) * barSpacing, y: bounds.size.height, width: barWidth, height: -barHeight)
barLayer.path = CGPath(rect: rect, transform: nil)
barLayer.fillColor = gradient.color(at: t).cgColor
}
}
private let gradient = Gradient(startColor: .red, endColor: .green)
private var barLayers = [CAShapeLayer]()
private func createOrDestroyBarLayers() {
while barLayers.count < data.count {
barLayers.append(CAShapeLayer())
layer.addSublayer(barLayers.last!)
}
while barLayers.count > data.count {
barLayers.removeLast().removeFromSuperlayer()
}
}
}
private extension UIColor {
var hsba: [CGFloat] {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha)
return [hue, saturation, brightness, alpha]
}
}
private struct Gradient {
init(startColor: UIColor, endColor: UIColor) {
self.startColor = startColor
self.startHsba = startColor.hsba
self.endColor = endColor
self.endHsba = endColor.hsba
}
let startColor: UIColor
let endColor: UIColor
let startHsba: [CGFloat]
let endHsba: [CGFloat]
func color(at t: CGFloat) -> UIColor {
let out = zip(startHsba, endHsba).map { $0 * (1.0 - t) + $1 * t }
return UIColor(hue: out[0], saturation: out[1], brightness: out[2], alpha: out[3])
}
func colors(from t0: CGFloat, to t1: CGFloat, count: Int) -> [UIColor] {
var colors = [UIColor]()
colors.reserveCapacity(count)
for i in 0 ..< count {
let s = CGFloat(i) / CGFloat(count - 1)
let t = t0 * (1 - s) + t1 * s
colors.append(color(at: t))
}
return colors
}
}
Result:

Computing complementary, triadic, tetradic, and analagous colors

I have created swift functions, where I send color value to and want to return triadic and tetrads values. It sort of works, but I am not happy about the color results. Can anyone help me to fine-tune the formula please?
I was following few sources, but the returned colours were too bright or saturated in comparison to several online web based color schemes. I know it's a matter of preference as well and I kinda like the results of the code below, but in some instances of colors the result of one color returned is way too close to the original one, so it's barely visible. It applies only to a few colors...
I was using the formula from here:
my code:
func getTriadColor(color: UIColor) -> (UIColor, UIColor){
var hue : CGFloat = 0
var saturation : CGFloat = 0
var brightness : CGFloat = 0
var alpha : CGFloat = 0
let triadHue = CGFloat(color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha))
let triadColor1 = UIColor(hue: (triadHue + 0.33) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
let triadColor2 = UIColor(hue: (triadHue + 0.66) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
return (triadColor1, triadColor2)
}
func getTetradColor(color: UIColor) -> (UIColor, UIColor, UIColor){
var hue : CGFloat = 0
var saturation : CGFloat = 0
var brightness : CGFloat = 0
var alpha : CGFloat = 0
let tetradHue = CGFloat(color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha))
let tetradColor1 = UIColor(hue: (tetradHue + 0.25) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
let tetradColor2 = UIColor(hue: (tetradHue + 0.5) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
let tetradColor3 = UIColor(hue: (tetradHue + 0.75) - 1.0, saturation: saturation, brightness: brightness, alpha: alpha)
return (tetradColor1, tetradColor2, tetradColor3)
}
And I also found nice clean code for finding complementary color, which I am very happy about the results
func getComplementColor(color: UIColor) -> UIColor{
let ciColor = CIColor(color: color)
let compRed: CGFloat = 1.0 - ciColor.red
let compGreen: CGFloat = 1.0 - ciColor.green
let compBlue: CGFloat = 1.0 - ciColor.blue
return UIColor(red: compRed, green: compGreen, blue: compBlue, alpha: 1.0)
}
Your screen shot is of this web page. (Wayback Machine link because, six years later, the page has been deleted.) The formulas on that page are incorrect, because they specify the use of the absolute value function instead of the modulo function. That is, for example, your screen shot defines
H1 = |(H0 + 180°) - 360°|
but consider what this gives for the input H0 = 90°:
H1 = |(90° + 180°) - 360°| = |270° - 360°| = |-90°| = 90°
Do you think that the complementary hue of H0 = 90° is H1 = 90°, the same hue?
The correct formula is
H1 = (H0 + 180°) mod 360°
where “mod” is short for “modulo” and means “the remainder after dividing by”. In other words, if the answer would be above 360°, subtract 360°. For H0 = 90°, this gives the correct answer of H1 = 270°.
But you don't even have this problem in your code, because you didn't use the absolute value function (or the modulo function) in your code. Since you're not doing anything to keep your hue values in the range 0…1, your hue values that are less than zero are clipped to zero, and your hue values above one are clipped to one (and both zero and one mean red).
Your getComplementColor is also not at all the standard definition of the “complementary color”.
Here are the correct definitions:
extension UIColor {
var complement: UIColor {
return self.withHueOffset(0.5)
}
var splitComplement0: UIColor {
return self.withHueOffset(150 / 360)
}
var splitComplement1: UIColor {
return self.withHueOffset(210 / 360)
}
var triadic0: UIColor {
return self.withHueOffset(120 / 360)
}
var triadic1: UIColor {
return self.withHueOffset(240 / 360)
}
var tetradic0: UIColor {
return self.withHueOffset(0.25)
}
var tetradic1: UIColor {
return self.complement
}
var tetradic2: UIColor {
return self.withHueOffset(0.75)
}
var analagous0: UIColor {
return self.withHueOffset(-1 / 12)
}
var analagous1: UIColor {
return self.withHueOffset(1 / 12)
}
func withHueOffset(offset: CGFloat) -> UIColor {
var h: CGFloat = 0
var s: CGFloat = 0
var b: CGFloat = 0
var a: CGFloat = 0
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
return UIColor(hue: fmod(h + offset, 1), saturation: s, brightness: b, alpha: a)
}
}
Here are some examples of complementary colors (original on top, complementary beneath):
Here are split complementary colors (original on top):
Here are triadic colors (original on top):
Here are tetradic colors (original on top):
Here are analagous colors (original in the middle):
Here is the playground I used to generate those images:
import XCPlayground
import UIKit
let view = UIView(frame: CGRectMake(0, 0, 320, 480))
view.backgroundColor = [#Color(colorLiteralRed: 0.9607843137254902, green: 0.9607843137254902, blue: 0.9607843137254902, alpha: 1)#]
let vStack = UIStackView(frame: view.bounds)
vStack.autoresizingMask = [ .FlexibleWidth, .FlexibleHeight ]
view.addSubview(vStack)
vStack.axis = .Vertical
vStack.distribution = .FillEqually
vStack.alignment = .Fill
vStack.spacing = 10
typealias ColorTransform = (UIColor) -> UIColor
func tile(color color: UIColor) -> UIView {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = color
return view
}
func strip(transforms: [ColorTransform]) -> UIStackView {
let strip = UIStackView()
strip.translatesAutoresizingMaskIntoConstraints = false
strip.axis = .Vertical
strip.distribution = .FillEqually
strip.alignment = .Fill
strip.spacing = 0
let hStacks = (0 ..< transforms.count).map { (i: Int) -> UIStackView in
let stack = UIStackView()
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .Horizontal
stack.distribution = .FillEqually
stack.alignment = .Fill
stack.spacing = 4
strip.addArrangedSubview(stack)
return stack
}
for h in 0 ..< 10 {
let hue = CGFloat(h) / 10
let color = UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1)
for (i, transform) in transforms.enumerate() {
hStacks[i].addArrangedSubview(tile(color: transform(color)))
}
}
return strip
}
vStack.addArrangedSubview(strip([
{ $0 },
{ $0.complement }]))
vStack.addArrangedSubview(strip([
{ $0 },
{ $0.splitComplement0 },
{ $0.splitComplement1 }]))
vStack.addArrangedSubview(strip([
{ $0 },
{ $0.triadic0 },
{ $0.triadic1 }]))
vStack.addArrangedSubview(strip([
{ $0 },
{ $0.tetradic0 },
{ $0.tetradic1 },
{ $0.tetradic2 }]))
vStack.addArrangedSubview(strip([
{ $0.analagous0 },
{ $0 },
{ $0.analagous1 }]))
XCPlaygroundPage.currentPage.liveView = view