Bezier doodling problem

I was trying to follow this tutorial about drawing strokes in which the line width varies with the speed of the touch. Advanced Freehand Drawing Techniques

I attempted to convert the code to Swift, and it mostly works. There's something wrong with the way the path is placed in the view that I can't understand. When "drawing" near the top of the screen it seems to work sort of as expected, but drawing towards the bottom of the screen the path increasingly goes really far from the touch point. And it is drawing lots of hills and valleys instead of lines.

I have gone over every line of code many times, and can't see what is going wrong.

Here is my translation for the first part of the tutorial, for the NaiveVarWidthView class.

import UIKit

class NaiveVarWidthView: UIView {

	var path: UIBezierPath?
	var incrementalImage: UIImage?
	var pts = [CGPoint].init(repeating: CGPoint.zero, count: 5)
	var ctr: Int = 0

	override init(frame: CGRect) {
		super.init(frame: frame)
		initializeView()
	}

	required init?(coder aDecoder: NSCoder) {
		super.init(coder: aDecoder)
		initializeView()
	}

	func initializeView() {
		self.backgroundColor = UIColor(white: 0.5, alpha: 1)
		isMultipleTouchEnabled = false
		path = UIBezierPath()
	}

	override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
		ctr = 0
		let touch = touches.first! as UITouch
		pts[0] = touch.location(in: self)
	}

	override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
		let touch = touches.first! as UITouch
		let p = touch.location(in: self)
		ctr += 1
		pts[ctr] = p
		if (ctr == 4) {
			pts[3] = CGPoint(x: (pts[2].x + pts[4].x) / 2.0, y: (pts[2].y + pts[4].y / 2.0))
			path?.move(to: pts[0])
			path?.addCurve(to: pts[3], controlPoint1: pts[1], controlPoint2: pts[2])

			UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)

			if (incrementalImage == nil) {
				let rectpath = UIBezierPath(rect: self.bounds)
				UIColor(white: 0.5, alpha: 1).setFill()
				rectpath.fill()
			}
			incrementalImage?.draw(at: .zero)
			UIColor.black.setStroke()

			var speed: Float = 0.0

			for i in 0..<3 {
				let dx = pts[i + 1].x - pts[i].x
				let dy = pts[i + 1].y - pts[i].y
				speed += sqrtf(Float(dx * dx + dy * dy))
			}

			let FUDGE_FACTOR = 100
			let width = Float(FUDGE_FACTOR) / speed

			path?.lineWidth = CGFloat(width)
			path?.stroke()

			incrementalImage = UIGraphicsGetImageFromCurrentImageContext()

			UIGraphicsEndImageContext()
			
			setNeedsDisplay()

			path?.removeAllPoints()
			pts[0] = pts[3]
			pts[1] = pts[4]
			ctr = 1
		}
	}

	override func draw(_ rect: CGRect) {
		incrementalImage?.draw(in: rect)
	}

	override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
		setNeedsDisplay()
	}

	override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
		touchesEnded(touches, with: event)
	}
}

Here is the code I translated from

#import "NaiveVarWidthView.h"

@implementation NaiveVarWidthView
{
    UIBezierPath *path;
    UIImage *incrementalImage;
    CGPoint pts[5];
    uint ctr;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setMultipleTouchEnabled:NO];
        path = [UIBezierPath bezierPath];
    }
    return self;
}

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

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint p = [touch locationInView:self];
    ctr++;
    pts[ctr] = p;
    if (ctr == 4)
    {
        pts[3] = CGPointMake((pts[2].x + pts[4].x)/2.0, (pts[2].y + pts[4].y)/2.0);
        [path moveToPoint:pts[0]];
        [path addCurveToPoint:pts[3] controlPoint1:pts[1] controlPoint2:pts[2]];

        UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0.0); // ................. (1)

        if (!incrementalImage)
        {
            UIBezierPath *rectpath = [UIBezierPath bezierPathWithRect:self.bounds];
            [[UIColor whiteColor] setFill];
            [rectpath fill];
        }
        [incrementalImage drawAtPoint:CGPointZero];
        [[UIColor blackColor] setStroke];

        float speed = 0.0;

        for (int i = 0; i < 3; i++)
        {
            float dx = pts[i+1].x - pts[i].x;
            float dy = pts[i+1].y - pts[i].y;
            speed += sqrtf(dx * dx + dy * dy);
        } // ................. (2)

#define FUDGE_FACTOR 100 // emperically determined
        float width = FUDGE_FACTOR/speed; // ................. (3)

        [path setLineWidth:width];
        [path stroke];
        incrementalImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        [self setNeedsDisplay];

        [path removeAllPoints]; // ................. (4)
        pts[0] = pts[3];
        pts[1] = pts[4];
        ctr = 1;

    }
}

- (void)drawRect:(CGRect)rect
{
    [incrementalImage drawInRect:rect];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self setNeedsDisplay];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self touchesEnded:touches withEvent:event];
}

@end

This usually happens when you add the absolute position of a point (it becomes increasingly larger towards the bottom of the screen) rather than properly handling the deltas (the dx/dy) and/or resetting the tracked position. Try looking into this first.

I have to admit, I don't know enough to troubleshoot this. I am reasonably sure I got the translation to Swift right, but I'm not even 100% sure of that.

Could you elaborate more on this? I've been using absolute position (well, in the view's coordinate), and not sure what problem it could run into.

No, I'm not saying that it is wrong in any way, I've just seen similar bugs where something writes the readings from gesture recognizers cumulatively: imagine my two consecutive readings are 100 and 101 on the x axis and instead of moving the path's point by 1 I would just add this 101.

1 Like

I tried adding a 5px uiview to the touch point, it always stays right with the touch. So maybe the math is messed up

Terms of Service

Privacy Policy

Cookie Policy