Home Reference Source

src/components/Skeleton.js

import Script from './Script';
import System from '../systems/System';
import Events from '../utils/Events';
import { angleDifference } from '../utils';

function findFrame(time, frames) {
  for (let i = 0, c = frames.length; i < c; ++i) {
    const a = frames[i];
    const b = frames[i + 1];
    if (!!b) {
      if (time >= a.time && time < b.time) {
        return a;
      }
    } else {
      if (time >= a.time) {
        return a;
      }
    }
  }

  return null;
}

function findFrames(time, frames) {
  if (frames.length < 2) {
    const frame = findFrame(time, frames);
    if (!!frame) {
      return { a: frame, b: frame };
    }

    return null;
  }

  for (let i = 1, c = frames.length; i < c; ++i) {
    const a = frames[i - 1];
    const b = frames[i];
    if (time >= a.time && time < b.time) {
      return { a, b };
    }
  }

  return null;
}

function sampleValue(sampler, from, to, timeFrom, timeTo, time) {
  const dt = timeTo - timeFrom;
  if (Math.abs(dt) > 0) {
    time = Math.max(timeFrom, Math.min(timeTo, time));
    return (to - from) * sampler((time - timeFrom) / dt) + from;
  }
  return to;
}

export default class Skeleton extends Script {

  static factory() {
    return new Skeleton();
  }

  static get propsTypes() {
    return {
      ...Script.propsTypes,
      asset: 'string_null',
      skin: 'string_null',
      animation: 'string_null',
      loop: 'boolean',
      speed: 'number',
      time: 'number',
      paused: 'boolean'
    };
  }

  get asset() {
    return this._asset;
  }

  set asset(value) {
    if (!value) {
      this._asset = null;
      this._data = null;
      return;
    }

    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._asset = value;
    this._slots = null;
    this._data = null;

    const { AssetSystem } = System.systems;
    if (!AssetSystem) {
      throw new Error('There is no registered AssetSystem!');
    }

    const asset = AssetSystem.get(`skeleton://${value}`);
    if (!asset) {
      throw new Error(`There is no asset loaded: ${value}`);
    }

    if (!!this.entity) {
      this.rebind();
    }

    this._data = asset.data;
    this._rebuildSkin = true;
  }

  get skin() {
    return this._skin;
  }

  set skin(value) {
    if (!value) {
      this._skin = null;
      return;
    }

    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._skin = value;
    this._rebuildSkin = true;
  }

  get animation() {
    return this._animation;
  }

  set animation(value) {
    if (!value) {
      this._animation = null;
      return;
    }

    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._animation = value;
  }

  get loop() {
    return this._loop;
  }

  set loop(value) {
    if (typeof value !== 'boolean') {
      throw new Error('`value` is not type of Boolean!');
    }

    this._loop = value;
  }

  get speed() {
    return this._speed;
  }

  set speed(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._speed = value;
  }

  get time() {
    return this._time;
  }

  set time(value) {
    if (typeof value !== 'number') {
      throw new Error('`value` is not type of Number!');
    }

    this._time = value;
  }

  get paused() {
    return this._paused;
  }

  set paused(value) {
    if (typeof value !== 'boolean') {
      throw new Error('`value` is not type of Boolean!');
    }

    this._paused = value;
  }

  get events() {
    return this._events;
  }

  constructor() {
    super();

    this._events = new Events();
    this._asset = null;
    this._skin = 'default';
    this._animation = null;
    this._speed = 1;
    this._loop = true;
    this._time = 0;
    this._paused = false;
    this._data = null;
    this._slots = null;
    this._bones = null;
    this._rebuildSkin = false;
  }

  dispose() {
    super.dispose();

    const { _events } = this;
    if (!!_events) {
      _events.dispose();
    }

    this._events = null;
    this._asset = null;
    this._animation = null;
    this._slots = null;
    this._data = null;
    this._slots = null;
    this._bones = null;
  }

  rebind() {
    const { entity, _data } = this;
    if (!entity || !_data) {
      return;
    }

    const slots = this._slots = {};
    const bones = this._bones = {};
    for (const key in _data.slots) {
      slots[key] = entity.findEntity(_data.slots[key]);
    }
    for (const key in _data.bones) {
      bones[key] = entity.findEntity(_data.bones[key]);
    }
  }

  applySkin(id = 'default') {
    if (typeof id !== 'string') {
      throw new Error('`id` is not type of String!');
    }

    const { _data, _slots } = this;
    if (!_data) {
      throw new Error('There is no skeleton data!');
    }
    if (!_slots) {
      throw new Error('There are no slots bindings!');
    }

    const { skins } = _data;
    if (!skins) {
      throw new Error('There are no skins in skeleton data!');
    }

    const skin = skins[id];
    if (!skin) {
      throw new Error(`Skin not found in skeleton data: ${id}`);
    }

    for (const key in _slots) {
      const slot = skin[key] || skins.default[key];
      if (!slot) {
        console.warn(`There is no slot in skeleton data: ${key}`);
        continue;
      }

      const attachmentName = Object.keys(slot)[0];
      if (!attachmentName) {
        console.warn(`There are no attachments in slot: ${key}`);
        continue;
      }

      this._applyAttachment(_slots[key], slot, attachmentName);
    }
  }

  applyAnimationFrame(id, time, loop = false) {
    const { _data, _slots, _bones, _skin } = this;
    if (!_data) {
      throw new Error('There is no skeleton data!');
    }

    const { animations, attachments, skins, pose } = _data;
    if (!animations) {
      throw new Error('There are no animations in skeleton data!');
    }
    if (!skins) {
      throw new Error('There are no skins in skeleton data!');
    }
    if (!pose) {
      throw new Error('There is no pose in skeleton data!');
    }

    const skin = skins[_skin];
    if (!skin) {
      throw new Error(`Skin not found in skeleton data: ${id}`);
    }

    const animation = animations[id];
    if (!animation) {
      throw new Error(`There is no animation: ${id}`);
    }

    const { bones, slots, duration } = animation;
    if (!!loop) {
      time = time % duration;
    }

    if (!!bones) {
      for (const boneKey in bones) {
        const bone = _bones[boneKey];
        if (!bone) {
          console.warn(`There is no bone: ${boneKey}`);
          continue;
        }

        const timelines = bones[boneKey];
        const { translate, rotate, scale } = timelines;
        const p = pose[boneKey];
        if (!!translate) {
          const frame = findFrames(time, translate);
          if (!!frame) {
            const { a, b } = frame;
            const { sample } = a;
            if (!!sample) {
              bone.setPosition(
                p.x + sampleValue(sample, a.x, b.x, a.time, b.time, time),
                p.y + sampleValue(sample, a.y, b.y, a.time, b.time, time)
              );
            }
          }
        }
        if (!!rotate) {
          const frame = findFrames(time, rotate);
          if (!!frame) {
            const { a, b } = frame;
            const { sample } = a;
            if (!!sample) {
              bone.setRotation(
                (p.rotation + sampleValue(
                  sample,
                  a.angle,
                  a.angle + angleDifference(b.angle, a.angle),
                  a.time,
                  b.time,
                  time
                )) * Math.PI / 180
              );
            }
          }
        }
        if (!!scale) {
          const frame = findFrames(time, scale);
          if (!!frame) {
            const { a, b } = frame;
            const { sample } = a;
            if (!!sample) {
              bone.setScale(
                p.scaleX + sampleValue(sample, a.x, b.x, a.time, b.time, time),
                p.scaleY + sampleValue(sample, a.y, b.y, a.time, b.time, time)
              );
            }
          }
        }
      }
    }

    if (!!slots) {
      for (const slotKey in slots) {
        const slot = _slots[slotKey];
        if (!slot) {
          console.warn(`There is no slot: ${slotKey}`);
          continue;
        }

        const timelines = slots[slotKey];
        const { attachment } = timelines;
        if (!!attachment) {
          const frame = findFrame(time, attachment);
          if (!!frame) {
            this._applyAttachment(
              slot,
              skin[slotKey] || skins.default[slotKey],
              frame.name
            );
          }
        }
      }
    }
  }

  playAnimation(id, looped = false) {
    this.time = 0;
    this.loop = looped;
    this.animation = id;
  }

  stopAnimation() {
    this.time = 0;
    this.animation = null;
  }

  onAttach() {
    super.onAttach();
    this.rebind();
  }

  onUpdate(deltaTime) {
    deltaTime *= 0.001;
    const { _rebuildSkin, _skin, _animation, _loop, _time } = this;

    if (_rebuildSkin) {
      this._rebuildSkin = false;
      this.applySkin(_skin);
    }

    if (!!_animation && !this._paused) {
      this.applyAnimationFrame(_animation, _time, _loop);
      this._time += deltaTime * this._speed;
      this._performAnimationEvents(_animation, _time, this._time, _loop);
    }
  }

  _performAnimationEvents(id, timePrev, time, loop) {
    const { _data, _events } = this;
    if (!_data) {
      throw new Error('There is no skeleton data!');
    }

    const { animations } = _data;
    if (!animations) {
      throw new Error('There are no animations in skeleton data!');
    }

    const animation = animations[id];
    if (!animation) {
      throw new Error(`There is no animation: ${id}`);
    }

    const { events, duration } = animation;
    const diff = time - timePrev;
    if (!!loop) {
      time = time % duration;
      timePrev = time - diff;
    }

    if (!!events) {
      for (const event of events) {
        if (event.time >= timePrev && event.time < time) {
          const { name } = event;
          if (name.startsWith('#')) {
            System.events.trigger(name.substr(1), event.int, event.float, event.string);
          } else {
            _events.trigger(name, event.int, event.float, event.string);
          }
        }
      }
    }
  }

  _applyAttachment(node, slot, id) {
    const atlasFrame = this._data.attachments[id];
    if (!atlasFrame) {
      console.warn(`There is no attachment in skeleton data: ${id}`);
      return;
    }

    const attachment = slot[id];
    if (!attachment) {
      console.warn(`There is no attachment in skeleton slots data: ${id}`);
      return;
    }

    const renderer = node.getComponent('AtlasSprite');
    renderer.atlas = atlasFrame;
    renderer.xOrigin = 0.5;
    renderer.yOrigin = 0.5;
    if ('width' in attachment) {
      renderer.width = attachment.width;
    }
    if ('height' in attachment) {
      renderer.height = attachment.height;
    }
    node.setPosition(attachment.x || 0, attachment.y || 0);
    node.setRotation((attachment.rotation || 0) * Math.PI / 180);
    node.setScale(attachment.scaleX || 1, attachment.scaleY || 1);
  }

}