RotateAndTranslateADrawing

Crowd-sourced documentation and tutorials by the Cocoa developer community
Jump to: navigation, search


Here's an example that shows how to spin an object centered about a point in a view.

SpinnerView.h

  1. import <Cocoa/Cocoa.h>

@interface SpinnerView : NSView {

   float rad;

} @end

SpinnerView.m

  1. import "SpinnerView.h"

@implementation SpinnerView

- (id)initWithFrame:(NSRect)frameRect { if ((self = [super initWithFrame:frameRect]) != nil) { // Add initialization code here } return self; }

- (float)slopeInRadiansFromVector:(NSSize)vector {

   float pi = 3.14159265359f;
   float vMag = sqrt(vector.width  vector.width + vector.height  vector.height); 
   return (vector.height >= 0.0f) ? acos(vector.width / vMag) : 2  pi - acos(vector.width / vMag);

}

- (void)drawLeftRightCursorWithRotation:(float)rotation {

   /////////////////////////////////////////////////////////////////
   //
   // arrow dimensions and drawing math
   //
   /////////////////////////////////////////////////////////////////
   
   float dim = 32.0;                               // overall length of double headed arrow
   float halfDim = dim / 2.0f;                     // half arrow length
   float triDim = dim / 3.5f;                      // arrow head dimension
   float inset = 1.0f;                             // arrow outline width
   float insetFactor = 1.0f + sqrt(2.0f);          // right triangle factor
   NSPoint left = NSMakePoint(-halfDim, 0.0f);     // point of left arrow head
   NSPoint right = NSMakePoint(halfDim, 0.0f);     // point of right arrow head
   // where the drawing origin will be translated (origin translated to center of view)
   NSRect visibleRect = [self visibleRect];
   NSPoint offset = NSMakePoint(NSMidX(visibleRect), NSMidY(visibleRect));
   
   /////////////////////////////////////////////////////////////////
   //
   // rotate and translate graphics state
   //
   /////////////////////////////////////////////////////////////////
   
   // remember the current graphics state
   [[NSGraphicsContext currentContext] saveGraphicsState];
   
   // translate and rotate graphics state (order is important, translate first before rotating)
   NSAffineTransform transform = [[[NSAffineTransform]] transform];
   [transform translateXBy:offset.x yBy:offset.y];
   [transform rotateByRadians:rotation];
   [transform concat];
   
   /////////////////////////////////////////////////////////////////
   //
   // resizeLeftRightCursor drawing centered at origin
   //
   /////////////////////////////////////////////////////////////////
   NSBezierPath bp = [[[NSBezierPath]] bezierPath];
   [[NSColor whiteColor] set];
   // arrow shaft background
   [bp moveToPoint:NSMakePoint(left.x + triDim - inset, left.y)];
   [bp lineToPoint:NSMakePoint(right.x - triDim + inset, right.y)];
   [bp setLineWidth:inset  3.0f];
   [bp stroke];
   // left arrowhead background
   [bp removeAllPoints]; [bp moveToPoint:left];
   [bp lineToPoint:NSMakePoint(left.x + triDim, triDim)];
   [bp lineToPoint:NSMakePoint(left.x + triDim, -triDim)];
   [bp lineToPoint:left];
   [bp fill];
   // right arrowhead background
   [bp removeAllPoints]; [bp moveToPoint:right];
   [bp lineToPoint:NSMakePoint(right.x - triDim, triDim)];
   [bp lineToPoint:NSMakePoint(right.x - triDim, -triDim)];
   [bp lineToPoint:right];
   [bp fill];
   [[NSColor blackColor] set];
   // left arrowhead
   [bp removeAllPoints]; [bp moveToPoint:NSMakePoint(left.x + inset  1.414, left.y)];
   [bp lineToPoint:NSMakePoint(left.x + triDim - inset, triDim - inset  insetFactor)];
   [bp lineToPoint:NSMakePoint(left.x + triDim - inset, inset  insetFactor - triDim)];
   [bp lineToPoint:NSMakePoint(left.x + inset  1.414, left.y)];
   [bp fill];
   // right arrowhead
   [bp removeAllPoints]; [bp moveToPoint:NSMakePoint(right.x - inset  1.414, right.y)];
   [bp lineToPoint:NSMakePoint(right.x - triDim + inset, triDim - inset  insetFactor)];
   [bp lineToPoint:NSMakePoint(right.x - triDim + inset, inset  insetFactor - triDim)];
   [bp lineToPoint:NSMakePoint(right.x - inset  1.414, right.y)];
   [bp fill];
   // arrow shaft 
   [bp removeAllPoints];
   [bp moveToPoint:NSMakePoint(left.x + triDim - inset, left.y)];
   [bp lineToPoint:NSMakePoint(right.x - triDim + inset, right.y)];
   [bp setLineWidth:inset];
   [bp stroke];
   // leave the graphics state as you found it
   [[NSGraphicsContext currentContext] restoreGraphicsState];

}

- (void)drawRect:(NSRect)rect {

   [[NSColor blueColor] set]; NSRectFill(rect);
   BOOL animate = YES;
   if (animate) rad += 0.1f;
   else rad = [self slopeInRadiansFromVector:NSMakeSize(1.0f, 1.0f)];
   [self drawLeftRightCursorWithRotation:rad];
   [self performSelector:@selector(display) withObject:nil afterDelay:0.01f];

}

@end

This code gives you the option to animate the spinning motion of a double headed arrow about an offset point or to simply rotate the double headed arrow about an offset point. The offset point is currently set to the center of the view, but you can easily change this location to any point you wish by changing the line:

   NSPoint offset = NSMakePoint(NSMidX(visibleRect), NSMidY(visibleRect));

If you set the "animate" variable to "NO", you will set the amount the drawing will be rotated to a fixed angle associated with the slope of a vector. The line

   else rad = [self slopeInRadiansFromVector:NSMakeSize(1.0f, 1.0f)];

sets the rotation (in radians) to pie over four (i.e. 45 degrees).


--zootbobbalu


Nice code. Puts a new 'spin' on an old cursor. But..."pie over four"? That takes the cake. A little half-baked, in other words. Are you completely crustworthy?


LOL!! that's pretty funny!! Just making sure everyone was paying attention ;-)

--zootbobbalu


Here's a simple example if the example above is hard to understand

SpinnerView.h

  1. import <Cocoa/Cocoa.h>

@interface SpinnerView : NSView {

   float rad;

} @end

SpinnerView.m

  1. import "SpinnerView.h"

@implementation SpinnerView

- (void)drawRect:(NSRect)rect {

   [[NSColor blueColor] set]; NSRectFill(rect);
   rad += 0.1f;
   
   // make a rectangle centered about the origin
   float width = 100.0f, height = 50.0f;    
   NSRect greenRect = NSMakeRect(-width / 2.0f, -height / 2.0f, width, height);
   
   // remember the current graphics state
   [[NSGraphicsContext currentContext] saveGraphicsState];
   // where the drawing origin will be translated (origin translated to center of view)
   NSRect visibleRect = [self visibleRect];
   NSPoint offset = NSMakePoint(NSMidX(visibleRect), NSMidY(visibleRect));
   NSAffineTransform transform = [[[NSAffineTransform]] transform];
   // translate and rotate graphics state (order is important, translate first before rotating)
   [transform translateXBy:offset.x yBy:offset.y];
   [transform rotateByRadians:rad];
   [transform concat];
   // draw a green rectangle that has been translated by "offset" and rotated "rad" radians 
   [[NSColor greenColor] set];
   NSRectFill(greenRect);
   // leave the graphics state as you found it
   [[NSGraphicsContext currentContext] restoreGraphicsState]; 
   [self performSelector:@selector(display) withObject:nil afterDelay:0.01f];

}

@end

--zootbobbalu


See also QuartzShapeDrawing

Warning: Both above examples use

   [self performSelector:@selector(display) withObject:nil afterDelay:0.01f];

I don't think it is strictly correct to perform a method, -display, that does not take an argument but pass an argument anyway.


It's also a really bad way to do animation. The correct way would be to create an NSTimer to call a method which calls setNeedsDisplay:YES. That way you can do things like easily cancel the animation, change its rate, etc.


From Apple's documentation on performSelector:withObject:afterDelay: -> Sends an aSelector message to the receiver sometime after delay. This method returns before the aSelector message is sent. The aSelector method should not have a significant return value and should take a single argument of type id, or no arguments; anArgument will be the argument passed in the message, or nil if the method does not take any arguments. Note that self and anArgument are retained until after the message is sent. See �Selectors� for a description of the SEL type.

I agree that a timer would be better. I wrote this example code mainly to demonstrate how to use transforms and before I figured out that you don't have to retain an NSTimer object (the run loop will retain the timer object for you after it has been added). Normally I use timers to animate things, so I do agree that calling setNeedsDisplay:YES and using a timer is the proper way to animate.

One side note on performSelector:withObject:afterDelay:. You can cancel a delayed method call with cancelPreviousPerformRequestsWithTarget:selector:object: or cancelPreviousPerformRequestsWithTarget:. This is nice, because you can filter delayed method calls by using the single argument as a key.

- (void)awakeFromNib {

   [self performSelector:@selector(delayedAction:) withObject:[[[NSNumber]] numberWithFloat:1.0f] afterDelay:1.0f];
   [self performSelector:@selector(delayedAction:) withObject:[[[NSNumber]] numberWithFloat:2.0f] afterDelay:2.0f];

}

- (void)delayedAction:(NSNumber )delayValue {

   [self cancelPreviousPerformRequestsWithTarget:self selector:@selector(delayedAction:) object:delayValue];
   [self performSelector:@selector(delayedAction:) withObject:delayValue afterDelay:[delayValue floatValue]];

}

Since you have the freedom to choose the selector, target and the single argument and the ability to cancel delayed requests based on the arguments used to create the request, using performSelector:withObject:afterDelay: and cancelPreviousPerformRequestsWithTarget:selector:object: is a convenient way to organize timed actions. Animations that require high precision should stick to using NSTimers, but all other cases are well suited for using performSelector:withObject:afterDelay:.

--zootbobbalu

Personal tools
Namespaces

Variants
Actions
Navigation
Toolbox