user626776
user626776

Reputation:

How to generate color palette that's gradually changing color

I am making a game similar to the stack game on iOS (https://apps.apple.com/us/app/stack/id1080487957).

In the screenshot, each box is in a color slightly different from the box below. The background is also changing, in case of conflict of background and boxes.

I am wondering how to generate this sequence of color palette for the boxes, and a corresponding background color that helps to make the box stand out.

What I have tried is very naive:

UIColor(
  red: ((0 + boxCount) % 255)/255, 
  green: ((100 + boxCount) % 255)/255, 
  blue: ((200 + boxCount) % 255)/255)
)

This doesn't look good, and I think the reasons are:

  1. unable to cover lots of good colors (e.g. having the same red & green, but different blue value)
  2. I don't know how to get the background color based on the box color under this naive algorithm.

But what's good about this is that it avoids grayscale color such as completely black, so that the boxes are always colorful.

Is there any better way for this problem?

EDIT:

To clarify, the color is not just lighter or darker. It can actually change color (e.g. the first screenshot changes from sky blue to greenish-yellow).

Upvotes: 0

Views: 527

Answers (1)

MadProgrammer
MadProgrammer

Reputation: 347314

A very long time ago, in a different programming language, I spent way too much time trying to come up with a blending algorithm which "worked" the way I expected it to.

Sooo, I spent a lot of time fiddling with it, until I came up with an approach which worked for me. So I've dragged the concept over to UIKit, for example.

Sample output

Now, this is a little basic. I'd considered writing an extension to make it easier to get the RGB colors, in a more "structured" way, as well as adding the blending algorithm as an extensions to UIColor, but you get the basic idea.

I've also included a basic darken and lighten algorithm, based on the same concept as the blend. In fact, if you have the color and know the number of samples (ie total number of bricks), you could easily just darken or lighten the base color by that factor, but where's the fun in that :P

Put the code in a playground and see what you get

import UIKit

public extension UIColor {
    
    func darken(by: Double) -> UIColor {
        let rgb = cgColor.components
        
        let red = max(0, (rgb?[0])! - CGFloat(1.0 * by))
        let green = max(0, (rgb?[1])! - CGFloat(1.0 * by))
        let blue = max(0, (rgb?[2])! - CGFloat(1.0 * by))
        let alpha = cgColor.alpha
        
        return UIColor(red: red, green: green, blue: blue, alpha: alpha)
    }
    
    func brighten(by: Double) -> UIColor {
        let rgb = cgColor.components
        
        let red = min(1.0, (rgb?[0])! + CGFloat(1.0 * by))
        let green = min(1.0, (rgb?[1])! + CGFloat(1.0 * by))
        let blue = min(1.0, (rgb?[2])! + CGFloat(1.0 * by))
        let alpha = cgColor.alpha
        
        return UIColor(red: red, green: green, blue: blue, alpha: alpha)
    }
}

func blend(from lhs: UIColor, to rhs: UIColor, by weight: Double) -> UIColor {
    
    var lhsRed: CGFloat = 0
    var lhsGreen: CGFloat = 0
    var lhsBlue: CGFloat = 0
    var lhsAlpha: CGFloat = 0

    lhs.getRed(&lhsRed, green: &lhsGreen, blue: &lhsBlue, alpha: &lhsAlpha)

    var rhsRed: CGFloat = 0
    var rhsGreen: CGFloat = 0
    var rhsBlue: CGFloat = 0
    var rhsAlpha: CGFloat = 0

    rhs.getRed(&rhsRed, green: &rhsGreen, blue: &rhsBlue, alpha: &rhsAlpha)
    
    let inverseWeight = 1.0 - weight
    
    let red = min(1.0, max(0.0, lhsRed * weight + rhsRed * inverseWeight))
    let green = min(1.0, max(0.0, lhsGreen * weight + rhsGreen * inverseWeight))
    let blue = min(1.0, max(0.0, lhsBlue * weight + rhsBlue * inverseWeight))
    let alpha = min(1.0, max(0.0, lhsAlpha * weight + rhsAlpha * inverseWeight))
    
    return UIColor(red: red, green: green, blue: blue, alpha: alpha)
}

let from = UIColor.red.darken(by: 0.5)
let to = UIColor.red.brighten(by: 0.5)
blend(from: from, to: to, by: 0.1)
blend(from: from, to: to, by: 0.2)
blend(from: from, to: to, by: 0.3)
blend(from: from, to: to, by: 0.4)
blend(from: from, to: to, by: 0.5)
blend(from: from, to: to, by: 0.6)
blend(from: from, to: to, by: 0.7)
blend(from: from, to: to, by: 0.8)
blend(from: from, to: to, by: 0.9)

For example, in the first screenshot in the link, it changes from sky blue on the bottom to greenish-yellow on the top)

Sure, so change the from color to .blue and the end color to .yellow or .green and see what you get

Blending

I'd also suggesting looking a things like SceneKit gradients as this might change your approach to the problem

Any idea how to make it an infinite sequence? (given input of boxCount which is number of boxes already placed in the tower). I am thinking about having an "anchorColors" array, then gradually blending from ith anchorColor to i+1th anchorColor using your blend function. What do you think? Oh, the boxCount will be incrementing as we keep placing the box on top of the tower. And the color of a given box will never change.

Much of this will come down to your own choices, designs and long term intentions.

If you have a unknown number of elements, you could start with a series of "base" colors. Then you could allocate n number of deviations between them, giving your a different gradient over a very wide range of colors.

For example, if you had 64 base colors, with 128 segments between each base colour you get something like 8192 colors ... So each time you use up a series of segments, you'd move onto the next base color pair (the old end color would become the new start color and the next color would become the end color)

Upvotes: 4

Related Questions