Precise pixel grid overlay in Core Graphics? - cocoa

In my experiments with creating a pixel-centered image editor I've been trying to draw a precise grid overlay to help guide users when trying to access certain pixels. However, the grid I draw isn't very even, especially at smaller sizes. It's a regular pattern of one slightly larger column for every few normal columns, so I think it's a rounding issue, but I can't see it in my code. Here's my code:
- (void)drawRect:(NSRect)dirtyRect
{
context = [[NSGraphicsContext currentContext] graphicsPort];
CGContextAddRect(context, NSRectToCGRect(self.bounds));
CGContextSetRGBStrokeColor(context, 1.0f, 0.0f, 0.0f, 1.0f);
CGContextStrokePath(context);
CGContextSetInterpolationQuality(context, kCGInterpolationNone);
CGContextSetShouldAntialias(context, NO);
if (image)
{
NSRect imageRect = NSZeroRect;
imageRect.size = CGImageGetSize([image CGImage]);
drawRect = [self bounds];
NSRect viewRect = drawRect;
CGFloat aspectRatio = imageRect.size.width / imageRect.size.height;
if (viewRect.size.width / viewRect.size.height <= aspectRatio)
{
drawRect.size.width = viewRect.size.width;
drawRect.size.height = imageRect.size.height * (viewRect.size.width / imageRect.size.width);
}
else
{
drawRect.size.height = viewRect.size.height;
drawRect.size.width = imageRect.size.width * (viewRect.size.height / imageRect.size.height);
}
drawRect.origin.x += (viewRect.size.width - drawRect.size.width) / 2.0;
drawRect.origin.y += (viewRect.size.height - drawRect.size.height) / 2.0;
CGContextDrawImage(context, drawRect, [image CGImage]);
if (showPixelGrid)
{
//Draw grid by creating start and end points for vertical and horizontal lines.
//FIXME: Grid is uneven, especially at smaller sizes.
CGContextSetStrokeColorWithColor(context, CGColorGetConstantColor(kCGColorBlack));
CGContextAddRect(context, drawRect);
CGContextStrokePath(context);
NSUInteger numXPoints = (NSUInteger)imageRect.size.width * 2;
NSUInteger numYPoints = (NSUInteger)imageRect.size.height * 2;
CGPoint xPoints[numXPoints];
CGPoint yPoints[numYPoints];
CGPoint startPoint;
CGPoint endPoint;
CGFloat widthRatio = drawRect.size.width / imageRect.size.width;
CGFloat heightRatio = drawRect.size.height / imageRect.size.height;
startPoint.x = drawRect.origin.x;
startPoint.y = drawRect.origin.y;
endPoint.x = drawRect.origin.x;
endPoint.y = drawRect.size.height + drawRect.origin.y;
for (NSUInteger i = 0; i < numXPoints; i += 2)
{
startPoint.x += widthRatio;
endPoint.x += widthRatio;
xPoints[i] = startPoint;
xPoints[i + 1] = endPoint;
}
startPoint.x = drawRect.origin.x;
startPoint.y = drawRect.origin.y;
endPoint.x = drawRect.size.width + drawRect.origin.x;
endPoint.y = drawRect.origin.y;
for (NSUInteger i = 0; i < numYPoints; i += 2)
{
startPoint.y += heightRatio;
endPoint.y += heightRatio;
yPoints[i] = startPoint;
yPoints[i + 1] = endPoint;
}
CGContextStrokeLineSegments(context, xPoints, numXPoints);
CGContextStrokeLineSegments(context, yPoints, numYPoints);
}
}
}
Any ideas?

UPDATE: I managed to get your code running with a few tweaks - where did CGImageGetSize() come from? - and I can't really see the problem, other than columns aren't all exactly even at extremely small sizes. That's just how it has to work though. The only way around this is to either fix scaling to be integer multiples of the image size - in other words, get the largest integer multiple of the image size smaller than the view size -or reduce the number of lines drawn on the screen at very small sizes to get rid of this artefact. There's a reason the pixel grid only becomes visible when you zoom in a long way in most editors. Not to mention that if the grid is still visible at 3-4x resolution you're making the view just way too busy.
I couldn't run the code you provided because there's a bunch of class ivars in there, but from a cursory glance, I'd say it has something to do with drawing on pixel boundaries. After you round to an integer to get rid of fuzzy AA artefacts (I notice you turned AA off, but ideally you shouldn't have to do that), you then need to add 0.5 to your origin to get your line drawn in the center of the pixel rather than on the boundary.
Like this:
+---X---+---+---+---+---+
| | | | Y | | |
+---+---+---+---+---+---+
X : CGPoint (1, 1)
Y : CGPoint (3.5, 0.5)
You want to draw from the center of the pixel, because otherwise your line straddles two pixels.
In other words, where you're setting up xPoints and yPoints, make sure to floor() or round() your values, and then add 0.5.

Related

Xcode UI on background thread to render image

I'm rendering an image with text for one of my apps and has a noticeable impact on UI performance (can be as big as ~1 second freeze), so I am doing it on a background thread. Since the image has text, using UILabels and other UIViews makes it easy to lay everything out, and I render the view containing everything to an image.
However, I get a warning from Xcode saying that it's not allowed on the background thread because it uses UIKit. Why am I not allowed to call UIKit on the background thread even though my use case is completely self-contained and isolated from any rendering onscreen?
To help the code below make more sense, it draws an image that is a listing of several items, each of which consists of two small square images and the name of the item all in a row. The list can have several columns. The code has been tweaked slightly (mostly variable names) to avoid showing proprietary code, but does the same job.
My code:
NSArray<MyItem*>* items; // These are the items that I'm drawing. They
// get set before the following code is called.
// Processing code:
const CGFloat TITLE_FONT_SIZE = 50; // font size of the title
const CGFloat ITEM_FONT_SIZE = 25; // font size of the item names
const int OUTER_PADDING = 60; // padding from the edge of the image to the main content
const int ROW_PADDING = 13; // padding between rows
const int COL_PADDING = 100; // padding between columns
const int PADDING = 20; // padding between content items in a row
const int BOX_SIZE = 25; // how high/wide each image is
const int ROW_HEIGHT = BOX_SIZE; // pixel height of a line
const int COL_WIDTH = 500; // pixel width of a column (image1, image2, and name)
// compute the dimensions of the image
UILabel* titleLabel = [[UILabel alloc] init];
titleLabel.font = [UIFont systemFontOfSize:TITLE_FONT_SIZE];
titleLabel.text = #"My image";
[titleLabel sizeToFit];
titleLabel.frame = CGRectMake(OUTER_PADDING, OUTER_PADDING / 2, titleLabel.frame.size.width, titleLabel.frame.size.height);
const int MIN_NUM_COLS = 1 + ((titleLabel.frame.size.width - COL_WIDTH) / (COL_WIDTH + COL_PADDING));
const int NORMAL_NUM_COLS = (int)ceil(sqrt([items count] / (COL_WIDTH / (ROW_HEIGHT))));
const int NUM_COLS = (MIN_NUM_COLS > NORMAL_NUM_COLS ? MIN_NUM_COLS : NORMAL_NUM_COLS);
const int NUM_ROWS = (int)ceil([items count] / (float)NUM_COLS);
const int NUM_OVERFLOW_ROWS = [items count] % NUM_ROWS;
const int titleWidth = titleLabel.frame.size.width;
const int defaultWidth = (NUM_COLS * (COL_WIDTH + COL_PADDING)) - COL_PADDING;
const int pixelWidth = (2 * OUTER_PADDING) + (titleWidth > defaultWidth ? titleWidth : defaultWidth);
const int pixelHeight = (2 * OUTER_PADDING) + (TITLE_FONT_SIZE + PADDING) + (NUM_ROWS * (ROW_HEIGHT + ROW_PADDING)) - ROW_PADDING;
const int nbytes = 4 * pixelHeight * pixelWidth;
byte* data = (byte*)malloc(sizeof(byte) * nbytes);
memset(data, 255, nbytes);
CGContextRef context = CGBitmapContextCreate(data, pixelWidth, pixelHeight, 8, 4 * pixelWidth, CGColorSpaceCreateDeviceRGB(), kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast);
// --------------------------------------------------
// create a view heirarchy and then draw to our context
UIView* mainView = [[UIView alloc] init];
[mainView addSubview:titleLabel];
// setup all the views
int keyIndex = 0;
CGFloat x = OUTER_PADDING;
CGFloat starty = titleLabel.frame.origin.y + titleLabel.frame.size.height + PADDING;
for (int col = 0; col < NUM_COLS; col++)
{
int nrows = (col == NUM_COLS + 1 ? NUM_OVERFLOW_ROWS : NUM_ROWS);
CGFloat y = starty;
for (int row = 0; (row < nrows) && (keyIndex < [items count]); row++)
{
CGFloat tempx = x;
MyItem* item = [items objectAtIndex:keyIndex];
UIImageView* imageview1 = [[UIImageView alloc] initWithImage:item.image1];
imageview1.frame = CGRectMake(tempx, y, BOX_SIZE, BOX_SIZE);
[mainView addSubview:imageview1];
tempx += BOX_SIZE + PADDING;
UIImageView* imageview2 = [[UIImageView alloc] initWithImage:item.imageview2];
imageview2.frame = CGRectMake(tempx, y, BOX_SIZE, BOX_SIZE);
[mainView addSubview:imageview2];
tempx += BOX_SIZE + PADDING;
UILabel* label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:ITEM_FONT_SIZE];
label.text = item.name;
[label sizeToFit];
label.center = CGPointMake(tempx + (label.frame.size.width / 2), imageview2.center.y);
[mainView addSubview:label];
y += ROW_HEIGHT + ROW_PADDING;
keyIndex++;
}
x += COL_WIDTH + COL_PADDING;
}
// --------------------------------------------------
// draw everything to actually generate the image
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, pixelHeight));
[mainView.layer renderInContext:context];
CGImageRef cgimage = CGBitmapContextCreateImage(context);
myCoolImage = [UIImage imageWithCGImage:cgimage];
CGImageRelease(cgimage);
CGContextRelease(context);
free(data);
As we've established in comments, what you're doing is both illegitimate and slow.
Arranging and sizing UILabel and UIImageView objects is slow, and calling
CALayer renderInContext is really slow.
And it isn't how you draw.
Everything you're doing has its analogue in the actual drawing world (Quartz 2D), and if you did it that way, not only would it be legal in the background, it probably wouldn't even need to be in the background because it would be so much faster. So:
Every place you use a UILabel, you can achieve exactly the same effect by using NSAttributedString draw... commands.
Every place you use a UIImageView, you can achieve exactly the same effect by using UIImage draw... commands.
Any of us who does any extensive drawing has learned to create structured layouts of the type you're making by using actual drawing code, and now is your chance to learn to do it too.

How is transparency achieved in cocoa applications

I am trying to understand how is transparency actually implemented in cocoa applications. I was expecting the standard blending equation to be used i.e.
BlendedColour = alpha * layerColour + (1-alpha)*backgroundColour
However, I noticed that there is the slight difference in the blended colour expected if the above equation is used. To verify it, I did a small experiment as follows:
1.) Created a window, added a transparency of 0.8 to the window and grabbed a screenshot.
2.) I took a screenshot of the part of the screen where I am overlaying the window in step one without the window and overlayed the same image as in step 1, using the equation mentioned above. (I used openCV for that).
There is a slight difference in the colours for the two images, if you look closely. I wanted to understand what is causing the difference.
Resources:
1.) Images from Step 1 and Step2 respectively
2.) Code used in step 1
NSRect windowRect = {0,0,200,200};
m_NSWindow = [[NSWindow alloc] initWithContentRect:windowRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
[m_NSWindow setTitle:#"overlayWindow"];
[m_NSWindow makeKeyAndOrderFront:nil];
g_imageView = [[NSImageView alloc] initWithFrame:NSMakeRect(0,0,200,200)];
[m_NSWindow.contentView addSubview:g_imageView];
[m_NSWindow setOpaque:NO];
[m_NSWindow setAlphaValue:0.8];
NSBitmapImageRep* imageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:nil
pixelsWide:200
pixelsHigh:200
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bitmapFormat:NSAlphaNonpremultipliedBitmapFormat
bytesPerRow:(200*4)
bitsPerPixel:32];
memcpy(imageRep.bitmapData,m_paintBuffer.data,160000);
NSSize imageSize = NSMakeSize(200,200);
NSImage* myImage = [[NSImage alloc] initWithSize: imageSize];
[myImage addRepresentation:imageRep];
[g_imageView setImage:myImage];
4.) Code for step 2
void overlayImage(const cv::Mat &background, const cv::Mat &foreground,
cv::Mat &output, cv::Point2i location)
{
background.copyTo(output);
// start at the row indicated by location, or at row 0 if location.y is negative.
for (int y = max(location.y, 0); y < background.rows; ++y)
{
int fY = y - location.y; // because of the translation
// we are done of we have processed all rows of the foreground image.
if (fY >= foreground.rows)
break;
// start at the column indicated by location,
// or at column 0 if location.x is negative.
for (int x = max(location.x, 0); x < background.cols; ++x)
{
int fX = x - location.x; // because of the translation.
// we are done with this row if the column is outside of the foreground image.
if (fX >= foreground.cols)
break;
// determine the opacity of the foregrond pixel, using its fourth (alpha) channel.
double opacity =
((double)foreground.data[fY * foreground.step + fX * foreground.channels() + 3])
/ 255.;
// and now combine the background and foreground pixel, using the opacity,
// but only if opacity > 0.
for (int c = 0; opacity > 0 && c < output.channels(); ++c)
{
unsigned char foregroundPx =
foreground.data[fY * foreground.step + fX * foreground.channels() + c];
unsigned char backgroundPx =
background.data[y * background.step + x * background.channels() + c];
output.data[y*output.step + output.channels()*x + c] =
backgroundPx * (1. - opacity) + foregroundPx * opacity;
}
}
}
}

Xcode : Need help moving an image inside screen bounds

I followed a guide about dragging multiple images on screen with gesture recognizers however i have a problem. I want the images to be dragged inside the bounds of the screen which maybe iPad or iPhone. The code i am using estimates the image's center so half of the image is getting off screen when reaching the bounds. Could you please help me with this?
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view];
if (recognizer.state == UIGestureRecognizerStateEnded) {
CGPoint velocity = [recognizer velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
CGFloat slideMult = magnitude / 200; //original divider 200
NSLog(#"magnitude: %f, slideMult: %f", magnitude, slideMult);
float slideFactor = 0.1 * slideMult /4; // Increase for more of a slide (original doesn't have a divider)
CGPoint finalPoint = CGPointMake(recognizer.view.center.x + (velocity.x * slideFactor),
recognizer.view.center.y + (velocity.y * slideFactor));
finalPoint.x = MIN(MAX(finalPoint.x, 0), self.view.bounds.size.width);
finalPoint.y = MIN(MAX(finalPoint.y, 0), self.view.bounds.size.height);
[UIView animateWithDuration:slideFactor*2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
recognizer.view.center = finalPoint;
} completion:nil];
}
}
Well, i managed to find out the solution to my problem. I divided the image width and height by two. In my case the results were 100 and 70. The rest can be seen in code box :
finalPoint.x = MIN(MAX(finalPoint.x, 70), self.view.bounds.size.width-70);
finalPoint.y = MIN(MAX(finalPoint.y, 100), self.view.bounds.size.height-100);

drawRect performance

I need to draw lots of polygons 500k to a million on the iPad. After experimenting, I can only get only get 1 fps if that. This is just an example my real code has some good sized polygons.
Here are a few question:
Why don't I have to add the Quartz Framework to my project?
If many of the polygons repeat can I leverage that with views or are they too heavy etc?
Any alternatives, QTPaint can handle this but dips into the gpu. Is there is anything like QT or ios?
Can Opengl increase 2d performance of this type?
Example drawrect:
//X Y Array of boxes
- (void)drawRect:(CGRect)rect
{
int reset = [self pan].x;
int markX = reset;
int markY = [self pan].y;
CGContextRef context = UIGraphicsGetCurrentContext();
for(int i = 0; i < 1000; i++)//1,000,000
{
for(int j = 0; j < 1000; j++)
{
CGContextMoveToPoint(context, markX, markY);
CGContextAddLineToPoint(context, markX, markY + 10);
CGContextAddLineToPoint(context, markX + 10, markY + 10);
CGContextAddLineToPoint(context, markX + 10, markY);
CGContextAddLineToPoint(context, markX, markY);
CGContextStrokePath(context);
markX+=12;
}
markY += 12;
markX = reset;
}
}
The pan just move the array of boxes around on screen with pan gesture. Any help or hints would greatly appreciated.
The key issue with your example is that it is not optimized. Whenever drawRect: is called, the device is rendering all 1,000,000 squares. Worse still, it's making 6,000,000 calls to those APIs in the loop. If you want to refresh this view at even a modest 30fps, that is 180,000,000 calls / second.
With your 'simple' example, the size of the draw area is 12,000px × 12,000px; the maximum area you can display on the iPad's display is 768×1024 (assuming full-screen portrait). Therefore, the code is wasting a lot of CPU resources drawing outside the visible area. UIKit has ways of handling this scenario with relative ease.
When managing content that is significantly larger than the visible area, you should limit drawing to only that which is visible. UIKit has a couple of ways of handing this; UIScrollView in combination with a view backed by a CATiledLayer is your best bet.
Steps:
Disclaimer: This is specifically an optimization of your example code above
Create a new View Based Application iPad project
Add a reference to the QuartzCore.framework
Create a new class, say MyLargeView, subclassed from UIView and add the following code:
:
#import <QuartzCore/QuartzCore.h>
#implementation MyLargeView
- (void)awakeFromNib {
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
tiledLayer.tileSize = CGSizeMake(512.0f, 512.0f);
}
// Set the layer's class to be CATiledLayer.
+ (Class)layerClass {
return [CATiledLayer class];
}
- (void)drawRect:(CGRect)rect {
// Drawing code
// only draws what is specified by the rect parameter
CGContextRef context = UIGraphicsGetCurrentContext();
// set up some constants for the objects being drawn
const CGFloat width = 10.0f; // width of rect
const CGFloat height = 10.0f; // height of rect
const CGFloat xSpace = 4.0f; // space between cells (horizontal)
const CGFloat ySpace = 4.0f; // space between cells (vertical)
const CGFloat tWidth = width + xSpace; // total width of cell
const CGFloat tHeight = height + ySpace;// total height of cell
CGFloat xStart = floorf(rect.origin.x / tWidth); // first visible cell (column)
CGFloat yStart = floorf(rect.origin.y / tHeight); // first visible cell (row)
CGFloat xCells = rect.size.width / tWidth + 1; // number of horizontal visible cells
CGFloat yCells = rect.size.height / tHeight + 1; // number of vertical visible cells
for(int x = xStart; x < (xStart + xCells); x++) {
for(int y = yStart; y < (yStart + yCells); y++) {
CGFloat xpos = x*tWidth;
CGFloat ypos = y*tHeight;
CGContextMoveToPoint(context, xpos, ypos);
CGContextAddLineToPoint(context, xpos, ypos + height);
CGContextAddLineToPoint(context, xpos + width, ypos + height);
CGContextAddLineToPoint(context, xpos + width, ypos);
CGContextAddLineToPoint(context, xpos, ypos);
CGContextStrokePath(context);
}
}
}
#end
Edit the view controller nib and add a UIScrollView to the view
Add a UIView to the UIScrollView and make sure it fills the UIScrollView
Change the class to MyLargeView
Set frame size of MyLargeView to 12,000×12,000
Finally, open up the view controller .m file and add the following override:
:
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
UIScrollView *scrollView = [self.view.subviews objectAtIndex:0];
scrollView.contentSize = CGSizeMake(12000, 12000);
}
If you look at the drawRect: call, it is only drawing into the area specified by the rect parameter, which will correspond to the tile size (512×512) for the CATiledLayer we configured in the awakeFromNib method. This will scale to a 1,000,000×1,000,000 pixel canvas.
Alternatives to look at are the ScrollViewSuite example, specifically 3_Tiling.
OpenGL is GPU hardware accelerated on iOS devices. Core Graphics drawing is not, and can be many many times slower when dealing with a large number of small graphics primitives (lines).
For lots of small squares, just writing them into a bitmap in C code is faster than Core Graphics line drawing. Then just draw the bitmap to the view once when done. But Open GL would be even faster.
point 4. OpenGL should do that fine. Check if you could reuse those objects and whether you could move some of the logic to GLSL code.
OpenGL performance optimization (in context of WebGL but most of it should apply): http://www.youtube.com/watch?v=rfQ8rKGTVlg
I don't know the details of iOS history so this may not have been an option when the question was first posted. However, I wanted to call out CAShapeLayer as a simple option when dealing with path performance problems. "iOS Core Animation: Advanced Techniques" (find it on Google Books) says CAShapeLayer "uses hardware-accelerated drawing" which I'm taking to mean that it's a GPU-based implementation. The same book has a good usage example in chapter 6, which boils down to this:
Create a CAShapeLayer
Configure its lineWidth, fillColor, strokeColor, etc.
Add the layer as a sublayer of your view's containerView.layer
To draw a path, just set it to the layer's "path" property
This made a gigantic performance difference in my app, as measured by Instruments. If your performance problem is path-based, don't wade into OpenGL before you've tried CAShapeLayer.
I encountered the same problem. After endless searching on google,CAShapeLayer saved me finally! Here is the detail steps you need to do:
Create a view with CAShapeLayer as it's layer type by override UIView's + (Class)layerClass method
Configure the layer's lineWidth, fillColor, strokeColor, etc.
Create an UIBezierPath instance
To draw a path,use UIBezierPath instance to add lines,curve,or acr etc, after you finished drawing, just set bezierPath.CGPath to the
layer's "path" property
Here is a simple demo to draw a simple curve when you touch the demo view:
//Simple ShapelayerView.m
-(instancetype)init {
self = [super init];
if (self) {
_bezierPath = [UIBezierPath bezierPath];
CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.strokeColor = [UIColor yellowColor].CGColor;
shapeLayer.fillColor = [UIColor blueColor].CGColor;
}
return self;
}
+ (Class)layerClass {
return [CAShapeLayer class];
}
- (void) customDrawShape {
CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer;
[_bezierPath removeAllPoints];
[_bezierPath moveToPoint:CGPointMake(10, 10)];
[_bezierPath addQuadCurveToPoint:CGPointMake(2, 2) controlPoint:CGPointMake(50, 50)];
shapeLayer.path = _bezierPath.CGPath;
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[super touchesBegan:touches withEvent:event];
[self customDrawShape];
}

Core Graphics: Drawing along a path with a normal gradient

There are a number of resources here and elsewhere on the web regarding how to draw with a gradient - fill or stroke.
However, AFAICT, none addresses the following requirement: how to draw a path with a normal gradient, whereby normal means orthogonal to the path. The net effect could be something like toothpaste or a tube when applied with a dark->light->dark linear gradient. Here is this idea in the case of a round rectangle:
round-rect tube http://muys.net/cadre_blanc.png
(this was hand drawn and the corners are not very good).
In the specific case of the round rect, I think I can achieve this effect with 4 linear gradients (the sides) and 4 radial gradients (the corners). But is there better?
Is there an easy solution for any path?
The only "easy" solution I can think of would be to stroke the path multiple times, reducing the stroke width and changing the color slightly each time, to simulate a gradient.
Obviously, this could be an expensive operation for complex paths so you would want to cache the result if possible.
#define RKRandom(x) (arc4random() % ((NSUInteger)(x) + 1))
#implementation StrokeView
- (void)drawRect:(NSRect)rect
{
NSRect bounds = self.bounds;
//first draw using Core Graphics calls
CGContextRef c = [[NSGraphicsContext currentContext] graphicsPort];
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, NSMidX(bounds), NSMidY(bounds));
CGContextSetMiterLimit(c,90.0);
CGContextSetLineJoin(c, kCGLineJoinRound);
CGContextSetLineCap(c, kCGLineCapRound);
for(NSUInteger f = 0; f < 20; f++)
{
CGPathAddCurveToPoint(
path,
NULL,
(CGFloat)RKRandom((NSInteger)NSWidth(bounds)) + NSMinX(bounds),
(CGFloat)RKRandom((NSInteger)NSHeight(bounds)) + NSMinY(bounds),
(CGFloat)RKRandom((NSInteger)NSWidth(bounds)) + NSMinX(bounds),
(CGFloat)RKRandom((NSInteger)NSHeight(bounds)) + NSMinY(bounds),
(CGFloat)RKRandom((NSInteger)NSWidth(bounds)) + NSMinX(bounds),
(CGFloat)RKRandom((NSInteger)NSHeight(bounds)) + NSMinY(bounds)
);
}
for(NSInteger i = 0; i < 8; i+=2)
{
CGContextSetLineWidth(c, 8.0 - (CGFloat)i);
CGFloat tint = (CGFloat)i * 0.15;
CGContextSetRGBStrokeColor (
c,
1.0,
tint,
tint,
1.0
);
CGContextAddPath(c, path);
CGContextStrokePath(c);
}
CGPathRelease(path);
//now draw using Cocoa drawing
NSBezierPath* cocoaPath = [NSBezierPath bezierPathWithRoundedRect:NSInsetRect(self.bounds, 20.0, 20.0) xRadius:10.0 yRadius:10.0];
for(NSInteger i = 0; i < 8; i+=2)
{
[cocoaPath setLineWidth:8.0 - (CGFloat)i];
CGFloat tint = (CGFloat)i * 0.15;
NSColor* color = [NSColor colorWithCalibratedRed:tint green:tint blue:1.0 alpha:1.0];
[color set];
[cocoaPath stroke];
}
}
#end

Resources