Related
I’m trying to create a lighting bolt using scenekit and I’m following this guide. So far I’ve got a vertical line in my scene using UIBezierPath with an extrusion to make it 3d but I’m not sure how to bend the “line” at the midpoint as described in the link.
func createBolt() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 1))
path.close()
let shape = SCNShape(path: path, extrusionDepth 0.2)
let color = UIColor.red
shape.firstMaterial?.diffuse.contents = color
let boltNode = SCNNode(geometry: shape)
boltNode.position.z = 0
sceneView.scene.rootNode.addChildNode(boltNode)
}
Algorithm is pretty straightforward:
You start with list of 1 segment from A to B, then on each generation you split each segment on 2 segments by shifting middle point on random offset on his norm
struct Segment {
let start: CGPoint
let end: CGPoint
}
/// Calculate norm of 2d vector
func norm(_ v: CGPoint) -> CGPoint {
let d = max(sqrt(v.x * v.x + v.y * v.y), 0.0001)
return CGPoint(x: v.x / d, y: v.y / -d)
}
/// Splitting segment on 2 segments with middle point be shifted by `offset` on norm
func split(_ segment: Segment, by offset: CGFloat) -> [Segment] {
var midPoint = (segment.start + segment.end) / 2
midPoint = norm(segment.end - segment.start) * offset + midPoint
return [
Segment(start: segment.start, end: midPoint),
Segment(start: midPoint, end: segment.end)
]
}
/// Generate bolt-like line from `start` to `end` with maximal started frequence of `maxOffset`
/// and `generation` of split loops
func generate(from start: CGPoint, to end: CGPoint, withOffset maxOffset: CGFloat, generations: Int = 6) -> UIBezierPath {
var segments = [Segment(start: start, end: end)]
var offset = maxOffset
for _ in 0 ..< generations {
segments = segments.flatMap { split($0, by: CGFloat.random(in: -offset...offset)) }
offset /= 2
}
let path = UIBezierPath()
path.move(to: start)
segments.forEach { path.addLine(to: $0.end) }
return path
}
// MARK: - Example
let start = CGPoint(x: 10, y: 10)
let end = CGPoint(x: 90, y: 90)
let path = generate(from: start, to: end, withOffset: 30, generations: 5)
// MARK: - Helpers
func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
}
func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
SceneKit make Lightning Bolts
This here gives you another approach how to create randomized, full 3D Lightning Bolts in SceneKit (thank you Harry!).
Create a new SceneKit project (for iOS) in Xcode using the default Game template - the one that shows the aircraft in 3D space - delete the aircraft and create an empty scene with a black background. Also globally define your sceneView (to be able to access it from other classes).
Add the following classes and extensions to a new Swift file (import SceneKit):
Classes
class LightningStrike:
class LightningStrike : Geometry {
var bolt:[Lightning] = []
var start = SCNVector3() // stores start position of the Bolt
var end = SCNVector3() // stores end position of the Bolt
static var delayTime = 0.0
override init() {
start = SCNVector3(0.0, +5.0, 0.0) // default, to be changed
end = SCNVector3(0.0, -5.0, 0.0) // default, to be changed
print("Lightning Strike initialized")
}
private func fadeOutBolt() {
for b in bolt {
SCNTransaction.begin()
SCNTransaction.animationDuration = 2.0
b.face.geometry?.firstMaterial?.transparency = 0.0
SCNTransaction.commit()
}
}
func strike() {
for b in bolt { b.face.removeFromParentNode() }
bolt.removeAll()
// Create Main Bolt
bolt.append(Lightning())
bolt[0].createBolt(start,end)
sceneView.scene?.rootNode.addChildNode(bolt[0].face)
// Create child Bolts
for _ in 0 ..< 15 { // number of child bolts
// let parent = Int.random(in: 0 ..< bolt.count) // random parent bolt, an other method
let parent : Int = 0
let start = bolt[parent].centerLine[10 + Int.random(in: 0 ..< 15)] // random node to start from off of parent, pay attention to: numSegments - changing numbers here can cause out of index crash
let length:SCNVector3 = bolt[parent].end.minus(start) // length from our start to end of parent
var end = SCNVector3()
end.x = start.x + length.x / 1.5 + Float.random(in: 0 ... abs(length.x) / 3) // adjust by playing with this numbers
end.y = start.y + length.y / 1.5 + Float.random(in: 0 ... abs(length.y) / 3) // adjust by playing with this numbers
end.z = start.z + length.z / 1.5 + Float.random(in: 0 ... abs(length.z) / 3) // adjust by playing with this numbers
bolt.append(Lightning())
let index = bolt.count-1
bolt[index].width = bolt[parent].width * 0.2
bolt[index].deviation = bolt[parent].deviation * 0.3
bolt[index].createBolt(start,end)
sceneView.scene?.rootNode.addChildNode(bolt[0].face)
}
// Reset delay time and schedule fadeOut
LightningStrike.delayTime = 0.0 // reset delay time
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.fadeOutBolt() }
// Here you can add a Sound Effect
}
deinit {
for b in bolt { b.face.removeFromParentNode() }
bolt.removeAll()
print("Lightning Strike deinitialized")
}
}
class Lightning:
class Lightning : Geometry {
let UNASSIGNED:Float = 999
var start = SCNVector3()
var end = SCNVector3()
var numSegments = Int() // use => 3,5,9,17,33,65
var width = Float()
var deviation = Float()
var vertices:[SCNVector3] = []
var normals:[SCNVector3] = []
var indices:[Int32] = []
var centerLine:[SCNVector3] = []
var face:SCNNode! = nil
override init() {
numSegments = 33 // 17
width = 0.1
deviation = 1.5
centerLine = Array(repeating: SCNVector3(), count: numSegments)
// indexed indices never change
var j:Int = 0
for i in 0 ..< numSegments-1 {
j = i * 3
indices.append(Int32(j + 0)) // 2 triangles on side #1
indices.append(Int32(j + 2))
indices.append(Int32(j + 3))
indices.append(Int32(j + 2))
indices.append(Int32(j + 5))
indices.append(Int32(j + 3))
indices.append(Int32(j + 2)) // side #2
indices.append(Int32(j + 1))
indices.append(Int32(j + 5))
indices.append(Int32(j + 1))
indices.append(Int32(j + 4))
indices.append(Int32(j + 5))
indices.append(Int32(j + 1)) // side #3
indices.append(Int32(j + 0))
indices.append(Int32(j + 4))
indices.append(Int32(j + 0))
indices.append(Int32(j + 3))
indices.append(Int32(j + 4))
}
}
func createNode() -> SCNGeometry {
for i in 0 ..< numSegments { centerLine[i].x = UNASSIGNED }
centerLine[0] = start
centerLine[numSegments-1] = end
var hop:Int = max(numSegments / 2,1)
var currentDeviation = deviation
while true {
for i in stride(from:0, to: numSegments, by:hop) {
if centerLine[i].x != UNASSIGNED { continue }
let p1 = centerLine[i-hop]
let p2 = centerLine[i+hop]
centerLine[i] = SCNVector3(
(p1.x + p2.x)/2 + Float.random(in: -currentDeviation ... currentDeviation),
(p1.y + p2.y)/2 + Float.random(in: -currentDeviation ... currentDeviation),
(p1.z + p2.z)/2 + Float.random(in: -currentDeviation ... currentDeviation))
}
if hop == 1 { break }
hop /= 2
currentDeviation *= 0.6
}
vertices.removeAll()
normals.removeAll()
// triangle of vertices at each centerLine node on XZ plane
let ss:[Float] = [ sin(0), sin(Float.pi * 2/3), sin(Float.pi * 4/3)]
let cc:[Float] = [ cos(0), cos(Float.pi * 2/3), cos(Float.pi * 4/3)]
var w = width
for i in 0 ..< numSegments {
for j in 0 ..< 3 {
vertices.append(SCNVector3(centerLine[i].x + cc[j] * w, centerLine[i].y, centerLine[i].z + ss[j] * w))
}
w *= 0.90 // bolt gets thinner towards endings
}
// normal for each vertex: position vs. position of neighbor on next node
var index1 = Int()
var index2 = Int()
func norm(_ v: SCNVector3) -> SCNVector3 {
let d = max(sqrt(v.x * v.x + v.y * v.y + v.z * v.z), 0.0001)
return SCNVector3(v.x / d, v.y / -d, v.z / d)
}
for i in 0 ..< numSegments {
for j in 0 ..< 3 {
index1 = i * 3 + j // point on current node
index2 = index1 + 3 // neighboring point on next node
if index2 >= vertices.count { index2 -= 6 } // last node references previous node instead
normals.append(norm(vertices[index1].minus(vertices[index2])))
}
}
let geoBolt = self.createGeometry(
vertices: vertices,
normals: normals,
indices: indices,
primitiveType: SCNGeometryPrimitiveType.triangles)
let boltMaterial : SCNMaterial = {
let material = SCNMaterial()
material.name = "bolt"
material.diffuse.contents = UIColor.init(hex: "#BAB1FFFF") // this is a very clear, almost white purple
material.roughness.contents = 1.0
material.emission.contents = UIColor.init(hex: "#BAB1FFFF") // this is a very clear, almost white purple
material.lightingModel = .physicallyBased
material.isDoubleSided = true
material.transparency = 0.0
return material
}()
geoBolt.firstMaterial = boltMaterial
// this makes the bolt not appearing all geometry at the same time - it's an animation effect
DispatchQueue.main.asyncAfter(deadline: .now() + LightningStrike.delayTime) {
boltMaterial.transparency = 1.0
}
LightningStrike.delayTime += 0.01665
// geoBolt.subdivisionLevel = 1 // give it a try or not...
return geoBolt
}
// Creates a Branch of the entire Bolt
func createBolt(_ nstart:SCNVector3, _ nend:SCNVector3) {
start = nstart
end = nend
face = SCNNode(geometry:createNode())
// This will add some glow around the Bolt,
// but it is **enourmous** performence and memory intense,
// you could try to add some SCNTechnique instead
// let gaussianBlur = CIFilter(name: "CIGaussianBlur")
// gaussianBlur?.name = "blur"
// gaussianBlur?.setValue(2, forKey: "inputRadius")
// face.filters = [gaussianBlur] as? [CIFilter]
sceneView.scene?.rootNode.addChildNode(face)
}
}
class Geometry:
class Geometry : NSObject {
internal func createGeometry(
vertices:[SCNVector3],
normals:[SCNVector3],
indices:[Int32],
primitiveType:SCNGeometryPrimitiveType) -> SCNGeometry
{
// Computed property that indicates the number of primitives to create based on primitive type
var primitiveCount:Int {
get {
switch primitiveType {
case SCNGeometryPrimitiveType.line:
return indices.count / 2
case SCNGeometryPrimitiveType.point:
return indices.count
case SCNGeometryPrimitiveType.triangles,
SCNGeometryPrimitiveType.triangleStrip:
return indices.count / 3
default : return 0
}
}
}
//------------------------
let vdata = NSData(bytes: vertices, length: MemoryLayout<SCNVector3>.size * vertices.count)
let vertexSource = SCNGeometrySource(
data: vdata as Data,
semantic: SCNGeometrySource.Semantic.vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.size)
//------------------------
let ndata = NSData(bytes: normals, length: MemoryLayout<SCNVector3>.size * normals.count)
let normalSource = SCNGeometrySource(
data: ndata as Data,
semantic: SCNGeometrySource.Semantic.normal,
vectorCount: normals.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.size)
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count)
let element = SCNGeometryElement(
data: indexData as Data, primitiveType: primitiveType,
primitiveCount: primitiveCount, bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [vertexSource, normalSource], elements: [element])
}
}
Extensions:
for SCNVector3:
extension SCNVector3
{
func length() -> Float { return sqrtf(x*x + y*y + z*z) }
func minus(_ other:SCNVector3) -> SCNVector3 { return SCNVector3(x - other.x, y - other.y, z - other.z) }
func normalized() -> SCNVector3 {
let len = length()
var ans = SCNVector3()
ans.x = self.x / len
ans.y = self.y / len
ans.z = self.z / len
return ans
}
}
for UIColor:
extension UIColor {
public convenience init?(hex: String) {
let r, g, b, a: CGFloat
if hex.hasPrefix("#") {
let start = hex.index(hex.startIndex, offsetBy: 1)
let hexColor = String(hex[start...])
if hexColor.count == 8 {
let scanner = Scanner(string: hexColor)
var hexNumber: UInt64 = 0
if scanner.scanHexInt64(&hexNumber) {
r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
a = CGFloat(hexNumber & 0x000000ff) / 255
self.init(red: r, green: g, blue: b, alpha: a)
return
}
}
}
return nil
}
}
Usage:
Init the class in your ViewController like so:
let lightningStrike = LightningStrike()
Also add a tap gesture recogniser (in viewDidLoad) for easy testing:
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
sceneView.addGestureRecognizer(tapGesture)
And the corresponding function that will trigger the Lightning Bolt:
#objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
lightningStrike.strike() // will fire a Lighting Bolt
}
Results:
Have fun with it.
In Swift, suppose I have an SCNVector3 variable called p and I want to be able to set p by the following code:
p = [a, b, c]
no matter [a, b, c] is either a [Float] or a [CGFloat].
How can I do that?
You can't make it generic, because you do need Float values to initialize SCNVector3, but you can unite a number of types with a protocol.
Here I made SCNVector3 ExpressibleByArrayLiteral with array elements that conform to protocol ConvertibleToFloat. That protocol handles the ability to covert the value to Float. I've implemented it for Float, CGFloat, Double, and Int and it can be extended to other types as needed:
import UIKit
import SceneKit
public protocol ConvertibleToFloat {
var floatValue: Float { get }
}
extension Float: ConvertibleToFloat {
public var floatValue: Float { return self }
}
extension CGFloat: ConvertibleToFloat {
public var floatValue: Float { return Float(self) }
}
extension Double: ConvertibleToFloat {
public var floatValue: Float { return Float(self) }
}
extension Int: ConvertibleToFloat {
public var floatValue: Float { return Float(self) }
}
extension SCNVector3: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = ConvertibleToFloat
public init(arrayLiteral: ConvertibleToFloat...) {
let values = arrayLiteral.map { $0.floatValue } + [0, 0, 0]
self.init(x: values[0], y: values[1], z: values[2])
}
}
Testing it out:
var sv = SCNVector3(0, 0, 0)
// Initialize with array literal of CGFloat
sv = [CGFloat(1.1), CGFloat(2.2), CGFloat(3.3)]
print(sv) // SCNVector3(x: 1.1, y: 2.2, z: 3.3)
// Initialize with array literal of Float
sv = [Float(1.2), Float(2.3), Float(3.4)]
print(sv) // SCNVector3(x: 1.2, y: 2.3, z: 3.4)
// Not enough values, rest default to 0
sv = [CGFloat(3)]
print(sv) // SCNVector3(x: 3.0, y: 0.0, z: 0.0)
// Initialize with empty array
sv = []
print(sv) // SCNVector3(x: 0.0, y: 0.0, z: 0.0)
// Initilize with [Double]
sv = [1.4, 2.6, 3.7]
print(sv) // SCNVector3(x: 1.4, y: 2.6, z: 3.7)
// Initialize with [Int]
sv = [8, 9, 10]
print(sv) // SCNVector3(x: 8.0, y: 9.0, z: 10.0)
// As in the question
let a: CGFloat = 3.1
let b: CGFloat = 4.2
let c: CGFloat = 5.3
sv = [a, b, c]
print(sv) // SCNVector3(x: 3.1, y: 4.2, z: 5.3)
My little extension from #vacawama ‘s code:
import UIKit
import SceneKit
// MARK: - RealNumber (protocol)
public protocol RealNumber {
var realValue: Float { get }
}
// MARK: - RealNumber (operations)
// -a (additive inverse)
public prefix func - (a:RealNumber) -> Float {
return -a.realValue
}
// a + b (addition)
public func + <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue + b.realValue
}
// a - b (substraction)
public func - <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue - b.realValue
}
// a * b (multiplication)
public func * <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue * b.realValue
}
// a / b (division)
public func / <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue / b.realValue
}
// MARK: - RealNumber (conforming types)
extension Float: RealNumber { public var realValue: Float { return self } }
extension CGFloat: RealNumber { public var realValue: Float { return Float(self) } }
extension Double: RealNumber { public var realValue: Float { return Float(self) } }
extension Int: RealNumber { public var realValue: Float { return Float(self) } }
// MARK: - test RealNumber
let c = CGFloat(3) // CGFloat
let f = Float(2) // Float
let d = 1.5 // Double
let i = 4 // Int
// test operations
i + d // 5.5
c - f // 1.0
i * d // 6.0
i / d // 2.667
// MARK: - RealVector (protocol)
public protocol RealVector: ExpressibleByArrayLiteral {
// must have dimension (of the vector space)
static var dimension: Int { get }
// can get/set coords of the vector
var coordinates: [Float] { get set }
// must have default init
init()
// can init with [RealNumber]
init(arrayLiteral: RealNumber...)
// must have vector addition, additive inverse, and scalar multiplication
static func + (u:Self, v:Self) -> Self // vector addition
static prefix func - (u:Self) -> Self // additive inverse
static func * (a:RealNumber, v:Self) -> Self // scalar multiplication
// can get/set coordinate by subscript index
subscript(i:Int) -> Float { get set }
}
// MARK: - RealVector (default behaviors)
extension RealVector {
// no default get/set coordinates
// you have to implement them yourself ‼️
// default subscript implementation
public subscript(i:Int) -> Float {
get { return coordinates[i] }
set {
var coords = self.coordinates
coords[i] = newValue
self.coordinates = coords
}
}
// default u + v (vector addition)
public static func + (u:Self, v:Self) -> Self {
var coords = (0...(Self.dimension - 1)).map { u[$0] + v[$0] }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default u + a (convenient vector addition)
public static func + (u:Self, a:RealNumber) -> Self {
var coords = (0...(Self.dimension - 1)).map { u[$0] + a.realValue }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default -v (additive inverse)
static public prefix func - (u:Self) -> Self {
var coords = (0...(Self.dimension - 1)).map { -u[$0] }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default * (scalar multiplication)
public static func * (a:RealNumber, v:Self) -> Self {
var coords = (0...(Self.dimension - 1)).map { a.realValue * v[$0] }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default / (scalar division)
public static func / (v:Self, a:RealNumber) -> Self {
return (1/a.realValue) * v
}
// other default operations ( u-v, v*a, u+=v, u-=v, u*=a, ... )
public static func - (u:Self, v:Self) -> Self { return u + (-v) }
public static func - (u:Self, a:RealNumber) -> Self { return u + (-a) }
public static func * (v:Self, a:RealNumber) -> Self { return a * v }
public static func += (u:inout Self, v:Self) { u = u + v }
public static func -= (u:inout Self, v:Self) { u = u - v }
public static func += (u:inout Self, a:RealNumber) { u = u + a }
public static func -= (u:inout Self, a:RealNumber) { u = u - a }
public static func *= (u:inout Self, a:RealNumber) { u = u * a }
public static func /= (u:inout Self, a:RealNumber) { u = u / a }
// default init with [RealNumber]
public init(arrayLiteral elements: RealNumber...) {
var coords = elements.map { $0.realValue } + [Float](repeating: 0, count: Self.dimension)
self.init()
self.coordinates = coords
}
// default init with another RealVector
public init<V:RealVector>(_ v:V) {
self.init()
self.coordinates = v.coordinates
}
}
// vector operations of two (different types of) RealVector
// U + V -> U
public func + <U:RealVector, V:RealVector>(u:U, v:V) -> U {
return u + U.init(v)
}
// U - V -> U
public func - <U:RealVector, V:RealVector>(u:U, v:V) -> U {
return u - U.init(v)
}
// MARK: - RealVector (conforming types)
// SCNVector3
extension SCNVector3: RealVector {
// 3-dimensional
public static var dimension: Int { return 3 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [x, y, z] }
set {
var coords = newValue + [0,0,0]
x = coords[0]
y = coords[1]
z = coords[2]
}
}
}// end: SCNVector3: RealVector
// CGPoint
extension CGPoint: RealVector {
// 2-dimensional
public static var dimension: Int { return 2 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [x, y].map { $0.realValue } }
set {
var coords = newValue + [0,0]
x = CGFloat(coords[0])
y = CGFloat(coords[1])
}
}
}// end: CGPoint
// CGVector
extension CGVector: RealVector {
// 2-dimensional
public static var dimension: Int { return 2 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [dx, dy].map { $0.realValue } }
set {
var coords = newValue + [0,0]
dx = CGFloat(coords[0])
dy = CGFloat(coords[1])
}
}
}// end: CGVector
// CGSize
extension CGSize: RealVector {
// 2-dimensional
public static var dimension: Int { return 2 }
// custiom get/set coordinates
public var coordinates: [Float] {
get { return [width, height].map { $0.realValue } }
set {
var coords = newValue + [0,0]
width = CGFloat(coords[0])
height = CGFloat(coords[1])
}
}
}
// SCNVector4
extension SCNVector4: RealVector {
// 4-dimensional
public static var dimension: Int { return 4 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [x, y, z, w] }
set {
var coords = newValue + [0,0,0,0]
x = coords[0]
y = coords[1]
z = coords[2]
w = coords[3]
}
}
}// end: SCNVector4
// CGRect
extension CGRect: RealVector {
// 4-dimensional
public static var dimension: Int { return 4}
// custom get/set coordinates
public var coordinates: [Float] {
get {
return [origin.x, origin.y, width, height].map { $0.realValue }
}
set {
var v = newValue + [0,0,0,0]
origin = [v[0], v[1]]
size = [v[2], v[3]]
}
}
}// end: CGRect
// MARK: - test RealVector
var p: CGPoint = [5, 6]
p.coordinates
CGPoint.dimension
// test init
p = CGPoint(SCNVector3(1,2,3)) // init with SCNVector3
p = [CGFloat(1.1), CGFloat(2.2), CGFloat(3.3)] // init with [CGFloat]
p = [Float(1.2), Float(2.3), Float(3.4)] // init with [Float]
p = [1.2, 3.4, 5.6] // init with [Double]
p = [1, 2, 3] // init with [Int]
p = [3] // Not enough values (rest default to 0)
p = [] // init with empty array
// test vector subscript
p[1] // get: 0.0
p[1] = 3 // set: 3.0
p // (0.0, 3.0)
// test vector operations
let u: SCNVector3 = [1,2,3]
let v: SCNVector3 = [4,5,6]
var w = SCNVector3(p) // init with CGPoint
w = -u
w = u + [1,2,3]
w = u + v
w = u - v
w = 2 * u
w = u * 2
w = u / 2
w += 0.5 // (1.0, 1.5, 2.0)
w -= 2 // (-1.0, -0.5, 0)
w *= 4 // (-4, -2, 0)
w /= 2 // (-2, -1, 0)
w += [7,8,9]// (5, 7, 9)
// vector linear combinations
w = 4*u - 3*v // (-8, -7, -6)
// vector operations of two different conforming types
w + p // (-8, -4, -6)
w - p // (-8, -10, -6)
I'm using the Swift Chart. I'd like to modify it to allow the user to select a range. The idea is to touch, swipe left/right, and then lift your finger. This should highlight the area swiped and provide a way to get the beginning and ending values of the swipe. I expect I'll need to modify the touchesBegan() and touchesEnded() events, but I don't know how.
Here's what I did to make this work:
I added range selection variables to the class
// Range selection
open var leftRangePoint: UITouch!
open var rightRangePoint: UITouch!
open var leftRangeLocation: CGFloat = 0
open var rightRangeLocation: CGFloat = 0
I modified touchesBegan()
leftRangePoint = touches.first!
leftRangeLocation = leftRangePoint.location(in: self).x
And added a routine to touchesEnded()
handleRangeTouchesEnded(touches, event: event)
Here's the full code:
// Chart.swift
//
// Created by Giampaolo Bellavite on 07/11/14.
// Copyright (c) 2014 Giampaolo Bellavite. All rights reserved.
import UIKit
public protocol ChartDelegate: class {
func didTouchChart(_ chart: Chart, indexes: [Int?], x: Float, left: CGFloat)
func didFinishTouchingChart(_ chart: Chart)
func didEndTouchingChart(_ chart: Chart)
}
typealias ChartPoint = (x: Float, y: Float)
public enum ChartLabelOrientation {
case horizontal
case vertical
}
#IBDesignable open class Chart: UIControl {
#IBInspectable
open var identifier: String?
open var series: [ChartSeries] = [] {
didSet {
setNeedsDisplay()
}
}
open var xLabels: [Float]?
open var xLabelsFormatter = { (labelIndex: Int, labelValue: Float) -> String in
String(Int(labelValue))
}
open var xLabelsTextAlignment: NSTextAlignment = .left
open var xLabelsOrientation: ChartLabelOrientation = .horizontal
open var xLabelsSkipLast: Bool = true
open var xLabelsSkipAll: Bool = true
open var yLabels: [Float]?
open var yLabelsFormatter = { (labelIndex: Int, labelValue: Float) -> String in
String(Int(labelValue))
}
open var yLabelsOnRightSide: Bool = false
open var labelFont: UIFont? = UIFont.systemFont(ofSize: 12)
#IBInspectable
open var labelColor: UIColor = UIColor.black
#IBInspectable
open var axesColor: UIColor = UIColor.gray.withAlphaComponent(0.3)
#IBInspectable
open var gridColor: UIColor = UIColor.gray.withAlphaComponent(0.3)
open var showXLabelsAndGrid: Bool = true
open var showYLabelsAndGrid: Bool = true
open var bottomInset: CGFloat = 20
open var topInset: CGFloat = 20
#IBInspectable
open var lineWidth: CGFloat = 2
weak open var delegate: ChartDelegate?
open var minX: Float?
open var minY: Float?
open var maxX: Float?
open var maxY: Float?
open var highlightLineColor = UIColor.gray
open var highlightLineWidth: CGFloat = 0.5
open var areaAlphaComponent: CGFloat = 0.1
open var leftRangePoint: UITouch!
open var rightRangePoint: UITouch!
open var leftRangeLocation: CGFloat = 0
open var rightRangeLocation: CGFloat = 0
fileprivate var highlightShapeLayer: CAShapeLayer!
fileprivate var layerStore: [CAShapeLayer] = []
fileprivate var drawingHeight: CGFloat!
fileprivate var drawingWidth: CGFloat!
fileprivate var min: ChartPoint!
fileprivate var max: ChartPoint!
typealias ChartLineSegment = [ChartPoint]
override public init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
convenience public init() {
self.init(frame: .zero)
commonInit()
}
private func commonInit() {
backgroundColor = UIColor.clear
contentMode = .redraw // redraw rects on bounds change
}
override open func draw(_ rect: CGRect) {
#if TARGET_INTERFACE_BUILDER
drawIBPlaceholder()
#else
drawChart()
#endif
}
open func add(_ series: ChartSeries) {
self.series.append(series)
}
open func add(_ series: [ChartSeries]) {
for s in series {
add(s)
}
}
open func removeSeriesAt(_ index: Int) {
series.remove(at: index)
}
open func removeAllSeries() {
series = []
}
open func valueForSeries(_ seriesIndex: Int, atIndex dataIndex: Int?) -> Float? {
if dataIndex == nil { return nil }
let series = self.series[seriesIndex] as ChartSeries
return series.data[dataIndex!].y
}
fileprivate func drawIBPlaceholder() {
let placeholder = UIView(frame: self.frame)
placeholder.backgroundColor = UIColor(red: 0.93, green: 0.93, blue: 0.93, alpha: 1)
let label = UILabel()
label.text = "Chart"
label.font = UIFont.systemFont(ofSize: 28)
label.textColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2)
label.sizeToFit()
label.frame.origin.x += frame.width/2 - (label.frame.width / 2)
label.frame.origin.y += frame.height/2 - (label.frame.height / 2)
placeholder.addSubview(label)
addSubview(placeholder)
}
fileprivate func drawChart() {
drawingHeight = bounds.height - bottomInset - topInset
drawingWidth = bounds.width
let minMax = getMinMax()
min = minMax.min
max = minMax.max
highlightShapeLayer = nil
// Remove things before drawing, e.g. when changing orientation
for view in self.subviews {
view.removeFromSuperview()
}
for layer in layerStore {
layer.removeFromSuperlayer()
}
layerStore.removeAll()
// Draw content
for (index, series) in self.series.enumerated() {
// Separate each line in multiple segments over and below the x axis
let segments = Chart.segmentLine(series.data as ChartLineSegment, zeroLevel: series.colors.zeroLevel)
segments.forEach({ segment in
let scaledXValues = scaleValuesOnXAxis( segment.map({ return $0.x }) )
let scaledYValues = scaleValuesOnYAxis( segment.map({ return $0.y }) )
if series.line {
drawLine(scaledXValues, yValues: scaledYValues, seriesIndex: index)
}
if series.area {
drawArea(scaledXValues, yValues: scaledYValues, seriesIndex: index)
}
})
}
drawAxes()
if showXLabelsAndGrid && (xLabels != nil || series.count > 0) {
drawLabelsAndGridOnXAxis()
}
if showYLabelsAndGrid && (yLabels != nil || series.count > 0) {
drawLabelsAndGridOnYAxis()
}
}
fileprivate func getMinMax() -> (min: ChartPoint, max: ChartPoint) {
// Start with user-provided values
var min = (x: minX, y: minY)
var max = (x: maxX, y: maxY)
// Check in datasets
for series in self.series {
let xValues = series.data.map({ (point: ChartPoint) -> Float in
return point.x })
let yValues = series.data.map({ (point: ChartPoint) -> Float in
return point.y })
let newMinX = xValues.min()!
let newMinY = yValues.min()!
let newMaxX = xValues.max()!
let newMaxY = yValues.max()!
if min.x == nil || newMinX < min.x! { min.x = newMinX }
if min.y == nil || newMinY < min.y! { min.y = newMinY }
if max.x == nil || newMaxX > max.x! { max.x = newMaxX }
if max.y == nil || newMaxY > max.y! { max.y = newMaxY }
}
// Check in labels
if xLabels != nil {
let newMinX = (xLabels!).min()!
let newMaxX = (xLabels!).max()!
if min.x == nil || newMinX < min.x! { min.x = newMinX }
if max.x == nil || newMaxX > max.x! { max.x = newMaxX }
}
if yLabels != nil {
let newMinY = (yLabels!).min()!
let newMaxY = (yLabels!).max()!
if min.y == nil || newMinY < min.y! { min.y = newMinY }
if max.y == nil || newMaxY > max.y! { max.y = newMaxY }
}
if min.x == nil { min.x = 0 }
if min.y == nil { min.y = 0 }
if max.x == nil { max.x = 0 }
if max.y == nil { max.y = 0 }
return (min: (x: min.x!, y: min.y!), max: (x: max.x!, max.y!))
}
fileprivate func scaleValuesOnXAxis(_ values: [Float]) -> [Float] {
let width = Float(drawingWidth)
var factor: Float
if max.x - min.x == 0 {
factor = 0
} else {
factor = width / (max.x - min.x)
}
let scaled = values.map { factor * ($0 - self.min.x) }
return scaled
}
fileprivate func scaleValuesOnYAxis(_ values: [Float]) -> [Float] {
let height = Float(drawingHeight)
var factor: Float
if max.y - min.y == 0 {
factor = 0
} else {
factor = height / (max.y - min.y)
}
let scaled = values.map { Float(self.topInset) + height - factor * ($0 - self.min.y) }
return scaled
}
fileprivate func scaleValueOnYAxis(_ value: Float) -> Float {
let height = Float(drawingHeight)
var factor: Float
if max.y - min.y == 0 {
factor = 0
} else {
factor = height / (max.y - min.y)
}
let scaled = Float(self.topInset) + height - factor * (value - min.y)
return scaled
}
fileprivate func getZeroValueOnYAxis(zeroLevel: Float) -> Float {
if min.y > zeroLevel {
return scaleValueOnYAxis(min.y)
} else {
return scaleValueOnYAxis(zeroLevel)
}
}
fileprivate func drawLine(_ xValues: [Float], yValues: [Float], seriesIndex: Int) {
// YValues are "reverted" from top to bottom, so 'above' means <= level
let isAboveZeroLine = yValues.max()! <= self.scaleValueOnYAxis(series[seriesIndex].colors.zeroLevel)
let path = CGMutablePath()
path.move(to: CGPoint(x: CGFloat(xValues.first!), y: CGFloat(yValues.first!)))
for i in 1..<yValues.count {
let y = yValues[i]
path.addLine(to: CGPoint(x: CGFloat(xValues[i]), y: CGFloat(y)))
}
let lineLayer = CAShapeLayer()
lineLayer.frame = self.bounds
lineLayer.path = path
if isAboveZeroLine {
lineLayer.strokeColor = series[seriesIndex].colors.above.cgColor
} else {
lineLayer.strokeColor = series[seriesIndex].colors.below.cgColor
}
lineLayer.fillColor = nil
lineLayer.lineWidth = lineWidth
lineLayer.lineJoin = kCALineJoinBevel
self.layer.addSublayer(lineLayer)
layerStore.append(lineLayer)
}
fileprivate func drawArea(_ xValues: [Float], yValues: [Float], seriesIndex: Int) {
// YValues are "reverted" from top to bottom, so 'above' means <= level
let isAboveZeroLine = yValues.max()! <= self.scaleValueOnYAxis(series[seriesIndex].colors.zeroLevel)
let area = CGMutablePath()
let zero = CGFloat(getZeroValueOnYAxis(zeroLevel: series[seriesIndex].colors.zeroLevel))
area.move(to: CGPoint(x: CGFloat(xValues[0]), y: zero))
for i in 0..<xValues.count {
area.addLine(to: CGPoint(x: CGFloat(xValues[i]), y: CGFloat(yValues[i])))
}
area.addLine(to: CGPoint(x: CGFloat(xValues.last!), y: zero))
let areaLayer = CAShapeLayer()
areaLayer.frame = self.bounds
areaLayer.path = area
areaLayer.strokeColor = nil
if isAboveZeroLine {
areaLayer.fillColor = series[seriesIndex].colors.above.withAlphaComponent(areaAlphaComponent).cgColor
} else {
areaLayer.fillColor = series[seriesIndex].colors.below.withAlphaComponent(areaAlphaComponent).cgColor
}
areaLayer.lineWidth = 0
self.layer.addSublayer(areaLayer)
layerStore.append(areaLayer)
}
fileprivate func drawAxes() {
let context = UIGraphicsGetCurrentContext()!
context.setStrokeColor(axesColor.cgColor)
context.setLineWidth(0.5)
// horizontal axis at the bottom
context.move(to: CGPoint(x: CGFloat(0), y: drawingHeight + topInset))
context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: drawingHeight + topInset))
context.strokePath()
// horizontal axis at the top
context.move(to: CGPoint(x: CGFloat(0), y: CGFloat(0)))
context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: CGFloat(0)))
context.strokePath()
// horizontal axis when y = 0
if min.y < 0 && max.y > 0 {
let y = CGFloat(getZeroValueOnYAxis(zeroLevel: 0))
context.move(to: CGPoint(x: CGFloat(0), y: y))
context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: y))
context.strokePath()
}
// vertical axis on the left
context.move(to: CGPoint(x: CGFloat(0), y: CGFloat(0)))
context.addLine(to: CGPoint(x: CGFloat(0), y: drawingHeight + topInset))
context.strokePath()
// vertical axis on the right
context.move(to: CGPoint(x: CGFloat(drawingWidth), y: CGFloat(0)))
context.addLine(to: CGPoint(x: CGFloat(drawingWidth), y: drawingHeight + topInset))
context.strokePath()
}
fileprivate func drawLabelsAndGridOnXAxis() {
let context = UIGraphicsGetCurrentContext()!
context.setStrokeColor(gridColor.cgColor)
context.setLineWidth(0.5)
var labels: [Float]
if xLabels == nil {
// Use labels from the first series
labels = series[0].data.map({ (point: ChartPoint) -> Float in
return point.x })
} else {
labels = xLabels!
}
let scaled = scaleValuesOnXAxis(labels)
let padding: CGFloat = 5
scaled.enumerated().forEach { (i, value) in
let x = CGFloat(value)
let isLastLabel = x == drawingWidth
// Add vertical grid for each label, except axes on the left and right
if x != 0 && x != drawingWidth {
context.move(to: CGPoint(x: x, y: CGFloat(0)))
if xLabelsSkipAll {
let height: CGFloat = bounds.height - 20.0
context.addLine(to: CGPoint(x: x, y: height))
} else {
context.addLine(to: CGPoint(x: x, y: bounds.height))
}
context.strokePath()
}
if (xLabelsSkipLast && isLastLabel) || xLabelsSkipAll {
// Do not add label at the most right position
return
}
// Add label
let label = UILabel(frame: CGRect(x: x, y: drawingHeight, width: 0, height: 0))
label.font = labelFont
label.text = xLabelsFormatter(i, labels[i])
label.textColor = labelColor
// Set label size
label.sizeToFit()
// Center label vertically
label.frame.origin.y += topInset
if xLabelsOrientation == .horizontal {
// Add left padding
label.frame.origin.y -= (label.frame.height - bottomInset) / 2
label.frame.origin.x += padding
// Set label's text alignment
label.frame.size.width = (drawingWidth / CGFloat(labels.count)) - padding * 2
label.textAlignment = xLabelsTextAlignment
} else {
label.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
// Adjust vertical position according to the label's height
label.frame.origin.y += label.frame.size.height / 2
// Adjust horizontal position as the series line
label.frame.origin.x = x
if xLabelsTextAlignment == .center {
// Align horizontally in series
label.frame.origin.x += ((drawingWidth / CGFloat(labels.count)) / 2) - (label.frame.size.width / 2)
} else {
// Give some space from the vertical line
label.frame.origin.x += padding
}
}
self.addSubview(label)
}
}
fileprivate func drawLabelsAndGridOnYAxis() {
let context = UIGraphicsGetCurrentContext()!
context.setStrokeColor(gridColor.cgColor)
context.setLineWidth(0.5)
var labels: [Float]
if yLabels == nil {
labels = [(min.y + max.y) / 2, max.y]
if yLabelsOnRightSide || min.y != 0 {
labels.insert(min.y, at: 0)
}
} else {
labels = yLabels!
}
let scaled = scaleValuesOnYAxis(labels)
let padding: CGFloat = 5
let zero = CGFloat(getZeroValueOnYAxis(zeroLevel: 0))
scaled.enumerated().forEach { (i, value) in
let y = CGFloat(value)
// Add horizontal grid for each label, but not over axes
if y != drawingHeight + topInset && y != zero {
context.move(to: CGPoint(x: CGFloat(0), y: y))
context.addLine(to: CGPoint(x: self.bounds.width, y: y))
if labels[i] != 0 {
// Horizontal grid for 0 is not dashed
context.setLineDash(phase: CGFloat(0), lengths: [CGFloat(5)])
} else {
context.setLineDash(phase: CGFloat(0), lengths: [])
}
context.strokePath()
}
let label = UILabel(frame: CGRect(x: padding, y: y, width: 0, height: 0))
label.font = labelFont
label.text = yLabelsFormatter(i, labels[i])
label.textColor = labelColor
label.sizeToFit()
if yLabelsOnRightSide {
label.frame.origin.x = drawingWidth
label.frame.origin.x -= label.frame.width + padding
}
// Labels should be placed above the horizontal grid
label.frame.origin.y -= label.frame.height
self.addSubview(label)
}
UIGraphicsEndImageContext()
}
fileprivate func drawHighlightLineFromLeftPosition(_ left: CGFloat) {
if let shapeLayer = highlightShapeLayer {
// Use line already created
let path = CGMutablePath()
path.move(to: CGPoint(x: left, y: 0))
path.addLine(to: CGPoint(x: left, y: drawingHeight + topInset))
shapeLayer.path = path
} else {
// Create the line
let path = CGMutablePath()
path.move(to: CGPoint(x: left, y: CGFloat(0)))
path.addLine(to: CGPoint(x: left, y: drawingHeight + topInset))
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.bounds
shapeLayer.path = path
shapeLayer.strokeColor = highlightLineColor.cgColor
shapeLayer.fillColor = nil
shapeLayer.lineWidth = highlightLineWidth
highlightShapeLayer = shapeLayer
layer.addSublayer(shapeLayer)
layerStore.append(shapeLayer)
}
}
func handleTouchEvents(_ touches: Set<UITouch>, event: UIEvent!) {
let point = touches.first!
let left = point.location(in: self).x
let x = valueFromPointAtX(left)
if left < 0 || left > (drawingWidth as CGFloat) {
// Remove highlight line at the end of the touch event
if let shapeLayer = highlightShapeLayer {
shapeLayer.path = nil
}
delegate?.didFinishTouchingChart(self)
return
}
drawHighlightLineFromLeftPosition(left)
if delegate == nil {
return
}
var indexes: [Int?] = []
for series in self.series {
var index: Int? = nil
let xValues = series.data.map({ (point: ChartPoint) -> Float in
return point.x })
let closest = Chart.findClosestInValues(xValues, forValue: x)
if closest.lowestIndex != nil && closest.highestIndex != nil {
// Consider valid only values on the right
index = closest.lowestIndex
}
indexes.append(index)
}
delegate!.didTouchChart(self, indexes: indexes, x: x, left: left)
}
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
handleTouchEvents(touches, event: event)
leftRangePoint = touches.first!
leftRangeLocation = leftRangePoint.location(in: self).x
}
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
handleTouchEvents(touches, event: event)
delegate?.didEndTouchingChart(self)
handleRangeTouchesEnded(touches, event: event)
}
func handleRangeTouchesEnded(_ touches: Set<UITouch>, event: UIEvent!) {
rightRangePoint = touches.first!
rightRangeLocation = rightRangePoint.location(in: self).x
// Make sure left is actually to the left
if rightRangeLocation < leftRangeLocation {
let rangePoint = leftRangePoint
let rangeLocation = leftRangeLocation
leftRangePoint = rightRangePoint
leftRangeLocation = rightRangeLocation
rightRangePoint = rangePoint
rightRangeLocation = rangeLocation
}
// Highlight the range
let layer = CAShapeLayer()
let width = rightRangeLocation - leftRangeLocation
layer.path = UIBezierPath(rect: CGRect(x: leftRangeLocation, y: topInset, width: width, height: drawingHeight)).cgPath
layer.fillColor = UIColor.red.cgColor
layer.opacity = 0.3
self.layer.addSublayer(layer)
}
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
handleTouchEvents(touches, event: event)
}
fileprivate func valueFromPointAtX(_ x: CGFloat) -> Float {
let value = ((max.x-min.x) / Float(drawingWidth)) * Float(x) + min.x
return value
}
fileprivate func valueFromPointAtY(_ y: CGFloat) -> Float {
let value = ((max.y - min.y) / Float(drawingHeight)) * Float(y) + min.y
return -value
}
fileprivate class func findClosestInValues(_ values: [Float],
forValue value: Float
) -> (
lowestValue: Float?,
highestValue: Float?,
lowestIndex: Int?,
highestIndex: Int?
) {
var lowestValue: Float?, highestValue: Float?, lowestIndex: Int?, highestIndex: Int?
values.enumerated().forEach { (i, currentValue) in
if currentValue <= value && (lowestValue == nil || lowestValue! < currentValue) {
lowestValue = currentValue
lowestIndex = i
}
if currentValue >= value && (highestValue == nil || highestValue! > currentValue) {
highestValue = currentValue
highestIndex = i
}
}
return (
lowestValue: lowestValue,
highestValue: highestValue,
lowestIndex: lowestIndex,
highestIndex: highestIndex
)
}
fileprivate class func segmentLine(_ line: ChartLineSegment, zeroLevel: Float) -> [ChartLineSegment] {
var segments: [ChartLineSegment] = []
var segment: ChartLineSegment = []
line.enumerated().forEach { (i, point) in
segment.append(point)
if i < line.count - 1 {
let nextPoint = line[i+1]
if point.y >= zeroLevel && nextPoint.y < zeroLevel || point.y < zeroLevel && nextPoint.y >= zeroLevel {
// The segment intersects zeroLevel, close the segment with the intersection point
let closingPoint = Chart.intersectionWithLevel(point, and: nextPoint, level: zeroLevel)
segment.append(closingPoint)
segments.append(segment)
// Start a new segment
segment = [closingPoint]
}
} else {
// End of the line
segments.append(segment)
}
}
return segments
}
fileprivate class func intersectionWithLevel(_ p1: ChartPoint, and p2: ChartPoint, level: Float) -> ChartPoint {
let dy1 = level - p1.y
let dy2 = level - p2.y
return (x: (p2.x * dy1 - p1.x * dy2) / (dy1 - dy2), y: level)
}
}
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
Can I make an array of SK nodes of which one is selected randomly and brought from the top to bottom of the screen. For example say I have 25 or so different platforms that will be falling out of the sky on a portrait iPhone. I need it to randomly select one of the platforms from the array to start and then after a certain amount of time/ or pixel space randomly select another to continue the same action until reaching the bottom etc. Im new to swift but have a pretty decent understanding of it. I haven't been able to find out how to create an array of SKsprite nodes yet either. Could someone help with this?
So far the only way I've been able to get any sort of effect similar to what I've wanted is by placing each of the nodes off the screen and adding them to a dictionary and making them move like this
class ObstacleStatus {
var isMoving = false
var timeGapForNextRun = Int(0)
var currentInterval = Int(0)
init(isMoving: Bool, timeGapForNextRun: Int, currentInterval: Int) {
self.isMoving = isMoving
self.timeGapForNextRun = timeGapForNextRun
self.currentInterval = currentInterval
}
func shouldRunBlock() -> Bool {
return self.currentInterval > self.timeGapForNextRun
}
and
func moveBlocks(){
for(blocks, ObstacleStatus) in self.blockStatuses {
var thisBlock = self.childNodeWithName(blocks)
var thisBlock2 = self.childNodeWithName(blocks)
if ObstacleStatus.shouldRunBlock() {
ObstacleStatus.timeGapForNextRun = randomNum()
ObstacleStatus.currentInterval = 0
ObstacleStatus.isMoving = true
}
if ObstacleStatus.isMoving {
if thisBlock?.position.y > blockMaxY{
thisBlock?.position.y -= CGFloat(self.fallSpeed)
}else{
thisBlock?.position.y = self.origBlockPosistionY
ObstacleStatus.isMoving = false
}
}else{
ObstacleStatus.currentInterval++
}
}
}
using this for the random function
func randomNum() -> Int{
return randomInt(50, max: 300)
}
func randomInt(min: Int, max:Int) -> Int {
return min + Int(arc4random_uniform(UInt32(max - min + 1)))
}
All this has been doing for me is moving the pieces down at random timed intervals often overlapping them, But increasing the min or max of the random numbers doesn't really have an affect on the actual timing of the gaps. I need to be able to specify a distance or time gap.
One of many possible solutions is to create a falling action sequence which calls itself recursively until no more platform nodes are left. You can control the mean "gap time" and the range of its random variation. Here is a working example (assuming the iOS SpriteKit game template):
import SpriteKit
extension Double {
var cg: CGFloat { return CGFloat(self) }
}
extension Int {
var cg: CGFloat { return CGFloat(self) }
}
func randomInt(range: Range<Int>) -> Int {
return range.startIndex + Int(arc4random_uniform(UInt32(range.endIndex - range.startIndex)))
}
extension Array {
func randomElement() -> Element? {
switch self.count {
case 0: return nil
default: return self[randomInt(0..<self.count)]
}
}
func apply<Ignore>(f: (T) -> (Ignore)) {
for e in self { f(e) }
}
}
class GameScene: SKScene {
var screenWidth: CGFloat { return UIScreen.mainScreen().bounds.size.width }
var screenHeight: CGFloat { return UIScreen.mainScreen().bounds.size.height }
let PlatformName = "Platform"
let FallenPlatformName = "FallenPlatform"
func createRectangularNode(#x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> SKShapeNode {
let rect = CGRect(x: x, y: y, width: width, height: height)
let path = UIBezierPath(rect: rect)
let node = SKShapeNode(path: path.CGPath)
return node
}
func createPlatformNodes(numNodes: Int, atHeight: CGFloat) -> [SKShapeNode] {
var padding = 20.cg
let width = (screenWidth - padding) / numNodes.cg - padding
padding = (screenWidth - width * numNodes.cg) / (numNodes.cg + 1)
let height = width / 4
var nodes = [SKShapeNode]()
for x in stride(from: padding, to: numNodes.cg * (width + padding), by: width + padding) {
let node = createRectangularNode(x: x, y: atHeight, width: width, height: height)
node.fillColor = SKColor.blackColor()
node.name = PlatformName
nodes.append(node)
}
return nodes
}
func createFallingAction(#by: CGFloat, duration: NSTimeInterval, timeGap: NSTimeInterval, range: NSTimeInterval = 0) -> SKAction {
let gap = SKAction.waitForDuration(timeGap, withRange: range)
// let fall = SKAction.moveToY(toHeight, duration: duration) // moveToY appears to have a bug: behaves as moveBy
let fall = SKAction.moveByX(0, y: -by, duration: duration)
let next = SKAction.customActionWithDuration(0) { [unowned self]
node, time in
node.name = self.FallenPlatformName
self.fallNextNode()
}
return SKAction.sequence([gap, fall, next])
}
func fallNextNode() {
if let nextNode = self[PlatformName].randomElement() as? SKShapeNode {
let falling = createFallingAction(by: screenHeight * 0.7, duration: 1, timeGap: 2.5, range: 2) // mean time gap and random range
nextNode.runAction(falling)
} else {
self.children.apply { ($0 as? SKShapeNode)?.fillColor = SKColor.redColor() }
}
}
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor.whiteColor()
for platform in createPlatformNodes(7, atHeight: screenHeight * 0.8) {
self.addChild(platform)
}
fallNextNode()
}
}