SceneKit - How to Add Animations to Change SCNNode's Color? - swift

I would like to know how I can animate an SCNNode's color using Swift.
For example: I would like the node to constantly be changing color or I would like the node to fade from black to blue.
Do I use the SCNAction fadeIn or fadeOut?

You can create a custom action.
If you have a red sphere in your scene
let sphereNode = scene.rootNode.childNode(withName: "sphere", recursively: false)!
sphereNode.geometry!.firstMaterial!.diffuse.contents = UIColor.red
This is how you build the custom action
let changeColor = SCNAction.customAction(duration: 10) { (node, elapsedTime) -> () in
let percentage = elapsedTime / 5
let color = UIColor(red: 1 - percentage, green: percentage, blue: 0, alpha: 1)
node.geometry!.firstMaterial!.diffuse.contents = color
}
Finally you just need to run the action on the sphere
sphereNode.runAction(changeColor)
Result

Got idea from #Luca Angeletti, I write the code so we can animate between any colors, include their alphas:
func aniColor(from: UIColor, to: UIColor, percentage: CGFloat) -> UIColor {
let fromComponents = from.cgColor.components!
let toComponents = to.cgColor.components!
let color = UIColor(red: fromComponents[0] + (toComponents[0] - fromComponents[0]) * percentage,
green: fromComponents[1] + (toComponents[1] - fromComponents[1]) * percentage,
blue: fromComponents[2] + (toComponents[2] - fromComponents[2]) * percentage,
alpha: fromComponents[3] + (toComponents[3] - fromComponents[3]) * percentage)
return color
}
Use:
let oldColor = UIColor.red
let newColor = UIColor(colorLiteralRed: 0.0, green: 0.0, blue: 1.0, alpha: 0.5)
let duration: TimeInterval = 1
let act0 = SCNAction.customAction(duration: duration, action: { (node, elapsedTime) in
let percentage = elapsedTime / CGFloat(duration)
node.geometry?.firstMaterial?.diffuse.contents = self.aniColor(from: newColor, to: oldColor, percentage: percentage)
})
let act1 = SCNAction.customAction(duration: duration, action: { (node, elapsedTime) in
let percentage = elapsedTime / CGFloat(duration)
node.geometry?.firstMaterial?.diffuse.contents = self.aniColor(from: oldColor, to: newColor, percentage: percentage)
})
let act = SCNAction.repeatForever(SCNAction.sequence([act0, act1]))
node.runAction(act)

I am using this function;
You just give name of material, how long animation will take and current color and
the which color will be reached.
You give color as arrays (red, green, blue and alpha);
let startColor: [CGFloat] = [0.5, 0.5, 1, 1]
let targetColor: [CGFloat] = [1, 1, 1, 1]
This is how you call function;
changeColorWithAnimation(duration : 5,
materialName: myCubeMaterial,
start : startColor,
end : targetColor)
func chageColorWithAnimation(duration: CGFloat,
materialName: SCNMaterial,
start: [CGFloat],
end: [CGFloat]){
let rs = (end[0] - start[0]) / duration
let gs = (end[1] - start[1]) / duration
let bs = (end[2] - start[2]) / duration
let alphas = (end[3] - start[3]) / duration
let changeColor = SCNAction.customAction(duration: TimeInterval(duration)) { (node, elapsedTime) -> () in
let red = start[0] + rs * elapsedTime
let green = start[1] + gs * elapsedTime
let blue = start[2] + bs * elapsedTime
let alpha = start[3] + alphas * elapsedTime
materialName.diffuse.contents = UIColor(displayP3Red:red,
green: green,
blue: blue,
alpha: alpha)
}
rootNode.runAction(changeColor)
}

Related

Giving a CAShapeLayer a 3d effect

I am playing around with countdown timer in swift. What I want to give the countdown circle a 3D shadow of effect.
the code below will operate the countdown time perfectly but i guess its more of the visuals i am trying to edit with it. is there any way to make the countdown circle larger and while giving it a 3d effect. If you run the code you will see it is just a 2d type of fill. I have been playing around with overlapping circles with a different color and alpha like a dark color to try and make it look more 3d, but Its definitely not the most efficient because it involves drawing multiple circles at once. Is there a way to get a similar 3d effect like the image below without having to redraw multiple overlapping circles. the code below is for the basic 2d flat version of the counter.
//for countdown timer: ----------------
let timeLeftShapeLayer = CAShapeLayer()
let bgShapeLayer = CAShapeLayer()
var timeLeft: TimeInterval = 10
var endTime: Date?
var timeLabel = UILabel()
var timer = Timer()
// here you create your basic animation object to animate the strokeEnd
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
func drawBgShape() {
bgShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
bgShapeLayer.strokeColor = UIColor.white.cgColor
bgShapeLayer.fillColor = UIColor.clear.cgColor
bgShapeLayer.lineWidth = 15
view.layer.addSublayer(bgShapeLayer)
}
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(arcCenter: CGPoint(x: view.frame.midX , y: view.frame.midY), radius:
100, startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 15
view.layer.addSublayer(timeLeftShapeLayer)
}
func addTimeLabel() {
timeLabel = UILabel(frame: CGRect(x: view.frame.midX-50 ,y: view.frame.midY/3, width: 100, height: 50))
timeLabel.textAlignment = .center
timeLabel.text = timeLeft.time
view.addSubview(timeLabel)
}
#objc func updateTime() {
if timeLeft > 0 {
timeLeft = endTime?.timeIntervalSinceNow ?? 0
timeLabel.text = timeLeft.time
} else {
timeLabel.text = "00:00"
timer.invalidate()
}
}
//-------------timer finish------------(extension for timer at bottom of file------
Extensions:
//extensions for timer
extension TimeInterval {
var time: String {
return String(format:"%02d:%02d", Int(self/60), Int(ceil(truncatingRemainder(dividingBy: 60))) )
}
}
extension Int {
var degreesToRadians : CGFloat {
return CGFloat(self) * .pi / 180
}
}
ViewDidload:
//for countdown timer: -------------
view.backgroundColor = UIColor(white: 0.94, alpha: 1.0)
drawBgShape()
drawTimeLeftShape()
addTimeLabel()
// here you define the fromValue, toValue and duration of your animation
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = timeLeft
// add the animation to your timeLeftShapeLayer
timeLeftShapeLayer.add(strokeIt, forKey: nil)
// define the future end time by adding the timeLeft to now Date()
endTime = Date().addingTimeInterval(timeLeft)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
To obtain 3D effects, you usually work with color gradients. In your use case, you would work with a radial CAGradientLayer. You have to mask this layer to see only the area you want to be visible. The path to be filled consists of the area of the outer and the inner circle.
This fill path can be created as follows:
let path = UIBezierPath(arcCenter: centerPoint, radius: outerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
let inner = UIBezierPath(arcCenter: centerPoint, radius: outerRadius - thickness, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path.append(inner.reversing())
For the gradients, you can use the locations parameter to specify an array of NSNumber objects that define the location of the gradient stops. The values must be in the range [0,1]. The corresponding associated colors of type CGColor are set in colors property.
In a simple case you could define something like:
gradient.locations = [0, //0
NSNumber(value: innerRadius / outerRadius), //1
NSNumber(value: middle / outerRadius), //2
1] //3
let colors = [color, //0
color, //1
color.lighter(), //2
color, //3
]
gradient.colors = colors.map { $0.cgColor }
However, the desired 3D appearance will be visible only after applying a mask with the corresponding path, see the right part of the figure:
Animation
It is easy to see that you can use one CAGradientLayer for the background and one for the foreground. The question then naturally arises, how can we animate the fill process with the foreground gradient?
This can be achieved by placing the foreground gradient over the background gradient and using a CAShapeLayer as a mask for the foreground gradient. In doing so, the animation is done similarly to the example in your question using the strokeEnd property. Since it is a mask, the foreground gradient becomes visible gradually.
Gradients
Gradients can contain several areas. 3D effects are usually achieved by combining slightly lighter or darker gradations of similar colors. For the demo example, I used this nice, minimally modified answer to get lighter or darker color variants.
Demo
Using the above points, this may look like the following:
The colors, distances and the gradients depend of course strongly on the requirements, this serves only as an example, how one could make such a thing. For the foreground gradient, two similar but different colors were chosen for the shadow area (inner circular area) and the outer circular area, which is in the light.
Self-Contained Complete Example
CircleProgressView.swift
import UIKit
class CircleProgressView: UIView {
private let backgroundGradient = CAGradientLayer()
private let foregroundGradient = CAGradientLayer()
private let timeLeftShapeLayer = CAShapeLayer()
private let backgroundMask = CAShapeLayer()
private let thickness: CGFloat
private let innerBackgroundColor: UIColor
private let outerBackgroundColor: UIColor
private let innerForegroundColor: UIColor
private let outerForegroundColor: UIColor
init(_ thickness: CGFloat,
_ innerBackgroundColor: UIColor,
_ outerBackgroundColor: UIColor,
_ innerForegroundColor: UIColor,
_ outerForegroundColor: UIColor) {
self.thickness = thickness
self.innerBackgroundColor = innerBackgroundColor
self.outerBackgroundColor = outerBackgroundColor
self.innerForegroundColor = innerForegroundColor
self.outerForegroundColor = outerForegroundColor
super.init(frame: .zero)
backgroundGradient.type = .radial
layer.addSublayer(backgroundGradient)
foregroundGradient.type = .radial
layer.addSublayer(foregroundGradient)
timeLeftShapeLayer.strokeColor = UIColor.white.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = thickness
layer.addSublayer(timeLeftShapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func circle(_ gradient: CAGradientLayer,
_ path: UIBezierPath,
_ outerRadius: CGFloat,
_ innerColor: UIColor,
_ outerColor: UIColor) {
let innerRadius = outerRadius - thickness
let middle = outerRadius - thickness / 2
let slice: CGFloat = thickness / 16
gradient.frame = bounds
gradient.locations = [0, //0
NSNumber(value: (innerRadius) / outerRadius), //1
NSNumber(value: (middle - slice) / outerRadius), //2
NSNumber(value: (middle) / outerRadius), //3
NSNumber(value: (middle + slice) / outerRadius), //4
1] //5
let colors = [innerColor, //0
innerColor, //1
innerColor.darker(), //2
outerColor, //3
outerColor.lighter(), //4
outerColor //5
]
gradient.colors = colors.map { $0.cgColor }
gradient.bounds = path.bounds
gradient.startPoint = CGPoint(x: 0.5, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 1)
}
override func layoutSubviews() {
super.layoutSubviews()
let outerRadius: CGFloat = min(bounds.width, bounds.height) / 2.0
let centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let path = UIBezierPath(arcCenter: centerPoint, radius: outerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
let inner = UIBezierPath(arcCenter: centerPoint, radius: outerRadius - thickness, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path.append(inner.reversing())
circle(backgroundGradient, path, outerRadius, innerBackgroundColor, outerBackgroundColor)
backgroundMask.frame = bounds
backgroundMask.path = path.cgPath
backgroundMask.lineWidth = 0
backgroundGradient.mask = backgroundMask
circle(foregroundGradient, path, outerRadius, innerForegroundColor, outerForegroundColor)
let middlePath = UIBezierPath(arcCenter: centerPoint, radius: outerRadius - thickness / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
middlePath.lineWidth = thickness
timeLeftShapeLayer.path = middlePath.cgPath
foregroundGradient.mask = timeLeftShapeLayer
timeLeftShapeLayer.strokeEnd = 0
}
func startAnimation() {
timeLeftShapeLayer.removeAllAnimations()
timeLeftShapeLayer.strokeEnd = 1
DispatchQueue.main.async {
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = 5
self.timeLeftShapeLayer.add(strokeIt, forKey: nil)
}
}
}
UIColor+Brightness.swift
Only for the sake of completeness, please note that the original can be found at https://stackoverflow.com/a/31466450.
import UIKit
extension UIColor {
func lighter(amount : CGFloat = 0.15) -> UIColor {
return hueColorWithBrightness(amount: 1 + amount)
}
func darker(amount : CGFloat = 0.15) -> UIColor {
return hueColorWithBrightness(amount: 1 - amount)
}
private func hueColorWithBrightness(amount: CGFloat) -> UIColor {
var hue: CGFloat = 0
var saturation: CGFloat = 0
var brightness: CGFloat = 0
var alpha: CGFloat = 0
if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
return UIColor( hue: hue,
saturation: saturation,
brightness: brightness * amount,
alpha: alpha )
} else {
return self
}
}
}
ViewController.swift
The call is rather unsurprising and should look something like this:
import UIKit
class ViewController: UIViewController {
private var circleProgressView: CircleProgressView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0xBB / 0xFF, green: 0xBB / 0xFF, blue: 0xBB / 0xFF, alpha: 1)
let innerBackgroundColor = UIColor(red: 0x65 / 0xFF, green: 0x79 / 0xFF, blue: 0x85 / 0xFF, alpha: 1)
let outerBackgroundColor = innerBackgroundColor
let innerForegroundColor = UIColor(red: 0xCF / 0xFF, green: 0xC9 / 0xFF, blue: 0x22 / 0xFF, alpha: 1)
let outerForegroundColor = UIColor(red: 0xF3 / 0xFF, green: 0xCA / 0xFF, blue: 0x46 / 0xFF, alpha: 1)
let progressView = CircleProgressView(24, innerBackgroundColor, outerBackgroundColor, innerForegroundColor, outerForegroundColor)
circleProgressView = progressView
progressView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(progressView)
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
button.setTitle("Start", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(onStart), for: .touchUpInside)
let margin: CGFloat = 24
NSLayoutConstraint.activate([
progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: margin),
progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
button.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: margin),
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: margin),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -margin),
button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -margin),
])
}
#objc func onStart() {
circleProgressView?.startAnimation()
}
}

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:

Swift Generate A Random Color On A Colorwheel

I'm using the SwiftHSVColorPicker framework and needed to generate a random color on the color wheel.My current way of doing works but something that brightness is off.Here is my code
func generateRandomColor() -> UIColor {
let lowerx : UInt32 = UInt32(0.0)
let upperx : UInt32 = 707
let randomNumberx = arc4random_uniform(upperx - lowerx) + lowerx
let lowery : UInt32 = UInt32(0.0)
let uppery : UInt32 = 707
let randomNumbery = arc4random_uniform(upperx - lowerx) + lowerx
let c = Colorwheel.colorWheel.hueSaturationAtPoint(CGPoint(x: Double(randomNumberx), y: Double(randomNumbery)))
let brightness = 1.0
return UIColor(hue: c.hue, saturation: c.saturation, brightness: CGFloat(brightness), alpha: 1.0)
}
Why don't you use something like
func getRandomColor() -> UIColor{
let randomRed:CGFloat = CGFloat(arc4random()) / CGFloat(UInt32.max)
let randomGreen:CGFloat = CGFloat(arc4random()) / CGFloat(UInt32.max)
let randomBlue:CGFloat = CGFloat(arc4random()) / CGFloat(UInt32.max)
return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
}
EDIT:
Try this, In this hue,brightness is also there
func generateRandomColor() -> UIColor {
let hue : CGFloat = CGFloat(arc4random() % 256) / 256 // use 256 to get full range from 0.0 to 1.0
let saturation : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from white
let brightness : CGFloat = CGFloat(arc4random() % 128) / 256 + 0.5 // from 0.5 to 1.0 to stay away from black
return UIColor(hue: hue, saturation: saturation, brightness: brightness, alpha: 1)
}
SwiftHSVColorPicker results