Amoeba-shaped controls with SpriteKit - swift

I am new to SpriteKit, and I am trying to create a distorted circle that acts like "amoeba". What I am trying to do is to use SKShapeNode initialised with with UIBezierPath created over 8-10 points on a circle with some randomness (something like):
let theta : Double = (Double(i) * Double.pi / 180.0) * (36.0 + Double.random(in: -1...1))
let radius : CGFloat = CGFloat(100.0 + Double.random(in: 0...20))
let a = CGFloat(cos(theta)) * radius
let b = CGFloat(sin(theta)) * radius
This part works, but then, I am starting to build the path using addCurve() method and the shapes come out really ugly - probably because I don't fully understand how the method works and what should I use for control points.
Appreciate if you have any better ideas or help me with using addCurve() in a better way.

Here is what I've done - posting as it might be useful to someone, parameters could be tweaked to your liking.
let path = UIBezierPath()
let numPoints = Int.random(in: 5...25)
let totalPoints = numPoints * 3
var pt : [CGPoint] = Array(repeating: CGPoint(), count: totalPoints)
var coef = 1.0
for i : Int in 0..<(totalPoints) {
let theta = (Double(i) * Double.pi / 180.0) * (360.0/Double(totalPoints) + Double.random(in: -1.0...1.0))
coef = (i % 3 == 2 ? 1 : 0.25)
let radius = CGFloat(100.0 * coef + Double.random(in: 0...20))
let a = CGFloat(cos(theta)) * radius
let b = CGFloat(sin(theta)) * radius
pt[i] = CGPoint(x: a, y: b)
// the code snippet below is to show the curve and critical points
let point : SKShapeNode = SKShapeNode(circleOfRadius: 5)
point.fillColor = i % 3 == 0 ? .green : .cyan
point.position = pt[i]
point.zPosition = 10
self.addChild(point)
// end of code snippet
}
path.move(to: CGPoint(x: self.view!.bounds.minX + pt[0].x, y: self.view!.bounds.minY + pt[0].y))
for i in 1...numPoints{
path.addCurve(to: pt[(i * 3) % (totalPoints)],
controlPoint1: pt[(i * 3 - 1)],
controlPoint2: pt[(i * 3 - 2)])
}
path.close()
let amoeba = SKShapeNode(path: path.cgPath)
amoeba.strokeColor = .orange
self.addChild(amoeba)

Related

Normalising a CGFloat Array to UIView frame height

I am plotting an array of CGFloat values within a UIBezierPath. Now I want to Normalise the values so the max value of the array will occupy the whole height.
So far
let scaleFactor = (waveView.frame.height) * readFile.maxValue
for point in readFile.points {
let nextPoint = CGPoint(x: soundPath.currentPoint.x + sampleSpace, y: middleY - (point * scaleFactor) - 1.0)
soundPath.addLine(to: nextPoint)
soundPath.move(to: nextPoint)
}
But it does not seem to work...
EDIT
ReadFile:
class ReadFile {
public enum ReadFileError:Error{
case errorReading(String)
}
/* Player Interval Measured in Miliseconds (By now..) */
var beginPosition:Int32 = 0
var endPosition:Int32 = 0
/* Sample Rate -> Default 48KHz */
var sampleRate:Double = 48000
var samplesSeconds:CGFloat = 5
var maxValue:CGFloat = 0
var points:[CGFloat] = []
}
And
sampleSpace = 0.2
Thank you Andy for your answer but I have finally figured it out.
I am drawing a sound wave so it has positives and negatives values.
heightMax = waveView.frame.height/2
Applying a rule of three (Spanish translation) I end up with this:
func drawSoundWave(windowLength:Int32){
// Drawing code
print("\(logClassName): Drawing!!!")
print("\(logClassName): points COUNT = \(readFile.points.count)")
let soundPath = UIBezierPath()
soundPath.lineWidth = lineWidth
soundPath.move(to: CGPoint(x:0.0 , y: middleY))
print("\(logClassName) max ")
for point in readFile.points{
let normalizedHeight:CGFloat = (waveView.frame.height * point) / (2 * readFile.maxValue)
let nextPoint = CGPoint(x: soundPath.currentPoint.x + sampleSpace, y: middleY - (normalizedHeight))
soundPath.addLine(to: nextPoint)
soundPath.move(to: nextPoint)
}
let trackLayer = CAShapeLayer()
trackLayer.path = soundPath.cgPath
waveView.layer.addSublayer(trackLayer)
trackLayer.strokeColor = UIColor.red.cgColor
trackLayer.lineWidth = lineWidth
trackLayer.fillColor = UIColor.green.cgColor
trackLayer.lineCap = kCALineCapRound
}
where
let normalizedHeight:CGFloat = (waveView.frame.height * point) / (2 * readFile.maxValue)
is the normalize value given a readFile.maxValue and waveView.frame.height

Why does this position always return the same X value?

I am using the following code to try to creating a CGPoint for a ball along the top of the screen:
func randomBallPosition() -> CGPoint {
let random = CGFloat((arc4random_uniform(8) + 1) / 10)
let randomX = CGFloat(self.frame.size.width * random)
let staticY = CGFloat(self.frame.size.height * 0.95)
return CGPoint(x: randomX, y: staticY)
}
However the ball is always placed at (0,y) and I'm unsure why.
You just need to divide by 10 after converting your integer to CGFloat:
let random = CGFloat((arc4random_uniform(8) + 1)) / 10

How do I compute the intersection of two circle's third set of coordinates under these conditions?

The small circle can move left and right by any amount
and I have to calculate the red dot’s coordinates wherever
its location is, if they intersect. I only calculate this under that condition. I must find the intersection and be sure that it is the intersection on the red dot, and not the intersection below it, so always the one with the higher Y value.
I have solved for all the distances of the triangles and blue dots.
How do I compute the red point?
If you want to look at my current code to help debug it, try this.
My Playground To Test:
//: Playground - noun: a place where people can play
import UIKit
infix operator **
let pretendWidth: CGFloat = 374
let pretendHeight: CGFloat = 7
// Testing scenario is pretendWidth..<(pretendWidth + (pretendHeight / 2))
let spacer: CGFloat = 0.5
extension CGFloat {
public static func **(base: CGFloat, exp: CGFloat) -> CGFloat {
return CGFloat(pow(Double(base), Double(exp)))
}
}
class BottomBarGradientNode: UIView {
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.clip(to: bounds)
// Gradient Creation
let locations: [CGFloat] = [0, 1]
let components: [CGFloat] = [0.2706, 0.6863, 0.8902, 1, 0, 0.8745, 0.7294, 1]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient: CGGradient = CGGradient(colorSpace: colorSpace, colorComponents: components, locations: locations, count: 2)!
let startPoint = CGPoint(x: bounds.maxX, y: bounds.maxY)
let endPoint = CGPoint(x: bounds.minX, y: bounds.minY)
let halfHeight = bounds.height / 2
let path = UIBezierPath()
let startPointForPath = CGPoint(x: bounds.width - halfHeight, y: 0)
path.move(to: startPointForPath)
let firstCenterPoint = CGPoint(x: bounds.width - halfHeight, y: halfHeight)
let secondCenterPoint = CGPoint(x: pretendWidth - bounds.height, y: 0)
Computation: if bounds.width > (pretendWidth + halfHeight) {
path.addArc(withCenter: secondCenterPoint, radius: bounds.height, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
} else if bounds.width > pretendWidth {
//
// ------------------------------------------------------------------------------------------------------------------------------------
// Though this looks like a complicated geometry problem, this is really best done as an ugly algebra problem.
// We want the coordinates of the red dot: (x,y)
// We know the red dot is on the big circle and since that circle is not moving I'm going to call it's center (0,0) thus:
// x^2 + y^2 = 49
// We also know that the red dot is on the little circle, it has a moving center but we know that the y value for that
// center is always -3.5. so we'll let the x-value of that center be t:
// (x-t)^2 + (y-3.5)^2 = (3.5)^2
// which expands to:
// x^2 - 2xt + t^2 + y^2 -7y + (3.5)^2 = (3.5)^2
// which when we plug in our other equation simplifies to:
// y = (1/7)(-2tx + 49 + t^2)
// plugging that back into the first equation gives:
// x^2 + ((1/7)(-2tx + 49 + t^2))^2 = 49
// which is terrible to look out but turns out to be a quadratic equation in x, so from this point you'd just simplify
// and plug it into the quadratic formula. Pick the value of x that is smaller in magnitude (be careful about negatives
// here). Then plug that x back into the first equation to solve for y.
// ------------------------------------------------------------------------------------------------------------------------------------
//
let boundsHeightSquared = bounds.height ** 2
let distanceFromOtherCenter = firstCenterPoint.x - secondCenterPoint.x
// x^2 + ((1/7)(-2tx + 49 + t^2))^2 = 49 <<<< translates to VVVVVV
//
// ((4/49)t^2 + 1)(x^2) + (-4t - (4t^3/49))(x) + (2t^2 + (t^4)/49) = 0
// ^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
// value1(a) value2(b) value3(c)
let value1 = ((4 * (distanceFromOtherCenter ** 2)) / boundsHeightSquared) + 1
let value2 = (-4 * distanceFromOtherCenter) - ((4 * (distanceFromOtherCenter ** 3)) / boundsHeightSquared)
let value3 = (2 * (distanceFromOtherCenter ** 2)) + ((distanceFromOtherCenter ** 4) / boundsHeightSquared)
let (first, second) = getQuadraticValues(a: value1, b: value2, c: value3)
// guarentee positive values
var areBothGreaterThanZero: Bool = false
var chosenX: CGFloat!
if first < 0 { chosenX = second }
else if second < 0 { chosenX = first }
else { chosenX = first < second ? first : second; areBothGreaterThanZero = true }
// y = (1/7)(-2tx + 49 + t^2)
var chosenY = (1 / bounds.height) * ((-2 * distanceFromOtherCenter * chosenX) + boundsHeightSquared - (distanceFromOtherCenter ** 2))
// last check on weird values
if chosenY < 0 && areBothGreaterThanZero {
chosenX = first < second ? first : second
chosenY = (1 / bounds.height) * ((-2 * distanceFromOtherCenter * chosenX) + boundsHeightSquared - (distanceFromOtherCenter ** 2))
}
// Computatation failed. Show full segment.
if chosenY < 0 {
print("Computation Failed")
path.addArc(withCenter: secondCenterPoint, radius: bounds.height, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
break Computation
}
// true point
let intersectingPoint = CGPoint(x: chosenX + secondCenterPoint.x, y: chosenY)
// c^2 = a^2 + b^2 - 2abCOS(C)
// (a^2 + b^2 - c^2) / 2ab = COS(C)
let topPoint = CGPoint(x: firstCenterPoint.x, y: 0)
// compute c (distance)
let firstDistanceBetweenPoints = getDistanceBetweenTwoPoints(firstPoint: intersectingPoint, secondPoint: topPoint)
// where a and b are halfHeight
let firstCosC = getCosC(a: halfHeight, b: halfHeight, c: firstDistanceBetweenPoints)
let firstAngle = acos(firstCosC)
path.addArc(withCenter: firstCenterPoint, radius: halfHeight, startAngle: CGFloat.pi * 1.5, endAngle: CGFloat.pi * 1.5 + firstAngle, clockwise: true)
// c^2 = a^2 + b^2 - 2abCOS(C)
// (a^2 + b^2 - c^2) / 2ab = COS(C)
let lastPoint = CGPoint(x: pretendWidth, y: 0)
// compute c (distance)
let secondDistanceBetweenPoints = getDistanceBetweenTwoPoints(firstPoint: lastPoint, secondPoint: intersectingPoint)
// where a and b are bounds.height
let secondCosC = getCosC(a: bounds.height, b: bounds.height, c: secondDistanceBetweenPoints)
let secondAngle = acos(secondCosC)
path.addArc(withCenter: secondCenterPoint, radius: bounds.height, startAngle: secondAngle, endAngle: CGFloat.pi / 2, clockwise: true)
} else {
path.addArc(withCenter: firstCenterPoint, radius: halfHeight, startAngle: CGFloat.pi * 1.5, endAngle: CGFloat.pi / 2, clockwise: true)
}
path.addLine(to: CGPoint(x: bounds.height, y: bounds.height))
let finalCenterPoint = CGPoint(x: bounds.height, y: 0)
path.addArc(withCenter: finalCenterPoint, radius: bounds.height, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true)
path.addLine(to: startPointForPath)
path.close()
path.addClip()
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
context.restoreGState()
}
}
func getDistanceBetweenTwoPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let diffX = (firstPoint.x - secondPoint.x) ** 2
let diffY = (firstPoint.y - secondPoint.y) ** 2
return sqrt(diffX + diffY)
}
func getSlopeBetweenTwoPoints(firstPoint: CGPoint, secondPoint: CGPoint) -> CGFloat {
let diffY = firstPoint.y - secondPoint.y
let diffX = firstPoint.x - secondPoint.x
return diffY / diffX
}
func getHypotenuse(firstDistance: CGFloat, secondDistance: CGFloat) -> CGFloat {
return sqrt((firstDistance ** 2) + (secondDistance ** 2))
}
func getQuadraticValues(a: CGFloat, b: CGFloat, c: CGFloat) -> (CGFloat, CGFloat) {
let first = (-b + sqrt((b ** 2) - (4 * a * c))) / (2 * a)
let second = (-b - sqrt((b ** 2) - (4 * a * c))) / (2 * a)
return (first, second)
}
func getCosC(a: CGFloat, b: CGFloat, c: CGFloat) -> CGFloat {
// (a^2 + b^2 - c^2) / 2ab = COS(C)
return ((a ** 2) + (b ** 2) - (c ** 2)) / (2 * a * b)
}
// Testing scenario is pretendWidth..<(pretendWidth + (height / 2))
let bounds = CGRect(x: 0, y: 0, width: pretendWidth + spacer, height: pretendHeight)
let bar = BottomBarGradientNode(frame: bounds)
Find both points of intersection then pick the appropriate one. Or formulate solution in terms of y coordinate then pick higher solution there to compute x coordinate for that.
The equation of a circle 1 is (x2+y2)+a1x+b1y+c1=0. Write both circles in this form, then subtract one equation from the other. The quadratic terms will cancel out, and the remaining equation describes the radical axis of the circles. ax+by+c=0 where a=a1−a2 and so on. Solve for x=−(by+c)/a. Plug this term into one of the original equations for the circle, and you have a quadratic equation in y.
Now a quadratic equation in y is of the form py2+qy+r=0 and has solutions −q±sqrt(q2−4pr)/2p. Look at the sign of p, then pick that same sign in front of the square root to get the solution with larger y value. Plug that back into the equation of the radical axis to compute the x coordinate.
If there is no intersection, q2−4pr < 0 and your solutions would become complex. If a=0 your radical axis is horizontal so you can't parametrize it by y value, and picking a solution by y value doesn't make any sense.

Swift SpriteKit contain nodes within screen width

I have randomly spawning nodes moving vertically down the view.
Here is the code for doing this:
let meteTexture = SKTexture(imageNamed: "redmete.png")
let movementAmount = arc4random() % UInt32(self.frame.width)
let meteOffset = CGFloat(movementAmount) - self.frame.width / 2
let meteTime = arc4random_uniform(4) + 3;
let moveMete = SKAction.move(by: CGVector(dx: 0, dy: -2 * self.frame.height), duration: TimeInterval(meteTime))
redmete = SKSpriteNode(texture: meteTexture)
redmete.position = CGPoint(x: self.frame.midX + meteOffset, y: self.frame.midY + self.frame.height / 2)
My only problem is that as the meteOffset uses the centre of the sprite therefore it can occasionally spawn so 50% or so is out of the view.
I have tried
let movementAmount = arc4random() % UInt32(self.frame.width - meteTexture.size().width / 2)
I've also tried
let meteOffset = CGFloat(movementAmount) - meteTexture.size().width / 2 - self.frame.width / 2
But neither keep the whole of the sprite within the view. How can I do this?
You will want the initial position to be between the sub range of your frame height that is inset by half the height of the meteor.
You would need to have :
let meteorHeight = meteTexture.size().height
let verticalRange = self.frame.height - meteorHeight
let randomXPosition = meteorHeight/2 + arc4random() % verticalRange
Same goes for the horizontal position (if you want that to be random as well) :
let meteorWidth = meteTexture.size().width
let horizontalRange = self.frame.width - meteorWidth
let randomYPosition = meteorWidth/2 + arc4random() % horizontalRange
You can then set the position directly with the random XY coordinates
redmete.position = CGPoint(x:randomXPosition, y:randomYPosition)
If you don't want the meteor to appear too close to the edges, you can reduce the value of the verticalRange and horizontalRange further by subtracting a fixed offset or multiplying by a fraction.
SOLVED:
let meteTexture = SKTexture(imageNamed: "redmete.png")
let movementAmount = arc4random() % UInt32(self.frame.width / 2)
let meteOffset = CGFloat(movementAmount) + meteTexture.size().width/2 - self.frame.width / 2
let meteTime = arc4random_uniform(4) + 3;
let moveMete = SKAction.move(by: CGVector(dx: 0, dy: -2 * self.frame.height), duration: TimeInterval(meteTime))
redmete = SKSpriteNode(texture: meteTexture)
redmete.position = CGPoint(x: self.frame.midX + meteOffset, y: self.frame.midY + self.frame.height / 2)

How to connect two SCNSpheres in 3D space using Bezier path in Swift?

I have the following code:
func createScene(){
count += 1
let sphereGeom = SCNSphere(radius: 1.5)
sphereGeom.firstMaterial?.diffuse.contents = UIColor.redColor()
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
let radius = 3.0
var radians = Double(0)
var yPosition = Float(5.4)
while count <= 20 {
if radians >= 2{
radians -= 2
}
let sphereNode = SCNNode(geometry: sphereGeom)
let angle = Double(radians * M_PI)
let xPosition = Float(radius * cos(angle))
let zPosition = Float(radius * sin(angle))
sphereNode.position = SCNVector3(xPosition, yPosition, zPosition)
let cgX = CGFloat(xPosition)
let cgY = CGFloat(yPosition)
path.addQuadCurveToPoint(CGPoint(x: cgX, y: cgY), controlPoint: CGPoint(x: (cgX / 2), y: (cgY / 2)))
path.addLineToPoint(CGPoint(x: (cgX - (cgX * 0.01)), y: cgY))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: (cgX / 2), y: ((cgY / 2) - (cgY * 0.01))))
let shape = SCNShape(path: path, extrusionDepth: 3.0)
shape.firstMaterial?.diffuse.contents = UIColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.eulerAngles.y = Float(-M_PI_4)
self.rootNode.addChildNode(shapeNode)
count += 1
radians += 0.5556
yPosition -= 1.35
self.rootNode.addChildNode(sphereNode)
}
I want to add a Bezier path connecting each sphere to the next one, creating a spiral going down the helix. For some reason, when I add this code, the shape doesn't even appear. But when I use larger x and y values, I see the path fine, but it is no way oriented to the size of the spheres. I don't understand why it disappears when I try to make it smaller.
Your SCNShape doesn't ever get extruded. Per Apple doc,
An extrusion depth of zero creates a flat, one-sided shape.
With larger X/Y values your flat shape happens to become visible. You can't build a 3D helix with SCNShape, though: the start and end planes of the extrusion are parallel.
You'll have to use custom geometry, or approximate your helix with a series of elongated SCNBox nodes. And I bet someone out there knows how to do this with a shader.