Как удобнее анимировать в 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>


Ответы (0 шт):