Swift Rope Verlets - swift

I'm trying to implement a verlet rope in swift. I'm running into the problem where when trying to fix the position of the point masses from constraints, they very rapidly grow apart and then the coordinates become NaNs. Any idea what could be wrong in my code?
import Foundation
private let DEF_POINT_COUNT = 10
private let CONST_ITERATIONS = 5
#objc class PointMass: DebugPrintable {
var point: NSPoint
private var oldPoint: NSPoint
var x: Double {
get { return Double(point.x) }
}
var y: Double {
get { return Double(point.y) }
}
var debugDescription: String {
get { return "\(point)" }
}
init(_ point: NSPoint) {
self.point = point
self.oldPoint = point
}
func updatePosition() {
let dx = point.x - oldPoint.x
let dy = (point.y - oldPoint.y)
oldPoint = point
point = NSPoint(x: point.x + dx, y: point.y + dy)
}
func updatePosition(point: NSPoint) {
let dx = point.x - self.point.x
let dy = point.y - self.point.y
self.oldPoint = NSPoint(x: oldPoint.x + dx, y: oldPoint.y + dy)
self.point = point
}
}
struct Constraint {
var p1: PointMass
var p2: PointMass
var len: Double
func fixPoints() {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
let dist = sqrt(dx*dx + dy*dy)
let diff = (dist - len)/len
p2.updatePosition(NSPoint(x: p2.x - diff*dx*0.5, y: p2.y - diff*dy*0.5))
p1.updatePosition(NSPoint(x: p1.x + diff*dx*0.5, y: p1.y + diff*dy*0.5))
}
}
#objc class Rope: NSObject {
let points: [PointMass]
let constraints: [Constraint]
init(anchor: NSPoint, end: NSPoint, length: Double, count: Int = DEF_POINT_COUNT) {
let anchorPoint = PointMass(anchor)
let endPoint = PointMass(end)
let dx = (anchorPoint.x - endPoint.x)/Double(count)
let dy = (anchorPoint.y - endPoint.y)/Double(count)
let constraintLength = length/Double(count)
var points = [endPoint]
var constraints: [Constraint] = []
for i in 1...count {
let prevPoint = points[i-1]
let newPoint = PointMass(NSPoint(x: prevPoint.x + dx, y: prevPoint.y + dy))
let constraint = Constraint(p1: prevPoint, p2: newPoint, len: constraintLength)
points.append(newPoint)
constraints.append(constraint)
}
self.points = points
self.constraints = constraints
}
func update(anchor: NSPoint, endPoint: NSPoint) {
points.first?.updatePosition(endPoint)
points.last?.updatePosition(anchor)
for point in points {
point.updatePosition()
}
for i in 0...CONST_ITERATIONS {
for constraint in constraints {
constraint.fixPoints()
}
}
}
}

Found it. The problem was in the fixPoints() method, as I suspected.
The line
let diff = (dist - len)/len
should be instead
let diff = (len - dist)/dist

Related

Bounce rays with enumerateBodies alongRayStart

I want to trace the path where a bullet will move in my SpriteKit GameScene.
I'm using "enumerateBodies(alongRayStart", I can easily calculate the first collision with a physics body.
I don't know how to calculate the angle of reflection, given the contact point and the contact normal.
I want to calculate the path, over 5 reflections/bounces, so first I:
Cast a ray, get all the bodies it intersects with, and get the closest one.
I then use that contact point as the start of my next reflection/bounce....but I'm struggling with what the end point should be set to....
What I think I should be doing is getting the angle between the contact point and the contact normal, and then calculating a new point opposite to that...
var points: [CGPoint] = []
var start: CGPoint = renderComponent.node.position
var end: CGPoint = crossHairComponent.node.position
points.append(start)
var closestNormal: CGVector = .zero
for i in 0...5 {
closestNormal = .zero
var closestLength: CGFloat? = nil
var closestContact: CGPoint!
// Get the closest contact point.
self.physicsWorld.enumerateBodies(alongRayStart: start, end: end) { (physicsBody, contactPoint, contactNormal, stop) in
let len = start.distance(point: contactPoint)
if closestContact == nil {
closestNormal = contactNormal
closestLength = len
closestContact = contactPoint
} else {
if len <= closestLength! {
closestLength = len
closestNormal = contactNormal
closestContact = contactPoint
}
}
}
// This is where the code is just plain wrong and my math fails me.
if closestContact != nil {
// Calculate intersection angle...doesn't seem right?
let v1: CGVector = (end - start).normalized().toCGVector()
let v2: CGVector = closestNormal.normalized()
var angle = acos(v1.dot(v2)) * (180 / .pi)
let v1perp = CGVector(dx: -v1.dy, dy: v1.dx)
if(v2.dot(v1perp) > 0) {
angle = 360.0 - angle
}
angle = angle.degreesToRadians
// Set the new start point
start = closestContact
// Calculate a new end point somewhere in the distance to cast a ray to, so we can repeat the process again
let x = closestContact.x + cos(angle)*100
let y = closestContact.y + sin(-angle)*100
end = CGPoint(x: x, y: y)
// Add points to array to draw them on the screen
points.append(closestContact)
points.append(end)
}
}
I guess you are looking for something like this right?
1. Working code
First of all let me post the full working code. Just create a new Xcode project based SpriteKit and
In GameViewController.swift set
scene.scaleMode = .resizeFill
Remove the usual label you find in GameScene.sks
Replace Scene.swift with the following code
>
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
var angle: CGFloat = 0
override func update(_ currentTime: TimeInterval) {
removeAllChildren()
drawRayCasting(angle: angle)
angle += 0.001
}
private func drawRayCasting(angle: CGFloat) {
let colors: [UIColor] = [.red, .green, .blue, .orange, .white]
var start: CGPoint = .zero
var direction: CGVector = CGVector(angle: angle)
for i in 0...4 {
guard let result = rayCast(start: start, direction: direction) else { return }
let vector = CGVector(from: start, to: result.destination)
// draw
drawVector(point: start, vector: vector, color: colors[i])
// prepare for next iteration
start = result.destination
direction = vector.normalized().bounced(withNormal: result.normal.normalized()).normalized()
}
}
private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {
let endVector = CGVector(
dx: start.x + direction.normalized().dx * 4000,
dy: start.y + direction.normalized().dy * 4000
)
let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)
var closestPoint: CGPoint?
var normal: CGVector?
physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
(physicsBody:SKPhysicsBody,
point:CGPoint,
normalVector:CGVector,
stop:UnsafeMutablePointer<ObjCBool>) in
guard start.distanceTo(point) > 1 else {
return
}
guard let newClosestPoint = closestPoint else {
closestPoint = point
normal = normalVector
return
}
guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
return
}
normal = normalVector
}
guard let p = closestPoint, let n = normal else { return nil }
return (p, n)
}
private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {
let start = point
let destX = (start.x + vector.dx)
let destY = (start.y + vector.dy)
let to = CGPoint(x: destX, y: destY)
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: to)
path.closeSubpath()
let line = SKShapeNode(path: path)
line.strokeColor = color
line.lineWidth = 6
addChild(line)
}
}
extension CGVector {
init(angle: CGFloat) {
self.init(dx: cos(angle), dy: sin(angle))
}
func normalized() -> CGVector {
let len = length()
return len>0 ? self / len : CGVector.zero
}
func length() -> CGFloat {
return sqrt(dx*dx + dy*dy)
}
static func / (vector: CGVector, scalar: CGFloat) -> CGVector {
return CGVector(dx: vector.dx / scalar, dy: vector.dy / scalar)
}
func bounced(withNormal normal: CGVector) -> CGVector {
let dotProduct = self.normalized() * normal.normalized()
let dx = self.dx - 2 * (dotProduct) * normal.dx
let dy = self.dy - 2 * (dotProduct) * normal.dy
return CGVector(dx: dx, dy: dy)
}
init(from:CGPoint, to:CGPoint) {
self = CGVector(dx: to.x - from.x, dy: to.y - from.y)
}
static func * (left: CGVector, right: CGVector) -> CGFloat {
return (left.dx * right.dx) + (left.dy * right.dy)
}
}
extension CGPoint {
func length() -> CGFloat {
return sqrt(x*x + y*y)
}
func distanceTo(_ point: CGPoint) -> CGFloat {
return (self - point).length()
}
static func - (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}
}
2. How does it work?
Lets have a look at what this code does. We'll start from the bottom.
3. CGPoint and CGVector extensions
These are just simple extensions (mainly taken from Ray Wenderlich's repository on GitHub) to simplify the geometrical operations we are going to perform.
4. drawVector(point:vector:color)
This is a simple method to draw a vector with a given color starting from a given point.
Nothing fancy here.
private func drawVector(point: CGPoint, vector: CGVector, color: SKColor) {
let start = point
let destX = (start.x + vector.dx)
let destY = (start.y + vector.dy)
let to = CGPoint(x: destX, y: destY)
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: to)
path.closeSubpath()
let line = SKShapeNode(path: path)
line.strokeColor = color
line.lineWidth = 6
addChild(line)
}
5. rayCast(start:direction) -> (destination:CGPoint, normal: CGVector)?
This method perform a raycasting and returns the ALMOST closest point where the ray enter in collision with a physics body.
private func rayCast(start: CGPoint, direction: CGVector) -> (destination:CGPoint, normal: CGVector)? {
let endVector = CGVector(
dx: start.x + direction.normalized().dx * 4000,
dy: start.y + direction.normalized().dy * 4000
)
let endPoint = CGPoint(x: endVector.dx, y: endVector.dy)
var closestPoint: CGPoint?
var normal: CGVector?
physicsWorld.enumerateBodies(alongRayStart: start, end: endPoint) {
(physicsBody:SKPhysicsBody,
point:CGPoint,
normalVector:CGVector,
stop:UnsafeMutablePointer<ObjCBool>) in
guard start.distanceTo(point) > 1 else {
return
}
guard let newClosestPoint = closestPoint else {
closestPoint = point
normal = normalVector
return
}
guard start.distanceTo(point) < start.distanceTo(newClosestPoint) else {
return
}
normal = normalVector
}
guard let p = closestPoint, let n = normal else { return nil }
return (p, n)
}
What does it mean ALMOST the closets?
It means the the destination point must be at least 1 point distant from the start point
guard start.distanceTo(point) > 1 else {
return
}
Ok but why?
Because without this rule the ray gets stuck into a physics body and it is never able to get outside of it.
6. drawRayCasting(angle)
This method basically keeps the local variables up to date to properly generate 5 segments.
private func drawRayCasting(angle: CGFloat) {
let colors: [UIColor] = [.red, .green, .blue, .orange, .white]
var start: CGPoint = .zero
var direction: CGVector = CGVector(angle: angle)
for i in 0...4 {
guard let result = rayCast(start: start, direction: direction) else { return }
let vector = CGVector(from: start, to: result.destination)
// draw
drawVector(point: start, vector: vector, color: colors[i])
// prepare next direction
start = result.destination
direction = vector.normalized().bounced(withNormal: result.normal.normalized()).normalized()
}
}
The first segment has starting point equals to zero and a direction diving my the angle parameter.
Segments 2 to 5 use the final point and the "mirrored direction" of the previous segment.
update(_ currentTime: TimeInterval)
Here I am just calling drawRayCasting every frame passing the current angle value and the increasing angle by 0.001.
var angle: CGFloat = 0
override func update(_ currentTime: TimeInterval) {
removeAllChildren()
drawRayCasting(angle: angle)
angle += 0.001
}
6. didMove(to view: SKView)
Finally here I create a physics body around the scene in order to make the ray bounce over the borders.
override func didMove(to view: SKView) {
self.physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
}
7. Wrap up
I hope the explanation is clear.
Should you have any doubt let me know.
Update
There was a bug in the bounced function. It was preventing a proper calculation of the reflected ray.
It is now fixed.

Drag a line from center in swift, makes it jump around

I am trying to create a LineSegment struct that will be able to be drawn in any view, and also I want to change its values with a Pan gesture recognizer.
I did it, and it works, BUT it jumps around when I try to drag it from the center.
<div class="cloudapp-embed" data-slug="1V3q0s420Q2t"><script async src="https://embed.cl.ly/embedded.gz.js" charset="utf-8"></script></div>
public struct LineSegment {
var a: CGPoint!
var b: CGPoint!
var center: CGPoint! {
get {
let centerDistance = a.distance(to: b) / 2
return getPointOnLineWithDistaceFromA(centerDistance)
}
set (newCenter) {
let axOffset = newCenter.x - a.x
let ayOffset = newCenter.y - a.y
let bxOffset = newCenter.x - b.x
let byOffset = newCenter.y - b.y
let newa = CGPoint(x: newCenter.x + axOffset, y: newCenter.y + ayOffset)
let newb = CGPoint(x: newCenter.x + bxOffset, y: newCenter.y + byOffset)
self.a = newa
self.b = newb
}
}
var shapeLayer: CAShapeLayer? = nil
var dragRadius: CGFloat = 30
init(a: CGPoint, b:CGPoint) {
self.a = a
self.b = b
}
}
extension LineSegment {
func getPointOnLineWithDistaceFromA(_ distance: CGFloat) -> CGPoint {
let d = a.distance(to: b)
let t = distance / d
let newP = CGPoint(x: (1 - t) * a.x + t * b.x, y: (1 - t) * a.y + t * b.y)
return newP
}
/// Grabs the closes point and moves it to 'to'
mutating func grabClosestPoint(to: CGPoint, grabCenter: Bool = false) {
if to.distance(to: a) < self.dragRadius {
self.a = to
} else if to.distance(to: b) < self.dragRadius {
self.b = to
} else if grabCenter && to.distance(to: self.center) < self.dragRadius {
self.center = to
}
}
}
I call the "grabClosestPoint" function with panGesture.
I do not know what is the problem, ether the math is wrong, or there is some UI problem that I can't figure out...
I figured that out. The math was wrong.
let axOffset = newCenter.x - a.x
let ayOffset = newCenter.y - a.y
let bxOffset = newCenter.x - b.x
let byOffset = newCenter.y - b.y
let newa = CGPoint(x: newCenter.x + axOffset, y: newCenter.y + ayOffset)
let newb = CGPoint(x: newCenter.x + bxOffset, y: newCenter.y + byOffset)
self.a = newa
self.b = newb
This is not only translating the edges, but also reversing them. So i changes it to:
let centerOffsetX = newCenter.x - self.center.x
let centerOffsetY = newCenter.y - self.center.y
let newa = CGPoint(x: a.x + centerOffsetX, y: a.y + centerOffsetY)
let newb = CGPoint(x: b.x + centerOffsetX, y: b.y + centerOffsetY)
self.a = newa
self.b = newb
Now it works great.
Problem is you didnt declare the distance variable anywhere, so it breaks the code

SpriteKit action like "Skew" or "Distort"

Is it possible to create an SKAction for SKSpriteNode in SpriteKit that generates the same effect as "Photoshop" with the Edit->Transform->Distort option?
Example:
I solve with this implementation:
Swift 5
extension SKSpriteNode {
func addSkew(value: CGFloat = -1){
var effectNode = SKEffectNode()
effectNode.shouldRasterize = true
effectNode.shouldEnableEffects = true
effectNode.addChild(SKSpriteNode(texture: texture))
effectNode.zPosition = 1
let transform = CGAffineTransform(a: 1 , b: 0,
c: value, d: 1,
tx: 0 , ty: 0)
let transformFilter = CIFilter(name: "CIAffineTransform")!
transformFilter.setValue(transform, forKey: "inputTransform")
effectNode.filter = transformFilter
addChild(effectNode)
texture = nil
}
}
You can create a skew using a 1x1 warp mesh. This is supported in iOS10.0+.
This extension receives the skew angle in degrees, and distorts around the anchor point of the given sprite.
Swift 4.2
extension SKWarpGeometryGrid {
public static var skewPosGridZero:[float2] {
get {
return [float2(0.0, 0.0), float2(1.0, 0.0),
float2(0.0, 1.0), float2(1.0, 1.0)]
}
}
public static func skewXPosGrid(_ skewX: CGFloat, node:SKSpriteNode? = nil) -> [float2] {
let anchorY:Float = Float(node?.anchorPoint.y ?? 0.5)
var skewPosGrid = skewPosGridZero
let offsetX = Float(tan(skewX.degToRad()) * (node == nil ? 1.0 : (node!.size.height/node!.size.width)) )
skewPosGrid[2][0] += offsetX * (1.0 - anchorY)
skewPosGrid[3][0] += offsetX * (1.0 - anchorY)
skewPosGrid[0][0] -= offsetX * anchorY
skewPosGrid[1][0] -= offsetX * anchorY
return skewPosGrid
}
public static func skewYPosGrid(_ skewY: CGFloat, node:SKSpriteNode? = nil) -> [float2] {
let anchorX:Float = Float(node?.anchorPoint.x ?? 0.5)
var skewPosGrid = skewPosGridZero
let offsetY = Float(tan(skewY.degToRad()) * (node == nil ? 1.0 : (node!.size.width/node!.size.height)) )
skewPosGrid[1][1] += offsetY * (1.0 - anchorX)
skewPosGrid[3][1] += offsetY * (1.0 - anchorX)
skewPosGrid[0][1] -= offsetY * anchorX
skewPosGrid[2][1] -= offsetY * anchorX
return skewPosGrid
}
public static func skewX(_ angle: CGFloat, node:SKSpriteNode? = nil) -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1, sourcePositions: skewPosGridZero, destinationPositions: skewXPosGrid(angle, node:node))
}
public static func skewY(_ angle: CGFloat, node:SKSpriteNode? = nil) -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1, sourcePositions: skewPosGridZero, destinationPositions: skewYPosGrid(angle, node:node))
}
public static func skewZero() -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1)
}
}
Example animation:
let spriteNode = SKSpriteNode(imageNamed: "tex")
spriteNode.anchorPoint = CGPoint(x:0.25, y:1.0)
let skewA = SKWarpGeometryGrid.skewX(-45.0, node: spriteNode)
let skewB = SKWarpGeometryGrid.skewX(45.0, node: spriteNode)
spriteNode.warpGeometry = skewB
if let skewActionA = SKAction.warp(to: skewA, duration: 3.0),
let skewActionB = SKAction.warp(to: skewB, duration: 3.0){
// Individual easing
skewActionA.timingMode = .easeInEaseOut
skewActionB.timingMode = .easeInEaseOut
spriteNode.run(SKAction.repeatForever(SKAction.sequence([skewActionA,skewActionB])))
}
The list of available SKAction's is here: https://developer.apple.com/reference/spritekit/skaction
There is none to do exactly what you describe. Instead, you can export multiple sprite images from a photo editing tool like Photoshop, and use an animation action like class func animate(with: [SKTexture], timePerFrame: TimeInterval).
This is a little more work, but should achieve the desired effect.

replacing CALayer and CABasicAnimation with SKScene and SKActions

I am trying to restructure this Github Swift project on Metaballs so that the circles are represented by SKShapeNodes that are moved around by SKActions instead of CABasicAnimation.
I am not interested in the various Metaball parameters (the handleLenRate, Spacing and so on) appearing in the viewController. I basically want to be able to specify a start and end position for the animation using SKActions.
I am not certain how to achieve this, especially how to replace the startAnimation function below with SKShapeNode's with SKActions:
func startAnimation() {
let loadingLayer = self.layer as! DBMetaballLoadingLayer
loadingAnimation = CABasicAnimation(keyPath: "movingBallCenterX")
loadingAnimation!.duration = 2.5
loadingAnimation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
loadingAnimation!.fromValue = NSValue(CGPoint:fromPoint)
loadingAnimation!.toValue = NSValue(CGPoint: toPoint)
loadingAnimation!.repeatCount = Float.infinity
loadingAnimation!.autoreverses = true
loadingLayer.addAnimation(loadingAnimation!, forKey: "loading")
}
Please see what I've been able to do below:
MBCircle class:
struct MBCircle {
var center: CGPoint = CGPointZero
var radius: CGFloat = 0.0
var frame: CGRect {
get {
return CGRect(x: center.x - radius, y: center.y - radius, width: 2 * radius, height: 2 * radius)
}
}
}
struct DefaultConfig {
static let radius: CGFloat = 15.0
static let mv: CGFloat = 0.6
static let maxDistance: CGFloat = 10 * DefaultConfig.radius
static let handleLenRate: CGFloat = 2.0
static let spacing: CGFloat = 160.0
}
GameScene class (representing both the DBMetaballLoadingLayer and DBMetaballLoadingView):
class GameScene: SKScene {
private let MOVE_BALL_SCALE_RATE: CGFloat = 0.75
private let ITEM_COUNT = 2
private let SCALE_RATE: CGFloat = 1.0//0.3
private var circlePaths = [MBCircle]()
var radius: CGFloat = DefaultConfig.radius
var maxLength: CGFloat {
get {
return (radius * 4 + spacing) * CGFloat(ITEM_COUNT)
}
}
var maxDistance: CGFloat = DefaultConfig.maxDistance
var mv: CGFloat = DefaultConfig.mv
var spacing: CGFloat = DefaultConfig.spacing {
didSet {
_adjustSpacing(spacing)
}
}
var handleLenRate: CGFloat = DefaultConfig.handleLenRate
var movingBallCenterX : CGFloat = 0.0 {
didSet {
if (circlePaths.count > 0) {
circlePaths[0].center = CGPoint(x: movingBallCenterX, y: circlePaths[0].center.y)
}
}
}
func _generalInit() {
circlePaths = Array(0..<ITEM_COUNT).map { i in
var circlePath = MBCircle()
circlePath.center = CGPoint(x: (radius * 10 + spacing) * CGFloat(i), y: radius * (1.0 + SCALE_RATE))
circlePath.radius = i == 0 ? radius * MOVE_BALL_SCALE_RATE : radius
circlePath.sprite = SKShapeNode(circleOfRadius: circlePath.radius)
circlePath.sprite?.position = circlePath.center
circlePath.sprite?.fillColor = UIColor.blueColor()
addChild(circlePath.sprite!)
return circlePath
}
}
func _adjustSpacing(spacing: CGFloat) {
if (ITEM_COUNT > 1 && circlePaths.count > 1) {
for i in 1..<ITEM_COUNT {
var circlePath = circlePaths[i]
circlePath.center = CGPoint(x: (radius*2 + spacing) * CGFloat(i), y: radius * (1.0 + SCALE_RATE))
}
}
}
func _renderPath(path: UIBezierPath) {
var shapeNode = SKShapeNode()
shapeNode.path = path.CGPath
shapeNode.fillColor = UIColor.blueColor()
addChild(shapeNode)
}
func _metaball(j: Int, i: Int, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat) {
let circle1 = circlePaths[i]
let circle2 = circlePaths[j]
let center1 = circle1.center
let center2 = circle2.center
let d = center1.distance(center2)
var radius1 = circle1.radius
var radius2 = circle2.radius
if (d > maxDistance) {
_renderPath(UIBezierPath(ovalInRect: circle2.frame))
} else {
let scale2 = 1 + SCALE_RATE * (1 - d / maxDistance)
radius2 *= scale2
_renderPath(UIBezierPath(ovalInRect: CGRect(x: circle2.center.x - radius2, y: circle2.center.y - radius2, width: 2 * radius2, height: 2 * radius2)))
}
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.moveToPoint(p1a)
pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLineToPoint(p2b)
pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLineToPoint(p1a)
pathJoinedCircles.closePath()
_renderPath(pathJoinedCircles)
}
func startAnimation() {
}
override func didMoveToView(view: SKView) {
_generalInit()
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
I didn't make any changes to the CGPointExtension class.
UPDATE
I am still trying to get the metaball effect, this is my progress so far based on suggestions from Alessandro Ornano:
import SpriteKit
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
var balls = [SKShapeNode]()
var distanceBtwBalls : CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
balls.append(ball)
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode
if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let seq = SKAction.sequence([zoomIn,zoomOut])
ball.runAction(seq,withKey: "zoom")
}
}
i += 1
}
movingBeziers()
}
func _renderPath(path: UIBezierPath) {
let shapeNode = SKShapeNode(path: path.CGPath)
shapeNode.fillColor = UIColor.blueColor()
addChild(shapeNode)
}
func movingBeziers() {
_renderPath(UIBezierPath(ovalInRect: dBCircle.frame))
for j in 1..<balls.count {
self.latestTestMetaball(j, circleShape: dBCircle, v: 0.6, handleLenRate: 2.0, maxDistance: self.distanceBtwBalls)
}
}
func latestTestMetaball (j: Int, circleShape: SKShapeNode, v: CGFloat, handleLenRate: CGFloat, maxDistance: CGFloat) {
let circle1 = circleShape
let circle2 = balls[j]
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(center2)
var radius1 = circle1.frame.width
var radius2 = circle2.frame.width
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handleLenRate, p1a.minus(p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.moveToPoint(p1a)
pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLineToPoint(p2b)
pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLineToPoint(p1a)
pathJoinedCircles.closePath()
let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
shapeNode.fillColor = UIColor.blueColor()
addChild(shapeNode)
}
}
You can easily achieve this kind of animation using moveToX and the timingMode parameter.
New Swift 3 translation below at the end of this answer.
To make an example I use the Xcode Sprite-Kit "Hello, World!" official project demo:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
/* Setup your scene here */
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Hello, World!"
myLabel.fontSize = 15
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(myLabel)
mediaTimingFunctionEaseInEaseOutEmulate(myLabel)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKLabelNode) {
let actionMoveLeft = SKAction.moveToX(CGRectGetMidX(self.frame)-100, duration:1.5)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(CGRectGetMidX(self.frame)+100, duration:1.5)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
}
Output:
Update (This part start to emulate the static ball and the dynamic ball moving left and right but without metaball animations)
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:1.7)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(dBStopX, duration:1.7)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode
if CGRectContainsRect(ball.frame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let seq = SKAction.sequence([zoomIn,zoomOut])
ball.runAction(seq,withKey: "zoom")
}
}
i += 1
}
}
}
New update with metaball animation:
Finally I've realize this result, my goal is to make it very similar to the original :
is it possible to make some variations to times (for example zoomIn or zoomOut time values or actionMoveLeft, actionMoveRight time values), this is the code:
import SpriteKit
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMoveToView(view: SKView) {
// Some parameters
let strokeColor = SKColor.orangeColor()
let dBHeight = CGRectGetMaxY(self.frame)-84 // 64 navigationController height + 20 reasonable distance
let dBStartX = CGRectGetMidX(self.frame)-160 // extreme left
let dBStopX = CGRectGetMidX(self.frame)+160 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPointMake(CGRectGetMidX(self.frame), dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clearColor()
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPointMake(dBStartX+(distanceBtwBalls*CGFloat(i)), dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clearColor()
if i == 0 || i == totalBalls-1 {
ball.hidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveToX(dBStartX, duration:2.5)
actionMoveLeft.timingMode = SKActionTimingMode.EaseInEaseOut
let actionMoveRight = SKAction.moveToX(dBStopX, duration:2.5)
actionMoveRight.timingMode = SKActionTimingMode.EaseInEaseOut
node.runAction(SKAction.repeatActionForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
//MARK: - _metaball original function
func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : NSTimeInterval = 0.015) {
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(center2)
var radius1 = radiusDBCircle
var radius2 = radiusBall
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.moveToPoint(p1a)
pathJoinedCircles.addCurveToPoint(p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLineToPoint(p2b)
pathJoinedCircles.addCurveToPoint(p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLineToPoint(p1a)
pathJoinedCircles.closePath()
let shapeNode = SKShapeNode(path: pathJoinedCircles.CGPath)
shapeNode.strokeColor = SKColor.orangeColor()
shapeNode.fillColor = UIColor.clearColor()
addChild(shapeNode)
let wait = SKAction.waitForDuration(vanishingTime)
self.runAction(wait,completion: {
shapeNode.removeFromParent()
})
}
override func update(currentTime: NSTimeInterval) {
var i = 0
self.enumerateChildNodesWithName("ball") {
node, stop in
let ball = node as! SKShapeNode
let enlargeFrame = CGRectMake(ball.frame.origin.x-self.radiusBall*3,ball.frame.origin.y,ball.frame.width+(self.radiusBall*6),ball.frame.height)
if CGRectContainsRect(enlargeFrame, self.dBCircle.frame) {
if (ball.actionForKey("zoom") == nil) {
let zoomIn = SKAction.scaleTo(1.5, duration: 0.25)
zoomIn.timingMode = SKActionTimingMode.EaseInEaseOut
let zoomOut = SKAction.scaleTo(1.0, duration: 0.25)
let wait = SKAction.waitForDuration(0.8)
let seq = SKAction.sequence([zoomIn,zoomOut,wait])
ball.runAction(seq,withKey: "zoom")
}
}
self._metaball(ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 4 * self.radiusBall)
i += 1
}
}
}
//MARK: - Extensions
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}
Swift 3:
(I've made a little change to maxDistance: 4 * self.radiusBall with maxDistance: 5 * self.radiusBall to become more similar to the original but you can change it as you wish)
import SpriteKit
class GameScene: SKScene {
var dBCircle : SKShapeNode!
let radiusDBCircle: CGFloat = 10
let radiusBall: CGFloat = 15
private let SCALE_RATE: CGFloat = 0.3
override func didMove(to view: SKView) {
let label = self.childNode(withName: "//helloLabel") as? SKLabelNode
label?.removeFromParent()
self.anchorPoint = CGPoint.zero
// Some parameters
let strokeColor = SKColor.orange
let dBHeight = self.frame.midY
let dBStartX = self.frame.midX-260 // extreme left
let dBStopX = self.frame.midX+260 // extreme right
let dBWidth = dBStopX - dBStartX
let totalBalls = 7 // first and last will be hidden
let ballArea = dBWidth / CGFloat(totalBalls-1)
let distanceBtwBalls = ((ballArea-(radiusBall*2))+radiusBall*2)
// Create dbCircle
dBCircle = SKShapeNode.init(circleOfRadius: radiusDBCircle)
dBCircle.position = CGPoint(x:self.frame.midX, y:dBHeight)
dBCircle.strokeColor = strokeColor
dBCircle.name = "dBCircle"
dBCircle.fillColor = UIColor.clear
addChild(dBCircle)
// Make static balls
for i in 0..<totalBalls {
let ball = SKShapeNode.init(circleOfRadius: radiusBall)
ball.position = CGPoint(x:dBStartX+(distanceBtwBalls*CGFloat(i)), y:dBHeight)
ball.strokeColor = strokeColor
ball.name = "ball"
ball.fillColor = UIColor.clear
if i == 0 || i == totalBalls-1 {
ball.isHidden = true
}
addChild(ball)
}
mediaTimingFunctionEaseInEaseOutEmulate(node: dBCircle,dBStartX: dBStartX,dBStopX: dBStopX)
}
func mediaTimingFunctionEaseInEaseOutEmulate(node:SKShapeNode,dBStartX:CGFloat,dBStopX:CGFloat) {
let actionMoveLeft = SKAction.moveTo(x: dBStartX, duration:2.5)
actionMoveLeft.timingMode = SKActionTimingMode.easeInEaseOut
let actionMoveRight = SKAction.moveTo(x: dBStopX, duration:2.5)
actionMoveRight.timingMode = SKActionTimingMode.easeInEaseOut
node.run(SKAction.repeatForever(SKAction.sequence([actionMoveLeft,actionMoveRight])))
}
//MARK: - _metaball original function
func _metaball(circle2:SKShapeNode, circle1:SKShapeNode, v: CGFloat, handeLenRate: CGFloat, maxDistance: CGFloat,vanishingTime : TimeInterval = 0.015) {
let center1 = circle1.position
let center2 = circle2.position
let d = center1.distance(point: center2)
var radius1 = radiusDBCircle
var radius2 = radiusBall
if (radius1 == 0 || radius2 == 0) {
return
}
var u1: CGFloat = 0.0
var u2: CGFloat = 0.0
if (d > maxDistance || d <= abs(radius1 - radius2)) {
return
} else if (d < radius1 + radius2) {
u1 = acos((radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d))
u2 = acos((radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d))
} else {
u1 = 0.0
u2 = 0.0
}
let angle1 = center1.angleBetween(point: center2)
let angle2 = acos((radius1 - radius2) / d)
let angle1a = angle1 + u1 + (angle2 - u1) * v
let angle1b = angle1 - u1 - (angle2 - u1) * v
let angle2a = angle1 + CGFloat(M_PI) - u2 - (CGFloat(M_PI) - u2 - angle2) * v
let angle2b = angle1 - CGFloat(M_PI) + u2 + (CGFloat(M_PI) - u2 - angle2) * v
let p1a = center1.point(radians: angle1a, withLength: radius1)
let p1b = center1.point(radians: angle1b, withLength: radius1)
let p2a = center2.point(radians: angle2a, withLength: radius2)
let p2b = center2.point(radians: angle2b, withLength: radius2)
let totalRadius = radius1 + radius2
var d2 = min(v * handeLenRate, p1a.minus(point: p2a).length() / totalRadius)
d2 *= min(1, d * 2 / totalRadius)
radius1 *= d2
radius2 *= d2
let cp1a = p1a.point(radians: angle1a - CGFloat(M_PI_2), withLength: radius1)
let cp2a = p2a.point(radians: angle2a + CGFloat(M_PI_2), withLength: radius2)
let cp2b = p2b.point(radians: angle2b - CGFloat(M_PI_2), withLength: radius2)
let cp1b = p1b.point(radians: angle1b + CGFloat(M_PI_2), withLength: radius1)
let pathJoinedCircles = UIBezierPath()
pathJoinedCircles.move(to: p1a)
pathJoinedCircles.addCurve(to: p2a, controlPoint1: cp1a, controlPoint2: cp2a)
pathJoinedCircles.addLine(to: p2b)
pathJoinedCircles.addCurve(to: p1b, controlPoint1: cp2b, controlPoint2: cp1b)
pathJoinedCircles.addLine(to: p1a)
pathJoinedCircles.close()
let shapeNode = SKShapeNode(path: pathJoinedCircles.cgPath)
shapeNode.strokeColor = SKColor.orange
shapeNode.fillColor = UIColor.clear
addChild(shapeNode)
let wait = SKAction.wait(forDuration: vanishingTime)
self.run(wait,completion: {
shapeNode.removeFromParent()
})
}
override func update(_ currentTime: TimeInterval) {
var i = 0
self.enumerateChildNodes(withName: "ball") {
node, stop in
let ball = node as! SKShapeNode
let enlargeFrame = CGRect(x:ball.frame.origin.x-self.radiusBall*3,y:ball.frame.origin.y,width:ball.frame.width+(self.radiusBall*6),height:ball.frame.height)
if enlargeFrame.contains(self.dBCircle.frame) {
if (ball.action(forKey: "zoom") == nil) {
let zoomIn = SKAction.scale(to: 1.5, duration: 0.25)
zoomIn.timingMode = SKActionTimingMode.easeInEaseOut
let zoomOut = SKAction.scale(to: 1.0, duration: 0.25)
let wait = SKAction.wait(forDuration: 0.7)
let seq = SKAction.sequence([zoomIn,zoomOut,wait])
ball.run(seq,withKey: "zoom")
}
}
self._metaball(circle2: ball, circle1: self.dBCircle, v: 0.6, handeLenRate: 2.0, maxDistance: 5 * self.radiusBall)
i += 1
}
}
}
//MARK: - Extensions
extension CGPoint {
func distance(point: CGPoint) -> CGFloat {
let dx = point.x - self.x
let dy = point.y - self.y
return sqrt(dx * dx + dy * dy)
}
func angleBetween(point: CGPoint) -> CGFloat {
return atan2(point.y - self.y, point.x - self.x)
}
func point(radians: CGFloat, withLength length: CGFloat) -> CGPoint {
return CGPoint(x: self.x + length * cos(radians), y: self.y + length * sin(radians))
}
func minus(point: CGPoint) -> CGPoint {
return CGPoint(x: self.x - point.x, y: self.y - point.y)
}
func length() -> CGFloat {
return sqrt(self.x * self.x + self.y + self.y)
}
}
Assuming that your question is how to replicate the same behaviour with nodes and SKAction, I believe this should do it.
var shapeNode : SKNode?
func startAnimation(){
if let node = self.shapeNode {
//assume that we have a node initialized at some point and added to the scene at some x1,y1 coordinates
/// we define parameters for the animation
let positionToReach = CGPoint(x: 100, y: 100) /// some random position
let currentPosition = node.position /// we need the current position to be able to reverse the "animation"
let animationDuration = 2.5 //loadingAnimation!.duration = 2.5
/// we define which actions will be run for the node
let actionForward = SKAction.moveTo(positionToReach, duration: animationDuration)
let actionBackwards = SKAction.moveTo(currentPosition, duration: animationDuration)
// we needed two actions to simulate loadingAnimation!.autoreverses = true
/// we wrap the actions in a sequence of actions
let actionSequence = SKAction.sequence([actionForward, actionBackwards]) /// animations to repeat
/// we want to repeat the animation forever
let actionToRepeat = SKAction.repeatActionForever(actionSequence) ///loadingAnimation!.repeatCount = Float.infinity
/// showtime
node.runAction(actionToRepeat)
}
}
Let me know if I need to update any part as I haven't tested it. You still need to use your actual values and objects.
I have referred to referred to How would I repeat an action forever in Swift? while making this reply.

MKOverlay Custom Stroke - CGPath

I need to have a custom MKOverlay stroke style.
I need a wider lighter color in the inside of the overlay, like this stroke.
I know how to draw it like this,
class PolygonRenderer:MKPolygonRenderer {
override func drawMapRect(mapRect: MKMapRect, zoomScale: MKZoomScale, inContext context: CGContext) {
let fullPath = CGPathCreateMutable()
for i in 0 ..< self.polygon.pointCount {
let point = self.pointForMapPoint(self.polygon.points()[i])
print(point)
if i == 0 {
CGPathMoveToPoint(fullPath, nil, point.x, point.y)
} else {
CGPathAddLineToPoint(fullPath, nil, point.x, point.y)
}
}
let baseWidth = 10 / zoomScale
CGContextAddPath(context, self.path)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().colorWithAlphaComponent(0.3).CGColor)
CGContextSetLineWidth(context, baseWidth * 2)
CGContextSetLineCap(context, self.lineCap)
CGContextStrokePath(context);
CGContextAddPath(context, self.path)
CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor)
CGContextSetLineWidth(context, baseWidth)
CGContextSetLineCap(context, self.lineCap)
CGContextStrokePath(context)
}
}
Is it possible to draw the path like the first image?
For all those that may find this question useful, I have found a way to do this myself. This process works for CGPaths that just contain points, i.e. it doesn't contain arcs. So here is the MKPolygonRender subclass I have created.
These extension were sourced from here thanks to
Logan
import UIKit
import MapKit
extension CGPoint {
func angleToPoint(comparisonPoint: CGPoint) -> CGFloat {
let originX = comparisonPoint.x - self.x
let originY = comparisonPoint.y - self.y
let bearingRadians = atan2f(Float(originY), Float(originX))
var bearingDegrees = CGFloat(bearingRadians).degrees
while bearingDegrees < 0 {
bearingDegrees += 360
}
return (bearingDegrees + 270).truncatingRemainder(dividingBy: 360)
}
}
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180.0 / M_PI)
}
}
class PolygonRenderer: MKPolygonRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
let strokeWidth = MKRoadWidthAtZoomScale(zoomScale) * self.lineWidth
let outerPath = CGMutablePath()
let innerPath = CGMutablePath()
var previousPoint = point(for: self.polygon.points()[self.polygon.pointCount - 1])
for i in 0 ..< self.polygon.pointCount {
let currentPoint = point(for: self.polygon.points()[i])
let nextPoint:CGPoint = point(for: self.polygon.points()[(i + 1) % self.polygon.pointCount])
let a : CGFloat = {
let lengthA = distance(currentPoint, b: nextPoint)
let lengthB = distance(previousPoint, b: currentPoint)
let lengthC = distance(nextPoint, b: previousPoint)
return CGFloat(Int(round(Double(angle(lengthA, b: lengthB, c: lengthC)))))
}()
let degrees = previousPoint.angleToPoint(comparisonPoint: currentPoint)
let strokeHyp: CGFloat = CGFloat(strokeWidth) / CGFloat(sind(Double(a) / 2))
var newPoint = CGPoint()
newPoint.x = CGFloat(Double(strokeHyp) * cos(degreesToRadians(Double(degrees + (360 - (a / 2))).truncatingRemainder(dividingBy: 360)))) + currentPoint.x
newPoint.y = CGFloat(Double(strokeHyp) * sin(degreesToRadians(Double(degrees + (360 - (a / 2))).truncatingRemainder(dividingBy: 360)))) + currentPoint.y
print("stroke width: ", strokeWidth, "stroke hyp: ", strokeHyp)
print("pointIndex: ", i,"dir: ", degrees)
if i == 0 {
outerPath.move(to: currentPoint)
innerPath.move(to: newPoint)
} else {
outerPath.addLine(to: currentPoint)
innerPath.addLine(to: newPoint)
}
previousPoint = currentPoint
}
outerPath.closeSubpath()
context.addPath(outerPath)
context.setLineWidth(strokeWidth)
context.setStrokeColor(self.strokeColor?.cgColor ?? UIColor.black.cgColor)
context.strokePath()
innerPath.closeSubpath()
context.addPath(innerPath)
context.setLineWidth(strokeWidth)
context.setStrokeColor(self.strokeColor?.withAlphaComponent(0.3).cgColor ?? UIColor.black.withAlphaComponent(0.3).cgColor)
context.strokePath()
}
func angle(_ a:CGFloat, b:CGFloat, c:CGFloat) -> CGFloat {
var angle = (a * a) + (b * b) - (c * c)
angle = angle / (2 * a * b)
return CGFloat(radiansToDegrees(acos(Double(angle))))
}
func distance(_ a:CGPoint,b:CGPoint)->CGFloat{
return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2));
}
func direction(_ center:CGPoint,point:CGPoint)->CGFloat{
let hyp = Double(distance(center, b: point))
let adj = Double(center.y - point.y)
var angle = CGFloat(radiansToDegrees(asin(adj/hyp)))
if point.x < center.x {
angle += 180
} else {
angle = 360 - angle
}
return round(angle)
}
func radiansToDegrees(_ radians:Double)->Double{
return (radians * 180.0 / Double.pi)
}
func degreesToRadians(_ degrees:Double)->Double{
return ((degrees - 90) * Double.pi / 180.0)
}
func sind(_ degrees: Double) -> Double {
return __sinpi(degrees/180.0)
}
}