Swift dynamic color change from a range of Colors - ios

I'm working on a donut-chart that needs to be able to support a huge amount of sections.
For this, each section of course needs it's own color. For this I need a way to dynamically make a new color for each section. This is easy. But it also needs to take a startColor and an EndColor, so it can use e.g. only blue colors.
I did manage to this using the following code:
var rStartValue: CGFloat = 16
var gStartValue: CGFloat = 177
var bStartValue: CGFloat = 216
var rEndValue: CGFloat = 30
var gEndValue: CGFloat = 30
var bEndValue: CGFloat = 38
for percentFill in percentFills {
let progressLine = CAShapeLayer()
if rStartValue < rEndValue {
rStartValue += 1
} else if rStartValue > rEndValue {
rStartValue -= 1
}
if gStartValue < gEndValue {
gStartValue += 1
} else if gStartValue > gEndValue {
gStartValue -= 1
}
if bStartValue < bEndValue {
bStartValue += 1
} else if bStartValue > bEndValue {
bStartValue -= 1
}
}
However. It's not the desired result yet.
I need it to step appropiately. Right now it just steps by 1 value in each loop. Which is okay if there's a ton of sections, but sometimes there's only maybe 17 or so.
So I need it to calculate how much to step based on the amount of sections.
After messing around with various versions of percentFills/various related variables.
I ended up here, hoping someone can help me figure out how to make these colours step correctly.
Here's an example of what I've been trying:
var rJump = CGFloat(percentFills.count)/(rStartValue-rEndValue)
var gJump = CGFloat(percentFills.count)/(gStartValue-gEndValue)
var bJump = CGFloat(percentFills.count)/(bStartValue-bEndValue)
Then replacing 1 in the loop with rJump, gJump or bJump. But Every variable I have tried to divide with the percentFills.count (total number of sections) doesn't provide the desired result.
The startValue should always be the first color, and the endValue should always be the last color. Then step from start to end, as evenly as possible.
Any help getting past this barrier would be greatly appreciated!

Instead of using RGB, you will probably be better off using HSL where the H is hue which is essentially the color.
You can use:
Swift:
init(hue hue: CGFloat, saturation saturation: CGFloat, brightness brightness: CGFloat, alpha alpha: CGFloat)
ObjectiveC:
+ (UIColor *)colorWithHue:(CGFloat)hue saturation:(CGFloat)saturation brightness:(CGFloat)brightness alpha:(CGFloat)alpha
all values are in the range 0.0 - 1.0.
See Apple Docs.
HSL is close to the way we experience color, RGB is the way most hardware displays color.
Here is an example from Interface Builder, the values are 0 - 360:

I figured it out.
It was a "little" problem with my math. Instead of doing:
var rJump = CGFloat(percentFills.count)/(rStartValue-rEndValue)
var gJump = CGFloat(percentFills.count)/(gStartValue-gEndValue)
var bJump = CGFloat(percentFills.count)/(bStartValue-bEndValue)
I simply needed to change it to this:
var rJump = (rStartValue-rEndValue)/CGFloat(percentFills.count)
var gJump = (gStartValue-gEndValue)/CGFloat(percentFills.count)
var bJump = (bStartValue-bEndValue)/CGFloat(percentFills.count)
That takes the difference in values. E.g. if you are starting red color is 100, and your end red color is 200, and you have 10 sections (or steps), it would look like this:
100-50 = 50 (This is the difference between the two)
50/10 (Then 50 is divided by the amount of sections (steps). = 5
Where 5 is the amount the red color needs to change for each step:
if rStartValue < rEndValue {
rStartValue += 5
} else if rStartValue > rEndValue {
rStartValue -= 5
}
Of course variable rJump should be used instead of the number.
Hoped that helped someone else if anyone runs into a similar issue.
Also Check out Zaph's answer. Using HBS as he mentions, you might only have to change 1 value and not 3, for each loop.

Related

Programmatically Lighten or Darken a hex color in dart

I am trying to convert this hash color code #159424 (GREEN-COLOR) to more darken and lighten programmatically. How to do this please help?
make green color darker
toDarkColor(String hashColor){
// how to convert that hash string to make green color darker?
}
make green color lighter
toLightColor(String hashColor){
// how to convert that hash string to make green color lighter?
}
For people who want to darken or lighten Color instead of hex string
// ranges from 0.0 to 1.0
Color darken(Color color, [double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(color);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
Color lighten(Color color, [double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(color);
final hslLight = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
return hslLight.toColor();
}
// usage
final lightRed = lighten(Colors.red);
final darkBlue = darken(Colors.blue, .3);
Live Demo
Color accurate solution with no plugin
The accepted answer changes the tint of colors when darkening (the tint is more saturated). Also its lightening function produces pure white with an amount of 0.3 for some colors although white should only be reached with an amount of 1.
The two following methods produce shades of the base color that seem 'darker' or 'lighter' without changing the tint.
import 'package:flutter/material.dart';
/// Darken a color by [percent] amount (100 = black)
// ........................................................
Color darken(Color c, [int percent = 10]) {
assert(1 <= percent && percent <= 100);
var f = 1 - percent / 100;
return Color.fromARGB(
c.alpha,
(c.red * f).round(),
(c.green * f).round(),
(c.blue * f).round()
);
}
/// Lighten a color by [percent] amount (100 = white)
// ........................................................
Color lighten(Color c, [int percent = 10]) {
assert(1 <= percent && percent <= 100);
var p = percent / 100;
return Color.fromARGB(
c.alpha,
c.red + ((255 - c.red) * p).round(),
c.green + ((255 - c.green) * p).round(),
c.blue + ((255 - c.blue) * p).round()
);
}
Example: darken a color by 15%.
final Color darkerGreen = darken(Color(0xFF159424), 15);
If starting from a Hex String value as OP asked, use J.M. Taylor' solution:
Color hexToColor(String code) {
return Color(int.parse(code.substring(0, 6), radix: 16) + 0xFF000000);
}
final Color darkerGreen = darken(hexToColor('#159424'));
Note: it's for Flutter projects as it uses the material's Color class.
My solution based on https://stackoverflow.com/a/58604669/7479173
extension ColorBrightness on Color {
Color darken([double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(this);
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
return hslDark.toColor();
}
Color lighten([double amount = .1]) {
assert(amount >= 0 && amount <= 1);
final hsl = HSLColor.fromColor(this);
final hslLight =
hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
return hslLight.toColor();
}
}
with this one can simply:
Colors.red.darken()
Colors.red.lighten()
Colors.red.lighten(0.1)
this works on any colors as long as you import the extension.
You can use tinycolor package:
TinyColor.fromString("#159424").darken(10).color
Edit:
You can convert Color back to hex string like this:
String toHex(Color color) {
return "#${color.red.toRadixString(16).padLeft(2, "0")}"
"${color.green.toRadixString(16).padLeft(2, "0")}"
"${color.blue.toRadixString(16).padLeft(2, "0")}";
}
or if you want opacity/alpha:
String toHex(Color color) {
return "#${color.alpha.toRadixString(16).padLeft(2, "0")}"
"${color.red.toRadixString(16).padLeft(2, "0")}"
"${color.green.toRadixString(16).padLeft(2, "0")}"
"${color.blue.toRadixString(16).padLeft(2, "0")}";
}
I used withLightness method of HSLColor to lighten the color.
HSLColor.fromColor(Colors.red).withLightness(0.95).toColor()
Since some parts of TinyColor seem broken, and I only really needed lighten and darken, NearHuscarl's answer was perfect for me.
However, it was missing one part that was necessary to completely answer the original question, which was converting hash color code (declared as a String) to Color.
To do that, you can use this:
Color hexToColor(String code) {
return Color(int.parse(code.substring(0, 6), radix: 16) + 0xFF000000);
}
The above is not my code, but something I learned from a tutorial here.
Then just combine that with NearHuscarl's code to get the desired effect:
final Color darkerGreen = darken(hexToColor('#159424'));
One liner with built-in method:
Color.lerp(myColor, Colors.white, 0.4) // 0 = keep as is, 1 = white

Convert circular, continuous UISlider value to a counter

In my app, I have created a circular, continuous slidable CircularSlider (this one). It has a minimumValue of 1.0 and a maximumValue of 10. Now, I'd like my users to be able to just keep sliding in circles to increase an NSInteger used somewhere in the app.
I tried doing that by writing the code below.
- (void)sliderChanged:(UICircularSlider *)slider {
int val = (int)slider.value;
slider.value = val;
int delta = val - self.oldValue;
if (self.oldValue == self.circularSlider.maximumValue && val == 0) {
delta = 1;
}
self.number += delta;
self.oldValue = val;
}
This works, but really sketch. Sometimes the value will drop by 10, caused by the slider giving me value of 10 and right afterwards a value of 0. It also doesn't work if the users starts scrubbing backwards and numbers start decreasing. I was wondering if there's a better way to achieve the same thing. Any ideas?
I think a better approach is to first figure out what direction the user moved. This can be done by checking which direction has the closest distance between the new and the old position. Care must be taken to check for passing the border. Then it is a simple matter of adjusting the delta depending on direction.
My solution code assumes the the slider starts at zero, which I would recommend you to use instead of 1, since calculations are easier. If you really want to start at 1 it can be adjusted. I also have defined the maximum value as a constant SLIDER_MAX_VALUE, which you could change to a variable instead. Finally, I changed self.number to a CGFloat, so do the cast when using the number instead. This is important, otherwise you get rounding errors when sliding. If you really want an integer, use two variables, and assign the integer variable from the float.
#define SLIDER_MAX_VAL 10
- (void)sliderChanged:(UICircularSlider *)slider {
CGFloat delta = slider.value - self.oldValue;
if ((delta > 0 && delta < SLIDER_MAX_VAL / 2) ||
(delta < 0 && delta < -SLIDER_MAX_VAL / 2)) {
// Moving forward
if (delta < 0)
delta += SLIDER_MAX_VAL;
} else {
// Moving backward
if (delta > 0)
delta -= SLIDER_MAX_VAL;
}
self.number += delta; // Change to CGFloat
self.oldValue = slider.value;
}

Randomly coloring sprites, but needs restrictions

I have a very, very specific issue.
I have six rectangles.
[] = a rectangle.
They have a format like this on the screen: [] [][] [][] [].
They all need to be a random color between red, green, and blue.
var colorize1 = SKAction.colorizeWithColor(.redColor(), colorBlendFactor: 1.0, duration: 0.1)
var colorize2 = SKAction.colorizeWithColor(.greenColor(), colorBlendFactor: 1.0, duration: 0.1)
var colorize3 = SKAction.colorizeWithColor(.blueColor(), colorBlendFactor: 1.0, duration: 0.1)
var actions = [colorize1, colorize2, colorize3]
var randomIndex = Int(arc4random_uniform(3))
var action = actions[randomIndex]
greenWall1.runAction(action)
greenWall2.runAction(action)
This code does that. BUT, I can't have the same color adjacent to the next pair.
[Wall1] [Wall2][Wall3] [Wall4][Wall5] [Wall6]
1 and 2 are pairs. 3 and 4 are pairs, etc.
Wall 1 and Wall 2 need to be the same color since they're pairs. 3 and 4 need to be a different random color. 5 and 6 need to be a different than WallPair1 and WallPair2.
Right now, I get up to three of the same colors on each pair. Maybe all reds.
I need to write code that says "if pair 1 (wall1 and wall2) are red for example, then the second pair(wall3 and wall4), and the third pair(wall5 and wall6), can't be red.
I can't figure this out.
Thanks so much for your help.
You make a bucket and pull your colours from that.
When the colorBucket becomes empty, refill it.
For example.
var colorBucket = [UIColor]()
func randomColor() -> UIColor {
if colorBucket.isEmpty {
fillBucket()
}
let randomIndex = Int(arc4random_uniform(UInt32(colorBucket.count)))
let randomColor = colorBucket[randomIndex]
colorBucket.removeAtIndex(randomIndex)
return randomColor
}
func fillBucket() {
colorBucket = [UIColor.redColor(), UIColor.greenColor(), UIColor.blueColor()]
}
This is the same method Tetris uses to ensure players don't go too long without seeing every shape.

UIBezierPath Percent of Length at Point

I'm building an app that features some graphical manipulation. I'm storing shapes as UIBezierPaths, and I want to allow users to touch points along the line to create saved locations. Using the wonderful answer to this question, and more specifically, this project, I'm able to place a point on a line knowing the percentage of its length the point rests on. This is half of my problem.
I want a way to take a point on a path, and derive the percent of its length.
My math-fu is extremely weak. I've studied bezier curves but I simply don't have the math to understand it.
I would humbly submit that "go back and learn geometry and trigonometry" is a correct answer, but sadly one I don't have time for at present. What I need is a way to fill in this method:
- (CGFloat)percentOfLengthAtPoint:(CGPoint)point onPath:(UIBezierPath*)path
Any help appreciated!
I have working code that solves my problem. I'm not particularly proud of it; the overall technique is essentially a brute-force attack on a UIBezierPath, which is kind of funny if you think about it. (Please don't think about it).
As I mentioned, I have access to a method that allows me to get a point from a given percentage of a line. I have taken advantage of that power to find the closest percentage to the given point by running through 1000 percentage values. To wit:
Start with a CGPoint that represents where on the line the user touched.
let pointA = // the incoming CGPoint
Run through the 0-1 range in the thousands. This is the set of percentages we're going to brute-force and see if we have a match. For each, we run pointAtPercentOfLength, from the linked project above.
var pointArray:[[String:Any]] = []
for (var i:Int = 0; i <= 1000; i++) {
let value = CGFloat(round((CGFloat(i) / CGFloat(1000)) * 1000) / 1000)
let testPoint = path.pointAtPercentOfLength(value)
let pointB = CGPoint(x: floor(testPoint.x), y: floor(testPoint.y))
pointArray.append(["point" : pointB, "percent" : value])
}
That was the hard part. Now we take the returning values and calculate the distance between each point and the touched point. Closest one is our winner.
// sort the damned array by distance so we find the closest
var distanceArray:[[String:Any]] = []
for point in pointArray {
distanceArray.append([
"distance" : self.distanceFrom(point["point"] as! CGPoint, point2: pointA),
"point" : point["point"],
"percent" : point["percent"] as! CGFloat
])
}
Here's the sorting function if you're interested:
func distanceFrom(point1:CGPoint, point2:CGPoint) -> CGFloat {
let xDist = (point2.x - point1.x);
let yDist = (point2.y - point1.y);
return sqrt((xDist * xDist) + (yDist * yDist));
}
Finally, I sort the array by the distance of the values, and pick out the winner as our closest percent.
let ordered = distanceArray.sort { return CGFloat($0["distance"] as! CGFloat) < CGFloat($1["distance"] as! CGFloat) }
ordered is a little dictionary that includes percent, the correct value for a percentage of a line's length.
This is not pretty code, I know. I know. But it gets the job done and doesn't appear to be computationally expensive.
As a postscript, I should point to what appears to be a proper resource for doing this. During my research I read this beautiful article by David Rönnqvist, which included an equation for calculating the percentage distance along a path:
start⋅(1-t)3 + 3⋅c1⋅t(1-t)2 + 3⋅c2⋅t2(1-t) + end⋅t3
I was just about to try implementing that before my final solution occurred to me. Math, man. I can't even brain it. But if you're more ambitious than I, and wish to override my 30 lines of code with a five-line alternative, everyone would appreciate it!
I think your approach is sound, but you could do this far more efficiently.
Instead of creating an two arrays of dicts (with a thousand elements each) and then sorting the array - just use a while loop to move from 0.0 to 1.0, calculate the distance to the touch point and keep track of the minimum distance.
For example:
var t:CGFloat = 0.0
let step:CGFloat = 0.001
var minDistance:CGFloat = -1.0
var minPoint:CGPoint = CGPointZero
var minT:CGFloat = -1;
while (t<1.0) {
let point = pointAtPercentOfLength(t)
let distance:CGFloat = self.distanceFrom(point, point2: pointA)
if (minDistance == -1.0 || distance < minDistance) {
minDistance = distance
minPoint = point
minT = t
}
t += step
}
print("minDistance: \(minDistance) minPoint: \(minPoint.x) \(minPoint.y) t\(minT)\n")

What is wrong with this "Do.. While" loop in Swift?

I am trying to write this in Swift (I am in step 54). In a UICollectionViewLayout class I have a function setup function
func setup() {
var percentage = 0.0
for i in 0...RotationCount - 1 {
var newPercentage = 0.0
do {
newPercentage = Double((arc4random() % 220) - 110) * 0.0001
println(newPercentage)
} while (fabs(percentage - newPercentage) < 0.006)
percentage = newPercentage
var angle = 2 * M_PI * (1 + percentage)
var transform = CATransform3DMakeRotation(CGFloat(angle), 0, 0, 1)
rotations.append(transform)
}
}
Here is how the setup function is described in the tutorial
First we create a temporary mutable array that we add objects to. Then
we run through our loop, creating a rotation each time. We create a
random percentage between -1.1% and 1.1% and then use that to create a
tweaked CATransform3D. I geeked out a bit and added some logic to
ensure that the percentage of rotation we randomly generate is a least
0.6% different than the one generated beforehand. This ensures that photos in a stack don't have the misfortune of all being rotated the
same way. Once we have our transform, we add it to the temporary array
by wrapping it in an NSValue and then rinse and repeat. After all 32
rotations are added we set our private array property. Now we just
need to put it to use.
When I run the app, I get a run time error in the while (fabs(percentage - newPercentage) < 0.006) line.
the setup function is called in prepareLayout()
override func prepareLayout() {
super.prepareLayout()
setup()
...
}
Without the do..while loop, the app runs fine. So I am wondering, why?
Turns out I had to be more type safe
newPercentage = Double(Int((arc4random() % 220)) - 110) * 0.0001
This must be a Swift bug. That code should NOT crash at runtime. It should either give a compiler error on the newPercentage = expression or it should correctly promote the types as C does.

Resources