ItsMeAgain
ItsMeAgain

Reputation: 595

How to Create Guitar-like string using Swift and SpriteKit

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

Answers (1)

Benzi
Benzi

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.

Sample guitar string

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

Related Questions