spritejs/spritejs

View on GitHub
src/node/node.js

Summary

Maintainability
D
2 days
Test Coverage
F
35%
import {mat2d} from 'gl-matrix';
import Attr from '../attribute/node';
import Animation from '../animation';
import ownerDocument from '../document';
import SpriteEvent from '../event/event';
import {parseFilterString, applyFilters} from '../utils/filter';
import applyRenderEvent from '../utils/render_event';

const changedAttrs = Symbol.for('spritejs_changedAttrs');
const attributes = Symbol.for('spritejs_attributes');

const _resolution = Symbol('resolution');
const _animations = Symbol('animations');

const _eventListeners = Symbol('eventListeners');
const _captureEventListeners = Symbol('captureEventListeners');
const _filters = Symbol('filters');
const _display = Symbol('display');

const _program = Symbol('program');
const _shaderAttrs = Symbol('shaderAttrs');
const _uniforms = Symbol('uniforms');

export default class Node {
  static Attr = Attr;

  constructor(attrs = {}) {
    this.attributes = new this.constructor.Attr(this);
    this[_resolution] = {width: 300, height: 150};
    Object.assign(this.attributes, attrs);
    // if(Object.seal) {
    //   Object.seal(this.attributes);
    // }
    this[_animations] = new Set();
    this[_eventListeners] = {};
    this[_captureEventListeners] = {};
  }

  get ancestors() {
    let parent = this.parent;
    const ret = [];
    while(parent) {
      ret.push(parent);
      parent = parent.parent;
    }
    return ret;
  }

  get animations() {
    return this[_animations];
  }

  get filters() {
    return this[_filters] || (this.parent && this.parent.filters);
  }

  get isVisible() {
    return false;
  }

  get layer() {
    if(this.parent) return this.parent.layer;
    return null;
  }

  get localMatrix() {
    const m = this.transformMatrix;
    const {x, y} = this.attributes;
    m[4] += x;
    m[5] += y;
    return m;
  }

  get opacity() {
    let opacity = this.attributes.opacity;
    if(this.parent && this.parent.opacity != null) {
      opacity *= this.parent.opacity;
    }
    return opacity;
  }

  get parentNode() {
    return this.parent;
  }

  get nextSibling() {
    return this.getNodeNearBy(1);
  }

  get previousSibling() {
    return this.getNodeNearBy(-1);
  }

  get program() {
    return this[_program];
  }

  /* get parent defined by connect method */

  get renderer() {
    if(this.parent) return this.parent.renderer;
    return null;
  }

  get renderMatrix() {
    if(this.__cacheRenderMatrix) return this.__cacheRenderMatrix;
    let m = this.localMatrix;
    const parent = this.parent;
    if(parent) {
      const renderMatrix = parent.__cacheRenderMatrix || parent.renderMatrix;
      if(renderMatrix) {
        m = mat2d(renderMatrix) * mat2d(m);
      }
    }
    return m;
  }

  get worldScaling() {
    const m = this.renderMatrix;
    return [Math.hypot(m[0], m[1]), Math.hypot(m[2], m[3])];
  }

  get worldRotation() {
    const m = this.renderMatrix;
    return Math.atan2(m[1], m[3]);
  }

  get worldPosition() {
    const m = this.renderMatrix;
    return [m[4], m[5]];
  }

  get uniforms() {
    return this[_uniforms];
  }

  /* get zOrder defined by connect method */

  /* attributes */

  get className() {
    return this.attributes.className;
  }

  set className(value) {
    this.attributes.className = value;
  }

  get id() {
    return this.attributes.id;
  }

  set id(value) {
    this.attributes.id = value;
  }

  get name() {
    return this.attributes.name;
  }

  set name(value) {
    this.attributes.name = value;
  }

  get zIndex() {
    return this.attributes.zIndex;
  }

  set zIndex(value) {
    this.attributes.zIndex = value;
  }

  get mesh() {
    return null;
  }

  get shaderAttrs() {
    return this[_shaderAttrs] || {};
  }

  activateAnimations() {
    const layer = this.layer;
    if(layer) {
      const animations = this[_animations];
      animations.forEach((animation) => {
        animation.baseTimeline = layer.timeline;
        animation.play();
        animation.finished.then(() => {
          animations.delete(animation);
        });
      });
      const children = this.children;
      if(children) {
        children.forEach((child) => {
          if(child.activateAnimations) child.activateAnimations();
        });
      }
    }
  }

  addEventListener(type, listener, options = {}) {
    if(type === 'mousewheel') type = 'wheel';
    if(typeof options === 'boolean') options = {capture: options};
    const {capture, once} = options;
    const eventListeners = capture ? _captureEventListeners : _eventListeners;
    this[eventListeners][type] = this[eventListeners][type] || [];
    this[eventListeners][type].push({listener, once});

    return this;
  }

  animate(frames, timing) {
    const animation = new Animation(this, frames, timing);
    if(this.effects) animation.applyEffects(this.effects);
    if(this.layer) {
      animation.baseTimeline = this.layer.timeline;
      animation.play();
      animation.finished.then(() => {
        this[_animations].delete(animation);
      });
    }
    this[_animations].add(animation);
    return animation;
  }

  attr(...args) {
    if(args.length === 0) return this.attributes[attributes];
    if(args.length > 1) {
      let [key, value] = args;
      if(typeof value === 'function') {
        value = value(this.attr(key));
      }
      this.setAttribute(key, value);
      return this;
    }
    if(typeof args[0] === 'string') {
      return this.getAttribute(args[0]);
    }
    Object.assign(this.attributes, args[0]);
    return this;
  }

  cloneNode() {
    const cloned = new this.constructor();
    const attrs = this.attributes[changedAttrs];
    cloned.attr(attrs);
    return cloned;
  }

  connect(parent, zOrder) {
    Object.defineProperty(this, 'parent', {
      value: parent,
      writable: false,
      configurable: true,
    });
    Object.defineProperty(this, 'zOrder', {
      value: zOrder,
      writable: false,
      configurable: true,
    });
    if(parent.timeline) this.activateAnimations();
    this.setResolution(parent.getResolution());
    this.forceUpdate();
    this.dispatchEvent({type: 'append', detail: {parent, zOrder}});
  }

  contains(node) {
    while(node && this !== node) {
      node = node.parent;
    }
    return !!node;
  }

  deactivateAnimations() {
    this[_animations].forEach(animation => animation.cancel());
    const children = this.children;
    if(children) {
      children.forEach((child) => {
        if(child.deactivateAnimations) child.deactivateAnimations();
      });
    }
  }

  disconnect() {
    const {parent, zOrder} = this;
    delete this.parent;
    delete this.zOrder;
    this.deactivateAnimations();
    this.dispatchEvent({type: 'remove', detail: {parent, zOrder}});
    if(parent) parent.forceUpdate();
  }

  dispatchEvent(event) {
    if(!(event instanceof SpriteEvent)) {
      event = new SpriteEvent(event);
    }
    event.target = this;
    let type = event.type;
    if(type === 'mousewheel') type = 'wheel';

    const elements = [this];
    let parent = this.parent;
    while(event.bubbles && parent) {
      elements.push(parent);
      parent = parent.parent;
    }

    // capture phase
    for(let i = elements.length - 1; i >= 0; i--) {
      const element = elements[i];
      const listeners = element[_captureEventListeners] && element[_captureEventListeners][type];
      if(listeners && listeners.length) {
        event.currentTarget = element;
        listeners.forEach(({listener, once}) => {
          listener.call(this, event);
          if(once) elements.removeEventListener(listener);
        });
        delete event.currentTarget;
      }
      if(!event.bubbles && event.cancelBubble) break;
    }
    // bubbling
    if(!event.cancelBubble) {
      for(let i = 0; i < elements.length; i++) {
        const element = elements[i];
        const listeners = element[_eventListeners] && element[_eventListeners][type];
        if(listeners && listeners.length) {
          event.currentTarget = element;
          listeners.forEach(({listener, once}) => {
            listener.call(this, event);
            if(once) elements.removeEventListener(listener);
          });
          delete event.currentTarget;
        }
        if(!event.bubbles || event.cancelBubble) break;
      }
    }
  }

  dispatchPointerEvent(event) {
    const {layerX: x, layerY: y} = event;
    if(this.isPointCollision(x, y)) {
      this.dispatchEvent(event);
      return true;
    }
    return false;
  }

  draw(meshes = []) {
    const mesh = this.mesh;

    if(mesh) {
      applyFilters(mesh, this.filters);
      meshes.push(mesh);
      if(this[_program]) {
        mesh.setProgram(this[_program]);
        const shaderAttrs = this[_shaderAttrs];
        if(shaderAttrs) {
          Object.entries(shaderAttrs).forEach(([key, setter]) => {
            mesh.setAttribute(key, setter);
          });
        }
        const uniforms = this[_uniforms];
        if(this[_uniforms]) {
          const _uniform = {};
          Object.entries(uniforms).forEach(([key, value]) => {
            if(typeof value === 'function') {
              value = value(this, key);
            }
            _uniform[key] = value;
          });
          mesh.setUniforms(_uniform);
        }
      }
      applyRenderEvent(this, mesh);
    }
    return meshes;
  }

  forceUpdate() {
    if(this.parent) this.parent.forceUpdate();
  }

  getAttribute(key) {
    return this.attributes[key];
  }

  getListeners(type, {capture = false} = {}) {
    const eventListeners = capture ? _captureEventListeners : _eventListeners;
    return [...(this[eventListeners][type] || [])];
  }

  getNodeNearBy(distance = 1) {
    if(!this.parent) return null;
    if(distance === 0) return this;
    const children = this.parent.children;
    const idx = children.indexOf(this);
    return children[idx + distance];
  }

  getWorldPosition(offsetX, offsetY) {
    const m = this.renderMatrix;
    const x = offsetX * m[0] + offsetY * m[2] + m[4];
    const y = offsetX * m[1] + offsetY * m[3] + m[5];
    return [x, y];
  }

  getOffsetPosition(x, y) {
    const m = mat2d.invert(this.renderMatrix);
    const offsetX = x * m[0] + y * m[2] + m[4];
    const offsetY = x * m[1] + y * m[3] + m[5];
    return [offsetX, offsetY];
  }

  getResolution() {
    return {...this[_resolution]};
  }

  isPointCollision(x, y) {
    if(!this.mesh) return false;
    const pointerEvents = this.attributes.pointerEvents;
    if(pointerEvents === 'none') return false;
    if(pointerEvents !== 'all' && !this.isVisible) return false;
    let which = 'both';
    if(pointerEvents === 'visibleFill') which = 'fill';
    if(pointerEvents === 'visibleStroke') which = 'stroke';
    return this.mesh.isPointCollision(x, y, which);
  }

  onPropertyChange(key, newValue, oldValue) {
    if(key !== 'id' && key !== 'name' && key !== 'className' && key !== 'pointerEvents' && key !== 'passEvents') {
      this.forceUpdate();
    }
    if(key === 'filter') {
      this[_filters] = parseFilterString(newValue);
    }
    if(key === 'zIndex' && this.parent) {
      this.parent.reorder();
    }
  }

  setAttribute(key, value) {
    if(key === 'attrs') {
      this.attr(value);
    }
    this.attributes[key] = value;
  }

  setMouseCapture() {
    if(this.layer) {
      this.layer.__mouseCapturedTarget = this;
    }
  }

  // layer.renderer.createProgram(fragmentShader, vertexShader, attributeOptions)
  setProgram(program) {
    this[_program] = program;
    this.forceUpdate();
  }

  setShaderAttribute(attrName, setter) {
    this[_shaderAttrs] = this[_shaderAttrs] || {};
    this[_shaderAttrs][attrName] = setter;
    this.forceUpdate();
  }

  setUniforms(uniforms) {
    this[_uniforms] = this[_uniforms] || {};
    Object.assign(this[_uniforms], uniforms);
    this.forceUpdate();
  }

  setResolution({width, height}) {
    const {width: w, height: h} = this[_resolution];
    if(w !== width || h !== height) {
      this[_resolution] = {width, height};
      // this.updateContours();
      this.forceUpdate();
      this.dispatchEvent({type: 'resolutionchange', detail: {width, height}});
    }
  }

  show() {
    if(this.attributes.display === 'none') {
      this.attributes.display = this[_display] || '';
    }
  }

  hide() {
    if(this.attributes.display !== 'none') {
      this[_display] = this.attributes.display;
      this.attributes.display = 'none';
    }
  }

  releaseMouseCapture() {
    if(this.layer && this.layer.__mouseCapturedTarget === this) {
      this.layer.__mouseCapturedTarget = null;
    }
  }

  remove() {
    if(this.parent && this.parent.removeChild) {
      this.parent.removeChild(this);
      return true;
    }
    return false;
  }

  removeAllListeners(type, options = {}) {
    if(typeof options === 'boolean') options = {capture: options};
    const capture = options.capture;

    const eventListeners = capture ? _captureEventListeners : _eventListeners;

    if(this[eventListeners][type]) {
      this[eventListeners][type] = [];
    }
    return this;
  }

  removeAttribute(key) {
    this.setAttribute(key, null);
  }

  removeEventListener(type, listener, options = {}) {
    if(typeof options === 'boolean') options = {capture: options};
    const capture = options.capture;

    const eventListeners = capture ? _captureEventListeners : _eventListeners;

    if(this[eventListeners][type]) {
      const listeners = this[eventListeners][type];
      if(listeners) {
        for(let i = 0; i < listeners.length; i++) {
          const {listener: _listener} = listeners[i];
          if(_listener === listener) {
            this[eventListeners][type].splice(i, 1);
            break;
          }
        }
      }
    }

    return this;
  }

  transition(sec, easing = 'linear') {
    const that = this,
      _animation = Symbol('animation');

    easing = easing || 'linear';

    let delay = 0;
    if(typeof sec === 'object') {
      delay = sec.delay || 0;
      sec = sec.duration;
    }

    return {
      [_animation]: null,
      cancel(preserveState = false) {
        const animation = this[_animation];
        if(animation) {
          animation.cancel(preserveState);
        }
      },
      end() {
        const animation = this[_animation];
        if(animation && (animation.playState === 'running' || animation.playState === 'pending')) {
          animation.finish();
        }
      },
      reverse() {
        const animation = this[_animation];
        if(animation) {
          if(animation.playState === 'running' || animation.playState === 'pending') {
            animation.playbackRate = -animation.playbackRate;
          } else {
            const direction = animation.timing.direction;
            animation.timing.direction = direction === 'reverse' ? 'normal' : 'reverse';
            animation.play();
          }
        }
        return animation.finished;
      },
      attr(prop, val) {
        this.end();
        if(typeof prop === 'string') {
          prop = {[prop]: val};
        }
        Object.entries(prop).forEach(([key, value]) => {
          if(typeof value === 'function') {
            prop[key] = value(that.attr(key));
          }
        });
        this[_animation] = that.animate([prop], {
          duration: sec * 1000,
          delay: delay * 1000,
          fill: 'forwards',
          easing,
        });
        return this[_animation].finished;
      },
    };
  }

  updateContours() {
    // override
  }
}

ownerDocument.registerNode(Node, 'node');