Draw an animated path in SpriteKit

I am trying to animate a line drawing on a path using SpriteKit. I implemented a working solution using SKActions and a separate implementation using CABasicAnimations. The SKAction solution is not very elegant; it creates and starts a new path on each iteration of the SKAction.repeatAction call (action: count :) with each new path, a little more complete than the previous one.

func start() { var i:Double = 1.0 let spinAction:SKAction = SKAction.repeatAction(SKAction.sequence([ SKAction.runBlock({ self.drawCirclePercent(i * 0.01) if (++i > 100.0) { i = 1.0 } }), SKAction.waitForDuration(0.01) ]), count: 100) runAction(spinAction) } func drawCirclePercent(percent:Double) { UIGraphicsBeginImageContext(self.size) let ctx:CGContextRef = UIGraphicsGetCurrentContext() CGContextSaveGState(ctx) CGContextSetLineWidth(ctx, lineWidth) CGContextSetRGBStrokeColor(ctx, 1.0, 1.0, 1.0, 1.0) CGContextAddArc(ctx, CGFloat(self.size.width/2.0), CGFloat(self.size.height/2.0), radius/2.0, CGFloat(M_PI * 1.5), CGFloat(M_PI * (1.5 + 2.0 * percent)), 0) CGContextStrokePath(ctx) let textureImage:UIImage = UIGraphicsGetImageFromCurrentImageContext() texture = SKTexture(image: textureImage) UIGraphicsEndImageContext() } 

While the code above works, it certainly is not pretty or efficient, and you should probably not use SKActions. The CABasicAnimation solution is much more elegant and much more efficient.

 let path:CGMutablePathRef = CGPathCreateMutable() CGPathAddArc(path, nil, 0, 0, 40.0, CGFloat(M_PI_2 * 3.0), CGFloat(M_PI_2 * 7.0), false) let pathLayer:CAShapeLayer = CAShapeLayer() pathLayer.frame = CGRectMake(100, 100, 80.0, 80.0) pathLayer.path = path pathLayer.strokeColor = SKColor.whiteColor().CGColor pathLayer.fillColor = nil pathLayer.lineWidth = 3.0 self.view.layer.addSublayer(pathLayer) let pathAnimation:CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd") pathAnimation.duration = 2.0 pathAnimation.fromValue = 0.0 pathAnimation.toValue = 1.0 pathLayer.addAnimation(pathAnimation, forKey: "strokeEndAnimation") 

My problem is that I would really prefer all the code to be contained in a subclass of SKSpriteNode (so much so that if the two solutions above are the only options I have, I would go with the first one). Is there a way by which I can improve the SKAction implementation to more closely resemble the CoreAnimation implementation, without having to enable CoreAnimation? Essentially, I wonder if SpriteKit has functionality that I don't know about that can be used to improve the first implementation.

+6
source share
2 answers

You can animate the SKShapeNode path by providing a custom parameter, strokeShader, which outputs based on several SKahader , v_path_distance and u_path_length . This hinted above, so the complete code for this. Inside the shader, u_current_percentage added by us and refers to the current point inside the path to which we want to stroke. In this case, the scene determines the pace of the animated stroking. Also note that strokeShader is a fragmented shader, displays RGB at every step, it allows you to control the color along the path, making it possible, for example, the color of the gradient.

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 an example of a subclass of SKScene 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 } } } 
+6
source

Based on Bobjt's answer , my shader that uses glowWidth from SKShapeNode ... So when I use this, I can use shapeNode.glowWidth = 5.0; etc.

 void main() { vec4 def = SKDefaultShading(); if ( u_path_length == 0.0 ) { gl_FragColor = vec4( 0xff / 255.0f, 0 / 255.0f, 0 / 255.0f, 1.0f ); } else if ( v_path_distance / u_path_length <= u_current_percentage ) { gl_FragColor = vec4( //0x1e / 255.0f, 0xb8 / 255.0f, 0xca / 255.0f, <-- original boring color :-P 0x1e / 255.0f * def.z, 0xb8 / 255.0f * def.z, 0xca / 255.0f * def.z, def.z ); } else { gl_FragColor = vec4( 0.0f, 0.0f, 0.0f, 0.0f ); } } 
+4
source

Source: https://habr.com/ru/post/973585/


All Articles