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
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" />