Undo with Multitouch Drawing in iOS

Undo with multitouch drawing in iOS

m_redoArray appears to be the big daddy, the one you draw from. I don't understand why you empty this out out in 'touchesBegan...', surely one of these arrays must carry through touchesBegan unaltered, or you'll be dropping stuff from the start of your drawing all the way through, no?

It appears to me that this is how you dropped the 'Hell' in your example here..

How to undo multitouch drawing in iOS

I would suggest the following (no code here):

  • create a second array pathUndoArray
  • Upon undo, remove the last entry from pathArray (if exists) and add it (at the end) to pathUndoArray, then redraw.
  • Upon redo, remove the last entry from pathUndoArray (if exists) and add it (at the end) to pathArray, then redraw.
  • Upon a new touch, you'll have to delete the pathUndoArray

You could also take a look at NSUndoManager.

Undo/Redo for drawing in iOS

I have found a solution for this, we need to create a array of array of DrawingPaths:

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// Do the above code, then
[m_undoArray addObject:self.currentPath];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[m_parentUndoArray addObject:[NSArray arrayWithArray:m_undoArray]];
}

and then stroke the path in DrawRect.

How to ignore certain UITouch Points in multitouch sequence

One solution is to store the topmost tap in touchesBegan and only draw this one.

As you have pointed out, you are not supposed to retain the UITouch instance, so I recommend using a weak reference instead.

This will only draw a single touch. If you wish to draw the touches of multiple fingers, you need another way of filtering out the hand (many drawing apps have user settings for telling the app the pose of the hand, for example, but this is of course more complicated).

Here is an idea on how to do it:

#import <QuartzCore/QuartzCore.h>

@interface TViewController () {
// We store a weak reference to the current touch that is tracked
// for drawing.
__weak UITouch* drawingTouch;
// This is the previous point we drawed to, or the first point the user tapped.
CGPoint touchStartPoint;
}
@end
@interface _TDrawView : UIView {
@public
CGLayerRef persistentLayer, tempLayer;
}
-(void)commitDrawing;
-(void)discardDrawing;
@end

@implementation TViewController

- (void) loadView
{
self.view = [[_TDrawView alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.view.opaque = YES;
self.view.multipleTouchEnabled = YES;
self.view.backgroundColor = [UIColor whiteColor];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Start with what we currently have
UITouch* topmostTouch = self->drawingTouch;
// Find the top-most touch
for (UITouch *touch in touches) {
CGPoint lastPoint = [touch locationInView:self.view];
if(!topmostTouch || [topmostTouch locationInView:self.view].y > lastPoint.y) {
topmostTouch = touch;
touchStartPoint = lastPoint;
}
}
// A new finger became the drawing finger, discard any previous
// strokes since last touchesEnded
if(self->drawingTouch != nil && self->drawingTouch != topmostTouch) {
[(_TDrawView*)self.view discardDrawing];
}
self->drawingTouch = topmostTouch;
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Always commit the current stroke to the persistent layer if the user
// releases a finger. This could need some tweaking for optimal user experience.
self->drawingTouch = nil;
[(_TDrawView*)self.view commitDrawing];
[self.view setNeedsDisplay];
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
const CGFloat red=0, green=0, blue=0, brush=1;
for (UITouch *touch in touches) {
// Find the touch that we track for drawing
if(touch == self->drawingTouch) {
CGPoint currentPoint = [touch locationInView:self.view];

// Draw stroke first in temporary layer
CGContextRef ctx = CGLayerGetContext(((_TDrawView*)self.view)->tempLayer);
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineWidth(ctx, brush );
CGContextSetRGBStrokeColor(ctx, red, green, blue, 1.0);
CGContextSetBlendMode(ctx,kCGBlendModeNormal);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, touchStartPoint.x, touchStartPoint.y);
CGContextAddLineToPoint(ctx, currentPoint.x, currentPoint.y);
CGContextStrokePath(ctx);
// Update the points so that the next line segment is drawn from where
// we left off
touchStartPoint = currentPoint;
// repaint the layer
[self.view setNeedsDisplay];
}
}
}

@end

@implementation _TDrawView

- (void) finalize {
if(persistentLayer) CGLayerRelease(persistentLayer);
if(tempLayer) CGLayerRelease(tempLayer);
}

- (void) drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
if(!persistentLayer) persistentLayer = CGLayerCreateWithContext(ctx, self.bounds.size, nil);
if(!tempLayer) tempLayer = CGLayerCreateWithContext(ctx, self.bounds.size, nil);

// Draw the persistant drawing
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), persistentLayer);
// Overlay with the temporary drawing
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), tempLayer);
}

- (void)commitDrawing {
// Persist the temporary drawing
CGContextRef ctx = CGLayerGetContext(persistentLayer);
CGContextDrawLayerAtPoint(ctx, CGPointMake(0, 0), tempLayer);
[self discardDrawing];
}
- (void)discardDrawing {
// Clears the temporary layer
CGContextRef ctx = CGLayerGetContext(tempLayer);
CGContextClearRect(ctx, self.bounds);
CGContextFlush(ctx);
}
@end

EDIT: I added the logic that if a new touch is detected, if there is currently any stroke being drawn with a higher y-value, it is removed, as we discussed in the comments.

The overlaying is done by painting two CGLayers. This code could be optimized a lot for performance, it should be looked at more as an illustration than production-ready code.

Simple multi-touch drawing application on iOS: Too slow (because drawRect() not additive ?)

I managed to resolve this as follows:

I create a new UIView subclass header and implementation files:

@interface fingerView : UIView {
}

Then in my main view, in header I declare 5 of these views:

fingerView* fview[5];

and in my main view implementation I create 5 views of this instance, one for each finger separately. Also, must make sure to make them, enable multitouch for each of them, and make sure that clearsContextBeforeDrawing is set to NO, as we will be updating tiny rects in each of them at a time, and we dont want the system to clear our work.

for(int i=0;i<5;i++) {
fview[i] = [[pView alloc] initWithFrame:topFrame];
[self addSubview: fview[i]];
[self sendSubviewToBack: fview[i]];
fview[i].opaque= NO;
fview[i].clearsContextBeforeDrawing = NO;
fview[i].multipleTouchEnabled = YES;
}

Now inside every finger view keep a large array (i use a simple array, say 10,000 long) of x and y positions that the finger had drawn on. Whenever a finger moves, the main view detects it, and calls a [fview[i] updatePos(newx, newy)], and crucially, we will command the view to only update a tiny potion of itself around these coordinates:

[fview[i] setNeedsDisplayInRect: fingerRect];

where fingerRect is a small rect centered at (newx, newy). Inside the drawRect method for every finger view,

- (void)drawRect:(CGRect)rect
{
if (movep==0) return;

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, r, g, b, 1);
CGContextSetLineWidth(context, linewidth);

//paint finger
CGContextBeginPath(context);
CGFloat slack= 15;
CGFloat minx= CGRectGetMinX(rect)-slack;
CGFloat maxx= CGRectGetMaxX(rect)+slack;
CGFloat miny= CGRectGetMinY(rect)-slack;
CGFloat maxy= CGRectGetMaxY(rect)+slack;
bool drawing = NO;
for(int i=0;i<movep;i++) {
CGFloat xx= x[i];
CGFloat yy= y[i];
if(xx>minx && xx<maxx && yy>miny && yy<maxy) {

if(drawing) {

// continue line
CGContextAddLineToPoint(context, xx, yy);
CGContextMoveToPoint(context, xx, yy);

} else {

// start drawing
CGContextMoveToPoint(context, xx, yy);
drawing= YES;
}

} else {
drawing= NO;
}
}

CGContextStrokePath(context);

and also, as I mentioned

- (void)updatePos: (CGFloat)xnew: (CGFloat) ynew
{
x[movep]= xnew;
y[movep]= ynew;
movep= movep+1;

Hopefully you can figure out how this works. Every view will look into this rectangle that has been modified, and checks all finger positions that went around that rect, and only draws those. This will come down to very few strokes, and so the entire code runs very fast.

The lesson overall is that UIViews are extremely optimized. As much as possible, try to make a whole bunch of them, update them only locally if at all, and let Apple's magic blend it all together.

Getting blur stroked while drawing in Cglayer

Substitute this in your code where you create your CGLayer:

if(self.currentDrawingLayer == nil)
{
CGFloat scale = self.contentScaleFactor;
CGRect bounds = CGRectMake(0, 0, self.bounds.size.width * scale, self.bounds.size.height * scale);
CGLayerRef layer = CGLayerCreateWithContext(context, bounds.size, NULL);
CGContextRef layerContext = CGLayerGetContext(layer);
CGContextScaleCTM(layerContext, scale, scale);
self.currentDrawingLayer = layer;
}

Adding a QuadCurve to UIBezierPath

Your code looks good. But to fix the problem you mentioned, change the current code to the following:

Change:

[path moveToPoint:m_previousPoint1];
[path addLineToPoint:mid1];

To:

[path moveToPoint:mid1];
[path addLineToPoint:m_previousPoint1];

And Change this:

[path addQuadCurveToPoint:mid2 controlPoint:m_currentPoint];

To:

[path addQuadCurveToPoint:m_currentPoint controlPoint:mid2];

Tested.


Addendum (WWDC Algorithm):

Background:

The idea is this according to WWDC:

1.) Instead of using the current point, we use the mid points as the start point and end point.

2.) As a result, we use the actual touch points as control points.

Analysis / Correction for your code:

So here is a simplified version of code I made making use the idea that was introduced in WWDC.

You got the idea. Almost. Given the above, we need to make changes to your code in touchesMoved to the following:

1.)

If we are using the mid point as the ToPoint value, we need to take care the first case
when there is only one current point, because
with only one current point, we cannot derive a mid point from it -
we need 2 points.

So, we would need to "read" one point past the current point initially to calculate the mid point. The following does that:

UITouch *touch = [touches anyObject];

m_previousPoint1 = m_currentPoint;
m_currentPoint = [touch locationInView:self];
mid1 = midPoint(m_currentPoint, m_previousPoint1);

if(counter == 1)
{
[path moveToPoint:m_currentPoint];
[path addLineToPoint:mid1];
[self setNeedsDisplay];
}

The variable counter is initially set to 0. So, nothing gets drawn until the second
pass when counter is 1. And when it is, we will have 2 points to calculate the mid point.

Then here comes the rest of the touches:

2.)

Once the first case is taken care of, we move forward to the rest of the curve and derive
appropriately the points with which we connect
the segments:

else if(counter > 1)
{
[path addQuadCurveToPoint:mid1 controlPoint:m_previousPoint1];
[self setNeedsDisplay];
}
counter++;

Here is the else if right after the first if above. We enter here when only the first case is handled, for that I use a simple counter and increment it every time touchesMoved gets called.

What happens here is that we are connecting from the previous mid point to mid1 using the previous point as control point. So, what about the current point? We are using it until the next pass.

3.) And finally, we take care the last segment of the curve in touchesEnded:

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[path addLineToPoint:[[touches anyObject] locationInView:self]];
[self setNeedsDisplay];
}

This simply draws a line from your mid point to the last point.

And finally in touchesBegan, I set counter = 0;, so the next curve will start the above process again.

I tested the above using both simulator and device, and here is a screen shot:

screenshot

And here is the complete source:

- (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event
{
UITouch *touch = [touches anyObject];
counter = 0;
m_previousPoint1 = [touch locationInView:self];
m_currentPoint = [touch locationInView:self];
}

//Find the midpoint
CGPoint midPoint(CGPoint p1, CGPoint p2)
{
return CGPointMake((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
}

- (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event
{
UITouch *touch = [touches anyObject];

m_previousPoint1 = m_currentPoint;
m_currentPoint = [touch locationInView:self];

mid1 = midPoint(m_currentPoint, m_previousPoint1);

[path setFlatness:1.0f];
[path setLineCapStyle:kCGLineCapRound];
[path setLineJoinStyle:kCGLineJoinRound];

if(counter == 1)
{
[path moveToPoint:m_currentPoint];
[path addLineToPoint:mid1];
[self setNeedsDisplay];
}
else if(counter > 1)
{
[path addQuadCurveToPoint:mid1 controlPoint:m_previousPoint1];
[self setNeedsDisplay];
}
counter++;
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[path addLineToPoint:[[touches anyObject] locationInView:self]];
[self setNeedsDisplay];
}


Related Topics



Leave a reply



Submit