Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to draw a tapered line + oval shadow in Cocoa

Background:

The shot below is of Mail.app in OS X Lion. When the source list gets too long, a nice shadowy line appears just above the buttons at the bottom of the source list. When you scroll, the source list moves under that shadowy line. When you expand the window so that everything in the source list fits without scrolling, the shadowy line disappears.

The question:

How can I draw this shadowy line using Cocoa? I'm aware of NSShadow and such, but it seems to me there's more going on here than just a shadow. There's a line that subtly fades to points (as if you applied a gradient mask to each end in Photoshop.) Likewise, the shadow is oval and tapers off as you approach the end of the lines. So it's not just a regular NSShadow, is it? (It's definitely not an image, as it scales nicely when you expand the width of the source view.)

Any tips on how to approach drawing this shape would be greatly appreciated.

enter image description here

And for the sticklers out there, no, this does not violate the NDA, as Mail.app has been shown publicly by Apple.

like image 514
Bryan Avatar asked Jun 16 '11 02:06

Bryan


1 Answers

General Idea:

.

  1. Create a layer "Layer A" with dimensions 150px × 10px
    and fill it with a Gradient with:
    • lower color: #535e71 opacity: 33%
    • upper color: #535e71 opacity: 0%
  2. Create a layer "Layer B" with dimensions 150px × 1px
    and fill it with solid #535e71 opacity: 50%
  3. Compose "Layer A" and "Layer B" together into "Layer C".
  4. Apply reflected gradient mask from #ffffff to #000000 to "Layer C".

Visual Steps:

enter image description here

Functional Code:

MyView.h:

#import <Cocoa/Cocoa.h>

@interface MyView : NSView {
@private

}

@end

MyView.m:

#import "MyView.h"

@implementation MyView

- (CGImageRef)maskForRect:(NSRect)dirtyRect {
    NSSize size = [self bounds].size;
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);

    CGContextClipToRect(context, *(CGRect*)&dirtyRect);

    CGRect rect = CGRectMake(0.0, 0.0, size.width, size.height);

    size_t num_locations = 3;
    CGFloat locations[3] = { 0.0, 0.5, 1.0 };
    CGFloat components[12] = {
        1.0, 1.0, 1.0, 1.0,  // Start color
        0.0, 0.0, 0.0, 1.0,  // Middle color
        1.0, 1.0, 1.0, 1.0,  // End color
    };

    CGGradientRef myGradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, num_locations);

    CGPoint myStartPoint = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPoint myEndPoint = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));

    CGContextDrawLinearGradient(context, myGradient, myStartPoint, myEndPoint, 0);

    CGImageRef theImage = CGBitmapContextCreateImage(context);
    CGImageRef theMask = CGImageMaskCreate(CGImageGetWidth(theImage), CGImageGetHeight(theImage), CGImageGetBitsPerComponent(theImage), CGImageGetBitsPerPixel(theImage), CGImageGetBytesPerRow(theImage), CGImageGetDataProvider(theImage), NULL, YES);

    [(id)theMask autorelease];

    CGColorSpaceRelease(colorSpace);
    CGContextRelease(context);

    return theMask;
}

- (void)drawRect:(NSRect)dirtyRect {
    NSRect nsRect = [self bounds];
    CGRect rect = *(CGRect*)&nsRect;
    CGRect lineRect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, (CGFloat)1.0);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    CGContextRef context = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort];
    CGContextClipToRect(context, *(CGRect*)&dirtyRect);
    CGContextClipToMask(context, rect, [self maskForRect:dirtyRect]);

    size_t num_locations = 2;
    CGFloat locations[2] = { 0.0, 1.0 };
    CGFloat components[8] = {
        0.315, 0.371, 0.450, 0.3,  // Bottom color
        0.315, 0.371, 0.450, 0.0  // Top color
    };

    CGGradientRef myGradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, num_locations);

    CGPoint myStartPoint = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
    CGPoint myEndPoint = CGPointMake(CGRectGetMinX(rect), CGRectGetMaxY(rect));

    CGContextDrawLinearGradient(context, myGradient, myStartPoint, myEndPoint, 0);

    CGContextSetRGBFillColor(context, 0.315, 0.371, 0.450, 0.5 );
    CGContextFillRect(context, lineRect);

    CGColorSpaceRelease(colorSpace);    
}

@end

(My first time using pure low-level CoreGraphics, thus possibly sub-par optimal, open for improvements.)

This is an actual screenshot of what the code above produces:
enter image description here
The drawing stretches to the view's dimensions.

(I formerly had two techniques shown here: "Technique A" & "Technique B".
"Technique B" provided superior results and was way simpler to implement as well, so I ditched "Technique A".
Some comments may still refer to "Technique A" though. Just ignore them and enjoy the fully functional code snippet.).

like image 95
Regexident Avatar answered Nov 15 '22 16:11

Regexident