I'm having some issues while trying to draw lines smoothly on touches moved when my UIView is zoomed in. The thing is that lines start to look very pixelated upon certain zoom level.
My view hierarchy is really simple as of now, since this is more like a proof of concept. I have a UIScrollView, my UIView as a child and also it is set as the zoom view.
My drawRect: implementation looks like this:
CGContextRef context = UIGraphicsGetCurrentContext();
[self.layer renderInContext:context];
CGPoint mid1 = midPoint(_previousPoint1, _previousPoint2);
CGPoint mid2 = midPoint(_currentPoint, _previousPoint1);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextMoveToPoint(context, mid1.x, mid1.y);
CGContextAddQuadCurveToPoint(context, _previousPoint1.x, _previousPoint1.y, mid2.x, mid2.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextStrokePath(context);
Dumping the contents of the layer into the context is an important part since I'm only refreshing the bounding box enclosing the path formed by the last three touched points (_previousPoint1, _previousPoint2 and _currentPoint).
That operation is actually done on touchesMoved method and the function that handles it is the following:
- (void)calculateMinImageArea:(CGPoint)pp1 :(CGPoint)pp2 :(CGPoint)cp
{
// calculate mid point
CGPoint mid1 = midPoint(pp1, pp2);
CGPoint mid2 = midPoint(cp, pp1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, mid1.x, mid1.y);
CGPathAddQuadCurveToPoint(path, NULL, pp1.x, pp1.y, mid2.x, mid2.y);
CGRect bounds = CGPathGetBoundingBox(path);
CGPathRelease(path);
CGRect drawBox = [self extendedRect:bounds];
UIGraphicsBeginImageContext(drawBox.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIGraphicsEndImageContext();
[self setNeedsDisplayInRect:drawBox];
}
I was able to find a workaround using CAShapeLayers to draw the paths. However, the low performance wasn't acceptable.
I would really appreciate if someone can point me to the right approach.
Thanks.
[self setContentScaleFactor:scale];
use this in scrollViewDidEndZooming: delegate method
Hope this help's you
Related
I want to change the width of the line with 3D-touch while drawing. I'm using the library ACEDrawingView which uses this code to draw:
- (void)draw
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context, path);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextSetAlpha(context, self.lineAlpha);
CGContextStrokePath(context);
}
I use this code to change the lineWidth based on the pressure in touchesMoved:
// save all the touches in the path
UITouch *touch = [touches anyObject];
//3d touch
CGFloat maximumPossibleForce = touch.maximumPossibleForce;
CGFloat force = touch.force;
CGFloat normalizedForce = force/maximumPossibleForce;
CGFloat lineWidth = normalizedForce * 10;
self.currentTool.lineWidth = lineWidth;
It all works good until I draw another line which crosses the previous one. Then it looks like the image above.
Any help would be greatly appreciated! Please ask for more code if needed, or take a quick look at ACEDrawingView. Thank you!
To sort out my last problem : Draw/Paint like sketch color inside a bezier path in UIView, I took an exact Image (of path) filled with selected color. Then, to draw that in UIView, for context color I took:
lineColor=[UIColor colorWithPatternImage:img];
in drawrect :
CGPoint mid1 = midPoint(previousPoint1, previousPoint2);
CGPoint mid2 = midPoint(currentPoint, previousPoint1);
[curImage drawInRect:CGRectZero];
CGContextRef context = UIGraphicsGetCurrentContext();
[self.layer renderInContext:context];
CGContextMoveToPoint(context, mid1.x, mid1.y);
CGContextAddQuadCurveToPoint(context,
previousPoint1.x,
previousPoint1.y,
mid2.x,
mid2.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetBlendMode(context, kCGBlendModeCopy);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context,lineColor.CGColor);
CGContextSetShouldAntialias(context, YES);
CGContextSetAllowsAntialiasing(context, YES);
CGContextSetFlatness(context,0.5);
CGContextSetAlpha(context, 1.0);
CGContextStrokePath(context);
It draws exactly in the path of view but on boundaries the color goes out like melting effect.
Source Image in which color has to be filled :
image to be used as colorWithPatternImage (img)
After drawing on touches ,resultimg Image :
What am I missing here to fill it perfectly?
Any help with core graphics is most welcomed!
Thank You!
To mask the image I also did :
-(void)clipView :(MyUIBezierPath*)path drawingView:(DrawingView*)myView
{
MyUIBezierPath *myClippingPath =path;
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = myClippingPath.CGPath;
myView.layer.mask = mask;
}
But colorWithPatternImage and this masking does the same trick i.e lets me draw only inside the path , the problem I am facing now is : It gives a bad effect on drawing on touch points around the boundaries of path !
Well its working with my own masking method via colorWithPatternImage , though masking it by clipping is the same thing.Drawing issue was because of the image being drawn at touch point in the method "calculateMinImageArea: CGPoint1 :CGPoint2 :CGPoint3". Changing its dimensions (height-width according to line width) sorts it out.
You can create a path points out of this image. Then make a CGPathRef or CGMutablePathRef and then use it as a mask to your CGContextRef.
The Problem: I have a small game where u can scratch a image above another image. The upper image becomes invisible where you are scratching. The whole thing is placed into a SpriteKit view. The problem is, that on weaker devices (iPhone4) the scratching image is only updated when the user stops scratching.
I assume that the image is only updated when the user does not scratch until the image is completely rendered and displayed.
Can someone suggest a better way to see the scratching immediately. I am aware that the scratching consumes a lot of performance but I dont mind if it lags a little.
Here the code:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self.view];
UIGraphicsBeginImageContext(self.view.frame.size);
[_scratchImage drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _lastPoint.x, _lastPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPoint.x, currentPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 25.0f );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextSetBlendMode (UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_scratchImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
_lastPoint = currentPoint;
SKTexture* scratchTexture = [SKTexture textureWithImage:_scratchImage];
_scratchSprite.texture = scratchTexture;
}
UPDATE
I ended up recording the points as suggested in the correct answer. But other than updating the image in a CADisplayLink callback I updated the screen in a
-(void)update(float currentTime)
callback from SpriteKit (Which is kind of the same for SpriteKit use cases)
My code in the update method looks like the following:
-(void)displayUpdated
{
UIGraphicsBeginImageContext(self.view.frame.size);
[_scratchImage drawInRect:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
CGMutablePathRef linePath = nil;
linePath = CGPathCreateMutable();
CGPathMoveToPoint(linePath, NULL, _lastPoint.x, _lastPoint.y);
for (NSValue* val in _recordedPoints) {
CGPoint p = [val CGPointValue];
CGPathAddLineToPoint(linePath, NULL, p.x, p.y);
NSLog(#"%f, %f", p.x, p.y);
_lastPoint = p;
}
//flush array
[_recordedPoints removeAllObjects];
CGContextAddPath(UIGraphicsGetCurrentContext(), linePath);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 25.0f );
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), 0.0, 0.0, 0.0, 1.0);
CGContextSetBlendMode (UIGraphicsGetCurrentContext(), kCGBlendModeClear);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_scratchImage = UIGraphicsGetImageFromCurrentImageContext();
CGPathRelease(linePath);
UIGraphicsEndImageContext();
SKTexture* scratchTexture = [SKTexture textureWithImage:_scratchImage];
_scratchSprite.texture = scratchTexture;
}
A more performant code would also hold a reference to the context but I could not manage to get that working. If someone can suggest a solution I would be happy. Nevertheless my Framerate on a iPhone 4 went up to 25-30 fps from about 5 fps.
I would try the following:
Measure in Instruments.app Time Profiler.
Speed up drawing by creating a CGBitmapContext as an instance variable. Then you can skip the [_scratchImage drawInRect:...] in each step.
Separate the drawing and the event handling. Right now, you will always create a new UIImage for each touch event. It could be better to coalesce the stroked lines so the creation of a UIImage does not happen as often. E.g. put the line segments in a queue and draw in a callback from CADisplayLink.
I don't know much about SpriteKit, In UIView, you should put your drawing code in drawRect method. Is it the same as SpriteKit ?
friends, I am working with UIBezierPaths, for free hand drawing , and everything works fine, for this I am storing my paths in a path array, everything works fine, while rendering, I am looping the whole array and rendering the paths,but soon as the array count increases, I see a lag while drawing, below is my drawRect code. please help me out in finding where I am going wrong
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *mytouch=[[touches allObjects] objectAtIndex:0];
m_previousPoint2 = m_previousPoint1;
m_previousPoint1 = [mytouch previousLocationInView:self];
m_currentPoint = [mytouch locationInView:self];
CGPoint mid1 = midPoint(m_previousPoint1, m_previousPoint2);
CGPoint mid2 = midPoint(m_currentPoint, m_previousPoint1);
testpath = CGPathCreateMutable();
CGPathMoveToPoint(testpath, NULL, mid1.x, mid1.y);
CGPathAddQuadCurveToPoint(testpath, NULL, m_previousPoint1.x, m_previousPoint1.y, mid2.x, mid2.y);
CGRect bounds = CGPathGetBoundingBox(testpath);
CGPathRelease(testpath);
CGRect drawBox = bounds;
//Pad our values so the bounding box respects our line width
drawBox.origin.x -= self.lineWidth * 2;
drawBox.origin.y -= self.lineWidth * 2;
drawBox.size.width += self.lineWidth * 4;
drawBox.size.height += self.lineWidth * 4;
CGContextRef context = UIGraphicsGetCurrentContext();
context = CGLayerGetContext(myLayerRef);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
[self setNeedsDisplayInRect:drawBox];
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
if(myLayerRef == nil)
{
myLayerRef = CGLayerCreateWithContext(context, self.bounds.size, NULL);
}
[self.layer renderInContext:context];
CGPoint mid1 = midPoint(m_previousPoint1, m_previousPoint2);
CGPoint mid2 = midPoint(m_currentPoint, m_previousPoint1);
CGContextMoveToPoint(context, mid1.x, mid1.y);
CGContextAddQuadCurveToPoint(context, m_previousPoint1.x, m_previousPoint1.y, mid2.x, mid2.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetFlatness(context, 0.1);
CGContextSetAllowsAntialiasing(context, true);
CGContextStrokePath(context);
[super drawRect:rect];
}
Code updated according to the below discussion by #borrrden
Regards
Ranjit
There is an excellent session in the WWDC 2012 sessions about this exact problem. I think it is called iOS performance: Graphics or something similar. You should definitely watch it.
The summary is, DON'T store it in a path. There is no need to keep this path information around. Store a separate context (CGLayer is best) and draw into that (just like you add points to the path, add points to the context intead). Then in your drawRect, simply draw that layer into the current graphics context. Also be sure to only call setNeedsDisplay on the rectangle that changed, or you will most likely run into problems on a high resolution device.
I have a UIView subclass on which the user can add a random CGPath. The CGPath is added by processing UIPanGestures.
I would like to resize the UIView to the minimal rect possible that contains the CGPath. In my UIView subclass, I have overridden sizeThatFits to return the minimal size as such:
- (CGSize) sizeThatFits:(CGSize)size {
CGRect box = CGPathGetBoundingBox(sigPath);
return box.size;
}
This works as expected and the UIView is resized to the value returned, but the CGPath is also "resized" proportionally resulting in a different path that what the user had originally drawn. As an example, this is the view with a path as drawn by the user:
And this is the view with the path after resizing:
How can I resize my UIView and not "resize" the path?
Use the CGPathGetBoundingBox. From Apple documentation:
Returns the bounding box containing all points in a graphics path. The
bounding box is the smallest rectangle completely enclosing all points
in the path, including control points for Bézier and quadratic curves.
Here a small proof-of-concept drawRect methods. Hope it helps you!
- (void)drawRect:(CGRect)rect {
//Get the CGContext from this view
CGContextRef context = UIGraphicsGetCurrentContext();
//Clear context rect
CGContextClearRect(context, rect);
//Set the stroke (pen) color
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
//Set the width of the pen mark
CGContextSetLineWidth(context, 1.0);
CGPoint startPoint = CGPointMake(50, 50);
CGPoint arrowPoint = CGPointMake(60, 110);
//Start at this point
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextAddLineToPoint(context, startPoint.x+100, startPoint.y);
CGContextAddLineToPoint(context, startPoint.x+100, startPoint.y+90);
CGContextAddLineToPoint(context, startPoint.x+50, startPoint.y+90);
CGContextAddLineToPoint(context, arrowPoint.x, arrowPoint.y);
CGContextAddLineToPoint(context, startPoint.x+40, startPoint.y+90);
CGContextAddLineToPoint(context, startPoint.x, startPoint.y+90);
CGContextAddLineToPoint(context, startPoint.x, startPoint.y);
//Draw it
//CGContextStrokePath(context);
CGPathRef aPathRef = CGContextCopyPath(context);
// Close the path
CGContextClosePath(context);
CGRect boundingBox = CGPathGetBoundingBox(aPathRef);
NSLog(#"your minimal enclosing rect: %.2f %.2f %.2f %.2f", boundingBox.origin.x, boundingBox.origin.y, boundingBox.size.width, boundingBox.size.height);
}