WFT WFT - 5 months ago 37
Objective-C Question

Triangle Gradient With Core Graphics

I'm trying to draw a triangle like this one in a view (one UIView, one NSView):

Triangle gradient

My first thought was CoreGraphics, but I couldn't find any information that would help me draw a gradient between three points of arbitrary color.

Any help?

Thanks!

Answer

Actually it's pretty simple with CoreGraphics. Below you can find code that renders given triangle, but first let's think how we can solve this problem.

Theory

Imagine equilateral triangle with side length w. All three angles are equal to 60 degrees:

Equilateral triangle

Each angle will represent component of a pixel: red, green or blue.

Lets analyze intensity of a green component in a pixel near top angle:

Analyzing

The more closer pixel to the angle, the more component intense it'll have and vice versa. Here we can decompose our main goal to smaller ones:

  1. Draw triangle pixel by pixel.
  2. For each pixel calculate value for each component based on distance from corresponding angle.

To solve first task we will use CoreGraphics bitmap context. It will have four components per pixel each 8 bits long. This means that component value may vary from 0 to 255. Fourth component is alpha channel and will be always equal to max value - 255. Here is example of how values will be interpolated for the top angle:

Interpolation example

Now we need to think how we can calculate value for component.

First, let's define main color for each angle:

Defining angle-color pair

Now let's choose an arbitrary point A with coordinates (x,y) on the triangle:

Choosing point

Next, we draw a line from an angle associated with red component and it passes through the A till it intersects with opposite side of a triangle:

Line

If we could find d and c their quotient will equal to normalized value of component, so value can be calculated easily:

Component value

Formula for finding distance between two points is simple:

Distance Formula

We can easily find distance for d, but not for c, because we don't have coordinates of intersection. Actually it's not that hard. We just need to build line equations for line that passes through A and line that describes opposite side of a triangle and find their intersection:

Line intersection

Having intersection point we can apply distance formula Distance Formula to find c and finally calculate component value for current point. Component value

Same flow applies for another components.

Code

Here is the code that implements concepts above:

+ (UIImage *)triangleWithSideLength:(CGFloat)sideLength {
    return [self triangleWithSideLength:sideLength scale:[UIScreen mainScreen].scale];
}

+ (UIImage *)triangleWithSideLength:(CGFloat)sideLength
                              scale:(CGFloat)scale {
    UIImage *image = nil;

    CGSize size = CGSizeApplyAffineTransform((CGSize){sideLength, sideLength * sin(M_PI / 3)}, CGAffineTransformMakeScale(scale, scale));

    size_t const numberOfComponents = 4;
    size_t width = ceilf(size.width);
    size_t height = ceilf(size.height);

    size_t realBytesPerRow = width * numberOfComponents;
    size_t alignedBytesPerRow = (realBytesPerRow + 0xFF) & ~0xFF;
    size_t alignedPixelsPerRow = alignedBytesPerRow / numberOfComponents;

    CGContextRef ctx = CGBitmapContextCreate(NULL,
                                             width,
                                             height,
                                             8,
                                             alignedBytesPerRow,
                                             CGColorSpaceCreateDeviceRGB(),
                                             (CGBitmapInfo)kCGImageAlphaPremultipliedLast);

    char *data = CGBitmapContextGetData(ctx);

    for (int i = 0; i < height; i++) {
        for (int j = 0; j < width; j++) {
            int edge = ceilf((height - i) / sqrt(3));

            if (j < edge || j > width - edge) {
                continue;
            }

            CGFloat redNormalized = 0;
            CGFloat greenNormalized = 0;
            CGFloat blueNormalized = 0;

            CGPoint currentTrianglePoint = (CGPoint){j / scale, (height - i) / scale};

            [self calculateCurrentValuesAtGiventPoint:currentTrianglePoint
                                           sideLength:sideLength
                                              sideOne:&redNormalized
                                              sideTwo:&greenNormalized
                                            sideThree:&blueNormalized];

            int32_t red = redNormalized * 0xFF;
            int32_t green = greenNormalized * 0xFF;
            int32_t blue = blueNormalized * 0xFF;

            char *pixel = data + (j + i * alignedPixelsPerRow) * numberOfComponents;

            *pixel = red;
            *(pixel + 1) = green;
            *(pixel + 2) = blue;
            *(pixel + 3) = 0xFF;
        }
    }

    CGImageRef cgImage = CGBitmapContextCreateImage(ctx);

    image = [[UIImage alloc] initWithCGImage:cgImage];

    CGContextRelease(ctx);
    CGImageRelease(cgImage);

    return image;
}

+ (void)calculateCurrentValuesAtGiventPoint:(CGPoint)point
                                 sideLength:(CGFloat)length
                                    sideOne:(out CGFloat *)sideOne
                                    sideTwo:(out CGFloat *)sideTwo
                                  sideThree:(out CGFloat *)sideThree {
    CGFloat height = sin(M_PI / 3) * length;

    if (sideOne != NULL) {
        // Side one is at 0, 0
        CGFloat currentDistance = sqrt(point.x * point.x + point.y * point.y);

        if (currentDistance != 0) {
            CGFloat a = point.y / point.x;
            CGFloat b = 0;

            CGFloat c = -height / (length / 2);
            CGFloat d = 2 * height;

            CGPoint intersection = (CGPoint){(d - b) / (a - c), (a * d - c * b) / (a - c)};

            CGFloat currentH = sqrt(intersection.x * intersection.x + intersection.y * intersection.y);

            *sideOne = 1 - currentDistance / currentH;
        } else {
            *sideOne = 1;
        }
    }

    if (sideTwo != NULL) {
        // Side two is at w, 0
        CGFloat currentDistance = sqrt(pow((point.x - length), 2) + point.y * point.y);

        if (currentDistance != 0) {
            CGFloat a = point.y / (point.x - length);
            CGFloat b = height / (length / 2);

            CGFloat c = a * -point.x + point.y;
            CGFloat d = b * -length / 2 + height;

            CGPoint intersection = (CGPoint){(d - c) / (a - b), (a * d - b * c) / (a - b)};

            CGFloat currentH = sqrt(pow(length - intersection.x, 2) + intersection.y * intersection.y);

            *sideTwo = 1 - currentDistance / currentH;
        } else {
            *sideTwo = 1;
        }
    }

    if (sideThree != NULL) {
        // Side three is at w / 2, w * sin60 degrees
        CGFloat currentDistance = sqrt(pow((point.x - length / 2), 2) + pow(point.y - height, 2));

        if (currentDistance != 0) {
            float dy = point.y - height;
            float dx = (point.x - length / 2);

            if (fabs(dx) > FLT_EPSILON) {
                CGFloat a = dy / dx;

                CGFloat b = 0;

                CGFloat c = a * -point.x + point.y;
                CGFloat d = 0;

                CGPoint intersection = (CGPoint){(d - c) / (a - b), (a * d - b * c) / (a - b)};

                CGFloat currentH = sqrt(pow(length / 2 - intersection.x, 2) + pow(height - intersection.y, 2));

                *sideThree = 1 - currentDistance / currentH;
            } else {
                *sideThree = 1 - currentDistance / height;
            }
        } else {
            *sideThree = 1;
        }
    }
}

Here is a triangle image produced by this code:

Rendered triangle

Comments