Swift 3 Generate evenly-spaced SKSpriteNodes along path drawn by user - ios

everyone! First of all, I'm aware that this question is very similar to Draw images evenly spaced along a path in iOS. However, that is in Objective-C (which I can't read) and it is in a normal ViewController working with CGImageRefs. I need it in swift and using SKSpriteNodes (not CGImageRefs). Here's my issue:
I'm trying to make a program that lets the user draw a simple shape (like a circle) and places SKSpriteNodes at fixed intervals along the path drawn by the user. I've got it working fine at a slow pace, but if the user draws too quickly then the nodes get placed too far apart. Here's an example of when I draw it slowly:
User-drawn path with nodes placed approximately 60 pixels apart from each other. Blue is the start node, purple is the end node.
The goal is that each node would have a physicsBody that kept entities from crossing the line drawn by the user (those entities wouldn't be able to squeeze in between evenly spaced nodes). If the user draws too fast, however, there will be a gap in defenses that I can't fix. For example:
Note the visibly larger gap between the 7th and 8th nodes. This occurred because I drew too quickly. Many people have questions that are slightly similar but are unhelpful for my task (e.g. place a specific amount of nodes evenly spaced along a path, rather than place as many nodes as neccessary to get them 60 pixels apart along the path).
In conclusion, here is my main question again: How can I place nodes perfectly spaced along a user-drawn path of any shape? Thank you in advance for your help! Here is my GameScene.swift file:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
let minDist: CGFloat = 60 //The minimum distance between one point and the next
var points: [CGPoint] = []
var circleNodes: [SKShapeNode] = []
override func didMove(to view: SKView) {
}
func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat {
let deltaX = fromPoint.x - toPoint.x
let deltaY = fromPoint.y - toPoint.y
let deltaXSquared = deltaX*deltaX
let deltaYSquared = deltaY*deltaY
return sqrt(deltaXSquared + deltaYSquared) //Return the distance
}
func touchDown(atPoint pos : CGPoint) {
self.removeAllChildren()
//The first time the user touches, we need to place a point and mark that as the firstCircleNode
print(pos)
points.append(pos)
//allPoints.append(pos)
let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
firstCircleNode.fillColor = UIColor.blue
firstCircleNode.strokeColor = UIColor.blue
firstCircleNode.position = pos
circleNodes.append(firstCircleNode)
self.addChild(firstCircleNode)
}
func touchMoved(toPoint pos : CGPoint) {
let lastIndex = points.count - 1 //The index of the last recorded point
let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
//vector_distance(vector_double2(Double(points[lastIndex].x), Double(points[lastIndex].y)), vector_double2(Double(pos.x), Double(pos.y))) //The distance between the user's finger and the last placed circleNode
if distance >= minDist {
points.append(pos)
//Add a box to that point
let newCircleNode = SKShapeNode(circleOfRadius: 5.0)
newCircleNode.fillColor = UIColor.red
newCircleNode.strokeColor = UIColor.red
newCircleNode.position = pos
circleNodes.append(newCircleNode)
self.addChild(newCircleNode)
}
}
func touchUp(atPoint pos : CGPoint) {
//When the user has finished drawing a circle:
circleNodes[circleNodes.count-1].fillColor = UIColor.purple //Make the last node purple
circleNodes[circleNodes.count-1].strokeColor = UIColor.purple
//Calculate the distance between the first placed node and the last placed node:
let distance = getDistance(fromPoint: points[0], toPoint: points[points.count-1])
//vector_distance(vector_double2(Double(points[0].x), Double(points[0].y)), vector_double2(Double(points[points.count - 1].x), Double(points[points.count - 1].y)))
if distance <= minDist { //If the distance is closer than the minimum distance
print("Successful circle")
} else { //If the distance is too far
print("Failed circle")
}
points = []
circleNodes = []
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}

You could try resizing the vector:
func touchMoved(toPoint pos : CGPoint) {
let lastIndex = points.count - 1 //The index of the last recorded point
let distance = getDistance(fromPoint: points[lastIndex], toPoint: pos)
if distance >= minDist {
// find a new "pos" which is EXACTLY minDist distant
let vx = pos.x - points[lastIndex].x
let vy = pos.y - points[lastIndex].y
vx /= distance
vy /= distance
vx *= minDist
vy *= minDist
let newpos = CGPoint(x: vx, y:vy)
points.append(newpos)
//Add a box to that point
let newCircleNode = SKShapeNode(circleOfRadius: 5.0)
newCircleNode.fillColor = UIColor.red
newCircleNode.strokeColor = UIColor.red
newCircleNode.position = newpos // NOTE
circleNodes.append(newCircleNode)
self.addChild(newCircleNode)
}
}
It probably won't be perfect but might look better.

I've figured it out! I was inspired by Christian Cerri's suggestion so I used the following code to make what I wanted:
import SpriteKit
import GameplayKit
// MARK: - GameScene
class GameScene: SKScene {
// MARK: - Allows me to work with vectors. Derived from https://www.raywenderlich.com/145318/spritekit-swift-3-tutorial-beginners
func subtract(point: CGPoint, fromPoint: CGPoint) -> CGPoint {
return CGPoint(x: point.x - fromPoint.x, y: point.y - fromPoint.y) //Returns a the first vector minus the second
}
func add(point: CGPoint, toPoint: CGPoint) -> CGPoint {
return CGPoint(x: point.x + toPoint.x, y: point.y + toPoint.y) //Returns a the first vector minus the second
}
func multiply(point: CGPoint, by scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x * scalar, y: point.y * scalar)
}
func divide(point: CGPoint, by scalar: CGFloat) -> CGPoint {
return CGPoint(x: point.x / scalar, y: point.y / scalar)
}
func magnitude(point: CGPoint) -> CGFloat {
return sqrt(point.x*point.x + point.y*point.y)
}
func normalize(aPoint: CGPoint) -> CGPoint {
return divide(point: aPoint, by: magnitude(point: aPoint))
}
// MARK: - Properties
let minDist: CGFloat = 60
var userPath: [CGPoint] = [] //Holds the coordinates collected when the user drags their finger accross the screen
override func didMove(to view: SKView) {
}
// MARK: - Helper methods
func getDistance (fromPoint: CGPoint, toPoint: CGPoint) -> CGFloat
{
let deltaX = fromPoint.x - toPoint.x
let deltaY = fromPoint.y - toPoint.y
let deltaXSquared = deltaX*deltaX
let deltaYSquared = deltaY*deltaY
return sqrt(deltaXSquared + deltaYSquared) //Return the distance
}
func touchDown(atPoint pos : CGPoint) {
userPath = []
self.removeAllChildren()
//Get the first point the user makes
userPath.append(pos)
}
func touchMoved(toPoint pos : CGPoint) {
//Get every point the user makes as they drag their finger across the screen
userPath.append(pos)
}
func touchUp(atPoint pos : CGPoint) {
//Get the last position the user was left touching when they've completed the motion
userPath.append(pos)
//Print the entire path:
print(userPath)
print(userPath.count)
plotNodesAlongPath()
}
/**
Puts nodes equidistance from each other along the path that the user placed
*/
func plotNodesAlongPath() {
//Start at the first point
var currentPoint = userPath[0]
var circleNodePoints = [currentPoint] //Holds the points that I will then use to generate circle nodes
for i in 1..<userPath.count {
let distance = getDistance(fromPoint: currentPoint, toPoint: userPath[i]) //The distance between the point and the next
if distance >= minDist { //If userPath[i] is at least minDist pixels away
//Then we can make a vector that points from currentPoint to userPath[i]
var newNodePoint = subtract(point: userPath[i], fromPoint: currentPoint)
newNodePoint = normalize(aPoint: newNodePoint) //Normalize the vector so that we have only the direction and a magnitude of 1
newNodePoint = multiply(point: newNodePoint, by: minDist) //Stretch the vector to a length of minDist so that we now have a point for the next node to be drawn on
newNodePoint = add(point: currentPoint, toPoint: newNodePoint) //Now add the vector to the currentPoint so that we get a point in the correct position
currentPoint = newNodePoint //Update the current point. Next we want to draw a point minDist away from the new current point
circleNodePoints.append(newNodePoint) //Add the new node
}
//If distance was less than minDist, then we want to move on to the next point in line
}
generateNodesFromPoints(positions: circleNodePoints)
}
func generateNodesFromPoints(positions: [CGPoint]) {
print("generateNodesFromPoints")
for pos in positions {
let firstCircleNode = SKShapeNode(circleOfRadius: 5.0)
firstCircleNode.fillColor = UIColor.blue
firstCircleNode.strokeColor = UIColor.blue
firstCircleNode.position = pos //Put the node in the correct position
self.addChild(firstCircleNode)
}
}
// MARK: - Touch responders
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchDown(atPoint: t.location(in: self)) }
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches { self.touchUp(atPoint: t.location(in: self)) }
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
And this results in the following:
No matter how quickly the user moves their finger, it places nodes evenly along their path. Thanks so much for your help, and I hope it helps more people in the future!

Related

Stretch SKSpriteNode between two points/touches

I have an SKSpriteNode in which I've set the centerRect property so that the node can be stretched to appear like a styled line. My intention is for the user to touch the screen, and draw/drag a straight line with the node. The line would pivot around an anchor point to remain straight.
In touchesBegan:, the node is added:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let positionInScene = touch.location(in: self)
if let _ = fgNode.childNode(withName: "laser") {
print("already there")
} else {
laser.centerRect = CGRect(x: 0.42857143, y: 0.57142857, width: 0.14285714, height: 0.14285714)
laser.anchorPoint = CGPoint(x: 0, y: 0.5)
laser.position = positionInScene
fgNode.addChild(laser)
}
}
And adjusted in touchesMoved::
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let positionInScene = touch.location(in: self)
stretchLaserTo(positionInScene)
}
The node is stretched and rotated with two functions:
func stretchLaserTo(_ point: CGPoint) {
let offset = point - laser.anchorPoint
let length = offset.length()
let direction = offset / CGFloat(length)
laser.xScale = length
rotate(sprite: laser, direction: direction)
}
func rotate(sprite: SKSpriteNode, direction: CGPoint) {
sprite.zRotation = atan2(direction.y, direction.x)
}
I think I'm somewhat on the right track. The line rotates with my touch and expands, however, it's extremely sensitive and doesn't stay with my touch. Maybe I'm going about it wrong. Is there a standard technique for doing something like this?
An example of this working can be seen here: https://imgur.com/A83L45i
I suggest you set anchor point of the sprite to (0, 0), set the sprite's scale to the distance between the sprite's position and the current touch location, and then rotate the sprite.
First, create a sprite and set its anchor point.
let laser = SKSpriteNode(color: .white, size: CGSize(width: 1, height: 1))
override func didMove(to view: SKView) {
laser.anchorPoint = CGPoint(x: 0, y: 0)
addChild(laser)
}
In touchesBegan, set the position of the sprite to the location of the touch. In this case, it's also the start of the line.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let positionInScene = touch.location(in: self)
laser.position = positionInScene
laser.setScale(1)
}
Update the sprite so that it forms a line that starts at the position of the sprite and ends at the current touch location.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let positionInScene = touch.location(in: self)
stretchLaserTo(positionInScene)
}
Stretch the sprite by setting its xScale to the distance from the start of the line to the location of the current touch and then rotate the sprite.
func stretchLaserTo(_ point: CGPoint) {
let dx = point.x - laser.position.x
let dy = point.y - laser.position.y
let length = sqrt(dx*dx + dy*dy)
let angle = atan2(dy, dx)
laser.xScale = length
laser.zRotation = angle
}

SpriteKit: calculate angle of joystick and change sprite based on that

I am making a RPG Birds-eye style game with SpriteKit. I made a joystick because a D-Pad does not give the player enough control over his character.
I cannot wrap my brain around how I would calculate the neccessary data needed to change the Sprite of my character based on the angle of the joystick thumb Node.
Here is my code I am using
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if isTracking == false && base.contains(location) {
isTracking = true
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location: CGPoint = touch.location(in: self)
if isTracking == true {
let v = CGVector(dx: location.x - base.position.x, dy: location.y - DPad.position.y)
let angle = atan2(v.dy, v.dx)
let deg = angle * CGFloat(180 / Double.pi)
let Length:CGFloat = base.frame.size.height / 2
let xDist: CGFloat = sin(angle - 1.57079633) * Length
let yDist: CGFloat = cos(angle - 1.57079633) * Length
print(xDist,yDist)
xJoystickDelta = location.x * base.position.x / CGFloat(Double.pi)
yJoystickDelta = location.y * base.position.y / CGFloat(Double.pi)
if base.contains(location) {
thumbNode.position = location
} else {
thumbNode.position = CGPoint(x: base.position.x - xDist, y: base.position.y + yDist)
}
}
}
}
Update method
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
if xJoystickDelta > 0 && yJoystickDelta < 0 {
print("forward")
}
}
The way I have set up right now tests the positive or negative state of the Joystick position in a cross method based on where the thumb Node is inside of the four marked sections below
I dont want it to do that
How can I set it up so that it changes the sprite based on where the thumb node is actually pointing inside my joysticks base like so.
I have been struggling with this for 3 days now so any help would be appreciated.
That looks far too complicated. Just compare the x and y components
of the difference vector v. Something like this:
if v.dx > abs(v.dy) {
// right
} else if v.dx < -abs(v.dy) {
// left
} else if v.dy < 0 {
// up
} else if v.dy > 0 {
// down
}

Swift 3 Gesture Recognizers path.boundingBox is infinite

I wanted to learn more about custom gesture recognizers so I was reading the Ray Wenderlich tutorial, which I planned to modify in order to learn the details and what I can change easily to learn how each piece works, but it was written in a previous version of Swift. Swift updated most of the code and I was able to fix the rest manually except that I am having trouble getting the touch gestures to be drawn on the screen, and no shapes are recognized as circles, which I'm hoping both tie back to the same problem. The website and code snippet are as follows:
https://www.raywenderlich.com/104744/uigesturerecognizer-tutorial-creating-custom-recognizers
import UIKit
import UIKit.UIGestureRecognizerSubclass
class CircleGestureRecognizer: UIGestureRecognizer {
fileprivate var touchedPoints = [CGPoint]() // point history
var fitResult = CircleResult() // information about how circle-like is the path
var tolerance: CGFloat = 0.2 // circle wiggle room, lower is more circle like higher is oval or other
var isCircle = false
var path = CGMutablePath() // running CGPath - helps with drawing
override func touchesBegan(_ touches: (Set<UITouch>!), with event: UIEvent) {
if touches.count != 1 {
state = .failed
}
state = .began
let window = view?.window
if let touches = touches, let loc = touches.first?.location(in: window) {
//print("path 1 \(path.currentPoint)")
path.move(to: CGPoint(x: loc.x, y: loc.y)) // start the path
print("path 2 \(path.currentPoint)")
}
//super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: (Set<UITouch>!), with event: UIEvent) {
// 1
if state == .failed {
return
}
// 2
let window = view?.window
if let touches = touches, let loc = touches.first?.location(in: window) {
// 3
touchedPoints.append(loc)
print("path 3 \(path.currentPoint)")
path.move(to: CGPoint(x: loc.x, y: loc.y))
print("path 4 \(path.currentPoint)")
// 4
state = .changed
}
}
override func touchesEnded(_ touches: (Set<UITouch>!), with event: UIEvent) {
print("path 5 \(path.currentPoint)")
// now that the user has stopped touching, figure out if the path was a circle
fitResult = fitCircle(touchedPoints)
// make sure there are no points in the middle of the circle
let hasInside = anyPointsInTheMiddle()
let percentOverlap = calculateBoundingOverlap()
isCircle = fitResult.error <= tolerance && !hasInside && percentOverlap > (1-tolerance)
state = isCircle ? .ended : .failed
}
override func reset() {
//super.reset()
touchedPoints.removeAll(keepingCapacity: true)
path = CGMutablePath()
isCircle = false
state = .possible
}
fileprivate func anyPointsInTheMiddle() -> Bool {
// 1
let fitInnerRadius = fitResult.radius / sqrt(2) * tolerance
// 2
let innerBox = CGRect(
x: fitResult.center.x - fitInnerRadius,
y: fitResult.center.y - fitInnerRadius,
width: 2 * fitInnerRadius,
height: 2 * fitInnerRadius)
// 3
var hasInside = false
for point in touchedPoints {
if innerBox.contains(point) {
hasInside = true
break
}
}
//print(hasInside)
return hasInside
}
fileprivate func calculateBoundingOverlap() -> CGFloat {
// 1
let fitBoundingBox = CGRect(
x: fitResult.center.x - fitResult.radius,
y: fitResult.center.y - fitResult.radius,
width: 2 * fitResult.radius,
height: 2 * fitResult.radius)
let pathBoundingBox = path.boundingBox
// 2
let overlapRect = fitBoundingBox.intersection(pathBoundingBox)
// 3
let overlapRectArea = overlapRect.width * overlapRect.height
let circleBoxArea = fitBoundingBox.height * fitBoundingBox.width
let percentOverlap = overlapRectArea / circleBoxArea
print("Percent Overlap \(percentOverlap)")
print("pathBoundingBox \(pathBoundingBox)")
print("path 6 \(path.currentPoint)")
return percentOverlap
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
state = .cancelled // forward the cancel state
}
}
As shown in the tutorial this bit of code is supposed to compare a bounding box for the path to a box that would fit a circle and compare the overlap, but when I print the pathBoundingBox is states: "pathBoundingBox (inf, inf, 0.0, 0.0)" which is probably why the percentOverlap is 0. I was thinking it was the path.move(to: loc) where loc is the first touch location but the documentation for move(to:) says "This method implicitly ends the current subpath (if any) and sets the current point to the value in the point parameter." so I'm struggling to figure out why the path.boundingBox is infinite...
That's not an infinite bounding box, it's just the opposite — a zero bounding box. The problem is that your path is empty.

Swift: Agar.io-like smooth SKCameraNode movement?

If anyone here has played Agar.io before, you'll probably be familiar with the way the camera moves. It slides toward your mouse depending on the distance your mouse is from the center of the screen. I'm trying to recreate this movement using an SKCameraNode and touchesMoved:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let pos = touches.first!.location(in: view)
let center = view!.center
let xDist = pos.x - center.x
let yDist = pos.y - center.y
let distance = sqrt((xDist * xDist) + (yDist * yDist)) * 0.02
camera?.position.x += xDist * distance * 0.02
camera?.position.y -= yDist * distance * 0.02
}
Surprisingly, this doesn't look too bad. However, there are two problems with this.
The first is that I want to use a UIPanGestureRecognizer for this, since it'd work far better with multiple touches. I just can't imagine how this could be done, since I can't think of a way to use it to get the distance from the touch to the center of the screen.
The second problem is that this movement isn't smooth. If the user stops moving their finger for a single frame, there is a huge jump since the camera snaps to an absolute halt. I'm awful at maths, so I can't think of a way to implement a nice smooth decay (or attack).
Any help would be fantastic!
EDIT: I'm now doing this:
private var difference = CGVector()
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let pos = touches.first!.location(in: view)
let center = view!.center
difference = CGVector(dx: pos.x - center.x, dy: pos.y - center.y)
}
override func update(_ currentTime: TimeInterval) {
let distance = sqrt((difference.dx * difference.dx) + (difference.dy * difference.dy)) * 0.02
camera?.position.x += difference.dx * distance * 0.02
camera?.position.y -= difference.dy * distance * 0.02
}
All I need to know now is a good way to get the difference variable to smoothly increase from the time the user touches the screen.
EDIT 2: Now I'm doing this:
private var difference = CGVector()
private var touching: Bool = false
private var fade: CGFloat = 0
private func touched(_ touch: UITouch) {
let pos = touch.location(in: view)
let center = view!.center
difference = CGVector(dx: pos.x - center.x, dy: pos.y - center.y)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touching = true
touched(touches.first!)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touched(touches.first!)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touching = false
touched(touches.first!)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
touching = false
}
override func update(_ currentTime: TimeInterval) {
if touching {
if fade < 0.02 { fade += 0.0005 }
} else {
if fade > 0 { fade -= 0.0005 }
}
let distance = sqrt((difference.dx * difference.dx) + (difference.dy * difference.dy)) * 0.01
camera?.position.x += difference.dx * distance * fade
camera?.position.y -= difference.dy * distance * fade
}
Can someone please help me before it gets any worse? The fade variable is incremented in an awful way and it's not smooth, and I just need someone to give me a slight hint of a better way to do this.
Try Linear Interpolation. Linear interpolation can make it so your object will slowly and smoothly speed up or slow down over time.

Artefact drawing in Swift

The code below draws lines by overriding touches, however there is an artefact that persists when drawing, seen in the images below.
When changing direction while zig zagging drawing across the screen, sometimes the line turns into a flat straight corner instead of remaining circular. The artefact is also experienced when drawing on the spot in small circles, the drawing point flashes half circles sometimes leaving half circles and partial circle residue when the finger leave the screen.
The artefacts are intermittent and not in an entirely consistent or predictable pattern making it difficult to find the issue in the code. It is present both in the simulator and on device in iOS7 - iOS9.
A zip containing two video screen captures of drawing dots and lines along with the Xcode project are uploaded to DropBox in a file called Archive.zip (23MB) https://www.dropbox.com/s/hm39rdiuk0mf578/Archive.zip?dl=0
Questions:
1 - In code, what is causing this dot/half circle artefact and how can it be corrected?
class SmoothCurvedLinesView: UIView {
var strokeColor = UIColor.blueColor()
var lineWidth: CGFloat = 20
var snapshotImage: UIImage?
private var path: UIBezierPath?
private var temporaryPath: UIBezierPath?
private var points = [CGPoint]()
private var totalPointCount = 0
override func drawRect(rect: CGRect) {
snapshotImage?.drawInRect(rect)
strokeColor.setStroke()
path?.stroke()
temporaryPath?.stroke()
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
points = [touch!.locationInView(self)]
totalPointCount = totalPointCount + 1
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
path?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
constructIncrementalImage()
path = nil
setNeedsDisplay()
}
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
touchesEnded(touches!, withEvent: event)
}
private func createPathStartingAtPoint(point: CGPoint) -> UIBezierPath {
let localPath = UIBezierPath()
localPath.moveToPoint(point)
localPath.lineWidth = lineWidth
localPath.lineCapStyle = .Round
localPath.lineJoinStyle = .Round
return localPath
}
private func constructIncrementalImage(includeTemporaryPath includeTemporaryPath: Bool = true) {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
strokeColor.setStroke()
snapshotImage?.drawAtPoint(CGPointZero)
path?.stroke()
if (includeTemporaryPath) { temporaryPath?.stroke() }
snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
This would appear to be a fascinating bug in addQuadCurveToPoint and addCurveToPoint where, if the control point(s) are on the same line as the two end points, it doesn't honor the lineJoinStyle. So you can test for this (by looking at the atan2 of the various points and make sure there are not the same), and if so, just do addLineToPoint instead:
I found that this revised code removed those artifacts:
private func updatePaths() {
// update main path
while points.count > 4 {
points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)
if path == nil {
path = createPathStartingAtPoint(points[0])
}
addCubicCurveToPath(path)
points.removeFirst(3)
}
// build temporary path up to last touch point
let pointCount = points.count
if pointCount == 2 {
temporaryPath = createPathStartingAtPoint(points[0])
temporaryPath?.addLineToPoint(points[1])
} else if pointCount == 3 {
temporaryPath = createPathStartingAtPoint(points[0])
addQuadCurveToPath(temporaryPath)
} else if pointCount == 4 {
temporaryPath = createPathStartingAtPoint(points[0])
addCubicCurveToPath(temporaryPath)
}
}
/// Add cubic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than cubic bezier path.
private func addCubicCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m23 = atan2(points[2].x - points[3].x, points[2].y - points[3].y)
let m03 = atan2(points[0].x - points[3].x, points[0].y - points[3].y)
if m01 == m03 || m23 == m03 || points[0] == points[3] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
somePath?.addLineToPoint(points[3])
} else {
somePath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
}
}
/// Add quadratic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than quadratic bezier path.
private func addQuadCurveToPath(somePath: UIBezierPath?) {
let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
let m12 = atan2(points[1].x - points[2].x, points[1].y - points[2].y)
let m02 = atan2(points[0].x - points[2].x, points[0].y - points[2].y)
if m01 == m02 || m12 == m02 || points[0] == points[2] {
somePath?.addLineToPoint(points[1])
somePath?.addLineToPoint(points[2])
} else {
somePath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
}
}
Also, this may be overly cautious, but it might be prudent to ensure that two successive points are never the same with a guard statements:
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch: AnyObject? = touches.first
let point = touch!.locationInView(self)
guard point != points.last else { return }
points.append(point)
totalPointCount = totalPointCount + 1
updatePaths()
if totalPointCount > 50 {
constructIncrementalImage(includeTemporaryPath: false)
path = nil
totalPointCount = 0
}
setNeedsDisplay()
}
If you find other situations where there are problems, you can repeat the debugging exercise that I just did. Namely, run the code until a problem manifested itself, but stop immediately and look at the log of points array to see what points caused a problem, and then create a init?(coder:) that consistently reproduced the problem 100% of the time, e.g.:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
points.append(CGPoint(x: 239.33332824707, y: 419.0))
points.append(CGPoint(x: 239.33332824707, y: 420.0))
points.append(CGPoint(x: 239.33332824707, y: 419.3))
updatePaths()
}
Then, with a consistently reproducible problem, the debugging was easy. So having diagnosed the problem, I then revised updatePaths until the problem was resolved. I then commented out init? and repeated the whole exercise.

Resources