spritejs/spritejs

View on GitHub
src/node/layer.js

Summary

Maintainability
C
1 day
Test Coverage
F
43%
import {Renderer, ENV, Figure2D, Mesh2D} from '@mesh.js/core';
import {Timeline} from 'sprite-animator';
import {mat2d} from 'gl-matrix';
import {requestAnimationFrame, cancelAnimationFrame} from '../utils/animation-frame';
import Group from './group';
import ownerDocument from '../document';
import {deleteTexture} from '../utils/texture';

const defaultOptions = {
  antialias: true,
  autoRender: true,
  alpha: true, // for wx-miniprogram
};

const _autoRender = Symbol('autoRender');
const _renderer = Symbol('renderer');
const _timeline = Symbol('timeline');

const _prepareRender = Symbol('prepareRender');
const _tickRender = Symbol('tickRender');

const _pass = Symbol('pass');
const _fbo = Symbol('fbo');
const _tickers = Symbol('tickers');

const _layerTransformInvert = Symbol('layerTransformInvert');

export default class Layer extends Group {
  constructor(options = {}) {
    super();
    if(!options.canvas) {
      const {width, height} = this.getResolution();
      const canvas = ENV.createCanvas(width, height, {
        offscreen: !!options.offscreen,
        id: options.id,
        extra: options.extra,
      });
      if(canvas.style) canvas.style.position = 'absolute';
      if(canvas.dataset) canvas.dataset.layerId = options.id;
      if(canvas.contextType) options.contextType = canvas.contextType;
      options.canvas = canvas;
    }
    const canvas = options.canvas;
    const opts = Object.assign({}, defaultOptions, options);
    this[_autoRender] = opts.autoRender;
    delete options.autoRender;
    const _Renderer = opts.Renderer || Renderer;
    this[_renderer] = new _Renderer(canvas, opts);
    // if(canvas.__gl__) {
    //   // fix blendFunc for node-canvas-webgl
    //   const gl = canvas.__gl__;
    //   gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
    // }
    this.options = options;
    this.id = options.id;
    this[_pass] = [];
    this.setResolution(canvas);
    this.canvas = canvas;
    this[_timeline] = new Timeline();
    this.__mouseCapturedTarget = null;
    this[_layerTransformInvert] = null;
  }

  get autoRender() {
    return this[_autoRender];
  }

  get displayRatio() {
    if(this.parent && this.parent.options) {
      return this.parent.options.displayRatio;
    }
    return 1.0;
  }

  get height() {
    const {height} = this.getResolution();
    return height / this.displayRatio;
  }

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

  /* override */
  get layer() {
    return this;
  }

  get offscreen() {
    return !!this.options.offscreen || this.canvas._offscreen;
  }

  get pass() {
    return this[_pass];
  }

  get prepareRender() {
    return this[_prepareRender] ? this[_prepareRender] : Promise.resolve();
  }

  /* override */
  get renderer() {
    return this[_renderer];
  }

  get renderOffset() {
    if(this.parent && this.parent.options) {
      const {left, top} = this.parent.options;
      return [left, top];
    }
    return [this.options.left | 0, this.options.top | 0];
  }

  get timeline() {
    return this[_timeline];
  }

  get width() {
    const {width} = this.getResolution();
    return width / this.displayRatio;
  }

  get localMatrix() {
    const {x, y} = this.attributes;
    return [1, 0, 0, 1, x, y];
  }

  get layerTransformInvert() {
    if(this[_layerTransformInvert]) return this[_layerTransformInvert];
    const m = this.transformMatrix;
    if(m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0) {
      return null;
    }
    this[_layerTransformInvert] = mat2d.invert(m);
    return this[_layerTransformInvert];
  }

  forceContextLoss() {
    const gl = this.renderer.glRenderer;
    if(gl) {
      const ext = gl.getExtension('WEBGL_lose_context');
      if(ext) {
        ext.loseContext();
        return true;
      }
    }
    return false;
  }

  // isPointCollision(x, y) {
  //   return true;
  // }

  addPass({vertex, fragment, options, uniforms} = {}) {
    if(this.renderer.glRenderer) {
      const {width, height} = this.getResolution();
      const program = this.renderer.createPassProgram({vertex, fragment, options});
      const figure = new Figure2D();
      figure.rect(0, 0, width / this.displayRatio, height / this.displayRatio);
      const mesh = new Mesh2D(figure);
      mesh.setUniforms(uniforms);
      mesh.setProgram(program);
      this[_pass].push(mesh);
      this.forceUpdate();
      return mesh;
    }
    return null;
  }

  // delete unused texture to release memory.
  deleteTexture(image) {
    return deleteTexture(image, this.renderer);
  }

  /* override */
  dispatchPointerEvent(event) {
    const type = event.type;
    if(type === 'mousedown' || type === 'mouseup' || type === 'mousemove') {
      const capturedTarget = this.__mouseCapturedTarget;
      if(capturedTarget) {
        if(capturedTarget.layer === this) {
          capturedTarget.dispatchEvent(event);
          return true;
        }
        this.__mouseCapturedTarget = null;
      }
    }
    let x,
      y;
    const layerTransformInvert = this.layerTransformInvert;
    if(layerTransformInvert) {
      x = event.x;
      y = event.y;
      const m = layerTransformInvert;
      const layerX = m[0] * x + m[2] * y + m[4];
      const layerY = m[1] * x + m[3] * y + m[5];
      delete event.x;
      delete event.y;
      delete event.layerX;
      delete event.layerY;
      Object.defineProperties(event, {
        layerX: {
          value: layerX,
          configurable: true,
        },
        layerY: {
          value: layerY,
          configurable: true,
        },
        x: {
          value: layerX,
          configurable: true,
        },
        y: {
          value: layerY,
          configurable: true,
        },
      });
    }
    const ret = super.dispatchPointerEvent(event);
    if(layerTransformInvert) {
      Object.defineProperties(event, {
        layerX: {
          value: x,
          configurable: true,
        },
        layerY: {
          value: y,
          configurable: true,
        },
        x: {
          value: x,
          configurable: true,
        },
        y: {
          value: y,
          configurable: true,
        },
      });
    }
    return ret;
  }

  /* override */
  forceUpdate() {
    if(!this[_prepareRender]) {
      if(this.parent && this.parent.hasOffscreenCanvas) {
        this.parent.forceUpdate();
        let _resolve = null;
        const prepareRender = new Promise((resolve) => {
          _resolve = resolve;
        });
        prepareRender._resolve = _resolve;
        this[_prepareRender] = prepareRender;
      } else {
        let _resolve = null;
        let _requestID = null;
        const prepareRender = new Promise((resolve) => {
          _resolve = resolve;

          if(this[_autoRender]) {
            _requestID = requestAnimationFrame(() => {
              delete prepareRender._requestID;
              this.render();
            });
          }
        });

        prepareRender._resolve = _resolve;
        prepareRender._requestID = _requestID;

        this[_prepareRender] = prepareRender;
      }
    }
  }

  getFBO() {
    const renderer = this.renderer.glRenderer;
    const {width, height} = this.getResolution();
    if(renderer && (!this[_fbo] || this[_fbo].width !== width || this[_fbo].height !== height)) {
      this[_fbo] = {
        width,
        height,
        target: renderer.createFBO(),
        buffer: renderer.createFBO(),
        swap() {
          [this.target, this.buffer] = [this.buffer, this.target];
        },
      };
      return this[_fbo];
    }
    return this[_fbo] ? this[_fbo] : null;
  }

  updateGlobalTransform() {
    if(this.layerTransformInvert) {
      const renderer = this.renderer;
      const globalMatrix = renderer.__globalTransformMatrix || renderer.globalTransformMatrix;
      renderer.__globalTransformMatrix = globalMatrix;

      const mOut = mat2d(1, 0, 0, 1, 0, 0);
      renderer.setGlobalTransform(...mat2d.multiply(mOut, globalMatrix, this.transformMatrix));
    }
  }

  /* override */
  onPropertyChange(key, newValue, oldValue) {
    super.onPropertyChange(key, newValue, oldValue);
    if(key === 'zIndex') {
      this.canvas.style.zIndex = newValue;
    }
    if(key === 'transform' || key === 'translate' || key === 'rotate' || key === 'scale' || key === 'skew') {
      const m = this[_layerTransformInvert];
      this[_layerTransformInvert] = null;
      this.updateGlobalTransform();
      if(m && !this.layerTransformInvert) {
        // unit matrix, recover globalMatrix
        const renderer = this.renderer;
        const globalMatrix = renderer.__globalTransformMatrix || renderer.globalTransformMatrix;
        renderer.setGlobalTransform(...globalMatrix);
      }
    }
  }

  _prepareRenderFinished() {
    if(this[_prepareRender]) {
      if(this[_prepareRender]._requestID) {
        cancelAnimationFrame(this[_prepareRender]._requestID);
      }
      this[_prepareRender]._resolve();
      delete this[_prepareRender];
    }
  }

  render({clear = true} = {}) {
    const fbo = this[_pass].length ? this.getFBO() : null;
    if(fbo) {
      this.renderer.glRenderer.bindFBO(fbo.target);
    }
    if(clear) this[_renderer].clear();
    const meshes = this.draw();
    if(meshes && meshes.length) {
      this.renderer.drawMeshes(meshes);
      if(this.canvas.draw) this.canvas.draw();
    }
    if(fbo) {
      const renderer = this.renderer.glRenderer;
      const len = this[_pass].length;
      const {width, height} = this.getResolution();
      const rect = [0, 0, width / this.displayRatio, height / this.displayRatio];
      this[_pass].forEach((pass, idx) => {
        pass.blend = true;
        pass.setTexture(fbo.target.texture, {rect});
        if(idx === len - 1) renderer.bindFBO(null);
        else {
          fbo.swap();
          renderer.bindFBO(fbo.target);
        }
        this[_renderer].clear();
        this.renderer.drawMeshes([pass]);
      });
    }
    this._prepareRenderFinished();
  }

  /* override */
  setResolution({width, height}) {
    const renderer = this.renderer;
    const m = renderer.__globalTransformMatrix || renderer.globalTransformMatrix;
    const offsetLeft = m[4];
    const offsetTop = m[5];
    const previousDisplayRatio = m[0];
    const {width: w, height: h} = this.getResolution();
    if(w !== width || h !== height) {
      super.setResolution({width, height});
      if(this.canvas) {
        this.canvas.width = width;
        this.canvas.height = height;
        if(renderer.updateResolution) renderer.updateResolution();
      }
      this.attributes.size = [width, height];
      if(this[_pass].length) {
        this[_pass].forEach((pass) => {
          const figure = new Figure2D();
          figure.rect(0, 0, width / this.displayRatio, height / this.displayRatio);
          pass.contours = figure.contours;
        });
      }
      // this.dispatchEvent({type: 'resolutionchange', width, height});
    }
    const [left, top] = this.renderOffset;
    const displayRatio = this.displayRatio;
    if(offsetLeft !== left || offsetTop !== top || previousDisplayRatio !== displayRatio) {
      // console.log(displayRatio, this.parent);
      renderer.setGlobalTransform(displayRatio, 0, 0, displayRatio, left, top);
      renderer.__globalTransformMatrix = null;
      this[_layerTransformInvert] = null;
      this.updateGlobalTransform();
      this.forceUpdate();
    }
  }

  /**
   * tick(handler, {originTime = 0, playbackRate = 1.0, duration = Infinity})
   * @param {*} handler
   * @param {*} options
   */
  tick(handler = null, {duration = Infinity, ...timelineOptions} = {}) {
    // this._prepareRenderFinished();
    const t = this.timeline.fork(timelineOptions);
    const layer = this;

    this[_tickers] = this[_tickers] || [];
    this[_tickers].push({handler, duration});

    const update = () => {
      let _resolve = null;
      let _requestID = null;
      const _update = () => {
        // const ret = handler ? handler(t.currentTime, p) : null;
        const ret = this[_tickers].map(({handler, duration}) => {
          const p = Math.min(1.0, t.currentTime / duration);
          const value = handler ? handler(t.currentTime, p) : null;
          return {value, p};
        });
        if(!layer[_tickRender]) {
          layer[_tickRender] = Promise.resolve().then(() => {
            if(layer[_autoRender]) layer.render();
            delete layer[_tickRender];
            for(let i = ret.length - 1; i >= 0; i--) {
              const {value, p} = ret[i];
              if(value === false || p >= 1.0) {
                this[_tickers].splice(i, 1);
              }
            }
            if(this[_tickers].length > 0) {
              update();
            }
          });
        }
      };

      if(this[_prepareRender] && this[_prepareRender]._type !== 'ticker') {
        cancelAnimationFrame(this[_prepareRender]._requestID);
        delete this[_prepareRender];
      }

      if(!this[_prepareRender]) {
        const prepareRender = new Promise((resolve) => {
          _resolve = resolve;
          _requestID = requestAnimationFrame(_update);
        });
        prepareRender._resolve = _resolve;
        prepareRender._requestID = _requestID;
        prepareRender._type = 'ticker';

        this[_prepareRender] = prepareRender;
      }
    };

    update();
  }

  toGlobalPos(x, y) {
    if(this.layerTransformInvert) {
      const m = this.transformMatrix;
      x = m[0] * x + m[2] * y + m[4];
      y = m[1] * x + m[3] * y + m[5];
    }

    const {width, height} = this.getResolution();
    const offset = this.renderOffset;
    const viewport = [this.canvas.clientWidth, this.canvas.clientHeight];

    x = x * viewport[0] / width + offset[0];
    y = y * viewport[1] / height + offset[1];

    const displayRatio = this.displayRatio;

    x *= displayRatio;
    y *= displayRatio;

    return [x, y];
  }

  toLocalPos(x, y) {
    const {width, height} = this.getResolution();
    const offset = this.renderOffset;
    const viewport = [this.canvas.clientWidth, this.canvas.clientHeight];
    x = x * width / viewport[0] - offset[0];
    y = y * height / viewport[1] - offset[1];

    const displayRatio = this.displayRatio;

    x /= displayRatio;
    y /= displayRatio;

    const m = this.layerTransformInvert;
    if(m) {
      x = m[0] * x + m[2] * y + m[4];
      y = m[1] * x + m[3] * y + m[5];
    }

    return [x, y];
  }
}

ownerDocument.registerNode(Layer, 'layer');