« Drop Shadows
// Introduction
Now that we have Rotate using controls and mouse events to handle all of our transformations, let's jazz up the drawing. We'll do it by adding a drop shadow to the path that we're drawing. Sounds difficult, right? Thanks to CoreGraphics and ShadowView, a NSView subclass by Andrew Zamler-Carhart, it's a fairly simple item to add.
For this tutorial, you'll need to download ShadowView.h and ShadowView.m. ShadowView adds the shadow functionality that's built into CoreGraphics to NSView. It works in a straightforward manner that I'll leave you to investigate on your own. This is after all Cocoa and we don't need to reinvent the wheel every time we reuse a feature.
WARNING: The API that is being accessed in this tutorial is a private, undocumented API. Use with caution. This functionality may change in a future system update.
Figure 1: The path shadowed.
// Preliminaries
Before we get started on implementing the shadows, let's get the interface tweaks out of the way. Drop shadows have four input parameters that define their appearance. We'll add a slider with a text field for each one. In addition, we will add a check box control to draw the bezier path as a filled shape. Lay out the controls as shown in Figure 2.
The following outlets and actions need to be added to the Controller class and connected to the appropriate control.
IBOutlet NSTextField *heightField; IBOutlet NSSlider *heightSlider; IBOutlet NSTextField *azimuthField; IBOutlet ABCRotation *azimuthSlider; IBOutlet NSTextField *radiusField; IBOutlet NSSlider *radiusSlider; IBOutlet NSTextField *kaField; IBOutlet NSSlider *kaSlider; - (IBAction)setHeight:(id)sender; - (IBAction)setAzimuth:(id)sender; - (IBAction)setRadius:(id)sender; - (IBAction)setKa:(id)sender; - (IBAction)toggleFill:(id)sender;
For each of the actions we'll simply pass the values into our view's class. While we're adding the outlets, we'll finish off the Controller class by modifying setFields to include the new controls.
- (void)setFields { /* ... */ [azimuthField setIntValue:[view azimuth]]; [azimuthSlider setIntValue:[view azimuth]]; [heightField setIntValue:[view height]]; [heightSlider setIntValue:[view height]]; [kaField setFloatValue:[view ka]]; [kaSlider setFloatValue:[view ka]]; [radiusField setIntValue:[view radius]]; [radiusSlider setIntValue:[view radius]]; } - (IBAction)setAzimuth:(id)sender { [view setAzimuth:[sender intValue]]; [self setFields]; [view setNeedsDisplay:YES]; } - (IBAction)setHeight:(id)sender { [view setHeight:[sender intValue]]; [self setFields]; [view setNeedsDisplay:YES]; } - (IBAction)setRadius:(id)sender { [view setRadius:[sender intValue]]; [self setFields]; [view setNeedsDisplay:YES]; } - (IBAction)setKa:(id)sender { [view setKa:[sender floatValue]]; [self setFields]; [view setNeedsDisplay:YES]; } - (IBAction)toggleFill:(id)sender { if ([view fill]) { [view setFill:NO]; } else { [view setFill:YES]; } [view setNeedsDisplay:YES]; }
// Mutating Access
Now that we have the Controller completed, we have to add all the mutator and accessor methods that we called and their instance variables to RotateView. Add the following variables and create their methods with the given format.
BOOL fill; int azimuth; int height; int radius; float ka; -(void)setKa:(float)value { ka = value; } -(float)ka { return ka; }
Next, we'll modify the initWithFrame to give initial values for all the variables we just added. These values will give a nice, soft shadow.
- (id)initWithFrame:(NSRect)frame { /* ... */ height = 3; azimuth = 90; radius = 4; ka = .6; } return self; }
// Where the Shadows Lie
Now that the boring stuff is out of the way, let's start the fun stuff. The first thing we have to do is change RotateView to be a subclass of ShadowView instead of NSView. To do this we'll change @interface RotateView : NSView in RotateView.h to span class="code">@interface RotateView : ShadowView
Finally, all we have to do is state what we'll draw shadows of. We do this in the drawRect method. At the top of the method we call showShadowHeight:radius:azimuth:ka: and pass to it the four values we have stored. This will turn the shadows on and anything that is drawn after it will have a shadow. We then draw everything out the way we previously had been. At the end of drawRect we call the hideShadow method to turn the shadows off.
- (void)drawRect:(NSRect)rect { /* ... */ NSAffineTransform *transform; [self showShadowHeight:[self height] radius:[self radius] azimuth:[self azimuth] ka:[self ka]]; /* ... */ [rawPath release]; [self hideShadow]; }
We have one other small piece of code to add, the filling of the bezier path. In drawRect we'll simply check if the fill flag is on and act appropriately.
- (void)drawRect:(NSRect)rect { /* ... */ [drawPath stroke]; if (fill) { [drawPath fill]; } [rawPath release]; /* ... */ }
// Shadow Anatomy 101
So, now that we're passing the variables all over the place, what do they mean? Let's look at what each one is and how it affects the shadow.
The first parameter is height. It controls the amount of separation between the object and its shadow. As the height value is increased, the shadow moves away from the object as if the object were being raised off the surface it is resting on.
Figure 3: Height parameter at 10 (a) and 50 (b). For both, Radius = 4, Ka= .6, Azimuth = 90.
The second parameter is the radius. It controls the focus of the shadow. At low values, the image is crisp. It then gets blurry as the value increases, making for softer shadows.
Figure 4: Radius parameter at 5 (a) and 50 (b). For both, Height = 50, Ka= .6, Azimuth = 90.
The third parameter is the azimuth. It controls the direction from which the light originates. The shadow will therefore fall in the opposite direction. For example, if the azimuth is set to 45 degrees, the shadow will fall at 225 degrees.
Figure 5: Azimuth parameter at 145 (a) and 325 (b). For both, Height = 50, Radius= 5, Ka= .6.
The final parameter is Ka. It is the ambient light coefficient which controls how much ambient light exists. In a nutshell, it will control how dark the shadow is. At a value of zero, there is no ambient light. Only the main light source illuminates the surface, which creates a shadow with maximum darkness. When the parameter is one, the ambient light is at the same intensity as the main light source. The result is no visible shadow since the ambient light will illuminate the shadowed area to the same level that the main light source illuminates the area outside the shadow.
Figure 6: Ka parameter at .1 (a) and .8 (b). For both, Height = 50, Radius= 5, Azimuth = 90.
// Conclusion
That's all it takes to shadow your drawings. If you get some really strange and unexpected results, make sure that hideShadow was called after your drawing routine. It will probably be the main point of error when using ShadowView.
You can also download a copy of my project from here. Tom Waters also has the program TestShadow available that gives a view to experiment with the four parameters.
Many of the links in this article are broken. For example, those to ShadowView.[hm] & Rotate.sit
Posted by: Anonymous on January 2, 2003 06:01 AMLooks like someone else noticed the links problem!
Hmm, thought I had those working. I'll take a look at it tonight when I get home.
Posted by: Brad on February 11, 2003 03:18 PMWhere do the circular sliders come from ? I haven't got those in my Interface Builder...
Posted by: Ulrik on March 15, 2003 12:36 PMI tried to use ShadowView with text, but the text get clipped at the bottom and to the right. Does anyone know how to fix this? Should I call enlargeFrame myself?
Posted by: Kristian on May 30, 2003 10:32 PMpanther introduced NSShadow which can accomplish similar results using a public api; panther only.
Posted by: jean-pierre on December 7, 2003 02:10 AM