The below code draws curved lines while the user moves their finger across the screen. The method used to draw the cubic bezier curve is addCurveToPoint. However this method requires a full 1.. 2.. 3.. 4.. touch points before it can draw the curve on screen. This method causes a very short but noticeable laggy feedback on screen as nothing is drawn while the new touch points are gathered.
With code example/s, what new code/edits can be introduced into the below code to draw for instance a temporary line while touch points 1.. 2.. 3.. are gathered, with the temporary line removed once the proper curve is drawn so that the impression is drawing has begun immediately?
Or, what other techniques are available and can be used to give the user immediate feedback and the impression drawing has started on screen even though the touch points are still being gathered before they are properly drawn?
Note, the solution must be suitable for iOS7, iOS8 and iOS9 (i.e.
Advanced Touch Handling in iOS9 won't be suitable).
// Swift 2 code below tested using Xcode 7.0.1.
class drawView: UIView {
var path:UIBezierPath?
var incrementalImage:UIImage?
var points = [CGPoint?](count: 5, repeatedValue: nil)
var counter:Int?
var infoView:UIView = UIView()
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = UIBezierPath()
path?.lineWidth = 20.0
strokeColor = UIColor.darkGrayColor()
path?.lineCapStyle = CGLineCap.Round
}
override init(frame: CGRect) {
super.init(frame: frame)
self.multipleTouchEnabled = false
path = UIBezierPath()
path?.lineWidth = 20.0
}
override func drawRect(rect: CGRect) {
incrementalImage?.drawInRect(rect)
strokeColor?.setStroke()
path?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
counter = 0
let touch: AnyObject? = touches.first
points[0] = touch!.locationInView(self)
infoView.removeFromSuperview()
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
counter = counter! + 1
points[counter!] = point
if counter == 4{
points[3]! = CGPointMake((points[2]!.x + points[4]!.x)/2.0, (points[2]!.y + points[4]!.y)/2.0)
path?.moveToPoint(points[0]!)
path?.addCurveToPoint(points[3]!, controlPoint1: points[1]!, controlPoint2: points[2]!)
self.setNeedsDisplay()
points[0]! = points[3]!
points[1]! = points[4]!
counter = 1
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
path?.removeAllPoints()
counter = 0
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap(){
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if((incrementalImage) == nil){
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
path?.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
What about drawing Beziers of lower order when the first points are available ?
2 points define a linear Bezier (i.e. a line segment),
3 points define a quadratic Bezier.
In addition, you can think of rubberbanding, i.e. redrawing the curve while the last point is still moving (the former ones remaining fixed), until you fix it and start moving yet another.
Related
I'm working on a simple freehand drawing app. Below is the sample code, I'm using the UIGesture class to track the scribble, i.e., pencil or finger movement, and then stroking those touch points on the UIImageView, the stroke edges are blurry hence I scaled the context between 4 - 8, which gives strokes sharp edges. But however, this severely affects the smoothness of writing/lags extremely because strokes are scaled. How to reduce this lag?
class CanvasView: UIImageView, UIGestureRecognizer {
var drawingImage: UIImage?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 5)
let context = UIGraphicsGetCurrentContext()
drawingImage?.drawAsPattern(in: bounds)
var touches = [UITouch]()
if let coalescedTouches = event.coalescedTouches(for: touch) {
touches = coalescedTouches
} else {
touches.append(touch)
}
for touch in touches {
drawStroke(context: context, touch: touch)
}
drawingImage = UIGraphicsGetImageFromCurrentImageContext()
if let predictedTouches = event.predictedTouches(for: touch) {
for touch in predictedTouches {
drawStroke(context: context, touch: touch)
}
}
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
private func drawStroke(context: CGContext?, touch: UITouch) {
let previousLocation = touch.previousLocation(in: self)
let location = touch.location(in: self)
var lineWidth: CGFloat = 3
UIColor.black.setStroke()
context?.setLineWidth(lineWidth)
context?.setLineCap(.round)
context?.move(to: previousLocation)
context?.addLine(to: location)
context?.strokePath()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
image = drawingImage
}}
In this code i am facing the lagging problem and High CPU usage. i have created three diff file.
First one DrawView.swift
class DrawView: UIView {
var lines: [Line] = []
var firstPoint: CGPoint!
var percentageOfXY:[String]=[]
var viewWidth:CGFloat!
var viewHeight:CGFloat!
var drawColor:UIColor!
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
viewWidth = frame.width
viewHeight = frame.height
let touch = touches.first as UITouch?
let start = touch!.locationInView(self)
let percentageX = round(((start.x)*100)/viewWidth*100)/100
let percentageY = round(((start.y)*100)/viewHeight*100)/100
firstPoint = CGPointMake(percentageX, percentageY)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
viewWidth = frame.width
viewHeight = frame.height
let touch = touches.first as UITouch?
let curr = touch!.locationInView(self)
let percentageX = round(((curr.x)*100)/viewWidth*100)/100
let percentageY = round(((curr.y)*100)/viewHeight*100)/100
let lastPoint = CGPointMake(percentageX, percentageY)
lines.append(Line(start: firstPoint, end: lastPoint, color: drawColor))
firstPoint = lastPoint
self.setNeedsDisplay()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?)
{
}
override func drawRect(rect: CGRect) {
for line in lines
{
let context = UIGraphicsGetCurrentContext()
CGContextSetLineCap(context, CGLineCap.Round)
CGContextSetLineWidth(context,3)
let startX = ((line.start.x)*viewWidth)/100
let startY = ((line.start.y)*viewHeight)/100
let endX = ((line.end.x)*viewWidth)/100
let endY = ((line.end.y)*viewHeight)/100
CGContextBeginPath(context)
CGContextMoveToPoint(context, startX,startY)
CGContextAddLineToPoint(context, endX,endY)
CGContextSetStrokeColorWithColor(context,line.color.CGColor)
CGContextStrokePath(context)
}
}
}
Second Line.swift for storing cordinates in formats.
class Line
{
var start: CGPoint!
var end: CGPoint!
var cord: [String]!
var color:UIColor!
init(start _start: CGPoint,end _end: CGPoint,color _color:UIColor)
{
start = _start
end = _end
color = _color
}
}
Third one for color selection.
#IBOutlet weak var drawView: DrawView!
override func viewDidLoad() {
super.viewDidLoad()
drawView.backgroundColor = UIColor.clearColor()
}
#IBAction func redColor(sender: AnyObject) {
let theDrawView = drawView as DrawView
var color:UIColor!
color = UIColor.redColor()
theDrawView.drawColor = color
}
#IBAction func greenColor(sender: AnyObject) {
let theDrawView = drawView as DrawView
var color:UIColor!
color = UIColor.greenColor()
theDrawView.drawColor = color
}
#IBAction func blueColor(sender: AnyObject) {
let theDrawView = drawView as DrawView
var color:UIColor!
color = UIColor.blueColor()
theDrawView.drawColor = color
}
In this i am drawing round but through lagging problem it's draw straight line.
Several problems:
Get UIGraphicsGetCurrentContext out of your loop.
Get CGContextSetLineCap out of your loop.
Get CGContextSetLineWidth out of your loop.
Kill CGContextBeginPath altogether--CGContextStrokePath does that for you.
You should invent a line object with an array of points so that you don't have to keep setting the color for every little segment. TouchesBegan..TouchesEnded would start and finish one of these line objects. (You'd have to be more careful if you allow multitouch, but it doesn't look like you do.)
If you fix the line object like above, you'd have a pair of nested loops. Then you would move the CGContextMoveToPoint out front of the inner loop and the CGContextSetStrokeColorWithColor and CGContextStrokePath out of the end of the inner loop.
But now to your real problem: moving your finger across the screen can easily give several thousand pixel crossings per second. Your touchesMoved is only getting called 60 times per second (more often on an iPad Pro). If you want your sketch to look smooth, you'll have to "make up" data that was never delivered. See for example Kochanek–Bartels or Catmull–Rom splines on wikipedia.
The code below draws smooth curved lines by overriding touches, but there is noticeable lagging or latency. The code uses addCurveToPoint and calls setNeedsDisplay after every 4 touch points which causes a jumpy appearance as the drawing doesn't keep up with finger movements. To remove the lagging or perceived latency, touch points 1, 2, 3 (leading up to touch point 4) could be temporarily filled with addQuadCurveToPoint and addLineToPoint.
How can this actually be achieved in code to remove perceived lagging by using a temporary Line and QuadCurved line before displaying a final Curved line?
If the below class is attached to one UIView (e.g. viewOne or self), how do I make a copy of the drawing to another UIView outside the class (e.g. viewTwo) after touchesEnded?
// ViewController.swift
import UIKit
class drawSmoothCurvedLinesWithLagging: UIView {
let path=UIBezierPath()
var incrementalImage:UIImage?
var points = [CGPoint?](count: 5, repeatedValue: nil)
var counter:Int?
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
autoreleasepool {
incrementalImage?.drawInRect(rect)
strokeColor = UIColor.blueColor()
strokeColor?.setStroke()
path.lineWidth = 20
path.lineCapStyle = CGLineCap.Round
path.stroke()
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
counter = 0
let touch: AnyObject? = touches.first
points[0] = touch!.locationInView(self)
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
counter = counter! + 1
points[counter!] = point
if counter == 2{
//use path.addLineToPoint ?
//use self.setNeedsDisplay() ?
}
if counter == 3{
//use path.addQuadCurveToPoint ?
//use self.setNeedsDisplay() ?
}
if counter == 4{
points[3]! = CGPointMake((points[2]!.x + points[4]!.x)/2.0, (points[2]!.y + points[4]!.y)/2.0)
path.moveToPoint(points[0]!)
path.addCurveToPoint(points[3]!, controlPoint1: points[1]!, controlPoint2: points[2]!)
self.setNeedsDisplay()
points[0]! = points[3]!
points[1]! = points[4]!
counter = 1
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
path.removeAllPoints()
counter = 0
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap(){
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if((incrementalImage) == nil){
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
path.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Yes, adding a curve every few points will give it a stuttering lag. So, yes, you can reduce this affect by adding a line to points[1], adding a quad curve to points[2] and adding a cubic curve to points[3].
As you said, make sure to add this to a separate path, though. So, in Swift 3/4:
class SmoothCurvedLinesView: UIView {
var strokeColor = UIColor.blue
var lineWidth: CGFloat = 20
var snapshotImage: UIImage?
private var path: UIBezierPath?
private var temporaryPath: UIBezierPath?
private var points = [CGPoint]()
override func draw(_ rect: CGRect) {
snapshotImage?.draw(in: rect)
strokeColor.setStroke()
path?.stroke()
temporaryPath?.stroke()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
points = [touch.location(in: self)]
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let point = touch.location(in: self)
points.append(point)
updatePaths()
setNeedsDisplay()
}
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPoint(x: (points[2].x + points[4].x)/2.0, y: (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStarting(at: points[0])
}
path?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
points.removeFirst(3)
temporaryPath = nil
}
// build temporary path up to last touch point
if points.count == 2 {
temporaryPath = createPathStarting(at: points[0])
temporaryPath?.addLine(to: points[1])
} else if points.count == 3 {
temporaryPath = createPathStarting(at: points[0])
temporaryPath?.addQuadCurve(to: points[2], controlPoint: points[1])
} else if points.count == 4 {
temporaryPath = createPathStarting(at: points[0])
temporaryPath?.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
finishPath()
}
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
finishPath()
}
private func finishPath() {
constructIncrementalImage()
path = nil
setNeedsDisplay()
}
private func createPathStarting(at point: CGPoint) -> UIBezierPath {
let localPath = UIBezierPath()
localPath.move(to: point)
localPath.lineWidth = lineWidth
localPath.lineCapStyle = .round
localPath.lineJoinStyle = .round
return localPath
}
private func constructIncrementalImage() {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
strokeColor.setStroke()
snapshotImage?.draw(at: .zero)
path?.stroke()
temporaryPath?.stroke()
snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
You could even marry this with iOS 9 predictive touches (as I described in my other answer), which could reduce lag even further.
To take this resulting image and use it elsewhere, you can just grab the incrementalImage (which I renamed to snapshotImage, above), and drop it into an image view of the other view.
For Swift 2 rendition, see previous revision of this answer.
The below class attaches to a UIView and draws lines while moving a finger across the screen. However, when moving a finger very fast from left to right moving from top to bottom, the drawing temporarily shows sharp pointy edges on changing direction. This occurs both on the device and simulator.
What is causing this issue and how can this artefact be eliminated in the code so that only smooth rounded, not sharp edges are seen when changing direction fast?
class drawLine: UIView
{
var comittedSegments: Int = 0
var points = [CGPoint]()
var committedPath = UIBezierPath()
var drawPath = UIBezierPath()
var incrementalImage: UIImage?
var strokeColor:UIColor?
override func drawRect(rect: CGRect) {
autoreleasepool {
incrementalImage?.drawInRect(rect)
strokeColor = UIColor.darkGrayColor()
strokeColor?.setStroke()
drawPath.lineWidth = 20
drawPath.lineCapStyle = CGLineCap.Round
drawPath.stroke()
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
comittedSegments = 0
committedPath.removeAllPoints()
points.removeAll()
points.append( touch!.locationInView(self) )
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
points.append( point )
if points.count == 5
{
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
committedPath.moveToPoint(points[0])
committedPath.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
comittedSegments = comittedSegments + 1
self.setNeedsDisplay()
points[0] = points[3]
points[1] = points[4]
points.removeRange(2...4)
drawPath = committedPath
}
else if points.count > 1
{
drawPath = committedPath.copy() as! UIBezierPath
drawPath.CGPath = committedPath.CGPath
drawPath.moveToPoint( points[0] )
for point in points[1..<points.count] {
drawPath.addLineToPoint(point)
}
self.setNeedsDisplay()
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
committedPath.removeAllPoints()
points.removeAll()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap() {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if(incrementalImage == nil) {
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
committedPath.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
In addition to setting lineCapStyle, set lineJoinStyle:
drawPath.lineJoinStyle = .Round
drawPath.lineCapStyle = .Round
Note: In the below question, I use the term 'lagging' when I probably mean 'latency' when drawing using the addCurveToPoint function.
Problem:
Both bezier curve functions, addQuadCurveToPoint and addCurveToPoint have one strength and one weakness each. The aim is to get the perfect combination of both, a perfect continuous smooth curved line that is lag-free when drawn. The images below show where the touch on the screen typically is in comparison to the updated drawing.
The below image uses the function addQuadCurveToPoint. It draws fast
with no lagging while drawing during touch events, but the end result is a
less perfect smooth curved line that appears more segmented.
The below image uses the function
addCurveToPoint. It draws near perfect continuous smooth curved
lines but is slower with some lag noticeable while drawing during
touch events.
Question:
Can anyone help explain or give a solution please:
how to get perfect addQuadCurveToPoint curved lines or lag-free addCurveToPoint curved lines?
Note: The focus of this question is immediate lagging from the initial touch event for addCurveToPoint, not lagging over time, and also the less perfect curve line for addQuadCurveToPoint.
This code example is just one type of many implementations of addCurveToPoint:
// Swift 2 code below tested using Xcode 7.0.1.
class drawView: UIView {
var path:UIBezierPath?
var incrementalImage:UIImage?
var points = [CGPoint?](count: 5, repeatedValue: nil)
var counter:Int?
var infoView:UIView = UIView()
var strokeColor:UIColor?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.multipleTouchEnabled = false
self.backgroundColor = UIColor.whiteColor()
path = UIBezierPath()
path?.lineWidth = 20.0
strokeColor = UIColor.darkGrayColor()
path?.lineCapStyle = CGLineCap.Round
}
override init(frame: CGRect) {
super.init(frame: frame)
self.multipleTouchEnabled = false
path = UIBezierPath()
path?.lineWidth = 20.0
}
override func drawRect(rect: CGRect) {
incrementalImage?.drawInRect(rect)
strokeColor?.setStroke()
path?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
counter = 0
let touch: AnyObject? = touches.first
points[0] = touch!.locationInView(self)
infoView.removeFromSuperview()
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
counter = counter! + 1
points[counter!] = point
if counter == 4{
points[3]! = CGPointMake((points[2]!.x + points[4]!.x)/2.0, (points[2]!.y + points[4]!.y)/2.0)
path?.moveToPoint(points[0]!)
path?.addCurveToPoint(points[3]!, controlPoint1: points[1]!, controlPoint2: points[2]!)
self.setNeedsDisplay()
points[0]! = points[3]!
points[1]! = points[4]!
counter = 1
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.drawBitmap()
self.setNeedsDisplay()
path?.removeAllPoints()
counter = 0
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
self.touchesEnded(touches!, withEvent: event)
}
func drawBitmap(){
UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0.0)
strokeColor?.setStroke()
if((incrementalImage) == nil){
let rectPath:UIBezierPath = UIBezierPath(rect: self.bounds)
UIColor.whiteColor().setFill()
rectPath.fill()
}
incrementalImage?.drawAtPoint(CGPointZero)
path?.stroke()
incrementalImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
This code example is just one type of many implementations of addQuadCurveToPoint:
// Swift 2 code below tested using Xcode 7.0.1.
class DrawableView: UIView {
let path=UIBezierPath()
var previousPoint:CGPoint
var lineWidth:CGFloat=20.0
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override init(frame: CGRect) {
previousPoint=CGPoint.zero
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
previousPoint=CGPoint.zero
super.init(coder: aDecoder)
let panGestureRecognizer=UIPanGestureRecognizer(target: self, action: "pan:")
panGestureRecognizer.maximumNumberOfTouches=1
self.addGestureRecognizer(panGestureRecognizer)
}
override func drawRect(rect: CGRect) {
// Drawing code
UIColor.darkGrayColor().setStroke()
path.stroke()
path.lineWidth=lineWidth
path.lineCapStyle = .Round
}
func pan(panGestureRecognizer:UIPanGestureRecognizer)->Void
{
let currentPoint=panGestureRecognizer.locationInView(self)
let midPoint=self.midPoint(previousPoint, p1: currentPoint)
if panGestureRecognizer.state == .Began
{
path.moveToPoint(currentPoint)
}
else if panGestureRecognizer.state == .Changed
{
path.addQuadCurveToPoint(midPoint,controlPoint: previousPoint)
}
previousPoint=currentPoint
self.setNeedsDisplay()
}
func midPoint(p0:CGPoint,p1:CGPoint)->CGPoint
{
let x=(p0.x+p1.x)/2
let y=(p0.y+p1.y)/2
return CGPoint(x: x, y: y)
}
}