8

Dipping my feet into some more Core Graphics drawing, I'm attempting to recreate a wicked looking metallic knob, and I've landed on what is probably a show-stopping issue.

There doesn't seem to be any way to draw angle gradients in Core Graphics. I see there's CGContextDrawRadialGradient() and CGContextDrawLinearGradient(), but there's nothing that I see that would allow me to draw an angle gradient. Does anyone know of a workaround, or a bit of framework hidden away somewhere to accomplish this without pre-rendering the knob into an image file?

AngleGradientKnob http://dl.dropbox.com/u/3009808/AngleGradient.png.

Greg Combs
  • 4,252
  • 3
  • 33
  • 47

2 Answers2

11

This is kind of thrown together, but it's the approach I'd probably take. This is creating an angle gradient by drawing it directly into a bitmap using some simple trig, then clipping it to a circle. I create a grid of memory using a grayscale colorspace, calculate the angle from a given point to the center, and then color that based on a periodic function, running between 0 to 255. You could of course expand this to do RGBA color as well.

Of course you'd cache this and play with the math to get the colors you want. This currently runs all the way from black to white, which doesn't look as good as you'd like.

- (void)drawRect:(CGRect)rect {
  CGImageAlphaInfo alphaInfo = kCGImageAlphaNone;
  CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
  size_t components = CGColorSpaceGetNumberOfComponents( colorSpace );
  size_t width = 100;
  size_t height = 100;
  size_t bitsPerComponent = 8;
  size_t bytesPerComponent = bitsPerComponent / 8;
  size_t bytesPerRow = width * bytesPerComponent * components;
  size_t dataLength = bytesPerRow * height;

  uint8_t data[dataLength];

  CGContextRef imageCtx = CGBitmapContextCreate( &data, width, height, bitsPerComponent,
                                      bytesPerRow, colorSpace, alphaInfo );

  NSUInteger offset = 0;
  for (NSUInteger y = 0; y < height; ++y) {
    for (NSUInteger x = 0; x < bytesPerRow; x += components) {
      CGFloat opposite = y - height/2.;
      CGFloat adjacent = x - width/2.;
      if (adjacent == 0) adjacent = 0.001;
      CGFloat angle = atan(opposite/adjacent);
      data[offset] = abs((cos(angle * 2) * 255));
      offset += components * bytesPerComponent;
    }
  }

  CGImageRef image = CGBitmapContextCreateImage(imageCtx);

  CGContextRelease(imageCtx);
  CGColorSpaceRelease(colorSpace);

  CGContextRef ctx = UIGraphicsGetCurrentContext();

  CGRect buttonRect = CGRectMake(100, 100, width, width);
  CGContextAddEllipseInRect(ctx, buttonRect);
  CGContextClip(ctx);

  CGContextDrawImage(ctx, buttonRect, image);
  CGImageRelease(image);
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Looking at your loop, it looks to me like it would be fairly simple to translate to a Core Image kernel that could generate an infinitely-large image of the gradient you showed. – Peter Hosey Aug 02 '11 at 09:02
  • Agreed, but the OP requested cocoa-touch which doesn't support CI kernel filters. On Mac, Core Image would definitely be useful here. – Rob Napier Aug 02 '11 at 11:37
  • Hypothetically speaking, if the next iOS version happened to support Core Image (and I'm not saying it will), would this include the necessary kernel filters, or is that something that would remain strictly desktop only? – Greg Combs Aug 03 '11 at 09:29
  • 2
    @Greg Combs: It wouldn't need to use any existing filters. I'm talking about writing a filter from the ground up with a translation of Rob's code for the kernel. Assuming CIFL were to remain unchanged in this hypothetical iOS version of CI, the same filter and kernel would work unchanged on both Mac and iOS. – Peter Hosey Aug 03 '11 at 11:31
  • 1
    It's under NDA, so I won't answer the question here, but for those of you also under NDA, you can find the answer in the docs (look at the top of the page in the second blue box): https://developer.apple.com/library/prerelease/ios/#documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html – Rob Napier Aug 03 '11 at 13:01
  • Would either of you, @peter or rob, be interested in showing me how the code for that kernel filter would look? I'm looking at some of the custom filter examples and still having a little difficulty figuring out an appropriate translation for rob's previous work. – Greg Combs Aug 03 '11 at 17:11
  • @Greg Combs: A filter kernel is invoked for every destination pixel (essentially asking you the question “what should this pixel's values be?”). Your implementation should resemble the body of the inner loop from Rob's answer. It shouldn't be too different; the biggest change will be that you effectively can't use the `if` statement, so you'll have to use the `?:` operator instead. – Peter Hosey Aug 03 '11 at 23:35
  • How would I make this work on a Mac? I'd like to be able to use it like I do with CGContextDrawLinearGradient() or so. – Aditya Vaidyam Mar 20 '12 at 01:50
  • 1
    I actually wanted an angle gradient last night, and when I went researching, I ran across your answer and my comments on it, and thus was emboldened to write the kernel that I described in my comments. I posted it as a Gist: https://gist.github.com/4604459 This generates an image of infinite extent, so, depending on what you want it for, you may need to feed it to CICrop or otherwise render a finite section of it. – Peter Hosey Jan 23 '13 at 23:48
3

To expand on what's in the comments to the accepted answer, here's the code for generating an angle gradient using Core Image. This should work in iOS 8 or later.

// generate a dummy image of the required size
UIGraphicsBeginImageContextWithOptions(CGSizeMake(256.0, 256.0), NO, [[UIScreen mainScreen] scale]);
CIImage *dummyImage = [CIImage imageWithCGImage:UIGraphicsGetImageFromCurrentImageContext().CGImage];

// define the kernel algorithm
NSString *kernelString = @"kernel vec4 circularGradientKernel(__color startColor, __color endColor, vec2 center, float radius) { \n"
"    vec2 point = destCoord() - center;"
"    float rsq = point.x * point.x + point.y * point.y;"
"    float theta = mod(atan(point.y, point.x), radians(360.0));"
"    return (rsq < radius*radius) ? mix(startColor, endColor, 0.5+0.5*cos(4.0*theta)) : vec4(0.0, 0.0, 0.0, 1.0);"
"}";

// initialize a Core Image context and the filter kernel
CIContext *context = [CIContext contextWithOptions:nil];
CIColorKernel *kernel = [CIColorKernel kernelWithString:kernelString];

// argument array, corresponding to the first line of the kernel string
NSArray *args = @[ [CIColor colorWithRed:0.5 green:0.5 blue:0.5],
                   [CIColor colorWithRed:1.0 green:1.0 blue:1.0],
                   [CIVector vectorWithCGPoint:CGPointMake(CGRectGetMidX(dummyImage.extent),CGRectGetMidY(dummyImage.extent))],
                   [NSNumber numberWithFloat:200.0]];

// apply the kernel to our dummy image, and convert the result to a UIImage
CIImage *ciOutputImage = [kernel applyWithExtent:dummyImage.extent arguments:args];
CGImageRef cgOutput = [context createCGImage:ciOutputImage fromRect:ciOutputImage.extent];
UIImage *gradientImage = [UIImage imageWithCGImage:cgOutput];
CGImageRelease(cgOutput);

This generates the following image:

Angle gradient made using Core Image

deltacrux
  • 1,186
  • 11
  • 18
  • 1
    This is excellent -- Thanks! I've taken the liberty of porting your example to Swift -- https://gist.github.com/grgcombs/f70b1f053b9cc3467e2d – Greg Combs Nov 30 '15 at 18:45
  • I didn't realise you'd still be interested in this problem four years later ;-P But your question helped me to solve this problem in my own project, so thank you! – deltacrux Nov 30 '15 at 23:34