I want to implement freeform drawing in my app. First, I tried the code inside drawLayer:inContext: and it gave me the result I wanted.
Drawing in CALayer:
But when I decided to implement the code inside drawRect:, this happened:
Even if I draw inside the white space, the drawing is rendered outside as shown above. The code I used is exactly the same. I copy-pasted it from drawLayer:inContext: to drawRect:. I didn't change a thing, so why is this happening?
The Code:
CGContextSaveGState(ctx);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, 1.0);
CGContextSetRGBStrokeColor(ctx, 1, 0, 0, 1);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, prevPoint.x, prevPoint.y);
for (NSValue *r in drawnPoints){
CGPoint pt = [r CGPointValue];
CGContextAddLineToPoint(ctx, pt.x, pt.y);
}
CGContextStrokePath(ctx);
CGContextRestoreGState(ctx);
I see you are using app in full screen mode where the view is centered and does not take full width of the screen.
It may be that CALayer has transform applied to it that translates the drawing from the left side of the screen to the center. This may not be the case with drawRect:. Try setting CGContext's transform matrix:
CGContextSaveGState(ctx);
CGFloat xOffset = CGRectGetMidX(screenFrame) - CGRectGetMidX(viewFrame);
CGContextTranslateCTM(ctx, xOffset, 0.0f);
// rest of drawing code
// ...
CGContextRestoreGState(ctx);
Related
I followed another StackOverflow post that explains how i could override the draw method of a PDFAnnotation so i could draw a picture instead of a traditional PDFAnnotation.
But sadly i was not able to achieve that and the annotation that is drawn on top of my pdf is still a regular one.
This is the code that i used :
#implementation PDFImageAnnotation { UIImage * _picture;
CGRect _bounds;};
-(instancetype)initWithPicture:(nonnull UIImage *)picture bounds:(CGRect) bounds{
self = [super initWithBounds:bounds
forType:PDFAnnotationSubtypeWidget
withProperties:nil];
if(self){
_picture = picture;
_bounds = bounds;
}
return self;
}
- (void)drawWithBox:(PDFDisplayBox) box
inContext:(CGContextRef)context {
[super drawWithBox:box inContext:context];
[_picture drawInRect:_bounds];
CGContextRestoreGState(context);
UIGraphicsPushContext(context);
};
#end
Does someone know how i could override the draw method so i could draw a custom Annotation ?
Thank You !
ps: i also tried to followed the tutorial on the apple dev site.
UPDATE :
Now i'm able to draw pictures using CGContextDrawImage but i'm not able to flip coordinates back in place. when i do that mi pictures are not drawn and it seems that they are put outside of the page but i'm not sure.
This is my new code :
- (void)drawWithBox:(PDFDisplayBox) box
inContext:(CGContextRef)context {
[super drawWithBox:box inContext:context];
UIGraphicsPushContext(context);
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0.0, _pdfView.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, _bounds, _picture.CGImage);
CGContextRestoreGState(context);
UIGraphicsPopContext();
}
I also tried to follow the tutorial on the Apple dev site.
Which one?
Custom Graphics
Adding Custom Graphics to a PDF
Because both include UIGraphicsPushContext(context) & CGContextSaveGState(context) calls, but your code doesn't. Do not blindly copy & paste examples, try to understand them. Read what these two calls do.
Fixed code:
- (void)drawWithBox:(PDFDisplayBox) box
inContext:(CGContextRef)context {
[super drawWithBox:box inContext:context];
UIGraphicsPushContext(context);
CGContextSaveGState(context);
[_picture drawInRect:_bounds];
CGContextRestoreGState(context);
UIGraphicsPopContext();
}
The image was drawn with CGRectMake(20, 20, 100, 100). It's upside down, because PDFPage coordinates are flipped (0, 0 = bottom/left). Leaving it as an exercise for OP.
Rotation
Your rotation code is wrong:
CGContextTranslateCTM(context, 0.0, _pdfView.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, _bounds, _picture.CGImage);
It's based on _pdfView bounds, but it should be based on the image bounds (_bounds). Here's the correct one:
- (void)drawWithBox:(PDFDisplayBox) box
inContext:(CGContextRef)context {
[super drawWithBox:box inContext:context];
UIGraphicsPushContext(context);
CGContextSaveGState(context);
CGContextTranslateCTM(context, _bounds.origin.x, _bounds.origin.y + _bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
[_picture drawInRect:CGRectMake(0, 0, _bounds.size.width, _bounds.size.height)];
CGContextRestoreGState(context);
UIGraphicsPopContext();
}
I used following code to draw line which works fine but How to move that drawn line within context by touch event?
- (void) drawLineFrom:(CGPoint)from to:(CGPoint)to width:(CGFloat)width
{
UIGraphicsBeginImageContext(self.frame.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextScaleCTM(ctx, 1.0f, -1.0f);
CGContextTranslateCTM(ctx, 0.0f, -self.frame.size.height);
if (drawImage != nil) {
CGRect rect = CGRectMake(0.0f, 0.0f, self.frame.size.width, self.frame.size.height);
CGContextDrawImage(ctx, rect, drawImage.CGImage);
}
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, width);
CGContextSetStrokeColorWithColor(ctx, self.drawColor.CGColor);
CGContextMoveToPoint(ctx, from.x, from.y);
CGContextAddLineToPoint(ctx, to.x, to.y);
CGContextStrokePath(ctx);
CGContextFlush(ctx);
drawImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
drawLayer.contents = (id)drawImage.CGImage;
}
How to get reference of drawn line from CGContext ? thanks in advance.
UIGraphicsBeginImageContext() begins an image context — one that is a grid of pixels.
drawImage = UIGraphicsGetImageFromCurrentImageContext(); and drawLayer.contents = (id)drawImage.CGImage; set the contents of a layer to a captured form of that grid of pixels.
The intermediate UIGraphicsEndImageContext() ends the context you had. The context no longer exists.
So to answer you question literally:
you can't ask an image context to tell you what was drawn to it and expect it to have any higher-level insight than the individual pixels that were plotted;
you can't ask the context you drew to anything, because it no longer exists.
The normal thing to do would be to create a UIView with a bunch of properties that describe whatever you want about the line. Implement -drawRect: and in there draw the line based on the properties. When you want to update the line, update the properties. Ensure the property setters make a call to setNeedsDisplay.
In UIKit things that are interactive should be subclasses of UIView. Views draw when requested to by the system. The normal pattern is pull, not push.
Searching the web for about 4 hours not getting an answer so:
How to draw a shadow on a path which has transparency?
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.8] CGColor]);
// Sample Path
CGContextMoveToPoint(c, 20.0, 10.0);
CGContextAddLineToPoint(c, 100.0, 40.0);
CGContextAddLineToPoint(c, 40.0, 70.0);
CGContextClosePath(c);
CGContextDrawPath(c, kCGPathFillStroke);
}
The first thing I notice, the shadow is only around the stroke. But that isn't the problem so far. The shadow behind the path/rect is still visible, which means: the shadow color is effecting the fill color of my path. The fill color should be white but instead its grey. How to solve this issue?
You will have to clip the context and draw twice.
First you create a reference to your path since you will have to use it a few times and save your graphics context so you can come back to it.
Then you clip the graphics context to a only draw outside of your path. This is done by adding your path to the path that covers the entire view. Once you have clipped you draw your path with the shadow so that it's draw on the outside.
Next you restore the graphics context to how it was before you clipped and draw your path again without the shadow.
It's going to look like this on an orange background (white background wasn't very visible)
The code to do the above drawing is this:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 2);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetFillColorWithColor(c, [[UIColor colorWithWhite:1.0 alpha:0.5] CGColor]);
// Sample Path
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 20.0, 10.0);
CGPathAddLineToPoint(path, NULL, 40.0, 70.0);
CGPathAddLineToPoint(path, NULL, 100.0, 40.0);
CGPathCloseSubpath(path);
// Save the state so we can undo the shadow and clipping later
CGContextSaveGState(c);
{ // Only for readability (so we know what are inside the save/restore scope
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGFloat width = CGRectGetWidth(self.frame);
CGFloat height = CGRectGetHeight(self.frame);
// Create a mask that covers the entire frame
CGContextMoveToPoint(c, 0, 0);
CGContextAddLineToPoint(c, width, 0);
CGContextAddLineToPoint(c, width, height);
CGContextAddLineToPoint(c, 0, height);
CGContextClosePath(c);
// Add the path (which by even-odd rule will remove it)
CGContextAddPath(c, path);
// Clip to that path (drawing will only happen outside our path)
CGContextClip(c);
// Now draw the path in the clipped context
CGContextAddPath(c, path);
CGContextDrawPath(c, kCGPathFillStroke);
}
CGContextRestoreGState(c); // Go back to before the clipping and before the shadow
// Draw the path without the shadow to get the transparent fill
CGContextAddPath(c, path);
CGContextDrawPath(c, kCGPathFillStroke);
}
If you want the entire shadow to be as strong and don't want the transparency of the fill color to make the shadow weaker then you can use a fully opaque color when filling the first time. It's going to get clipped so it won't be visible inside the path anyway. It will only affect the shadow.
Per your request in comments, here's a more in-depth exploration. Consider the following screenshot (StackOverflow shrinks it for me -- it helps to look at it full size.):
What you're seeing here is 5 different drawing approaches (top to bottom) over three different backgrounds (left to right). (I've also dropped the fill alpha from 0.8 to 0.5 to make the effects easier to see.) The three different drawing approaches are (top to bottom):
Just the stroke, not the fill, with a shadow
The way you posted in the code in your original question
The stroke and fill, with no shadow applied
Just the shadow, by itself
The way #DavidRönnqvist proposed in his answer.
The three different backgrounds should be self explanatory.
You said in your original question:
The first thing I notice, the shadow is only around the stroke.
This is why I included the first drawing approach. That's what it really looks like when there is just the stroke, with no fill, and (therefore) only the stroke is being shadowed.
Then, you said:
But that isn't the problem so far. The shadow behind the path/rect is
still visible, which means: the shadow color is effecting the fill
color of my path. The fill color should be white but instead its grey.
Your original code is the next version (#2). What you're seeing there is that the shadow for the stroke is darker than the shadow for the fill. This is because the stroke color's alpha is 1.0 and the fill's alpha is less than 1.0. This might be easier to see in version #4 which is just the shadow -- it's darker around the edge where the stroke is. Version #3 shows the drawing without a shadow. See you you can see the red and the image semi-obsurced in the fill of the shape? So in your original drawing you're seeing the object's own shadow through the object itself.
If that's not making sense, try thinking of a piece of glass that's got a tint to it (if you're into photography, think of a Neutral Density Filter). If you hold that glass between a light source and another surface, and then peek from the side and look just at the lower surface, you know that the semi-transparent glass is going to cast some shadow, but not as dark a shadow as something completely opaque (like a piece of cardboard). This is what you're seeing -- you're looking through the object at it's shadow.
Version #5 is #DavidRönnqvist's approach. The eye-fooling effect I was talking about in my comment is easiest to appreciate (for me, anyway) by looking at the shapes drawn over the image background. What it ends up looking like (in version #5) is that the shape is a bordered, copied, portion of the image that's been overlaid with a semi-transparent white mask of some sort. If you look back at version #3, it's clear, in the absence of the shadow, what's going on: you're looking through the semi-transparent shape at the image beneath. Then if you look at version #4, it's also clear that you have a shadow being cast by an object that's behind your eye/camera. From there, I would argue that that's also clear when looking at version #2 over the image what's going on (even if it's less clear over a solid color). At first glance, my eye/brain doesn't know what it's looking at in version #5 -- there's a moment of "visual dissonance" before I establish the mental model of "copied, masked, portion of the image floating above the original image."
So if that effect (#5) was what you were going for, then David's solution will work great. I just wanted to point out that it's sort of a non-intuitive effect.
Hope this is helpful. I've put the complete sample project I used to generate this screenshot on GitHub.
CGFloat lineWidth = 2.0f;
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGContextSetLineWidth(c, lineWidth);
CGContextSetStrokeColorWithColor(c, [[UIColor whiteColor] CGColor]);
CGContextSetShadowWithColor(c, CGSizeMake(0, 5), 5.0, [[UIColor blackColor]CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathStroke);
CGContextRestoreGState(c);
someRect.origin.x += lineWidth/2;
someRect.origin.y += lineWidth/2;
someRect.size.width -= lineWidth;
someRect.size.height -= lineWidth;
CGContextClearRect(c, someRect);
CGContextSetFillColorWithColor(c, [[[UIColor whiteColor] colorWithAlphaComponent:0.8] CGColor]);
CGContextAddRect(c, someRect);
CGContextDrawPath(c, kCGPathFill);
NSShadow* shadow = [[NSShadow alloc] init];
[shadow setShadowColor: [NSColor blackColor]];
[shadow setShadowOffset: NSMakeSize(2.1, -3.1)];
[shadow setShadowBlurRadius: 5];
NSBezierPath* bezierPath = [NSBezierPath bezierPath];
[bezierPath moveToPoint: NSMakePoint(12.5, 6.5)];
[bezierPath curveToPoint: NSMakePoint(52.5, 8.5) controlPoint1: NSMakePoint(40.5, 13.5) controlPoint2: NSMakePoint(52.5, 8.5)];
[bezierPath lineToPoint: NSMakePoint(115.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(150.5, 6.5)];
[bezierPath lineToPoint: NSMakePoint(201.5, 13.5)];
[bezierPath lineToPoint: NSMakePoint(222.5, 8.5)];
[NSGraphicsContext saveGraphicsState];
[shadow set];
[[NSColor blackColor] setStroke];
[bezierPath setLineWidth: 1];
[bezierPath stroke];
[NSGraphicsContext restoreGraphicsState];
I have an NSView that display an image, and i'd like to make this view acts like a cropping image effect. Then i make 3 rectangles (imageRect, secRect and IntersectRect), the imageRect is the rect which show an image, secRect is rect which just act to darken whole imageRect, and the intersectRect is a rect which like an observe rect, what i want to do is like make a "hole" on secRect to see directly into imageRect (without the darken). here's my drawRect method :
- (void)drawRect:(NSRect)rect {
// Drawing code here.
NSImage *image = [NSImage imageNamed:#"Lonely_Tree_by_sican.jpg"];
NSRect imageRect = [self bounds];
[image compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver ];
if (NSIntersectsRect([myDrawRect currentRect], [self bounds])) {
//get the intersectionRect
intersectionRect = NSIntersectionRect([myDrawRect currentRect], imageRect);
//draw the imageRect
[image compositeToPoint:imageRect.origin operation:NSCompositeSourceOver];
//draw the secRect and fill it with black and alpha 0.5
NSRect secRect = NSMakeRect(imageRect.origin.x, imageRect.origin.y, imageRect.size.width, imageRect.size.height);
[[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:0.0 alpha:0.5] set];
[NSBezierPath fillRect:secRect];
//have no idea for the intersectRect
/*[image compositeToPoint:intersectionRect.origin
fromRect:secLayer
operation:NSCompositeXOR
fraction:1.0];*/
}
//draw the rectangle
[myDrawRect beginDrawing];
}
I have my own class (myDrawRect) to draw a rectangle based on mouse click on [self bounds], so just ignore the beginDrawing command.
Any help would be fine, thanks. Hebbian.
You're doing far more work than you need to, and you're using deprecated methods (the compositeToPoint:operation: and compositeToPoint:fromRect:operation:fraction: methods) to do it.
All you need to do is send the image a single drawInRect:fromRect:operation:fraction: message. The fromRect: parameter is the rectangle you want to crop to; if you don't want to scale the cropped section, then the destination rect (the drawInRect: parameter) should have the same size.
About the only extra work you may need to do is if the image may be bigger than the view and you want to only draw the section that's within the view's bounds: When that happens, you'll need to inset the crop rectangle by the difference in size between the crop rectangle and the view bounds.
I currently have an NSView that draws a grid pattern (essentially a guide of horizontal and vertical lines) with the idea being that a user can change the spacing of the grid and the color of the grid.
The purpose of the grid is to act as a guideline for the user when lining up objects. Everything works just fine with one exception. When I resize the NSWindow by dragging the resize handle, if my grid spacing is particularly small (say 10 pixels). the drag resize becomes lethargic in nature.
My drawRect code for the grid is as follows:
-(void)drawRect:(NSRect)dirtyRect {
NSRect thisViewSize = [self bounds];
// Set the line color
[[NSColor colorWithDeviceRed:0
green:(255/255.0)
blue:(255/255.0)
alpha:1] set];
// Draw the vertical lines first
NSBezierPath * verticalLinePath = [NSBezierPath bezierPath];
int gridWidth = thisViewSize.size.width;
int gridHeight = thisViewSize.size.height;
int i;
while (i < gridWidth)
{
i = i + [self currentSpacing];
NSPoint startPoint = {i,0};
NSPoint endPoint = {i, gridHeight};
[verticalLinePath setLineWidth:1];
[verticalLinePath moveToPoint:startPoint];
[verticalLinePath lineToPoint:endPoint];
[verticalLinePath stroke];
}
// Draw the horizontal lines
NSBezierPath * horizontalLinePath = [NSBezierPath bezierPath];
i = 0;
while (i < gridHeight)
{
i = i + [self currentSpacing];
NSPoint startPoint = {0,i};
NSPoint endPoint = {gridWidth, i};
[horizontalLinePath setLineWidth:1];
[horizontalLinePath moveToPoint:startPoint];
[horizontalLinePath lineToPoint:endPoint];
[horizontalLinePath stroke];
}
}
I suspect this is entirely to do with the way that I am drawing the grid and am open to suggestions on how I might better go about it.
I can see where the inefficiency is coming in, drag-resizing the NSWindow is constantly calling the drawRect in this view as it resizes, and the closer the grid, the more calculations per pixel drag of the parent window.
I was thinking of hiding the view on the resize of the window, but it doesn't feel as dynamic. I want the user experience to be very smooth without any perceived delay or flickering.
Does anyone have any ideas on a better or more efficient method to drawing the grid?
All help, as always, very much appreciated.
You've inadvertently introduced a Schlemiel into your algorithm. Every time you call moveToPoint and lineToPoint in your loops, you are actually adding more lines to the same path, all of which will be drawn every time you call stroke on that path.
This means that you are drawing one line the first time through, two lines the second time through, three lines the third time, etc...
A quick fix would be to use a new path each time through the loop simply perform the stroke after the loop (with thanks to Jason Coco for the idea):
path = [NSBezierPath path];
while (...)
{
...
[path setLineWidth:1];
[path moveToPoint:startPoint];
[path lineToPoint:endPoint];
}
[path stroke];
Update: Another approach would be to avoid creating that NSBezierPath altogether, and just use the strokeLineFromPoint:toPoint: class method:
[NSBezierPath setDefaultLineWidth:1];
while (...)
{
...
[NSBezierPath strokeLineFromPoint:startPoint toPoint:endPoint];
}
Update #2: I did some basic benchmarking on the approaches so far. I'm using a window sized 800x600 pixels, a grid spacing of ten pixels, and I'm having cocoa redraw the window a thousand times, scaling from 800x600 to 900x700 and back again. Running on my 2GHz Core Duo Intel MacBook, I see the following times:
Original method posted in question: 206.53 seconds
Calling stroke after the loops: 16.68 seconds
New path each time through the loop: 16.68 seconds
Using strokeLineFromPoint:toPoint: 16.68 seconds
This means that the slowdown was entirely caused by the repetition, and that any of the several micro-improvements do very little to actually speed things up. This shouldn't be much of a surprise, since the actual drawing of pixels on-screen is (almost always) far more processor-intensive than simple loops and mathematical operations.
Lessons to be learned:
Hidden Schlemiels can really slow things down.
Always profile your code before doing unnecessary optimization
You should run Instruments Cpu Sampler to determine where most of the time is being spent and then optimized based on that info. If it's the stroke, put it outside the loop. If it's drawing the path, try offloading the rendering to the gpu. See if CALayer can help.
Maybe to late for the party, however someone could find this helpful. Recently, I needed a custom components for a customer, in order to recreate a grid resizable overlay UIView. The following should to the work, without issues even with very little dimensions.
The code is for iPhone (UIView), but it can be ported to NSView very quickly.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
//corners
CGContextSetLineWidth(context, 5.0);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, 15, 0);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, 0, 15);
CGContextMoveToPoint(context, rect.size.width, 0);
CGContextAddLineToPoint(context, rect.size.width-15, 0);
CGContextMoveToPoint(context, rect.size.width, 0);
CGContextAddLineToPoint(context, rect.size.width, 15);
CGContextMoveToPoint(context, 0, rect.size.height);
CGContextAddLineToPoint(context, 15, rect.size.height);
CGContextMoveToPoint(context, 0, rect.size.height);
CGContextAddLineToPoint(context, 0, rect.size.height-15);
CGContextMoveToPoint(context, rect.size.width, rect.size.height);
CGContextAddLineToPoint(context, rect.size.width-15, rect.size.height);
CGContextMoveToPoint(context, rect.size.width, rect.size.height);
CGContextAddLineToPoint(context, rect.size.width, rect.size.height-15);
CGContextStrokePath(context);
//border
CGFloat correctRatio = 2.0;
CGContextSetLineWidth(context, correctRatio);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
//grid
CGContextSetLineWidth(context, 0.5);
for (int i=0; i<4; i++) {
//vertical
CGPoint aPoint = CGPointMake(i*(rect.size.width/4), 0.0);
CGContextMoveToPoint(context, aPoint.x, aPoint.y);
CGContextAddLineToPoint(context,aPoint.x, rect.size.height);
CGContextStrokePath(context);
//horizontal
aPoint = CGPointMake(0.0, i*(rect.size.height/4));
CGContextMoveToPoint(context, aPoint.x, aPoint.y);
CGContextAddLineToPoint(context,rect.size.width, aPoint.y);
CGContextStrokePath(context);
}
}