Reputation: 27
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.
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
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.
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
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
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