javascriptanimationhtml5-canvasrotationeasing

HTML5 Canvas - Rotate Canvas Using Easing Function


Problem:

Easing rotation through HTML5 canvas animation, stopping at the desired angle (or degree of rotation) at the end of the animation. Essentially, stretching this easing animation (smoothly) over the course of the entire animation's time-span; without being tied to an indeterministic fps value.

Takeaways:

The _rotate function operates properly for jumping to specific degree values in a single call.

The problem occurs when you want to smoothly _progress through this rotation, from 0 to _desiredAngle.

Attempting to obtain each semi-degree by simply multiplying _desiredAngle and the current _progress (0 - 1), of the animation, doesn't produce the right semi-degrees required for a smooth transition. This is my thinking ATM, and I may be looking at this incorrectly.

Having an _fpsEstimate value does narrow the window on that 90 degree translation. The caveat here is that this animation is now tied to that magic number, and depending on the load takin (for multiple animations) this value will change. To retain accuracy while under load, I'm attempting to find a more exacting method of achieving a similar result; without having to divide by step counters, or fps in flux.

Example - Canvas

let _canvas       = document.querySelector ( 'canvas' );

let _context      = _canvas.getContext ( '2d' );

let _coordinates  = { x: 100, y: 100 }

let _desiredAngle = 90;

let _time         = 3000;

let _fpsEstimate  = 65;

////    FUNCTIONS    ///////////////////////////////////////

/**
 * Timing function for easeInSine
 * @param       {number} timeFraction                       Timing fraction
 * @return      {number}                                    Progress
 */
function _timingFunction ( timeFraction )
{
    return 1 - Math.cos ( ( timeFraction * Math.PI ) / 2 );
}

/**
 * Rotate canvas
 * @param       {number} degree                             Degree to rotate
 * @param       {Object} coordinates                        X & Y coordinates
 */
function _rotate ( degree, coordinates )
{
    let [ _x, _y ] = [ coordinates.x, coordinates.y ];


    _context.save ( );

    _context.translate ( _x, _y );

    _context.rotate ( degree * Math.PI / 180 );

    _context.translate ( -_x, -_y );
}

/**
 * Animate object
 */
function _animate ( )
{
    let _start = performance.now ( );

    let _width  = 50;
        
    let _height = 50;


    requestAnimationFrame (

        function animate ( time )
        {
            _context.clearRect ( 0, 0, 500, 500 );


            let _timeFraction =  ( time - _start ) / _time;         // timeFraction goes from 0 to 1

            let _progress     = _timingFunction ( _timeFraction );  // calculate the current animation state

            let _rotateAmount = _progress * _desiredAngle / _fpsEstimate;          // calculate distance between current progress & desire angle
            
            
            _rotate ( _rotateAmount, _coordinates );

        
            _context.strokeRect ( _coordinates.x - ( _width / 2 ), _coordinates.y - ( _height / 2 ), _width, _height );


            ( _timeFraction < 1 )

                ? requestAnimationFrame ( animate )

                : console.log ( 'complete !' );
        }
    );
}

////    INIT    ////////////////////////////////////////////

_animate ( );
<canvas width='500' height='500'></canvas>

Example - CSS

This can be accomplished via CSS easily. However, I am looking for the same solution via in canvas.

.rotate   { animation: rotation 2s;            }
.easing   { animation-timing-function: easeIn; }
.infinite { animation-iteration-count: 1;      }

@keyframes rotation 
{
  from { transform: rotate(0deg);  }
  to   { transform: rotate(90deg); }
}
<img src="https://pngimg.com/uploads/square/square_PNG35.png" class="rotate linear infinite" width="75" height="75" />


Solution

  • Solution

    It appears as though I have found a solution, from this 8 year old post Precise rotation around world axis using Tween.

    The key seems simple... track the accumulated _rotation of each semi rotation (_rotate), tracking the distance between each angle; i.e. 0 - _angle.

    Note: This _rotation value should also be zeroed out at the beginning of each animation.

    Then, calculate how much you need to _rotate the animation, by multiplying _progress by the desired _angle subtracted by the present _rotation distance.

    let _rotate = _progress * ( _angle - _rotation );
    

    Working Solution

    let _canvas       = document.querySelector ( 'canvas' );
    
    let _context      = _canvas.getContext ( '2d' );
    
    let _coordinates  = { x: 100, y: 100 }
    
    let _angle        = 90;
    
    let _time         = 2000;
    
    let _rotation     = undefined;          // tracks n rotation(s) through each cycle
    
    ////    FUNCTIONS    ///////////////////////////////////////
    
    /**
     * Timing function for easeInSine
     * @param       {number} timeFraction                       Timing fraction
     * @return      {number}                                    Progress
     */
    function _timingFunction ( timeFraction )
    {
        return 1 - Math.cos ( ( timeFraction * Math.PI ) / 2 );
    }
    
    /**
     * Rotate canvas
     * @param       {number} degree                             Degree to rotate
     * @param       {Object} coordinates                        X & Y coordinates
     */
    function _rotateCanvas ( degree, coordinates )
    {
        let [ _x, _y ] = [ coordinates.x, coordinates.y ];
    
    
        _context.save ( );
    
        _context.translate ( _x, _y );
    
        _context.rotate ( degree * Math.PI / 180 );
    
        _context.translate ( -_x, -_y );
    }
    
    /**
     * Animate object
     */
    function _animate ( )
    {
        let _start = performance.now ( );
    
        let _width  = 50;
            
        let _height = 50;
    
            _rotation = 0;
        
        
        requestAnimationFrame (
    
            function animate ( time )
            {
                _context.clearRect ( 0, 0, 500, 500 );
    
    
                let _timeFraction =  ( time - _start ) / _time;         // timeFraction goes from 0 to 1
    
                let _progress     = _timingFunction ( _timeFraction );  // calculate the current animation state
    
                let _rotate       = _progress * ( _angle - _rotation ); // calculate distance between current progress & desire angle
                
                
                _rotateCanvas ( _rotate, _coordinates );
    
                
                _rotation += _rotate;       // <== sum rotational distance through each cycle
                
            
                _context.strokeRect ( _coordinates.x - ( _width / 2 ), _coordinates.y - ( _height / 2 ), _width, _height );
    
    
                ( _timeFraction < 1 )
    
                    ? requestAnimationFrame ( animate )
    
                    : console.log ( 'complete !' );
            }
        );
    }
    
    ////    INIT    ////////////////////////////////////////////
    
    _animate ( );
    <canvas width='500' height='500'></canvas>

    Final Thoughts

    This solution appears to maintain the easing gradient(s) when using different easing formulas, while accurately tying each animation's length (in _time); creating a smooth transition, while scaling each transition type with each animation loop.

    CSS Comparison

    .rotate   { animation: rotation 2s;            }
    .easing   { animation-timing-function: easeIn; }
    .infinite { animation-iteration-count: 1;      }
    
    @keyframes rotation 
    {
      from { transform: rotate(0deg);  }
      to   { transform: rotate(90deg); }
    }
    <img src="https://pngimg.com/uploads/square/square_PNG35.png" class="rotate linear infinite" width="75" height="75" />