Vulkan
Vulkan

Reputation: 1074

How can I create a gradient effect using color divisions?

I'm trying to create a table view (custom class not UITableView) where its results have a gradient effect like in the example image below:

enter image description here

What I tried:

  1. I successfully managed to add the correct gradient as a background to every table cell but I need it to be the color of the text of each label not the background of each cell. Question. (I asked this one.)

FAILED ATEMPT:

enter image description here

  1. Creating a custom gradient image and adding it as colorWithPatternImage: to each label but since the gradient is one every cell looks the same. Question.

FAILED ATEMPT:

enter image description here

Last thing to try:

Suppose you have two colors, color1 and color2. A gradient can result by mixing these colors. In the picture above color1 = purple and color2 = orange. It would be easy to create a gradient effect by dividing the gradient in sections based on the number of results and then find the average color of each section and use it as the text color of each corresponding result.

For example:

5 results = 5 divisions.

The result is not as detailed because each text is a solid color but it is equally impressive when the text is small:

enter image description here

The problem is, for two colors like these:

Purple: 128.0, 0.0, 255.0

Orange: 255.0, 128.0, 0.0

How do you divide it in 5 sections and find the average of each section?

I could do this using the eyedropper tool in pixelmator but only if I knew the fixed number of results, won't work with 6 results.

I can't approach it with math, I don't know where to begin.

Any ideas?

Upvotes: 2

Views: 1181

Answers (4)

c.lamont.dev
c.lamont.dev

Reputation: 767

For Xamarin iOS C#

public static List<UIColor> DividedColors(UIColor firstColor, UIColor secondColor, int sections)
{
    nfloat firstRed = 0;
    nfloat firstGreen = 0;
    nfloat firstBlue = 0;
    nfloat firstAlpha = 0;
    firstColor.GetRGBA(out firstRed, out firstGreen, out firstBlue, out firstAlpha);

    nfloat secondRed = 0;
    nfloat secondGreen = 0;
    nfloat secondBlue = 0;
    nfloat secondAlpha = 0;
    secondColor.GetRGBA(out secondRed, out secondGreen, out secondBlue, out secondAlpha);

    nfloat Mix(nfloat first, nfloat second, nfloat ratio)
    {
        return first + ratio * (second - first);
    }

    List<UIColor> colors = new List<UIColor>();
    var ratioPerSection = 1.0f / sections;

    for (int i = 0; i < sections; i++)
    {
        var newRed = Mix(firstRed, secondRed, ratioPerSection * i);
        var newGreen = Mix(firstGreen, secondGreen, ratioPerSection * i);
        var newBlue = Mix(firstBlue, secondBlue, ratioPerSection * i);
        var newAlpha = Mix(firstAlpha, secondAlpha, ratioPerSection * i);
        var newColor = new UIColor(newRed, newGreen, newBlue, newAlpha);
        colors.Add(newColor);
    }

    return colors;
}

Upvotes: 0

rob mayoff
rob mayoff

Reputation: 385670

So you want this:

screenshot

There are several ways you could implement this. Here's a way that's pixel-perfect (it draws the gradient through the labels instead of making each label a solid color).

Make a subclass of UILabel. In your subclass, override drawTextInRect: to draw the gradient.

Let's declare the subclass like this:

IB_DESIGNABLE
@interface GradientLabel: UILabel

@property (nonatomic, weak) IBOutlet UIView *gradientCoverageView;
@property (nonatomic, strong) IBInspectable UIColor *startColor;
@property (nonatomic, strong) IBInspectable UIColor *endColor;

@end

Connect the gradientCoverageView outlet to a view that covers the entire area of the gradient—so, a view that covers all five of the labels. This could be the superview of the labels, or it could just be a hidden view that you have set up to (invisibly) fill the same area.

Set startColor to purple and endColor to orange.

We'll draw the gradient-filled text (in our drawTextInRect: override) in three steps:

  1. Call super to just draw the text normally.

  2. Set the graphics context blend mode kCGBlendModeSourceIn. This blend mode tells Core Graphics to draw only where the context has already been drawn, and to overwrite whatever was drawn there. Thus Core Graphics treats the text drawn by super as a mask.

  3. Draw the gradient with the start point at the top of the coverage view and the end point at the bottom of the coverage view, not at the top and bottom of the current label. Thus the gradient spans the entire coverage view, but is masked to only show up where the text of the current label was drawn in step 1.

Here's the implementation:

@implementation GradientLabel

- (void)drawTextInRect:(CGRect)rect {
    [super drawTextInRect:rect];

    CGContextRef gc = UIGraphicsGetCurrentContext();
    NSArray *colors = @[(__bridge id)self.startColor.CGColor, (__bridge id)self.endColor.CGColor];
    CGGradientRef gradient = CGGradientCreateWithColors(CGBitmapContextGetColorSpace(gc), (__bridge CFArrayRef)colors, NULL);

    UIView *coverageView = self.gradientCoverageView ?: self;
    CGRect coverageRect = [coverageView convertRect:coverageView.bounds toView:self];
    CGPoint startPoint = coverageRect.origin;
    CGPoint endPoint = { coverageRect.origin.x, CGRectGetMaxY(coverageRect) };

    CGContextSaveGState(gc); {
        CGContextSetBlendMode(gc, kCGBlendModeSourceIn);
        CGContextDrawLinearGradient(gc, gradient, startPoint, endPoint, 0);
    } CGContextRestoreGState(gc);

    CGGradientRelease(gradient);
}

@end

Here's the layout in my storyboard:

storyboard

For all five of the GradientLabel instances, I set the startColor to purple and the endColor to orange and I connected the gradientCoverageView outlet to the enclosing stack view.

Upvotes: 1

nathangitter
nathangitter

Reputation: 9777

You can use math on the rgb values of the colors.

To get the rgb values, you can use the getRed:green:blue:alpha method on UIColor. Then all you have to do is average the colors together based on how many sections you need.

Here is a function that should return an array of colors based on a start and end color, and how many divisions you need.

Solution

func divideColors(firstColor: UIColor, secondColor: UIColor, sections: Int) -> [UIColor] {

    // get rgb values from the colors
    var firstRed: CGFloat = 0; var firstGreen: CGFloat = 0; var firstBlue: CGFloat = 0; var firstAlpha: CGFloat = 0;
    firstColor.getRed(&firstRed, green: &firstGreen, blue: &firstBlue, alpha: &firstAlpha)
    var secondRed: CGFloat = 0; var secondGreen: CGFloat = 0; var secondBlue: CGFloat = 0; var secondAlpha: CGFloat = 0;
    secondColor.getRed(&secondRed, green: &secondGreen, blue: &secondBlue, alpha: &secondAlpha)

    // function to mix the colors
    func mix(_ first: CGFloat, _ second: CGFloat, ratio: CGFloat) -> CGFloat { return first + ratio * (second - first) }

    // variable setup
    var colors = [UIColor]()
    let ratioPerSection = 1.0 / CGFloat(sections)

    // mix the colors for each section
    for section in 0 ..< sections {
        let newRed = mix(firstRed, secondRed, ratio: ratioPerSection * CGFloat(section))
        let newGreen = mix(firstGreen, secondGreen, ratio: ratioPerSection * CGFloat(section))
        let newBlue = mix(firstBlue, secondBlue, ratio: ratioPerSection * CGFloat(section))
        let newAlpha = mix(firstAlpha, secondAlpha, ratio: ratioPerSection * CGFloat(section))
        let newColor = UIColor(red: newRed, green: newGreen, blue: newBlue, alpha: newAlpha)
        colors.append(newColor)
    }

    return colors

}

Your question is tagged as Objective-C, but it should be easy enough to convert the Swift code above to Objective-C since you would use the same UIColor API.

Here is some code to test the above function (perfect for a Swift playground).

Testing Code

let sections = 5
let view = UIView(frame: CGRect(x: 0, y: 0, width: 250, height: 50))
for (index, color) in divideColors(firstColor: UIColor.purple, secondColor: UIColor.orange, sections: sections).enumerated() {
    let v = UIView(frame: CGRect(x: CGFloat(index) * 250 / CGFloat(sections), y: 0, width: 250 / CGFloat(sections), height: 50))
    v.backgroundColor = color
    view.addSubview(v)
}
view.backgroundColor = .white

Test Result

An image of the five colors, evenly divided between purple and orange

It also works for any number of sections and different colors!

An image of more colors, evenly divided between red and yellow

Upvotes: 4

Tim Kokesh
Tim Kokesh

Reputation: 879

Well, here is how to do the solid-color mathy bit at the end:

If you have n + 1 colors, for 0 <= m <= n:

color.red[m] = (purple.red * (m/n)) + (orange.red * ((n-m)/n);
color.green[m] = (purple.green * (m/n)) + (orange.green * ((n-m)/n));
color.blue[m] = (purple.blue * (m/n)) + (orange.blue * ((n-m)/n));

Hope this helps.

Upvotes: 0

Related Questions