Как удобнее анимировать в canvas?
Всё кажется простым в учебных примерах: Один квадрат или круг, бери-да меняй координаты.
А когда это игровой персонаж с руками-ногами и прочими деталями, оказывается достаточно муторно возиться с координатами, и при этом не путаться. Хорошо бы уметь оформлять анимацию в более читаемом виде, из серии "подними руку за секунду, ударь за 0.5 секунды".
Какие есть варианты? (реализация может быть сколь угодно некрасивым и запутанным, главное, чтобы в результате была возможность удобно и быстро писать анимацию)
На данный момент остановился на этом (код вырван из контекста и сокращен насколько возможно):
let FPS = 30; /* только 30 или 60 */
let hand = { /* некий вложенный объект, описывающий руку персонажа */
rotate: 70,
};
let ANIMATE = {
LIST: {},
/* Хранит список функций, которые вызываются перед каждым requestAnimationFrame()
и меняют координаты в заранее переданных объектах через this.add() */
add: function(obj) {
this.LIST[obj.name] = getClosureFn( obj );
},
delete: function(obj) {
delete this.LIST[obj.name];
}
};
function getClosureFn(obj) {
/*
obj → {
name: "test",
change: [
{root: hand, prop: "rotate", to: -90, ms: 1000},
{sleep: 1000},
{root: hand, prop: "rotate", to: 70, ms: 500},
],
}
change: в указанном объекте `root` менять свойство `prop` до указанного значения за `ms`
Как только значение пересекает точку `to`, переключиться на следующий объект.
*/
let index = 0;
let curr = obj.change[index];
let step = getStep(curr);
return function _animate() {
if( curr._sleeping ) return;
if( curr.sleep ) {
curr._sleeping = true;
setTimeout(function() {
curr._sleeping = false;
switch_next_obj();
}, curr.sleep);
return;
}
let newValue = curr.root[ curr.prop ] + step;
let reached_end;
if( (step < 0 && newValue <= curr.to) || (step > 0 && newValue >= curr.to) ) {
newValue = curr.to;
reached_end = true;
}
curr.root[ curr.prop ] = newValue;
if( reached_end ) return switch_next_obj();
/***/
function switch_next_obj() {
index++;
curr = obj.change[index];
if( !curr ) return ANIMATE.delete(obj);
step = curr.sleep ? null : getStep(curr);
}
}
/***/
function getStep(obj) {
/* obj → {root: hand, prop: "rotate", to: -90, ms: 1000} */
let from = obj.root[ obj.prop ];
return (obj.to - from) / (FPS * obj.ms / 1000 );
}
}
/***/
document.getElementById('attack').addEventListener('click', function() {
ANIMATE.add({
name: "test",
change: [
{root: hand, prop: "rotate", to: -120, ms: 500},
{sleep: 500},
{root: hand, prop: "rotate", to: 70, ms: 250},
],
});
});
function call_before_each_frame() {
Object.values( ANIMATE.LIST ).forEach(fn => fn());
}
/***/
run_demo();
/* Ненужные детали */
function run_demo() {
animation_process();
function animation_process() {
let MISS_FRAME = false;
if( FPS !== 30 ) FPS = 60;
frame_loop();
function frame_loop() {
if( FPS === 30 ) MISS_FRAME = !MISS_FRAME;
if( MISS_FRAME ) return requestAnimationFrame(frame_loop);
call_before_each_frame();
draw();
requestAnimationFrame(frame_loop);
}
}
/***/
function draw() {
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 500, 250);
ctx.fillStyle = "#123";
path(
rotateMatrix(
[
[100, 100],
[200, 100],
[200, 120],
[100, 120],
],
[100, 110],
hand.rotate
)
);
function rotateMatrix(matrix, centerPoint, angle) {
/*** Берет массив точек, крутит вокруг указанной точки ***/
if (angle === 0) return matrix;
let [cx, cy] = centerPoint;
let sin = Math.sin(angle * Math.PI / 180);
let cos = Math.cos(angle * Math.PI / 180);
for (let i = 0, len = matrix.length; i < len; i++) {
let px = matrix[i][0];
let py = matrix[i][1];
matrix[i][0] = cos * (px - cx) - sin * (py - cy) + cx;
matrix[i][1] = sin * (px - cx) + cos * (py - cy) + cy;
}
return matrix;
}
function path(matrix) {
ctx.beginPath();
for (let i = 0; i < matrix.length; i++) {
ctx.lineTo(matrix[i][0], matrix[i][1]);
}
ctx.closePath();
ctx.fill();
}
}
}
canvas {
border: 1px solid red;
}
<button id="attack">Удар</button><br>
<canvas id="canvas" width="500" height="250"></canvas>