Swift. UIBezierPath shape detection - swift

I working with UIBezierPath and shape detection. For painting im using "UIPanGestureRecognizer".
Example of code:
My shape definder
var gesture = UIPanGestureRecognizer()
.
.
.
view.addGestureRecognizer(gesture.onChange { \[weak self\] gesture in
let point = gesture.location(in: self.view)
let shapeL = CAShapeLayer()
shapeL.strokeColor = UIColor.black.cgColor
shapeL.lineWidth = 2
shapeL.fillColor = UIColor.clear.cgColor
switch gesture.state {
case .began:
//some code
currentBezierPath = UIBezierPath()
break
case .changed:
//some code
shapeLayer.path = self.currentBezierPath.cgPath
break
case .ended:
//define what user was painted(circle, rectangle, etc)
shapeDefinder(path: currentBezierPath)
break
default:
break
})
shapeDefinder
func shapeDefinder(path: UIBezierPath) {
if(path.hasFourRightAngles()){
// square
}
}
extension hasFourRightAngles
extension UIBezierPath {
func hasFourRightAngles() -> Bool {
guard self.currentPoint != .zero else {
// empty path cannot have angles
return false
}
let bounds = self.bounds
let points = [
bounds.origin,
CGPoint(x: bounds.minX, y: bounds.minY),
CGPoint(x: bounds.maxX, y: bounds.maxY),
CGPoint(x: bounds.minX, y: bounds.maxY)
]
let angleTolerance = 5.0 // in degrees
var rightAngleCount = 0
for i in 0...3 {
let p1 = points[i]
let p2 = points[(i+1)%4]
let p3 = points[(i+2)%4]
let angle = p2.angle(between: p1, and: p3)
if abs(angle - 90) <= angleTolerance {
rightAngleCount += 1
}
}
return rightAngleCount >= 4
}
}
and
extension CGPoint {
func angle(between p1: CGPoint, and p2: CGPoint) -\> CGFloat {
let dx1 = self.x - p1.x
let dy1 = self.y - p1.y
let dx2 = p2.x - self.x
let dy2 = p2.y - self.y
let dotProduct = dx1*dx2 + dy1*dy2
let crossProduct = dx1*dy2 - dx2*dy1
return atan2(crossProduct, dotProduct) \* 180 / .pi
}
}
but my method hasFourRightAngles() doesnt work, it always has true.
Cant understand how i can detect square(the user must draw exactly a square, if the user draws a circle, then the check should not pass.)
Maybe someone know about some library which works with UIBezierPath for detect shapes?

The bounds of a path are always a rectangle, no matter the shape, so you should expect this function to always return true. From the docs:
The value in this property represents the smallest rectangle that completely encloses all points in the path, including any control points for Bézier and quadratic curves.
If you want to consider the components of the path itself, you'd need to iterate over its components using it's CGPath. See applyWithBlock for how to get the elements. That said, this probably won't work very well, since you likely don't care precisely how the shape was drawn. If you go down this road, you'll probably want to do some work to simplify the curve first, and perhaps put the stokes in a useful order.
If the drawing pattern itself is the important thing (i.e. the user's gesture is what matters), then I would probably keep track of whether this could be a rectangle at each point of the drawing. Either it needs to be roughly colinear to the previous line, or roughly normal. And then the final point must be close to the original point.
The better approach is possibly to consider the final image of the shape, regardless of how it was drawn, and then classify it. For various algorithms to do that, see How to identify different objects in an image?

Your code to get the bounds of your path will not let you tell if the lines inside the path make right angles. As Rob says in his answer, the bounding box of a path will always be a rectangle, so your current test will always return true.
The bounding box of a circle will be a square, as will the box of any shape who's horizontal and vertical maxima and minima are equal.
It is possible to interrogate the internal elements of the underlying CGPath and look for a series of lines that make a square. I suggest searching for code that parses the elements of a CGPath.
Note that if you are checking freehand drawing, you will likely need some "slop" in your calculations to allow for shapes that are close to, but not exactly squares, or you will likely never find a perfect square.
Also, what if the path contains a square plus other shape elements? You will need to decide how to handle situations like that.

Related

Swift Combine and reverse UIBezierPaths

The problem I am facing is reversing subpaths. Take this example:
let circlePaths: [UIBezierPath] = ...
let rectanglePath: UIBezierPath = ... // a rectangle
let totalPath: UIBezierPath = .init()
for path in circlePaths {
totalPath.append(path)
}
rectanglePath.append(totalPath)
It should look like this:
Now ideally I want to cut out all the circles using
bezierPath.append(totalPath.reversing())
However the effect is not as expected. I expect the two circles to make up a path and this one is reversed, however in reality both circle paths are reversed, which causes the intersection to be part of the path (reversing() twice has no effect). I'd like to combine the circle paths into one with the intersection not being present but as part of the path. I want the smaller circle to "extend" the larger circle as a path.
Any idea how I would do it?
Edit 1: Here is an image how the resulting path should look like.
If you need to actually create a single path as the combination / union of your multiple paths, you may want to look at one of the libraries that are out there.
However, if you only need that visual output, this might be a usable approach.
Create 3 paths -- outer rect, large circle, small circle.
Stroke and Fill the outer rect path
Stroke both circle paths
Fill both circle paths
An example UIView class:
class FakeUnionUIBezierPaths : UIView {
override func draw(_ rect: CGRect) {
// yellow line width
let lWidth: CGFloat = 8
// paths are stroked on the center, so
// use one-half the line width to adjust ediges
let hWidth: CGFloat = lWidth * 0.5
// border / outer rect, inset by one-half line width
let dRect: CGRect = rect.insetBy(dx: hWidth, dy: hWidth)
// first circle
// full-height
// aligned to left edge
var c1Rect: CGRect = dRect
c1Rect.size.width = c1Rect.height
// second circle
// half-height
// aligned to right edge
var c2Rect: CGRect = dRect
c2Rect.size.height *= 0.5
c2Rect.size.width = c2Rect.height
c2Rect.origin.x = dRect.width - c2Rect.width + hWidth
c2Rect.origin.y = dRect.height * 0.25
let pRect: UIBezierPath = UIBezierPath(rect: dRect)
let p1: UIBezierPath = UIBezierPath(ovalIn: c1Rect)
let p2: UIBezierPath = UIBezierPath(ovalIn: c2Rect)
UIColor.yellow.setStroke()
UIColor.blue.setFill()
// same line-width for all three paths
[pRect, p1, p2].forEach { p in
p.lineWidth = lWidth
}
// stroke and fill the border / outer rect
pRect.stroke()
pRect.fill()
// stroke both circle paths
p1.stroke()
p2.stroke()
// fill both circle paths
p1.fill()
p2.fill()
}
}
Output:

Convert between UIKit and SpriteKit coordinate systems

I'm new to iOS programming, and have almost no experience with SpriteKit, so please forgive me if this is a ridiculous question.
I've been trying to make a basic grid with a 2D array, and I would prefer to work with it from top-left being 0, 0.
After researching the differences in coordinate systems between UIKit and SpriteKit, I came across this answer about Converting Between View and Scene Coordinates but it doesn't seem to change the y value the way I thought it would. I am guessing that I'm not using it right, or maybe this is not what it's meant to do, I don't know.
When I try this:
let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)
it doesn't seem to have any effect on the y value.
I have found that when I change to "y: -cy" in the line let cellCoordinates = CGPoint(x: cx, y: cy)
Then it does seem to work the way I am hoping for, but I don't know if that's the only solution or if doing this will work as expected under more complicated situations.
Here is the code I am working with:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
var background: SKShapeNode!
background = SKShapeNode(rectOf: CGSize(width: frame.size.width, height: frame.size.height))
background.fillColor = SKColor.lightGray
self.addChild(background)
let margin = CGFloat(50)
let width = frame.size.width - margin
let height = frame.size.height - margin
let centerX = frame.midX - width / 2
let centerY = frame.midY - height / 2
var grid: SKShapeNode!
grid = SKShapeNode(rectOf: CGSize(width: width, height: height))
grid.strokeColor = SKColor.clear
self.addChild(grid)
let numRows = 2
let numCols = 3
let cellWidth = width / CGFloat(numCols)
for r in 0..<numRows {
for c in 0..<numCols {
let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
//***
let cellCoordinates = CGPoint(x: cx, y: cy)
//***
let cellNode = SKShapeNode(rectOf: CGSize(width: cellWidth, height: cellWidth))
let convertedCoordinates = convert(cellCoordinates, to: grid)
print(cellCoordinates.y, convertedCoordinates.y)
cellNode.strokeColor = SKColor.black
cellNode.lineWidth = 5
cellNode.fillColor = SKColor.darkGray
cellNode.position = convertedCoordinates
let textNode = SKLabelNode(text: String("\(r),\(c)"))
textNode.fontName = "Menlo"
textNode.fontSize = 60
textNode.verticalAlignmentMode = .center
textNode.position = convertedCoordinates
grid.addChild(cellNode)
grid.addChild(textNode)
}
}
}
}
This is more a philosophical answer than an implementation one. As far as somehow flipping SpriteKit's coordinate system, well, you're going to be fighting it constantly. Better to just embrace the system as it is.
The essence of your question though is more one of separation of model and view. When you say
I would prefer to work with it from top-left being 0, 0
what you mean is that mentally you're thinking of the game as a grid of cells with 0,0 at the top left. That's perfectly fine and natural. That's your model of the game. But what are you writing in the code?
let cx = centerX + (cellWidth / 2) + (CGFloat(c) * cellWidth)
let cy = centerY + (cellWidth / 2) + (CGFloat(r) * cellWidth)
let cellCoordinates = CGPoint(x: cx, y: cy)
let convertedCoordinates = convert(cellCoordinates, to: grid)
That's your view struggling to get out. You have the abstract model grid that you're indexing with r,c with 0,0 at the upper left and whose coordinates increase in unit steps down and to the right. Then there's the view of the model, which might depend on screen resolution, aspect ratio, device orientation, whatever. If you keep the two mentally separate, you'll usually find that you can isolate the translation between the two systems to a small interface. In those places you may have to do things like scale the axes or flip one of them, or stretch things in one direction to match aspect ratios.
In a case like this, if you start with your mental model with your preferred 0,0 in the upper left and think about how the game operates, it'll often be in terms of the cells. OK, that suggests that maybe a 2D array or an array of arrays is natural. Maybe the cells will eventually become a class in your game. They'll probably have a node property that stores the SpriteKit node. You might wind up with something like this:
struct boardPosition {
let row: Int
let col: Int
}
class Cell {
let pos: boardPosition
let node: SKNode
init(pos: boardPosition, in board: Board) {
self.pos = pos
node = SKShapeNode(...)
board.node.addChild(node)
}
}
class Board {
let cells: [[Cell]]
let node: SKNode
init(numRows: Int, numColumns: Int) {
...
}
func movePiece(from: boardPosition, to: boardPosition) {
let piece = cell[from.row][from.col].removePiece()
cell[to.row][to.col].addPiece(piece)
}
}
The operation of the game will be in terms of your mental model. The fact that the y-coordinates of the cells' SKNode nodes happen to decrease as the row index increases will be completely buried.
Set all nodes applicable and scene’s anchor point to 0,1 to get it to mount to the top left corner and set your world node’s (if you do not have one, I recommend adding it, it is a basic SKNode that you use to place all of your game nodes in, allowing you to use a separate node for things not applicable to the game world, like hud and overlays) yScale to -1 to have y increment downward instead of upward.
Edit:
When dealing with SKShapeNodes, you do not have to worry about the images being inversed unless you have an obscure shape. When designing the CGPath for the obscure shape, just flip it.
shape.path = shape.path!.copy(using:CGAffineTransform(scaleX:1,y:-1))
The bigger problem is SKShapeNode does not have anchor points. You instead need to move the entire CGPath
To do this, add the following line:
shape.path = shape.path!.copy(using:CGAffineTransform(translationX:shape.frame.width/2,y:shape.frame.height/2))
If dealing with SKSprite nodes in the future....
This will cause your assets to be upside down, so all you would need to do is have your assets flipped before import, use a secondary node to flip the y axis, or assign all nodes with a yScale of -1. Flipping all of your assets prior to import vertically would be the cheapest method, I believe you can flip it inside xcassets as well, but I need to verify that when I get back on a MacOS again.

How to apply these transformations to a line in SpriteKit

I'm not sure what tools I should use for what I'm trying to do since I'm only really familiar with SKSpriteNodes and a little bit with SKShapeNodes.
My mission is as follows:
Add a line to the scene, SKShapeNode?
Rotate the line along it's bottom point (beginning point?) by some angle. Imagine a clock hand for this, rotating around the bottom point
Find the new point (x,y coord) of the top point (end point?) after the line has been translated
Does anyone know how I can accomplish this? I'm currently using an SKShapeNode for my line and rotating it with .zRotation but I can't seem to accomplish my goal. There doesn't seem to be an achorPoint property for SKShapeNodes, so I can't change the point of rotation. Also I'm clueless on how to find the position of the end point of my line AFTER it has been rotated, I created it as follows:
let linePath = CGMutablePath()
linePath.move(to: begin)
linePath.addLine(to: end)
let line = SKShapeNode()
line.path = linePath
line.strokeColor = UIColor.black
line.lineWidth = 5
SceneCoordinator.shared.gameScene.addChild(line)
I'm rotating using:
public func rotate(angle: Double) {
var transform = CGAffineTransform(rotationAngle: CGFloat(angle))
line.path = linePath.mutableCopy(using: &transform)
}
SKShapeNode can be pretty expensive if you notice your FPS dropping. Also, you can easily turn a shape into a sprite to get .anchorPoint, but be warned that anchorpoint's behavior is not always as expected and you may have bugs later on (especially with physics):
func shapeToSprite(_ shape: SKShapeNode) -> SKSpriteNode {
let sprite = SKSpriteNode(texture: SKView().texture(from: shape))
sprite.physicsBody = shape.physicsBody // Or create a new PB from alpha mask (may be slower, IDK)
shape.physicsBody = nil
return sprite
}
override func didMove(to view: SKView) {
let shape = SKShapeNode(circleOfRadius: 60)
let sprite = shapeToSprite(shape)
sprite.anchorPoint = CGPoint()
addChild(sprite)
}
Otherwise, you are going to have to either 1), redraw the line with the correct rotation, 2), rotate the shape then reposition it at a new location...
Both are going to be MATH :[ so hopefully the anchorppoint works for you

How to create the effect of a circular object entering and separating from a thick substance

Based on the image below (I used different colours for circle and flat surface so they can be seen, but in the end the colours will be the same), using Swift and Spritekit, I am trying to create the effect of a circular object entering a thick substance (not necessarily sticky) and separating from the thick substance. Basically, when the circular object is separating, it will pull away from the flat surface as it forms into a circle.
I wanted to use image animation frames, but since the objects are SKSpriteNodes with physics bodies this will make timing the collision of objects with animation quite difficult. Another approach would be using CAAnimation, but I don't know how this can be combined with SKSpriteNodes with physics bodies. How can I create this separation effect using any of the above stated approaches or a different one?
UPDATE
The image below shows the change in the surface of the thick substance as the circular object enters the thick substance till it's submerged.
You are looking for a fluid simulator
This is indeed possible with modern hardware.
Let's have a look at what we are going to build here.
Components
In order to achieve that we'll need to
create several molecules having a physics body and a blurred image
use a Shader to apply a common color to every pixel having alpha > 0
Molecule
Here's the Molecule class
import SpriteKit
class Molecule: SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "molecule")
super.init(texture: texture, color: .clear, size: texture.size())
let physicsBody = SKPhysicsBody(circleOfRadius: 8)
physicsBody.restitution = 0.2
physicsBody.affectedByGravity = true
physicsBody.friction = 0
physicsBody.linearDamping = 0.01
physicsBody.angularDamping = 0.01
physicsBody.density = 0.13
self.physicsBody = physicsBody
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Shader
Next we need a Fragment Shader, let's create a file with name Water.fsh
void main() {
vec4 current_color = texture2D(u_texture, v_tex_coord);
if (current_color.a > 0) {
current_color.r = 0.0;
current_color.g = 0.57;
current_color.b = 0.95;
current_color.a = 1.0;
} else {
current_color.a = 0.0;
}
gl_FragColor = current_color;
}
Scene
And finally we can define the scene
import SpriteKit
class GameScene: SKScene {
lazy var label: SKLabelNode = {
return childNode(withName: "label") as! SKLabelNode
}()
let effectNode = SKEffectNode()
override func didMove(to view: SKView) {
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
effectNode.shouldEnableEffects = true
effectNode.shader = SKShader(fileNamed: "Water")
addChild(effectNode)
}
var touchLocation: CGPoint?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let touchLocation = touch.location(in: self)
if label.contains(touchLocation) {
addRedCircle(location: touchLocation)
} else {
self.touchLocation = touchLocation
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchLocation = nil
}
override func update(_ currentTime: TimeInterval) {
if let touchLocation = touchLocation {
let randomizeX = CGFloat(arc4random_uniform(20)) - 10
let randomizedLocation = CGPoint(x: touchLocation.x + randomizeX, y: touchLocation.y)
addMolecule(location: randomizedLocation)
}
}
private func addMolecule(location: CGPoint) {
let molecule = Molecule()
molecule.position = location
effectNode.addChild(molecule)
}
private func addRedCircle(location: CGPoint) {
let texture = SKTexture(imageNamed: "circle")
let sprite = SKSpriteNode(texture: texture)
let physicsBody = SKPhysicsBody(circleOfRadius: texture.size().width / 2)
physicsBody.restitution = 0.2
physicsBody.affectedByGravity = true
physicsBody.friction = 0.1
physicsBody.linearDamping = 0.1
physicsBody.angularDamping = 0.1
physicsBody.density = 1
sprite.physicsBody = physicsBody
sprite.position = location
addChild(sprite)
}
}
The full project is available on my GitHub account https://github.com/lucaangeletti/SpriteKitAqua
From a high level of understanding there are two ways to do this.
THE BAD WAY (but works better when fluid has textures): Create the sprite sheet in advance, then overlay an additional child of the SKSpriteNode object. The frame in the animation sprite will be a function of distance from the ball to the surface when distance between them is less than some amount. The desired distance range (range) will have to be mapped to the sprites frame number (frameIndex). f(range) = frameIndex. Linear interpolation will help out here. More on interpolation later.
THE RIGHT WAY: Make the fluid a curve object, then animate the points on the curve with linear interpolation between the initial, intermediate and final states. This will require three curves each one with the same number of points. Let the initial fluid state be F1. Model F1 points as the static fluid. Let the fluid state be F2 when the ball is halfway submerged. Model F2 points to look like the ball submerged at its maximum width. Let the fluid state be F3 when the ball is 75% submerged. Notice that when the ball is fully submerged the fluid looks unchanged. This is why when the ball is 75% submerged it has the maximum surface tension grabbing the ball. As far as SpriteKit goes, you may use these objects:
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddQuadCurveToPoint(path, NULL, 50, 100, 100, 0);
CGPathAddLineToPoint(path, NULL, 50, -100);
CGPathCloseSubpath(path);
SKShapeNode *shape = [[SKShapeNode alloc]init];
shape.path = path;
Then detect when the ball is on the outside of the fluid by using the vector cross product with 3D vectors, even if your project is in 2D.
Ball Vector (Vb)
^
|
(V) O---> Closest Fluid Surface Vector (Vs)
V = Vb x Vs
Then look at the Z component of V called Vz.
If (Vz < 0), the ball is outside of the fluid:
Create a variable t:
t = distOfBall/radiusOfBall
Then for every ordered point in your fluid shapes do the following:
newFluidPointX = F1pointX*(t-1) + F2pointX*t
newFluidPointY = F1pointY*(t-1) + F2pointY*t
If Vz > 0), the ball is inside of the fluid:
t = -(((distOfBall/radiusOfBall) + 0.5)^2) *4 + 1
newFluidPointX = F2pointX*(t-1) + F3pointX*t
newFluidPointY = F2pointY*(t-1) + F3pointY*t
This works because any two shapes can be blended together using interpolation.
The parameter "t" acts as a percentage to blend between two shapes.
You can actually create seamless blends between any two shapes, so long as the number of points are the same. This is how a man morphs into a wolf in Hollywood movies, or how a man can morph into a liquid puddle. The only principle in play for those effects is interpolation. Interpolation is a very powerful tool. It is defined as:
L = A*(t-1) + B*t
where t is in between 0.0 and 1.0
and A and B is what you are morphing from and to.
For more on interpolation see:
Wiki Article
For further study. If you are considering animating any dynamic shapes, I would consider understanding Bezier curves. Pomax has a wonderful article on the topic. Although many frameworks have curves in them, having a general understanding of how they work will allow you to manipulate them extensivly or roll your own features where the framework is lacking. Her is Pomax's article:
A Primer on Curves
Good luck on your progress:)

How to generate a new node when another one hits a certain y value

I have a circle moving up a line, and when that circle reaches a certain y point, how can I make it so that another node would generate from below?
Here is the code I currently have for populating the circles, but I am not able to use it with a physics body, as it generates too many nodes and slows down my app:
func createCirclesOnLine(line: CGFloat) {
var currentY : CGFloat = -110
let maxY = self.size.width * 15
let spacing : CGFloat = 120
while currentY < maxY {
let circle = SKSpriteNode(imageNamed: "first#2x")
circle.physicsBody?.dynamic = false
circle.position = CGPointMake(line, currentY)
//circle.physicsBody?.restitution = -900
circle.size = CGSizeMake(75, 75)
// circle.physicsBody = SKPhysicsBody(rectangleOfSize: circle.size)
let up = SKAction.moveByX(0, y: 9000, duration: 90)
circle.runAction(up)
foregroundNode.addChild(circle)
currentY += CGFloat((random() % 400) + 70)
}
Will post more code if necessary.
There are two ways you can go about this. One is to simply check every circle's y position to see if it's above the screen. You'll need a reference to the circles so...
class GameScene: SKScene {
var circles = Array<SKSpriteNode>()
...
In your createCirlcesOnLine function, add each circle to the array as you create it.
...
self.addChild(circle)
circles.append(circle)
Then, in your update method, enumerate through the circles to see if any of them are above the top of the screen.
override func update(currentTime: NSTimeInterval) {
for circle in circles {
if circle.position.y > self.size.height + circle.size.height/2 {
//Send circle back to the bottom using the circle's position property
}
}
}
This solution will work but causes a lot of unnecessary checks on every update cycle.
A second more efficient (and slightly more complicated) recommendation is to add an invisible node above the top of the screen that stretches the screen width. When the circle collides with it, just move it to the bottom of the screen. Look into implementing the SKPhysicsContactDelegate protocol and what needs to happen for that to work. If you run into problems with this solution, post a separate question with those issues.