CAShapeLayer. Does the line pass through the point? - ios

I use CAShapeLayer in order to draw a line on the screen. In the method touchesEnded I want to check " Does the line pass through the point?". In my code when I press on the any part of the screen the method contains returns always true. Perhaps, I have problem in line.frame = (view?.bounds)!. How can I fix it?
Sorry for my bad English.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let firstPosition = touch?.location(in: self)
if atPoint(firstPosition!) == lvl1 {
let firstPositionX = firstPosition?.x
let firstPositionY = frame.size.height - (firstPosition?.y)!
view?.layer.addSublayer(line)
line.lineWidth = 8
let color = #colorLiteral(red: 0.8078431487, green: 0.02745098062, blue: 0.3333333433, alpha: 1).cgColor
line.strokeColor = color
line.fillColor = nil
line.frame = (view?.bounds)!
path.move(to: CGPoint(x: firstPositionX!, y: firstPositionY))
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let firstPosition = touch?.location(in: self)
if atPoint(firstPosition!) == lvl1 {
let firstPositionX = firstPosition?.x
let firstPositionY = frame.size.height - (firstPosition?.y)!
path.addLine(to: CGPoint(x: firstPositionX!, y: firstPositionY))
line.path = path.cgPath
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if line.contains(screenCenterPoint) {
print("ok")
}
}

Problem
The method func contains(_ p: CGPoint) -> Bool of CAShapeLayer returns true if the bounds of the layer contains the point. (see documentation)
So you cannot use this to check if the line contains a point.
There is however, another method with the same name in the class CGPath that returns whether the specified point is interior to the path. But since you only stroke your path and you don't fill the interior, this method will not give the desired result either.
Solution
The trick is to create an outline of your path using:
let outline = path.cgPath.copy(strokingWithWidth: line.lineWidth, lineCap: .butt, lineJoin: .round, miterLimit: 0)
And then check if the interior of the outline contains your screenCenterPoint
if outline.contains(screenCenterPoint) {
print("ok")
}
Performance considerations
Since you are checking the containment only when touches end, I think that creating an outline of the path does not add too much overhead.
When you want to check the containment in realtime, for example inside the touchesMoved function, calculating an outline may produce some overhead because this method is called a lot of times per second. Also the longer the path becomes, the longer it will take to calculate the outline.
So in realtime it is better to generate only the outline of the last drawn segment and then check if that outline contains your point.
If you want to reduce overhead seriously, you can write your own containment function. Containment of a point in a straight line is fairly simple and can be reduced to the following formula:
Given a line from start to end with width and a point p
Calculate:
dx = start.x - end.x
dy = start.y - end.y
a = dy * p.x - dx * p.y + end.x * start.y - end.y * start.x
b = hypot(dy, dx)
The line contains point p if:
abs(a/b) < width/2 and p is in the bounding box of the line.

Related

Cannot find CGPoint in CGPath

I have the following function for drawing a line between two points:
override func draw(from fromPoint: CGPoint, to toPoint: CGPoint) {
let path = UIBezierPath()
path.move(to: fromPoint)
path.addLine(to: toPoint)
path.close()
layer.path = path.cgPath
layer.strokeColor = pencil.color.cgColor
layer.lineWidth = pencil.strokeSize
}
Which is being called in touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
This draws a line and works fine, the line draws itself on the correct screen position.
Then I have another functionality where the user can erase previous drawings, and I'm trying to get if a touch position, is contained by any of the drawn paths:
private func findLayer(in touch: UITouch) -> CAShapeLayer? {
let point = touch.location(in: view)
// all the drawn paths are contained in CAShapeLayers
// Circles and rectangle layers have a corresponding frame that contains the UIBezierPath.
if let hitLayer = view?.layer.hitTest(point) as? CAShapeLayer,
hitLayer.path?.contains(point) == true || hitLayer.frame.contains(point) {
return hitLayer
}
guard let sublayers = view?.layer.sublayers else { return nil }
// this extra check is for layers that dont have a frame (Lines, Angle and free drawing)
for layer in sublayers {
if let shapeLayer = layer as? CAShapeLayer,
shapeLayer.path?.contains(point) == true || shapeLayer.frame.contains(point) {
return shapeLayer
}
}
return nil
}
The problem is that when the user draws a line, the findLayer function doesn't ever return the Layer with the line, but it works perfectly when the user draws a circle or a rectangle.
I don't want to give Line drawings a frame, because then the hit box could be too big, and the user could delete the drawing even if the touch isn't near the real path.
How can a find if a touch point is part of a CAShapeLayer.path ?

Preventing draggable view to go off screen

I've got a button that is draggable and I don't want the user to be able to drag the view so the sides get cut off by the edges of the screen.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.superview)
self.center = CGPoint(x: position.x, y: position.y)
}
}
The way I'm doing it is a huge train of if / else statements checking against allowed areas on the screen.
let halfOfButton = CGFloat(85 / 2)
let heightOfTopBar = CGFloat(55) + halfOfButton
let widthOfScreen = CGFloat((self.superview?.frame.width)!) - halfOfButton
let heightOfScreen = CGFloat((self.superview?.frame.height)!) - heightOfTopBar
let heightOfBotBar = CGFloat((self.superview?.frame.height)! - heightOfTopBar)
if position.x > halfOfButton && position.x < widthOfScreen {
self.center = CGPoint(x: position.x, y: position.y)
} else {
if position.x < halfOfButton {
self.center = CGPoint(x: halfOfButton, y: position.y)
} else {
self.center = CGPoint(x: widthOfScreen, y: position.y)
}
}
if position.y > heightOfTopBar && position.y < heightOfScreen && position.x > halfOfButton && position.x < widthOfScreen {
self.center = CGPoint(x: position.x, y: position.y)
} else {
if position.y < heightOfTopBar {
self.center = CGPoint(x: position.x, y: heightOfTopBar)
} else {
self.center = CGPoint(x: position.x, y: heightOfScreen)
}
}
}
Is there a better way to accomplish this though? That code above doesn't even fully work yet. It has a lot of flaws in the logic where I'm saying "if the Y is okay, then just allow the center of your touch to be the center of the button" but then that overrides the code I'm writing in the X where even though the Y might be okay, the X isn't.
I can figure this out logic'ing it out this way, but surely there's a way better way of doing this?
Try something like this:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self.superview)
var x:CGFloat = position.x
var y:CGFloat = position.y
let parentWidth = button.superView!.frame.size.width
let parentHeight = button.superView!.frame.size.height
let width = button.frame.size.width / 2
let height = button.frame.size.height / 2
x = max(width, x)
x = min(x, parentWidth - width)
y = max(height, y)
y = min(y, parentHeight - height)
self.center = CGPoint(x: x, y: y)
}
}
I dealt with this issue awhile back and solved it with this approach.
First, to account for the size of the button, and not wanting to clip parts of it, I create a view (for this example, let's call it containerView) that is placed over the area you want the button to appear in. This view needs to have its margins reduced from the superview such that its top & bottom margins are half of the button's height, and it's leading & trailing edges are half of the button's width.
With that set up, it is now fairly trivial to contain its movement with a built-in function that offers better performance without the if statements.
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else {
return
}
let location = touch.location(in: containerView)
if containerView.point(inside: location, with: event) {
button.center = location
}
}
Now, depending on the coordinate systems you are using, it is possible that the button will not be properly positioned where you want it with this technique. If that does happen, you should be able to resolve it by modifying the location value prior to assigning it to the button.center property.
The modification should be to shift it right or down based on your leading & top margins. Ideally, you would use storyboard constraints for this, which you can then link to your controller, allowing you to directly get these values for the adjustment.

SKNode follow another SKNode within SKConstraint bounds

I am attempting to simulate an eye with SpriteKit.
The pupil of the eye tracks the users's finger as it moves across the screen, but must stay within the bounds of the eye.
I have attempted to solve this unsuccessfully with the use of SKConstraint.
Edit
My thought was to apply SKConstraints against the pupil to restrict its bounds to the eye. Any touches (i.e. touchesMoved(), etc) will be applied to the pupil in the form of of SKAction.moveTo() and SpriteKit handles maintaining the pupil within the eye bound.
let touchPoint = CGPoint()
SKAction.moveTo( touchPoint, duration: 2)
The code for the video is available: https://gist.github.com/anonymous/f2356e07d1ac0e67c25b1940662d72cb
A picture is worth a thousand words...
Imagine the pupil is the small, white, filled circle. The blue box simulates a user moving their finger across the screen.
Ideally, the pupil follows the blue box around the screen and follows the path as defined by the yellow circle.
iOS 10 | Swift 3 | Xcode 8
Instead of constraining by distance, you can use an orientation constraint to rotate a node to face toward the touch location. By adding the "pupil" node with an x offset to the node being constrained, the pupil will move toward the touch point. Here's an example of how to do that:
let follow = SKSpriteNode(color:.blue, size:CGSize(width:10,height:10))
let pupil = SKShapeNode(circleOfRadius: 10)
let container = SKNode()
let maxDistance:CGFloat = 25
override func didMove(to view: SKView) {
addChild(container)
// Add the pupil at an offset
pupil.position = CGPoint(x: maxDistance, y: 0)
container.addChild(pupil)
// Node that shows the touch point
addChild(follow)
// Add an orientation constraint to the container
let range = SKRange(constantValue: 0)
let constraint = SKConstraint.orient(to: follow, offset: range)
container.constraints = [constraint]
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
follow.position = location
adjustPupil(location: location)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for t in touches {
let location = t.location(in: self)
follow.position = location
adjustPupil(location: location)
}
}
// Adjust the position of the pupil within the iris
func adjustPupil(location:CGPoint) {
let dx = location.x - container.position.x
let dy = location.y - container.position.y
let distance = sqrt(dx*dx + dy*dy)
let x = min(distance, maxDistance)
pupil.position = CGPoint(x:x, y:0)
}
I wanted to wait for a response before I posted an answer, but #0x141E and I debated about how constraints work, so here is the code. Use it to find out where you are going wrong.
import SpriteKit
class GameScene: SKScene {
static let pupilRadius : CGFloat = 30
static let eyeRadius : CGFloat = 100
let follow = SKSpriteNode(color:.blue, size:CGSize(width:10,height:10))
let pupil = SKShapeNode(circleOfRadius: pupilRadius)
override func didMove(to view: SKView) {
let eye = SKShapeNode(circleOfRadius: GameScene.eyeRadius)
eye.strokeColor = .white
eye.fillColor = .white
addChild(eye)
pupil.position = CGPoint(x: 0, y: 0)
pupil.fillColor = .blue
eye.addChild(pupil)
addChild(follow)
pupil.constraints = [SKConstraint.distance(SKRange(lowerLimit: 0, upperLimit: GameScene.eyeRadius-GameScene.pupilRadius), to: eye)]
}
func moveFollowerAndPupil(_ location:CGPoint){
follow.position = location
pupil.position = convert(location, to: pupil.parent!)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.forEach({moveFollowerAndPupil($0.location(in: self))})
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
touches.forEach({moveFollowerAndPupil($0.location(in: self))})
}
}
As you can see, no use of square root on my end, hopefully Apple is smart enough to not use it either since it is not needed, meaning this should in theory be faster than manually doing the distance formula.

Making a "Joy stick" swift

What I have been trying to do is create a "Joy stick" that moves a player around. Here is what I have so far:
import UIKit
import SpriteKit
import SceneKit
class GameViewController: UIViewController, SCNSceneRendererDelegate {
var isTracking = false
var firstTrackingLocation = CGPoint.zero
var trackingVelocity = CGPoint.zero
var trackingDistance : CGFloat = 0.0
var previousTime : NSTimeInterval = 0.0
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene(named: "art.scnassets/level.scn")!
let scnView = self.view as! SCNView
scnView.delegate = self
scnView.scene = scene
scnView.showsStatistics = true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking == false {
for touch in touches {
isTracking = true
let location = touch.locationInView(self.view)
firstTrackingLocation = location
}
}
}
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
if isTracking {
trackingVelocity = touches.first!.locationInView(self.view)
}
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
isTracking = false
trackingVelocity = CGPoint.zero
}
func renderer(renderer: SCNSceneRenderer, updateAtTime time: NSTimeInterval) {
if isTracking == true {
let scnView = self.view as! SCNView
let character = scnView.scene!.rootNode.childNodeWithName("person", recursively: true)
let deltaTime = time - previousTime
let pointsPerSecond: CGFloat = 1.0 * CGFloat(deltaTime)
var xResult:CGFloat = 0.0
var yResult:CGFloat = 0.0
let point = firstTrackingLocation
let endPoint = trackingVelocity
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
if direction.x > direction.y {
let movePerSecond = pointsPerSecond/direction.x
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
} else {
let movePerSecond = pointsPerSecond/direction.y
xResult = direction.x*movePerSecond
yResult = direction.y*movePerSecond
}
character!.position = SCNVector3(CGFloat(character!.position.x) + (xResult), CGFloat(character!.position.y), CGFloat(character!.position.z) + (yResult))
let camera = scnView.scene?.rootNode.childNodeWithName("camera", recursively: true)
camera?.position = SCNVector3(CGFloat(camera!.position.x) + (xResult), CGFloat(camera!.position.y), CGFloat(camera!.position.z) + (yResult))
}
previousTime = time
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Landscape
}
}
Now this works except if you drag your finger to the other side of the phone the character moves 10 times faster then it would if you barely moved your finger. So what I would like to have, is a Joy stick that moves the character the same speed if you drag a little bit or to the other side of the screen. And I would also like if you changed direction at the other side of the screen that the character would move the other way. So, my guess is that there needs to be a lastPoint saved then when the touchesMoved gets called that somehow we calculate a direction from lastPoint to the currentPoint and then move the character in renderer. I understand that most of this code is probably rubbish, but thanks in advance.
Your joystick should be a value from 0 to 1, you need to determine the radius of your joystick, then calculate distance of (point touched to center of control) and the angle of the control with arc tan.
Now we need to ensure we never go past maxRadius, so if our distance is > maxRadius, we just set it to max radius, then we divide this value by our maxRadius to get out distance ratio.
Then we just take the cos and sin of our angle, and multiply it by our distance ratio, and get the x and y ratio values. (Should be between 0 and 1)
Finally, take this x and y value, and multiply it to the speed at which your object should be moving at.
let maxRadius = 100 //Your allowable radius
let xDist = (p2.x - p1.x)
let yDist = (p2.y - p1.y)
let distance = (sqrt((xDist * xDist) + (yDist * yDist))
let angle = atan2(yDist , xDist )
let controlDistanceRatio = (distance > maxRadius) ? 1 : distance / maxRadius
let controllerX = cos(angle) * controlDistanceRatio
let controllerY = sin(angle) * controlDistanceRatio
This seems like a good case to use a custom UIGestureRecognizer. See Apple API Reference.
In this particular case you would create a continuous gesture. The resultant CGVector would be calculated from the center (origin) point of your joystick on screen. The recognizer would fail if the joystick node isn't selected and end if unselected (deselected). The resultant CGVector will be updated while the gesture's state is moved.
Now the tricky part to figure out would be moving the node image in such a way that allows the user to have the feeling of a joystick. For this you may need to update the node texture and make slight adjustments to the node position to give the appearance of moving a node around.
See if this helps you: Single Rotation Gesture Recognizer
Let me know if this points you in the right direction.
Your stated problem is the character moves at a variable rate depending on the how far from the start point the user drags their finger. The crucial point in the code seems to be this
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
The difference between endPoint and point is variable and so you are getting a variable magnitude in your direction. To simplify, you could just put in a constant value like
let direction = CGPoint(x: 10, y: 10)
That gets the character moving at a constant speed when the user presses the joystick, but the character is always moving the same direction.
So somehow you've got to bracket in the variable directional values. The first thing that comes to mind is using min and max
let direction = CGPoint(x: endPoint.x - point.x, y: endPoint.y - point.y)
direction.x = min(direction.x, 10)
direction.x = max(direction.x, -10)
direction.y = min(direction.y, 10)
direction.y = max(direction.y, -10)
That seems like it would keep the magnitude of the direction values between -10 and 10. That doesn't make the speed completely constant, and it allows faster travel along diagonal lines than travel parallel to the x or y axis, but maybe it is closer to what you want.

Detect the degrees in a circle with users touch

Im looking to create a swift function to return the degrees (Float) of the users touch on an image (SKSpritenode). In touchesBegan, I know how to detect the x & y positions of my image. The idea is to create a function that takes in these positions and returns the degrees.
Amended - The following code now works:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.anchorPoint = CGPointMake(0.5, 0.5)
myNode.position = CGPointMake(0, -myNode.frame.height / 2)
self.addChild(myNode)
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
/* Called when a touch begins */
for touch in touches {
let location = touch.locationInNode(self)
if myNode.containsPoint(location) {
print("tapped!")
let origin = myNode.position
let touch = touch.locationInNode(myNode.parent!)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
print("degrees = \(degrees)")
}
}
}
You need to compare the user's touch position to an origin point, which might be the centre of your sprite node for example. Here's some code to get you started:
let origin = CGPoint(x: 0, y: 0)
let touch = CGPoint(x: 100, y: 100)
let diffX = touch.x - origin.x
let diffY = touch.y - origin.y
let radians = atan2(diffY, diffX)
let degrees = radians * CGFloat(180 / M_PI)
That last value – degrees – is the one you want if you want to show users information. If you want to do more calculations, you should probably stick with radians.

Resources