0

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

3
  • please clarify the problem. what do you mean by "missing gaps"? Commented Feb 13 at 14:10
  • Sure, I'll update the code and the screenshot soon so it's easier to see. The red dashes are what I'm animating, and they are skipping on the straight sections of the spline. Commented Feb 13 at 17:23
  • Updated the code and the screenshot, hopefully it's easier to see what I mean. Also the order of animation the dashes seem to begin in the middle, go left, then animate to the bottom of the spline path. Commented Feb 13 at 21:11

2 Answers 2

2

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)
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.