mhergon
mhergon

Reputation: 1678

iOS - How to draw smooth line with finger in "touchesMoved:" method

I'm trying to calculate control points for a "addCurveToPoint:" or "addQuadCurveToPoint:" but I don't know how to do it.

I tried some code examples, but nothing... Someone can help me with that?

Thanks!

Upvotes: 0

Views: 5551

Answers (2)

squ1dd13
squ1dd13

Reputation: 84

Whilst I'm aware that this question is both a) very old, and b) basically answered already, I decided that it would be worth sharing some code here just in case it helps people who come from Google.

The original drawing code on which this solution is loosely based can be found here.

I recently had to do a similar thing in a Cocoa app, and implemented a solution that didn't require much code. This method lerps between the previous point value and the current one but also factors in movement speed so that the line keeps up with the mouse. I've added comments to explain the steps of the process.

(I know the question is for iOS, but the code would not be difficult to modify for iOS as it uses Core Graphics. It also would not be very hard to translate from Objective-C++ into pure Objective-C.)

// Somewhere in another file...
@interface CIDDrawView : NSView {
    NSMutableArray *brushStrokes;
    NSMutableArray *strokePoints;
}

@end

// Implementation
#include <QuartzCore/QuartzCore.h>
#include <chrono>
#include <cmath>
#include <algorithm>

float lerp(float a, float b, float f) {
    return a + f * (b - a);
}

NSPoint lerpPoint(NSPoint a, NSPoint b, float f) {
    return {
        lerp(a.x, b.x, f),
        lerp(a.y, b.y, f)
    };
}

float pointDistance(NSPoint a, NSPoint b) {
    return std::sqrt(std::pow(b.x - a.x, 2) + std::pow(b.y - a.y, 2));
}

inline auto msTime() {
    return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
}

@implementation CIDDrawView

-(id)initWithFrame:(NSRect)frame {
    if((self = [super initWithFrame:frame])) {
        brushStrokes = [NSMutableArray array];
    }
    
    return self;
}

-(NSPoint)mousePointForEvent:(NSEvent *)e {
    return [self convertPoint:e.locationInWindow fromView:nil];
}

-(void)mouseDown:(NSEvent *)event {
    // Begin new stroke.
    strokePoints = [NSMutableArray array];
    
    // Add the new stroke to the strokes array.
    [brushStrokes addObject:strokePoints];
    
    // Add the first point to the new stroke.
    NSPoint mousePoint = [self mousePointForEvent:event];
    [strokePoints addObject:@(mousePoint)];
}

-(void)mouseDragged:(NSEvent *)event {
    static auto lastPointTime = msTime();
    
    // The reference speed used to normalise the mouse movement speed.
    // Unit is px/s, but that doesn't matter much.
    const float refSpeed = 600.f;
    
    // How long it takes for the lerped points to reach the user's mouse location.
    // Lower values smooth the line less, but higher values make the line catch up more slowly.
    const float lerpAmount = 3.f;
    
    NSPoint mousePoint = [self mousePointForEvent:event];
    
    // Only modify the point value if this is not a new stroke.
    if([strokePoints count] > 1) {
        NSPoint lastPoint = [[strokePoints lastObject] pointValue];
        
        // Calculate the time since the last point was added.
        auto timeNow = msTime();
        float secs = float(timeNow - lastPointTime) / 1000.f;
        
        // Normalise the mouse speed.
        float movementSpeed = std::min(1.0f, (pointDistance(mousePoint, lastPoint) / secs) / refSpeed);
        
        // Lerp between the last point and the current one by the lerp amount (factoring in the speed).
        mousePoint = lerpPoint(lastPoint, mousePoint, movementSpeed / lerpAmount);
        
        lastPointTime = timeNow;
    }
    
    // Add the point to the stroke.
    [strokePoints addObject:@(mousePoint)];
    [self setNeedsDisplay:YES];
}

-(void)mouseUp:(NSEvent *)event {
    NSPoint mousePoint = [self mousePointForEvent:event];
    [strokePoints addObject:@(mousePoint)];
    
    [self setNeedsDisplay:YES];
}

-(void)drawRect:(NSRect)rect {
    const CGColorRef lineColor = [[NSColor blackColor] CGColor];
    const float lineWidth = 1.f;
    
    [[NSColor whiteColor] setFill];
    NSRectFill(rect);
    
    if(![brushStrokes count]) {
        // No strokes to draw.
        return;
    }
    
    CGContextRef context = [[NSGraphicsContext currentContext] CGContext];
    
    for(NSArray *stroke in brushStrokes) {
        CGContextSetLineWidth(context, lineWidth);
        CGContextSetStrokeColorWithColor(context, lineColor);
        
        unsigned long strokePointCount = [stroke count];
        NSPoint startPoint = [[stroke firstObject] pointValue];
        
        CGContextBeginPath(context);
        CGContextMoveToPoint(context, startPoint.x, startPoint.y);
        
        // Add lines to the points, skipping the first mouse point.
        for(unsigned long i = 1; i < strokePointCount; ++i) {
            NSPoint point = [stroke[i] pointValue];
            
            CGContextAddLineToPoint(context, point.x, point.y);
        }
        
        // Stroke the path.
        CGContextDrawPath(context, kCGPathStroke);
    }
}

@end

Comparison

I tried to keep the drawing the same, but there are still some differences. This is partially down to the fact that it's slightly harder to draw without the smoothing.

With

Screenshot of window containing smoothed drawing

Without

Image of unsmoothed drawing

Upvotes: 0

chris
chris

Reputation: 249

check smooth drawing,a great tutorial that show you how to draw smooth line. step by step. easy to understand,and easy to implement. but it is just show you how to draw smooth line, no other function like eraser,etc.

if you wanna do a drawing app, check Smooth-Line-View, a simple drawing app.

if you are familiar with c++, you should check GLPaint, it is a drawing sample app provided by Apple, GLPaint

Upvotes: 5

Related Questions