text: 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);
      }
    }
  }
  