Reputation: 595
I want to create a guitar-like string effect, similar to the one in Les Paul's 96th Birthday Google Doodle. I would like to acknowledge that I've seen a few similar questions that approach this using Javascript and CSS. I am interested in using purely Swift and SpriteKit. I would appreciate any advice on how I can achieve this.
Upvotes: 2
Views: 245
Reputation: 2459
You can model your guitar string using a SKShapeNode
. In the sample code below, I have created a scene that responds to touch events that forwards that to its child string node. The stringiness is modelled using a cosine wave function and dampening of amplitude applied.
For the sample, I have created a simple path by adding lines between the string's starting, plucked and ending positions. You can create a smoother line using spline curves (one way to is to construct a Catmull Rom spline that passes along those points, and convert that to a Bezier spline which can be rendered directly with SKShapeNode
).
I also control the maximum amount you can pluck a string when computing the amplitude. In a real string, the amount of flex towards the ends of the string will be much lesser than that at the middle of the string. You can model this using some ramping function applied to the amplitude.
You can control the frequency of the string's vibrations using the parameters passed in to the cos
function in update
. Note that I have hardcoded a time delta for this example.
Tweaking the above parameters will allow you to nail your plucking effect to your desired configuration.
import UIKit
import SpriteKit
import PlaygroundSupport
let view = SKView(frame: .init(origin: .zero, size: .init(width: 300, height: 300)))
PlaygroundPage.current.liveView = view
class GuitarString : SKShapeNode {
let start: CGPoint
let end: CGPoint
var intermediate: CGPoint = .zero
var amplitude: CGFloat = 0.0
required init(start: CGPoint, end: CGPoint) {
self.start = start
self.end = end
self.intermediate = start
let path = CGMutablePath()
path.move(to: start)
path.addLine(to: end)
super.init()
self.path = path
self.strokeColor = .red
self.lineWidth = 2.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func pluckStringBegan(at point: CGPoint) {
intermediate = point
amplitude = min(abs(point.y - start.y), 100) * (point.y - start.y < 0 ? -1 : 1)
t = 0
}
var t:CGFloat = 0.0
func update(dt: CGFloat) {
amplitude *= 0.95
t += dt
let a = amplitude * cos(t * 10)
// to get a smoother line, you might
// consider creating a catmull rom spline
// that passes through start, f and end
// and convert that to a bezier curve
let path = CGMutablePath()
path.move(to: start)
let f = CGPoint(x: intermediate.x, y: a + start.y)
path.addLine(to: f)
path.addLine(to: end)
self.path = path
}
}
class GuitarScene : SKScene {
override init(size: CGSize) {
super.init(size: size)
self.isUserInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
let p = t.location(in: self.children[0])
(self.children[0] as! GuitarString).pluckStringBegan(at: p)
}
override func update(_ currentTime: TimeInterval) {
(self.children[0] as! GuitarString).update(dt: 1/20)
}
}
let scene = GuitarScene(size: view.frame.size)
scene.scaleMode = .aspectFit
view.presentScene(scene)
let string = GuitarString(start: .init(x: 0, y: 150), end: .init(x: 300, y: 150))
string.position = .zero
string.zPosition = 100
scene.addChild(string)
Upvotes: 3