spritejs/spritejs

View on GitHub
src/animation/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
F
7%
import {Animator, Effects} from 'sprite-animator';
import rgba from 'color-rgba';
import {requestAnimationFrame, cancelAnimationFrame} from '../utils/animation-frame';
import {sizeToPixel} from '../utils/attribute_value';

function parseValue(v) {
  if(typeof v === 'string') {
    v = v.trim();
    if(/^[0-9.]+%$/.test(v)) return parseFloat(v) / 100;
    if(/^([\d.]+)(px|pt|pc|in|cm|mm|em|ex|rem|q|vw|vh|vmax|vmin)$/.test(v)) return sizeToPixel(v);
    // const c = rgba(v);
    // return c.length > 0 ? c : v;
  }
  return v;
}

function colorEffect(from, to, p, s, e) {
  if(typeof from === 'string') from = rgba(from);
  if(typeof to === 'string') to = rgba(to);
  return Effects.default(from, to, p, s, e);
}

function stringEffect(from, to, p, s, e) {
  return Effects.default(from, to, p, s, e);
}

Effects.default = function (from, to, p, s, e) {
  if(Array.isArray(from) && Array.isArray(to)) {
    return from.map((v, i) => {
      return v + (p - s) / (e - s) * (to[i] - v);
    });
  }

  if(typeof from === 'number' && typeof to === 'number') {
    return from + (p - s) / (e - s) * (to - from);
  }

  if(p - s > e - p) {
    return to;
  }
  return from;
};

Effects.fillColor = colorEffect;
Effects.strokeColor = colorEffect;
Effects.bgcolor = colorEffect;
Effects.borderColor = colorEffect;
Effects.text = stringEffect;

export default class Animation extends Animator {
  constructor(sprite, frames, timing) {
    const initAttrs = sprite.attr();

    Object.entries(initAttrs).forEach(([key, value]) => {
      initAttrs[key] = Effects[key] ? value : parseValue(value);
    });

    frames = frames.map(({...frame}) => {
      const ret = {};
      Object.entries(frame).forEach(([key, value]) => {
        ret[key] = Effects[key] ? value : parseValue(value);
      });
      return ret;
    });

    super(initAttrs, frames, timing);
    this.target = sprite;
    this.setter = function (frame, target) { target.attr(frame) };
  }

  get playState() {
    if(!this.target.parent) {
      return 'idle';
    }
    return super.playState;
  }

  get finished() {
    // set last frame when finished
    // because while the web page is not focused
    // requestAnimationFrame will not trigger while deferTime of
    // the animator is still running
    return super.finished.then(() => {
      const that = this;
      return new Promise((resolve) => {
        function update() {
          that.setter(that.frame, that.target);
          const playState = that.playState;
          if(playState === 'finished' || playState === 'idle') {
            cancelAnimationFrame(that.requestId);
            resolve();
          } else {
            requestAnimationFrame(update);
          }
        }
        update();
      });
    });
  }

  finish() { // finish should change attrs synchronously
    super.finish();
    cancelAnimationFrame(this.requestId);
    this.setter(this.frame, this.target);
  }

  play() {
    if(!this.target.parent || this.playState === 'running') {
      return;
    }

    super.play();

    this.setter(this.frame, this.target);

    const that = this;
    this.ready.then(() => {
      that.setter(that.frame, that.target);
      that.requestId = requestAnimationFrame(function update() {
        const target = that.target;
        if(typeof document !== 'undefined'
          && document.documentElement
          && document.documentElement.contains
          && target.layer
          && target.layer.canvas
          && !document.documentElement.contains(target.layer.canvas)) {
          // if dom element has been removed stop animation.
          // it usually occurs in single page applications.
          that.cancel();
          return;
        }
        const playState = that.playState;
        that.setter(that.frame, that.target);
        if(playState === 'idle') return;
        if(playState === 'running') {
          that.requestId = requestAnimationFrame(update);
        } else if(playState === 'paused' || playState === 'pending' && that.timeline.currentTime < 0) {
          // playbackRate < 0 will cause playState reset to pending...
          that.ready.then(() => {
            that.setter(that.frame, that.target);
            that.requestId = requestAnimationFrame(update);
          });
        }
      });
    });
  }

  cancel(preserveState = false) {
    cancelAnimationFrame(this.requestId);
    if(preserveState) {
      this.setter(this.frame, this.target);
      super.cancel();
    } else {
      super.cancel();
      this.setter(this.frame, this.target);
    }
  }
}