I've been googling this for a while now and trying out various things from StackOverflow etc.
I want to draw paths, then animate a line that 'grows' over that path.
I'm coming to the conclusion it is NOT possible to animate a shape or bezierpath in an SKScene.
Using the CABasicAnimation stuff would work, but I can't figure out how to add layers or views...
Using SKActions isn't really what I want to do... i can only scale etc. Not really grow as i need.
It feels like there is a massive gap in what SpriteKit can do here. Has anyone found this? Anyone found a way around it ?
I'm really stuck here.. The whole concept of my game idea relys on this feature and I can't figure out a way to even start !!! 😟
appreciate any ideas or pointers.
thanks
Squillop, yes you can animate SKShapeNode's path by supplying a custom strokeShader that outputs based on a few of SKShader's properties, v_path_distance and u_path_length. Note that within the shader supplied below "u_current_percentage" is added by us and refers to the current point within the path we want stroked up to. By that, the scene determines the pace of the animated stroking. Also note that strokeShader being a fragment shader, outputs an RGB at every step, it allows you to control the color as you go, allowing the stroke to be a gradient color for example.
The shader is added as a file to the Xcode project "animateStroke.fsh":
void main()
{
if ( u_path_length == 0.0 ) {
gl_FragColor = vec4( 0.0, 0.0, 1.0, 1.0 );
} else if ( v_path_distance / u_path_length <= u_current_percentage ) {
gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );
} else {
gl_FragColor = vec4( 0.0, 0.0, 0.0, 0.0 );
}
}
And the sample SKScene subclass using it:
import SpriteKit
import GameplayKit
func shaderWithFilename( _ filename: String?, fileExtension: String?, uniforms: [SKUniform] ) -> SKShader {
let path = Bundle.main.path( forResource: filename, ofType: fileExtension )
let source = try! NSString( contentsOfFile: path!, encoding: String.Encoding.utf8.rawValue )
let shader = SKShader( source: source as String, uniforms: uniforms )
return shader
}
class GameScene: SKScene {
let strokeSizeFactor = CGFloat( 2.0 )
var strokeShader: SKShader!
var strokeLengthUniform: SKUniform!
var _strokeLengthFloat: Float = 0.0
var strokeLengthKey: String!
var strokeLengthFloat: Float {
get {
return _strokeLengthFloat
}
set( newStrokeLengthFloat ) {
_strokeLengthFloat = newStrokeLengthFloat
strokeLengthUniform.floatValue = newStrokeLengthFloat
}
}
override func didMove(to view: SKView) {
strokeLengthKey = "u_current_percentage"
strokeLengthUniform = SKUniform( name: strokeLengthKey, float: 0.0 )
let uniforms: [SKUniform] = [strokeLengthUniform]
strokeShader = shaderWithFilename( "animateStroke", fileExtension: "fsh", uniforms: uniforms )
strokeLengthFloat = 0.0
let cameraNode = SKCameraNode()
self.camera = cameraNode
let strokeHeight = CGFloat( 200 ) * strokeSizeFactor
let path1 = CGMutablePath()
path1.move( to: CGPoint.zero )
path1.addLine( to: CGPoint( x: 0, y: strokeHeight ) )
// prior to a fix in iOS 10.2, bug #27989113 "SKShader/SKShapeNode: u_path_length is not set unless shouldUseLocalStrokeBuffers() is true"
// add a few points to work around this bug in iOS 10-10.1 ->
// for i in 0...15 {
// path1.addLine( to: CGPoint( x: 0, y: strokeHeight + CGFloat( 0.001 ) * CGFloat( i ) ) )
// }
path1.closeSubpath()
let strokeWidth = 17.0 * strokeSizeFactor
let path2 = CGMutablePath()
path2.move( to: CGPoint.zero )
path2.addLine( to: CGPoint( x: 0, y: strokeHeight ) )
path2.closeSubpath()
let backgroundShapeNode = SKShapeNode( path: path2 )
backgroundShapeNode.lineWidth = strokeWidth
backgroundShapeNode.zPosition = 5.0
backgroundShapeNode.lineCap = .round
backgroundShapeNode.strokeColor = SKColor.darkGray
addChild( backgroundShapeNode )
let shapeNode = SKShapeNode( path: path1 )
shapeNode.lineWidth = strokeWidth
shapeNode.lineCap = .round
backgroundShapeNode.addChild( shapeNode )
shapeNode.addChild( cameraNode )
shapeNode.strokeShader = strokeShader
backgroundShapeNode.calculateAccumulatedFrame()
cameraNode.position = CGPoint( x: backgroundShapeNode.frame.size.width/2.0, y: backgroundShapeNode.frame.size.height/2.0 )
}
override func update(_ currentTime: TimeInterval) {
// the increment chosen determines how fast the path is stroked. Note this maps to "u_current_percentage" within animateStroke.fsh
strokeLengthFloat += 0.01
if strokeLengthFloat > 1.0 {
strokeLengthFloat = 0.0
}
}
}