7 Replies
      Latest reply: Jan 23, 2017 10:44 AM by Bobjt RSS
      squillop Level 1 Level 1 (0 points)

        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

        • Re: Is it possible to animate a path similar to how StrokeEnd works in SpriteKit ?
          Bobjt Apple Staff Apple Staff (60 points)

          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
                  }      
              }
          }
          
          
            • Re: Is it possible to animate a path similar to how StrokeEnd works in SpriteKit ?
              squillop Level 1 Level 1 (0 points)

              This is awesome, many thanks!!

              How exactly can I set the colour of the animation?  I've studied the code and I can see how to set the overall colour in the shader, by manipulating the

                      gl_FragColor

              value.

              But I can't see how to change this on the fly from withing GameScene??

              i figure something like .setValue on the  gl_FragColor attibute?  am i getting warm??

              again, many thanks !

                • Re: Is it possible to animate a path similar to how StrokeEnd works in SpriteKit ?
                  Bobjt Apple Staff Apple Staff (60 points)

                  No problem. Here's an example that colors the stroke with a gradient:

                   

                  Shader file "gradientStroke.fsh":

                   

                  void main()
                  {
                  
                      if ( u_path_length == 0.0 ) {
                    
                          gl_FragColor = vec4( 1.0, 0.0, 1.0, 1.0 ); // error as u_path_length should never be zero, draw magenta
                    
                      } else if ( v_path_distance / u_path_length <= u_current_percentage ) { // draw color up to the current stroke percentage
                          float c = v_path_distance / u_path_length; // c is a small number that becomes 1.0 toward the end of the stroke
                          float v = 1.0 - c; // v is 1.0 at the start and approaches 0 toward the end of the stroke.
                          // at any point in the stroke, v + c = 1.0
                  
                          vec4 s = u_color_start;
                          vec4 e = u_color_end;
                    
                          gl_FragColor = vec4( clamp( s.r*v + e.r*c, 0.0, 1.0 ), // clamp between 0.0-1.0 just in case
                                               clamp( s.g*v + e.g*c, 0.0, 1.0 ),
                                               clamp( s.b*v + e.b*c, 0.0, 1.0 ),
                                               clamp( s.a*v + e.a*c, 0.0, 1.0 ) );
                      } else { // draw nothing!
                          gl_FragColor = vec4( 0.0, 0.0, 0.0, 0.0 );
                      }
                  }
                  

                   

                  Scene subclass:

                   

                  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!
                  
                      // define the start and end colors here
                      var startColorUniform = SKUniform( name: "u_color_start", vectorFloat4: vector_float4( [1.0, 1.0, 0.0, 1.0] ) )
                      var endColorUniform = SKUniform( name: "u_color_end", vectorFloat4: vector_float4( [1.0, 0.0, 0.0, 1.0] ) )
                  
                      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 ) {
                          // percentage is the only variable uniform since start and end color don't change
                          strokeLengthUniform = SKUniform( name: "u_current_percentage", float: 0.0 )
                      
                          // pass all uniforms to the shader
                          let uniforms: [SKUniform] = [strokeLengthUniform, startColorUniform, endColorUniform]
                          strokeShader = shaderWithFilename( "gradientStroke", fileExtension: "fsh", uniforms: uniforms )
                          strokeLengthFloat = 0.0
                          let cameraNode = SKCameraNode()
                          self.camera = cameraNode
                          let path = CGMutablePath()
                          path.addRoundedRect(in: CGRect( x: 0, y: 0, width: 200, height: 150 ), cornerWidth: 35, cornerHeight: 35, transform: CGAffineTransform.identity )
                          let strokeWidth = 17.0 * strokeSizeFactor
                      
                          let shapeNode = SKShapeNode( path: path )
                          shapeNode.lineWidth = strokeWidth
                          shapeNode.lineCap = .round
                          addChild( shapeNode )
                          shapeNode.addChild( cameraNode )
                          shapeNode.strokeShader = strokeShader
                      
                          let rect = shapeNode.calculateAccumulatedFrame()
                          cameraNode.position = CGPoint( x: rect.size.width/2.0, y: rect.size.height/2.0 )
                      
                      }
                  
                      override func update(_ currentTime: TimeInterval) {
                          strokeLengthFloat += 0.01
                          if strokeLengthFloat > 1.0 {
                              strokeLengthFloat = 0.0
                          }
                      
                      }
                  }
                  
                • Re: Is it possible to animate a path similar to how StrokeEnd works in SpriteKit ?
                  zappy Level 1 Level 1 (0 points)

                  Hi, sorry to bump this topic but I think I have run into the problem noted in the comment:

                  // prior to a fix in iOS 10.2, bug #27989113  "SKShader/SKShapeNode: u_path_length is not set unless shouldUseLocalStrokeBuffers() is true" 
                  

                   

                  Things work well on a iOS 10.2 device, but not on a iOS 9.3.5 device. Indeed it seems like the problem is with u_path_length. I am only using a simple circle with the skshapenode - it seems like u_path_length is not correctly calculated by SpriteKit (or some other part) which makes u_path_length be 0 or just not set... it totally breaks iOS 9.3.5 device, screen goes black. I haven't figured out a workaround. Any info?