Strange behavior with UIBezierPath addClip - objective-c

I'm trying to learn CoreGraphics and I have encountered a strange behavior.
I draw a rectangle and in it I draw given amount of diamonds,the shapes can be drawn with different filling (empty, filled and with stripes) my draw rect function looks like this:
- (void)drawRect:(CGRect)rect
{
UIBezierPath* roundedRect = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:9.0];
//don't draw where the round corners "cut" the rectangle
[roundedRect addClip];
//set a white background
[[UIColor whiteColor] setFill];
UIRectFill(self.bounds);
//set a black frame
[[UIColor darkGrayColor] setStroke];
[roundedRect stroke];
self.shade = STRIPED;
self.color = [UIColor greenColor];
self.number = 3;
rectOffset = self.bounds.size.width / (self.number * 2);
[self drawDiamondNumberOfTimes:self.number startOrigin:self.bounds.origin];
}
drawDiamondNumberOfTimes:startOrigin: is a recursive function that calculates the rectangle in which the shape will be drawing and draw the diamond boundaries using stroke
- (void) drawDiamondNumberOfTimes:(int) p_times startOrigin:(CGPoint) p_origin
{
if( p_times > 0)
{
CGRect drawArea;
drawArea.origin = CGPointMake(p_origin.x + rectOffset-shapeSize.width/2, self.bounds.size.height/4);
drawArea.size = shapeSize;
UIBezierPath *diamond = [[UIBezierPath alloc] init];
[diamond moveToPoint:CGPointMake(drawArea.origin.x, drawArea.origin.y+ shapeSize.height/2)];
[diamond addLineToPoint:CGPointMake(drawArea.origin.x+shapeSize.width/2, drawArea.origin.y)];
[diamond addLineToPoint:CGPointMake(drawArea.origin.x+shapeSize.width, drawArea.origin.y+ shapeSize.height/2)];
[diamond addLineToPoint:CGPointMake(drawArea.origin.x+shapeSize.width/2, drawArea.origin.y+ shapeSize.height)];
[diamond closePath];
[self.color setStroke];
[diamond stroke];
[self drawShadeOfDraw:diamond atRect:drawArea];
drawArea.origin.x += rectOffset + shapeSize.width/2;
[self drawDiamondNumberOfTimes:p_times-1 startOrigin:drawArea.origin ];
}
}
drawShadeOfDraw:atRect: set the different filling, where the strange behavior occurs.
With empty and solid fills it works perfect but with stripes, if I write [p_symbol addClip] then I get always one diamond striped even if self.number is set to 2 or 3. Without [p_symbol addClip] I get the correct number of diamonds but, of course, the stripes are all over the rectangle, here is the code for drawShadeOfDraw:atRect:
- (void)drawShadeOfDraw:(UIBezierPath*)p_symbol atRect:(CGRect)p_drawArea
{
switch (self.shade)
{
case STRIPED:
{
[p_symbol addClip];
for( int y = p_drawArea.origin.y; y < p_drawArea.origin.y+ p_drawArea.size.height; y+= 6)
{
[p_symbol moveToPoint:CGPointMake(p_drawArea.origin.x, y)];
[p_symbol addLineToPoint:CGPointMake(p_drawArea.origin.x+shapeSize.width, y)];
[self.color setStroke];
[p_symbol stroke];
}
break;
}
case SOLID:
{
[self.color setFill];
[p_symbol fill];
break;
}
default:
{
break;
}
}
}
Here are some images:
What am I doing wrong?

Adding to the clip is semi-permanent. Once the clipping area has been "reduced", you can't grow it again, per se. You can only restore it to a previous state. To do that you call CGContextSaveGState() before setting up context state (including clipping), do some drawing with that state, and then call CGContextRestoreGState() afterward to restore the context state to what it was before.
So, I suggest that you bracket your two methods with calls to save and restore the context state. Use UIGraphicsGetCurrentContext() to get a reference to the current context.

Related

Lines drawn using bezierPath and CAShapeLayer, not smooth

I have this code to draw lines on image user takes. I have successfully added feature to draw what ever the user feel like drawing on his/her image. But the lines which i draw is not smooth enough. I have tried the answers stack overflow experts suggested to others. But mine still looks rough. And one speciality is that I use pan gesture instead of touch, will that make any problems? Expert advices and codes are needed. Thanks in advance. Happy Coding. !
And this is from where I call the drawlineFrom method:
-(void)drawLineFrom:(CGPoint)from endPoint:(CGPoint)to
{
counter++;
bezierPath = UIBezierPath.bezierPath;
NSLog(#"UIGestureRecognizerStateChanged here");
[bezierPath moveToPoint: CGPointMake(from.x, from.y)];
[bezierPath addLineToPoint:CGPointMake(to.x, to.y)];
[bezierPath closePath];
bezierPath.usesEvenOddFillRule = YES;
box = [CAShapeLayer layer];
box.frame = CGRectMake(0, 0, 500, 500);
box.path = bezierPath.CGPath;
box.strokeColor = [UIColor colorWithRed:0.992 green:0.322 blue:0.212 alpha:1.00].CGColor;
box.fillColor = [UIColor colorWithRed:0.992 green:0.322 blue:0.212 alpha:1.00].CGColor;
box.lineWidth = 5;
[self.myImage.layer addSublayer:box ];
[layerarray addObject:box];
NSLog(#"AAAAARRRRTTTT%#",layerarray);
}
{
startPoint = [gesture locationInView:self.selfieImage];
if(gesture.state == UIGestureRecognizerStateChanged)
{
NSLog(#"Touched here \n :X-%f \n Y-%f",[gesture locationInView:self.selfieImage].x,[gesture locationInView:self.selfieImage].y);
startPoint = lastPoint;
lastPoint = [gesture locationInView:self.selfieImage];
[self drawLineFrom:startPoint endPoint:lastPoint];
}

NSView with masked CIFilter for OS X app

I am developing an app that contains lots of custom NSView objects being moved around. I have implemented a gaussian blur background filter for one of the custom NSView subclasses like so:
- (id)init {
self = [super init];
if (self) {
...
CIFilter *saturationFilter = [CIFilter filterWithName:#"CIColorControls"];
[saturationFilter setDefaults];
[saturationFilter setValue:#.5 forKey:#"inputSaturation"];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:#2.0 forKey:#"inputRadius"];
self.wantsLayer = YES;
self.layer.backgroundColor = [NSColor clearColor].CGColor;
self.layer.masksToBounds = YES;
self.layer.needsDisplayOnBoundsChange = YES;
self.layerUsesCoreImageFilters = YES;
[self updateFrame]; //this is where the frame size is set
self.layer.backgroundFilters = #[saturationFilter, blurFilter];
...
return self;
}
else return nil;
}
This works great and creates a gaussian blur effect within the entire contents of the view. The problem is that I do not want the gaussian blur to cover the entire view. There is about an (intentional) 12px padding between the actual size of the NSView and the drawing of its content box:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* strokeColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:1];
NSColor* fillColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:.2];
...
[strokeColor setStroke];
[fillColor setFill];
NSBezierPath *box = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(self.bounds.origin.x + 12, self.bounds.origin.y + 12, self.bounds.size.width - 24, self.bounds.size.height - 24) xRadius:6 yRadius:6];
box.lineWidth = 6;
[box stroke];
[box fill];
...
}
The reason for this padding is that there are some pieces of the GUI that inhabit this region and are drawn seamlessly into the containing box. I would like to mask the Blur effect to only have effect on the interior of the drawn box rather than the entire view. Here is what I have tried.
ATTEMPT 1: Create a sublayer
I created a sublayer in the NSView with the appropriately sized frame, and added the blur effect to this sublayer. PROBLEM: The blur effect seems to only apply to the immediate parent layer, so rather than blur the contents behind the NSView, it blurs the contents of the NSView's self.layer (which is basically empty).
ATTEMPT 2: Create a masking layer
I tried to create a masking layer and set it to self.layer.mask. However, since the positions of the GUI content do change (via the DrawRect function), I would need to get a copy of the current layer to use as the masking layer. I tried the following code, but it had no effect.
self.layer.mask = nil;
NSArray *bgFilters = self.layer.backgroundFilters;
self.layer.backgroundFilters = nil;
CALayer *maskingLayer = self.layer.presentationLayer;
self.layer.mask = maskingLayer;
self.layer.backgroundFilters = bgFilters;
ATTEMPT 3: Draw a masking layer directly
I could not find any examples of how to draw directly on a layer. I can not use a static UIImage to mast with, because, as I said above, the mask has to change with user interaction. I was looking for something equivalent to the DrawRect function. Any help would be appreciated.
SO...
It seems to me that the sublayer way would be the best and simplest way to go, if I could just figure out how to change the priority of the blur effect to be the background behind the NSView not the NSView's background layer behind the sublayer.
Well, I would still like to know if there is a more elegant way, but I have found a solution that works. Basically, I have created a masking layer from an NSImage drawn from a modified version of the drawRect function:
- (id)init {
self = [super init];
if (self) {
// SETUP VIEW SAME AS ABOVE
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = [NSImage imageWithSize:self.frame.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
[self drawMask:self.bounds];
return YES;
}];
maskLayer.frame = self.bounds;
self.layer.mask = maskLayer;
return self;
}
else return nil;
}
- (void)drawMask:(NSRect)dirtyRect {
[[NSColor clearColor] set];
NSRectFill(self.bounds);
[[NSColor blackColor] set];
// SAME DRAWING CODE AS drawRect
// EXCEPT EVERYTHING IS SOLID BLACK (NO ALPHA TRANSPARENCY)
// AND ONLY NEED TO DRAW PARTS THAT EFFECT THE EXTERNAL BOUNDARIES
}

Not expanding NSScroller

The NSScroller automatically expands it's width when the user hovers over it.
However, the document view has pretty little space, and this is why the scroller should not expand.
How can I disable this behaviour?
This maybe a little too late, but something like this might help?
1) Create custom scroller for your vertical scrollbar.
2) Override -drawKnob to force draw knob the default size even when it is to be drawn 'expanded'.
-(void)drawKnob
{
NSRect knobSlot = [self rectForPart:NSScrollerKnob];
if(sFlags.isHoriz)
{
knobSlot.size.height = 9;
knobSlot.origin.y = 6;
}
else
{
knobSlot.size.width = 9;
knobSlot.origin.x = 6;
}
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:knobSlot xRadius:5 yRadius:5];
[[NSColor scrollBarColor] set];
[path fill];
}
3) Depending on if you still want the knob slot or not, override -drawKnobSlotInRect:
-(void)drawKnobSlotInRect:(NSRect)slotRect highlight:(BOOL)flag
{
NSRect newRect = slotRect;
if(sFlags.isHoriz)
newRect.origin.y = 4;
else
newRect.origin.x = 4;
[super drawKnobSlotInRect:newRect highlight:flag];
}

NSScroller graphical glitches/lag

I have the following NSScroller subclass that creates a scroll bar with a rounded white knob and no arrows/slot (background):
#implementation IGScrollerVertical
- (void)drawKnob
{
NSRect knobRect = [self rectForPart:NSScrollerKnob];
NSRect newRect = NSMakeRect(knobRect.origin.x, knobRect.origin.y, knobRect.size.width - 4, knobRect.size.height);
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:newRect xRadius:7 yRadius:7];
[[NSColor whiteColor] set];
[path fill];
}
- (void)drawArrow:(NSScrollerArrow)arrow highlightPart:(int)flag
{
// We don't want arrows
}
- (void)drawKnobSlotInRect:(NSRect)rect highlight:(BOOL)highlight
{
// Don't want a knob background
}
#end
This all works fine, except there is a noticable lag when I use the scroller. See this video:
http://twitvid.com/70E7C
I'm confused as to what I'm doing wrong, any suggestions?
Fixed the issue, just had to fill the rect in drawKnobSlotInRect:

How do I manage the drawing of custom data (not photos) in an NSView that has an area dynamically larger than what is viewable?

UPDATE: Relative to the questions and answers below, it seems I have a misunderstanding of the NSView class in relation to the custom classes I'm trying to draw and the wrapping NSScrollView. In the end, what I'm trying to figure out is how do I manage the dynamic drawing of custom data (not photos) in an NSView that has an area larger than what is viewable?
I am not looking for a handout, but I am a novice to Cocoa and I thought I was doing best-practice based on Apple's docs, but it seems I've gotten the fundamentals wrong. Apple's documentation is incredibly detailed, technical, centered entirely around working with photos, and thus useless to me. The related code examples provided by Apple (e.g. Sketch) get the document size from the printer paper sizes in their typically oblique fashion, and that's not what I need. I've scoured the web for tutorials, examples and the like, but I'm not finding much of anything (and I promise to write one when I get this sorted out).
I'm porting this code from REALbasic where I have this completely working, even with Undo commands, but the paradigms to do so are entirely different. This just isn't "clicking" for me. I appreciate the help given, I'm still missing something here, anything else folks have to offer is appreciated.
Thanks
I have a subclassed NSView where I'm creating a piano-roll MIDI interface. I am trying to resolve a few problems:
Drawing artifacts during and after scrolling
Lines not spanning across the visible area during and after scrolling
While scrolling and sometimes on mouseDown, the horizontal scroller jumps to the right 1 (one) pixel, but I don't have scrollToPoint implemented anywhere yet.
Symptoms that relate to the above:
Implementing adjustScroll makes everything worse.
mouseDown corrects all of the problems except sometimes the 1-pixel jump to the right.
If I uncomment the NSLog command the beginning of drawRect nothing draws.
Apple's documentation mentions pixel-accurate drawing, but (of course) offers up no examples on how this can be achieved. I've been using the floor() function to try to get consistent values, but once I start tacking on scrollToPoint or any other complexity, things go haywire.
Please see the linked image as an example. The screenshot, if you can believe it, actually cleans up what I see on screen. There are double lines almost everywhere at half opacity as well. The same is applied to any objects I draw as well.
Graphics Artifacts and inconsistencies in a subclassed NSView generated after scrolling http://www.oatmealandcoffee.com/external/NSViewArtifacts.png
Here is the code. I hate giving up so much publicly, but I've searched everywhere for clues, and if the Internet is any indication I'm the only person with this problem, and I really just want to get this sorted out and move forward. There is a lot, and there is more to come, but these is the core stuff I really need to get right, and, frankly, I am at a loss on how to correct it.
- (void)drawRect:(NSRect)rect {
//NSLog(#"OCEditorView:drawRect: START");
[self setFrame:[[self EditorDocument] DocumentRect]];
[[NSGraphicsContext currentContext] setShouldAntialias:NO];
// CLEAR BACKGROUND
[[[self EditorDocument] ColorWhiteKey] set];
NSRectFill(rect);
// BACKGROUND KEYS
int firstRowLine = 0; //NSMinY(rect); //<- adding the function results in bad spacing on scrolling
int currentRowLine = 0;
int lastRowLine = NSMaxY(rect);
//NSLog(#"lastRowLine:%d", lastRowLine);
float currentZoomY = [self ZoomY];
for (currentRowLine = firstRowLine; currentRowLine <= lastRowLine; currentRowLine += currentZoomY) {
int currentTone = floor(currentRowLine / [self ZoomY]);
BOOL isBlackKey = [[self MusicLib] IsBlackKey:currentTone];
//NSLog(#"%d, tone:%d, black:%d", [self MusicLib], currentTone, isBlackKey);
if (isBlackKey) {
[[[self EditorDocument] ColorBlackKey] set];
} else {
[[NSColor whiteColor] set];
}
NSBezierPath *rowLine = [NSBezierPath bezierPath];
NSPoint bottomLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
NSPoint bottomRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
NSPoint topRightPoint = NSMakePoint(NSMaxX(rect), currentRowLine + [self ZoomY]);
NSPoint topLeftPoint = NSMakePoint(NSMinX(rect), currentRowLine + [self ZoomY]);
[rowLine moveToPoint:bottomLeftPoint];
[rowLine lineToPoint:bottomRightPoint];
[rowLine lineToPoint:topRightPoint];
[rowLine lineToPoint:topLeftPoint];
[rowLine closePath];
[rowLine fill];
BOOL isOctave = [[self MusicLib] IsOctave:currentTone];
if (isOctave) {
[[[self EditorDocument] ColorXGrid] set];
NSBezierPath *octaveLine = [NSBezierPath bezierPath];
NSPoint leftPoint = NSMakePoint(NSMinX(rect), currentRowLine);
NSPoint rightPoint = NSMakePoint(NSMaxX(rect), currentRowLine);
[octaveLine moveToPoint:leftPoint];
[octaveLine lineToPoint:rightPoint];
[octaveLine stroke];
}
}
// BACKGROUND MEASURES
//[[self EditorDocument].ColorYGrid setStroke];
int firstColumnLine = 0;
int currentColumnLine = 0;
int lastColumnLine = NSMaxX(rect);
int snapToValueInBeats = [[self EditorDocument] SnapToValue];
int snapToValueInPixels = floor(snapToValueInBeats * [self ZoomX]);
int measureUnitInBeats = floor([[self EditorDocument] TimeSignatureBeatsPerMeasure] * [[self EditorDocument] TimeSignatureBasicBeat]);
int measureUnitInPixels = floor(measureUnitInBeats * [self ZoomX]);
for (currentColumnLine = firstColumnLine; currentColumnLine <= lastColumnLine; currentColumnLine += snapToValueInPixels) {
//int currentBeat = floor(currentColumnLine / [self ZoomX]);
int isAMeasure = currentColumnLine % measureUnitInPixels;
int isAtSnap = currentColumnLine % snapToValueInPixels;
if ((isAMeasure == 0) || (isAtSnap == 0)) {
if (isAtSnap == 0) {
[[NSColor whiteColor] set];
}
if (isAMeasure == 0) {
[[[self EditorDocument] ColorXGrid] set];
}
NSBezierPath *columnLine = [NSBezierPath bezierPath];
NSPoint startPoint = NSMakePoint(currentColumnLine, NSMinY(rect));
NSPoint endPoint = NSMakePoint(currentColumnLine, NSMaxY(rect));
[columnLine moveToPoint:startPoint];
[columnLine lineToPoint:endPoint];
[columnLine setLineWidth:1.0];
[columnLine stroke];
} // isAMeasure or isAtSnap
} // currentColumnLine
// NOTES
for (OCNoteObject *note in [[self EditorDocument] Notes]) {
OCNoteObject *currentNote = note;
NSRect noteBounds = [self GetRectFromNote:currentNote];
//NSLog(#"noteBounds:%d", noteBounds);
// set the color for the note fill
// this will have to come from the parent Track
NSMutableArray *trackColors = [self EditorDocument].TrackColors;
if (note.Selected) {
[[trackColors objectAtIndex:0] set];
} else {
[[trackColors objectAtIndex:1] set];
}
[NSBezierPath fillRect:noteBounds];
// outline
[[NSColor blackColor] set];
[NSBezierPath strokeRect:noteBounds];
} // for each note
/*
if (EditorController.startingUpApplication == YES) {
[self setDefaultSettingForApplicationStartUp];
}
*/
//NSLog(#"OCEditorView:drawRect: END");
}
- (void)mouseDown:(NSEvent *)theEvent {
//NSLog(#"OCEditorObject:mouseDown: START");
// This converts the click into coordinates
MouseDownPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// Calculate the beat and pitch clicked into...
float startBeat = floor(MouseDownPoint.x / [self ZoomX]);
float pitch = floor(MouseDownPoint.y / [self ZoomY]);
float length = [[self EditorDocument] NewNoteLength];
//NSLog(#"X:%f, Y:%f", MouseDownPoint.x, MouseDownPoint.y);
//NSLog(#"beat:%f, pitch:%f", startBeat, pitch);
LastDragPoint = MouseDownPoint; // save the point just in case.
OCNoteObject *note = [self GetClickedNoteFromPoint:MouseDownPoint];
if ([EditorController EditorMode] == AddObjectMode) {
//NSLog(#"AddObjectMode)");
float snapToX = [[self EditorDocument] SnapToValue];
float snappedStartBeat = floor(startBeat / snapToX) * snapToX;
//NSLog(#"%f = %f / %f * %f", snappedStartBeat, startBeat, snapToX, snapToX);
OCNoteObject *newNote = [[self EditorDocument] CreateNote:snappedStartBeat Pitch:pitch Length:length];
//NSLog(#"newNote:%d", newNote);
[newNote Deselect];
} else if ([EditorController EditorMode] == EditObjectMode) {
//NSLog(#"EditObjectMode");
// if nothing was clicked, then clear the selections
// else if the shift key was pressed, add to the selection
if (note == nil) {
[self SelectNone];
} else {
//NSLog(#"mouseDown note.pitch:%f, oldPitch:%f", note.Pitch, note.OldPitch);
BOOL editingSelection = (([theEvent modifierFlags] & NSShiftKeyMask) ? YES : NO);
if (editingSelection) {
if (note.Selected) {
[self RemoveFromSelection:note];
} else {
[self AddToSelection:note];
}
} else {
if (note.Selected) {
// do nothing
} else {
[self SelectNone];
[self AddToSelection:note];
}
}
[self SetOldData];
} // (note == nil)
} else if ([EditorController EditorMode] == DeleteObjectMode) {
if (note != nil) {
[self RemoveFromSelection:note];
[[self EditorDocument] DestroyNote:note];
} // (note != nil)
} // EditorMode
[self setFrame:[[self EditorDocument] DocumentRect]];
[self setNeedsDisplay:YES];
}
- (void)mouseDragged:(NSEvent *)theEvent {
//NSLog(#"mouseDragged");
NSPoint currentDragPoint = [self convertPoint:[theEvent locationInWindow] fromView:nil];
// NSLog(#"currentDragPoint: %d", currentDragPoint)
float snapToValueInBeats = [[self EditorDocument] SnapToValue];
int deltaXinPixels = floor(currentDragPoint.x - MouseDownPoint.x);
int deltaYinPixels = floor(currentDragPoint.y - MouseDownPoint.y);
int deltaXinBeats = floor(deltaXinPixels / [self ZoomX]);
int deltaY = floor(deltaYinPixels / [self ZoomY]);
int deltaX = floor(deltaXinBeats / snapToValueInBeats) * snapToValueInBeats;
for (OCNoteObject *note in [self Selection]) {
[self MoveNote:note DeltaX:deltaX DeltaY:deltaY];
}
LastDragPoint = currentDragPoint;
[self autoscroll:theEvent];
[self setNeedsDisplay:YES]; //artifacts are left if this is off.
}
- (void)mouseUp:(NSEvent *)theEvent {
if ([EditorController EditorMode] == AddObjectMode) {
} else if ([EditorController EditorMode] == EditObjectMode) {
} else if ([EditorController EditorMode] == DeleteObjectMode) {
}
[self setNeedsDisplay:YES];
}
I could very well be missing something obvious, but I think I'm too close to the code to see the solution for what it is. Any help is greatly appreciated! Thanks!
I think you are misunderstanding the way drawRect: and its argument works:
The message drawRect: is sent by cocoa whenever your view or parts of it need to be redrawn. The CGRect argument is the bounding box of all updated areas for the current redraw. That means that you should not derive any positions of objects within your view from this rectangle. It is only passed to the method to allow for optimized drawing: if something is completely outside of this rectangle it does not need to be redrawn.
You should calculate all positions within your view from the views coordinate system: [self bounds]. This does not change each time drawRect: is performed and gives you an origin and size for the contents of the view.
There are a couple of other issues with your code (for instance, don't call setFrame: from within drawRect:) but I think you should first get the coordinates right and then look further into how to calculate pixel-aligned coordinates for your rectangles.
This code of yours looks rather more elaborate than it needs to be. Check out NSCenterScanRect(), and NSRectFillListWithColors(). Also, it's rather wasteful to create and discard paths in -drawRect:.