KleinerWarden
KleinerWarden

Reputation: 27

Animating dashes sequentially over a spline path in SpriteKit: How can I fix the gaps that appear on the straight sections?

I've created a SpriteNode subclass which is instantiated in the scene as a spline, and then I call the function .animateDottedPath().
I expect the dashes to animate over 5 seconds along the original path.

The animation almost works, however in the screenshot, there are these gaps missing when I animate using the copy of the original path.

I've looked at the debug output, and the path components returned make me suspicious. My guess is that something with the path copied using fromSplinePath.copy(dashingWithPhase: 100.0, lengths: [10]) doesn't translate well when I call self.dottedPathCopy.addPath(self.dottedPathComponents[self.dashIndex]) inside the _animatePath action.

Apologies for messy code, no need to suggest rewrites, but I would greatly appreciate any answer to offer some insight as to why there are these large gaps in the mutable path.

enter image description here

import SpriteKit

/// First, initialize a spline with node = SKShapeNode(splinePoints: &points, count: points.count)
// then create a dashed path for animation with DashedSplinePath(fromSplinePath: node.path!)
// the animation uses dashed paths from a copy of the original spline and adds them sequentially
class DashedSplinePath: SKShapeNode {
    var dottedPathCopy: CGMutablePath!
    var dottedPathComponents: [CGPath] = []
    
    var drawDuration: TimeInterval = 5
    var dashIndex = 0
    
    private var _animatePath: SKAction {
        return SKAction.customAction(withDuration: drawDuration, actionBlock: { (node, timeEl) in
            
            let currentPathComponentIndex = Int( Float(self.dottedPathComponents.count) * Float(timeEl / self.drawDuration) - 1 )
            if(self.dashIndex < currentPathComponentIndex) {
                
                self.dottedPathCopy.addPath(self.dottedPathComponents[self.dashIndex])
                print(self.dottedPathComponents[self.dashIndex])
                self.path = self.dottedPathCopy
                self.dashIndex += 1
                print(self.dashIndex)
            }
        })
    }
    
    func animateDottedPath() {
        self.dashIndex = 0
        self.dottedPathComponents = self.path!.componentsSeparated()
        
        self.dottedPathCopy = dottedPathComponents.first!.mutableCopy()
        self.alpha = 1.0
        self.zPosition = 0.0
        
        self.run(_animatePath)
    }
    
    
    init(fromSplinePath: CGPath) {
        super.init()
        self.path = fromSplinePath.copy(dashingWithPhase: 100.0, lengths: [10])
        
        self.zPosition = -1
        self.glowWidth = 0
        self.strokeColor = NSColor(red: 1.0, green: 0.3, blue: 0.3, alpha: 1.0)
        self.lineWidth = 10
        self.alpha = 1.0
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Here's an existing stack overflow article I used to understand how the dashed/dotted path works from the original spline: Drawing dashed line in Sprite Kit using SKShapeNode

Upvotes: 0

Views: 55

Answers (2)

Fault
Fault

Reputation: 1339

you can achieve this effect using a shader. in fact SKShapeNode path lengths are easily animatable due to the v_path_distance and u_path_length shader inputs documented here.

enter image description here

class GameScene: SKScene {
    var dottedLine:SKShapeNode?
    
    override func didMove(to view: SKView) {
        
        //a SKShapeNode containing a bezier path
        let startPoint = CGPoint(x: -200, y: 0)
        let control = CGPoint(x: 0, y: 300)
        let endPoint = CGPoint(x: 200, y: 150)
        
//multiplatform beziers ftw
#if os(macOS)
        let bezierPath = NSBezierPath()
        bezierPath.move(to: startPoint)
        bezierPath.curve(to: endPoint, controlPoint: control)
#elseif os(iOS)
        let bezierPath = UIBezierPath()
        bezierPath.move(to: startPoint)
        bezierPath.addQuadCurve(to: endPoint, controlPoint: control)
#endif
        
        let dottedPath = bezierPath.cgPath.copy(dashingWithPhase: 1, lengths: [10])
        dottedLine = SKShapeNode(path: dottedPath)
        dottedLine?.lineWidth = 5
        dottedLine?.strokeColor = .white
        self.addChild(dottedLine ?? SKNode())

        //shader code
        let shader_lerp_path_distance:SKShader = SKShader(source: """
//v_path_distance and u_path_length defined at
//https://developer.apple.com/documentation/spritekit/creating-a-custom-fragment-shader
void main(){
    //draw based on an animated value (u_lerp) in range 0-1
    if (v_path_distance < (u_path_length * u_lerp)) {
        gl_FragColor = texture2D(u_texture, v_tex_coord); //sample texture and draw fragment 

    } else {
        gl_FragColor = 0; //else don't draw
    }
}
""")
        
        //set up shader uniform
        let u_lerp = SKUniform(name: "u_lerp", float:0)
        shader_lerp_path_distance.uniforms = [ u_lerp ]
        dottedLine?.strokeShader = shader_lerp_path_distance
        
        //animate a value from 0-1 and update the shader uniform
        let DURATION = 3.0
        func lerp(a:CGFloat, b:CGFloat, fraction:CGFloat) -> CGFloat {
            return (b-a) * fraction + a
        }
        let animation = SKAction.customAction(withDuration: DURATION) { (node : SKNode!, elapsedTime : CGFloat) -> Void in
            let fraction = CGFloat(elapsedTime / CGFloat(DURATION))
            let i = lerp(a:0, b:1, fraction:fraction)
            u_lerp.floatValue = Float(i)
        }
        dottedLine?.run(animation)
    }
}

Upvotes: 2

clns
clns

Reputation: 2324

Your problem seems to be related to how Core Graphics' componentsSeparated(using:) works with a dashed path. Separating it into components and adding them back will not result in the same path.

You can easily see this behavior when doing this in your init:

// dashed path
let dottedPath = fromSplinePath.copy(dashingWithPhase: 0.0, lengths: [10])

// components separated and added to new path (NOT THE SAME AS ORIGINAL)
let dottedPathComponents = dottedPath.componentsSeparated()
let dottedPathCopy = dottedPathComponents.first!.mutableCopy()!
dottedPathComponents.dropFirst().forEach { dottedPathCopy.addPath($0) }

self.path = dottedPathCopy

dashed-cgpath-separated-issue

Printing the two paths dottedPath and dottedPathCopy will result in different outputs.

Since componentsSeparated(using:) does not have any documentation, it is not clear how it works.

You will likely have to find a different way to achieve what you want.

Upvotes: 1

Related Questions