Extensions

Source: renderers/Stage2D.js

import { arraySet } from "../../extensions/utils/arraySet";
import { noop } from "../../extensions/utils/noop";
import { Buffer } from "../core/Buffer";
import { Utils } from "../core/Utils";
import { Container } from "../display/Container";
import { Image } from "../display/Image";
import { Item } from "../display/Item";
import { Light } from "../display/Light";
import { StageContainer } from "../display/StageContainer";
import "../math/PointType";
import { BASE_VERTEX_SHADER_ATTRIBUTES, BASE_VERTEX_SHADER_UNIFORMS, TINT_TYPE_SHADER } from "../utils/shaderUtils";
import { BatchRenderer } from "./BatchRenderer";
import { LightRenderer } from "./LightRenderer";

/**
 * @typedef {Object} Stage2DConfig
 * @extends {RendererConfig}
 */

// Prefix for rendering functions.
const _RENDERING_TYPE_PREFIX = "_draw";
// Mouse move event type.
const _INTERACTION_EVENT_TYPE_MOUSE_MOVE = "mousemove";
// Mouse down event type.
const _INTERACTION_EVENT_TYPE_MOUSE_DOWN = "mousedown";
// Mouse up event type.
const _INTERACTION_EVENT_TYPE_MOUSE_UP = "mouseup";
// Mouse click event type.
const _INTERACTION_EVENT_TYPE_CLICK = "click";
// Touch start event type.
const _INTERACTION_EVENT_TYPE_TOUCH_START = "touchstart";
// Touch move event type.
const _INTERACTION_EVENT_TYPE_TOUCH_MOVE = "touchmove";
// Touch end event type.
const _INTERACTION_EVENT_TYPE_TOUCH_END = "touchend";
// Mapped pointer move event type.
const _INTERACTION_EVENT_MAPPED_TYPE_POINTER_CLICK = "PointerClick";
// Mapped pointer move event type.
const _INTERACTION_EVENT_MAPPED_TYPE_POINTER_DOWN = "PointerDown";
// Mapped pointer move event type.
const _INTERACTION_EVENT_MAPPED_TYPE_POINTER_MOVE = "PointerMove";
// Mapped pointer move event type.
const _INTERACTION_EVENT_MAPPED_TYPE_POINTER_UP = "PointerUp";
// Mapped pointer move event type.
const _INTERACTION_EVENT_MAPPED_TYPE_POINTER_OUT = "PointerOut";
// Mapped pointer move event type.
const _INTERACTION_EVENT_MAPPED_TYPE_POINTER_OVER = "PointerOver";

// List of interaction event types.
const _INTERACTION_EVENT_TYPES = [
  _INTERACTION_EVENT_TYPE_MOUSE_MOVE,
  _INTERACTION_EVENT_TYPE_MOUSE_DOWN,
  _INTERACTION_EVENT_TYPE_MOUSE_UP,
  _INTERACTION_EVENT_TYPE_CLICK,
  _INTERACTION_EVENT_TYPE_TOUCH_START,
  _INTERACTION_EVENT_TYPE_TOUCH_MOVE,
  _INTERACTION_EVENT_TYPE_TOUCH_END,
];

// Mapping of interaction event types to pointer event types.
const _INTERACTION_EVENT_MAPPED_TYPES = {
  [_INTERACTION_EVENT_TYPE_CLICK]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_CLICK,
  [_INTERACTION_EVENT_TYPE_MOUSE_DOWN]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_DOWN,
  [_INTERACTION_EVENT_TYPE_TOUCH_START]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_DOWN,
  [_INTERACTION_EVENT_TYPE_MOUSE_MOVE]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_MOVE,
  [_INTERACTION_EVENT_TYPE_TOUCH_MOVE]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_MOVE,
  [_INTERACTION_EVENT_TYPE_MOUSE_UP]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_UP,
  [_INTERACTION_EVENT_TYPE_TOUCH_END]: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_UP,
};

/**
 * <pre>
 *  Stage2D renderer
 *    - Renders multiple textures
 * </pre>
 * @extends {BatchRenderer}
 */
export class Stage2D extends BatchRenderer {
  /**
   * Creates an instance of Stage2D.
   * @constructor
   * @param {Stage2DConfig} config
   */
  constructor(config = {}) {
    config = Utils.initRendererConfig(config);
    config.useTint = config.useTint ?? true;

    // prettier-ignore
    Utils.setLocations(config, [
      "aD",
      "aE",
      "uF",
    ]);

    super(config);

    const { $MAX_RENDER_COUNT } = this;

    this.container = new StageContainer(this);

    this._batchItems = 0;

    this[_RENDERING_TYPE_PREFIX + Item.RENDERING_TYPE] = this[_RENDERING_TYPE_PREFIX + Light.RENDERING_TYPE] = noop;
    this[_RENDERING_TYPE_PREFIX + Image.RENDERING_TYPE] = this._drawImage;
    this[_RENDERING_TYPE_PREFIX + Container.RENDERING_TYPE] = this._drawContainer;
    this._batchDraw = this._batchDraw.bind(this);

    this._mousePosition = { x: 0, y: 0 };

    // prettier-ignore
    this._dataBuffer = new Buffer(
      "aD", 
      $MAX_RENDER_COUNT,
      3,
      4
    );

    // prettier-ignore
    this._distortionBuffer = new Buffer(
      "aE",
      $MAX_RENDER_COUNT,
      4,
      2
    );

    this._onMouseEventHandler = this._onMouseEventHandler.bind(this);
    const { canvas } = this.context;
    const { lightRenderer } = config;

    _INTERACTION_EVENT_TYPES.forEach((type) => canvas.addEventListener(type, this._onMouseEventHandler));

    lightRenderer && this.attachLightRenderer(lightRenderer);
  }

  /**
   * Attach LightRenderer
   * Recommended to set LightRenderer when using Light
   * @param {LightRenderer} v
   */
  attachLightRenderer(v) {
    this[_RENDERING_TYPE_PREFIX + Light.RENDERING_TYPE] = v.addLightForRender.bind(v);
  }

  /**
   * Detach LightRenderer
   */
  detachLightRenderer() {
    this[_RENDERING_TYPE_PREFIX + Light.RENDERING_TYPE] = noop;
  }

  /**
   * Description placeholder
   */
  destruct() {
    const { canvas } = this.context;
    _INTERACTION_EVENT_TYPES.forEach((type) => canvas.removeEventListener(type, this._onMouseEventHandler));
  }

  /**
   * @ignore
   */
  _handleMouseEvent() {
    const { _currentEvent, _eventTarget, _previousEventTarget } = this;

    if (_currentEvent) {
      if (_eventTarget !== _previousEventTarget) {
        const newEvent = { ..._currentEvent };

        _previousEventTarget &&
          _previousEventTarget.callEventHandler(_previousEventTarget, {
            ...newEvent,
            type: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_OUT,
          });

        _eventTarget &&
          _eventTarget.callEventHandler(_eventTarget, {
            ...newEvent,
            type: _INTERACTION_EVENT_MAPPED_TYPE_POINTER_OVER,
          });
      }

      _eventTarget &&
        _eventTarget.callEventHandler(_eventTarget, {
          ..._currentEvent,
          type: _INTERACTION_EVENT_MAPPED_TYPES[_currentEvent.type],
        });

      this._previousEventTarget = _eventTarget;
    }

    this._currentEvent = null;
  }

  /**
   * @param {*} x
   * @param {*} y
   * @ignore
   */
  _setMousePosition(x, y) {
    this._isMousePositionSet = true;

    const { matrixCache } = this.container;
    const { _mousePosition } = this;

    _mousePosition.x = (x - this.widthHalf) * matrixCache[0];
    _mousePosition.y = (y - this.heightHalf) * matrixCache[3];
  }

  /**
   * @param {Item} item
   * @ignore
   */
  _drawItem(item) {
    const { $renderTime } = this;

    item.update($renderTime);
    item.callbackBeforeRender(item, $renderTime);
    item.renderable && this[_RENDERING_TYPE_PREFIX + item.RENDERING_TYPE](item);
    item.callbackAfterRender(item, $renderTime);
  }

  /**
   * @param {Container} container
   * @ignore
   */
  _drawContainer(container) {
    const { children } = container;
    const { length } = children;

    let i = -1;

    while (++i < length) {
      this._drawItem(children[i]);
    }
  }

  /**
   * @param {Image} image
   * @ignore
   */
  _drawImage(image) {
    this.context.setBlendMode(image.blendMode, this._batchDraw);

    const { _batchItems } = this;
    const dataBufferId = _batchItems * 12;
    const matrixBufferId = _batchItems * 16;
    const itemParent = image.parent;
    const dataBufferData = this._dataBuffer.data;
    const matrixBufferData = this.$matrixBuffer.data;

    if (itemParent) {
      if (this._isMousePositionSet && image.interactive && image.isContainsPoint(this._mousePosition)) {
        this._eventTarget = image;
      }

      arraySet(dataBufferData, image.colorCache, dataBufferId);
      arraySet(dataBufferData, image.textureRepeatRandomCache, dataBufferId + 8);
      dataBufferData[dataBufferId + 4] = image.alpha * itemParent.getPremultipliedAlpha();
      dataBufferData[dataBufferId + 5] = image.tintType * itemParent.getPremultipliedUseTint();
      dataBufferData[dataBufferId + 6] = this.context.useTexture(
        image.texture,
        this.$renderTime,
        false,
        this._batchDraw,
      );
      dataBufferData[dataBufferId + 7] = image.distortion.distortTexture;

      arraySet(matrixBufferData, image.matrixCache, matrixBufferId);
      arraySet(matrixBufferData, image.textureMatrixCache, matrixBufferId + 6);
      arraySet(matrixBufferData, image.textureCropCache, matrixBufferId + 12);

      arraySet(this._distortionBuffer.data, image.distortionCache, _batchItems * 8);

      ++this._batchItems === this.$MAX_RENDER_COUNT && this._batchDraw();
    }
  }

  /**
   * @ignore
   */
  _batchDraw() {
    const { $gl, $locations, context, _batchItems } = this;

    if (_batchItems) {
      if (context.textureIds.length) {
        $gl.uniform1iv($locations.uB, context.textureIds);
        $gl.uniform2fv($locations.uF, context.textureSizes);
      }
      this.$uploadBuffers();
      this.$drawInstanced(_batchItems);
      this._batchItems = 0;
    }
  }

  /**
   * @ignore
   */
  _onMouseEventHandler(event) {
    this._currentEvent = event;
    const { canvas } = this.context;
    this._setMousePosition(
      (canvas.width / canvas.offsetWidth) * event.offsetX,
      (canvas.height / canvas.offsetHeight) * event.offsetY,
    );
  }

  /**
   * @ignore
   */
  $render() {
    this._eventTarget = null;
    this._drawItem(this.container);
    this._batchDraw();
    this._isMousePositionSet = false;
    this._handleMouseEvent();
  }

  /**
   * @ignore
   */
  $uploadBuffers() {
    const { $gl } = this;
    this._dataBuffer.upload($gl);
    this._distortionBuffer.upload($gl);
    super.$uploadBuffers();
  }

  /**
   * @ignore
   */
  $createBuffers() {
    const { $gl, $locations } = this;
    super.$createBuffers();
    this._dataBuffer.create($gl, $locations);
    this._distortionBuffer.create($gl, $locations);
  }

  // prettier-ignore
  /**
   * @returns {string}
   * @ignore
   */
  $createVertexShader() {
    const { useRepeatTextures } = this._config;
    const { maxTextureImageUnits } = Utils.INFO;

    return `${Utils.GLSL.DEFINE.Z}` +

    `${BASE_VERTEX_SHADER_ATTRIBUTES}` +
    `in mat4x2 ` +
      `aE;` +
    `in mat3x4 ` +
      `aD;` +
    `in mat4 ` +
      `aB;` +

    `${BASE_VERTEX_SHADER_UNIFORMS}` +

    `uniform vec2 ` +
      `uF[${maxTextureImageUnits}];` +

    `out vec2 ` +
      `v0;` +
    `flat out float ` +
      `v1;` +
    `flat out vec4 ` +
      `v2,` +
      `v3,` +
      `v4` +
    `${useRepeatTextures ? `,v5`: ``};` +

    `vec2 clcQd(vec2 p){` +
      `return mix(` + 
        `mix(` + 
          `aE[0],` + 
          `aE[1],` + 
          `p.x` + 
        `),` + 
        `mix(` + 
          `aE[3],` + 
          `aE[2],` + 
          `p.x` + 
        `),` + 
        `p.y` + 
      `);` +
    `}` +

    `void main(){` +
      `vec2 ` +
        `tPs=clcQd(aA);` +

      `gl_Position=vec4(` + 
        `mat3(aB[0].xy,0,aB[0].zw,0,aB[1].xy,1)*` + 
        `vec3(tPs,1.),` + 
        `1` + 
      `)*vec4(1.,uA,1.,1.);` +

      `v0=(` + 
          `mat3(aB[1].zw,0,aB[2].xy,0,aB[2].zw,1)*` + 
          `vec3(` +
            `mix(` + 
              `tPs,` + 
              `aA,` + 
              `aD[1].w` + 
            `),` +
            `1.` +
          `)` + 
        `).xy;` +
      `v1=aD[1].x;` +
      `v2.xy=aD[1].yz;` +
      `v2.zw=v2.y>-1.?.5/uF[int(v2.y)]:Z.yy;` +
      `v3=aB[3];` +
      `v4=aD[0];` +
      `${useRepeatTextures ? `v5=aD[2];` : ``}` +
    `}`;
  }

  // prettier-ignore
  /**
   * @returns {string}
   * @ignore
   */
  $createFragmentShader() {
    const { _config } = this;
    const { maxTextureImageUnits } = Utils.INFO;
    const { useRepeatTextures, useTint } = _config;

    const getSimpleTexColor = (modCoordName) => `gTxC(v2.y,v3,${modCoordName})`;

    return `${Utils.GLSL.DEFINE.Z}` +
    `${Utils.GLSL.DEFINE.RADIANS_360}` +

    `in vec2 ` +
      `v0;` +
    `flat in float ` +
      `v1;` +
    `flat in vec4 ` +
      `v2,` +
      `v3,` +
      `v4` +
      `${useRepeatTextures ? `,v5` : ``};` +

    `uniform sampler2D ` +
      `uB[${maxTextureImageUnits}];` +

    `out vec4 ` +
      `oCl;` +

    `vec4 gTxC(float i,vec4 s,vec2 m){` +
      `vec2 ` + 
        `sxy=s.xy,` +
        `szw=s.zw,` +
        `ofs=szw*m,` +
        `cp=clamp(` +
          `sxy+ofs,` +
          `sxy+v2.zw,` +
          `sxy+szw-v2.zw` +
        `);` +
      `${Array(maxTextureImageUnits).fill().map((v, i) => 
      `if(i<${i + 1}.)` + 
        `return texture(uB[${i}],cp);`).join(``)}` +
      `return Z.yyyy;` +
    `}` +

    `float cs(float a,float b,float v){` +
      `v=abs(v);` +
      `float ` + 
        `t=-2.*v+2.;` +
      `v=mix(2.*v*v,1.-t*t*.5,step(.5,v));` +
      `return a*(1.-v)+b*v;` +
    `}` +

    `${useRepeatTextures
      ? `${Utils.GLSL.RANDOM2}` +
        `vec4 gCB(vec2 c0){` +
          `vec2 ` +
            `uv=v0;` +

          `float ` +
            `rnd=rand2(c0/100.),` +
            `rndDg=rnd*RADIANS_360*v5.x;` +

          `if(rndDg>0.){` +
            `vec2 ` +
              `rt=vec2(sin(rndDg),cos(rndDg));` +
            `uv=vec2(uv.x*rt.y-uv.y*rt.x,uv.x*rt.x+uv.y*rt.y);` +
          `}` +

          `return ${getSimpleTexColor(`fract(uv)`)};` +
        `}` +

        `float gRCB(vec2 st,vec2 c0,vec2 uv){` +
          `float ` +
            `rnd=rand2((c0+st)/100.);` +
          `return (1.-(v5.w*rnd-v5.w*.5)*v5.y)*` +
            `cs(0.,1.,1.-st.x-uv.x)*cs(0.,1.,1.-st.y-uv.y);` +
        `}`
      : ``}` +

    `void main(){` +
      `if(v2.y>-1.){` +
        `vec2 ` +
          `uv=fract(v0);` +

        `${useRepeatTextures
          ? `if(v5.x>0.||v5.y>0.){` +
              `vec2 ` +
                `c0=floor(v0);` +
              `vec4 ` +
                `rc=vec4(` + 
                  `gRCB(Z.xx,c0,uv),` +
                  `gRCB(Z.yx,c0,uv),` +
                  `gRCB(Z.xy,c0,uv),` +
                  `gRCB(Z.yy,c0,uv)` +
                `);` +
              `oCl=mix(` +
                `gCB(Z.xy+c0),` +
                `clamp(` +
                  `gCB(Z.xx+c0)*rc.x+` +
                  `gCB(Z.yx+c0)*rc.y+` +
                  `gCB(Z.xy+c0)*rc.z+` +
                  `gCB(Z.yy+c0)*rc.w` +
                `,0.,1.),` +
                `vec4(v5.z)` + 
              `);` +
              `oCl.a=mix(` + 
                `oCl.a,` + 
                `clamp(` +
                  `oCl.a*(` +
                    `rc.x+` +
                    `rc.y+` +
                    `rc.z+` +
                    `rc.w` +
                  `),0.,1.),` + 
                  `v5.y` +
                `);` +
            `}else `
          : ``}oCl=${getSimpleTexColor(`uv`)};` +
      `}else oCl=vec4(1.);` +

      `oCl.a*=v1;` +

      `if(oCl.a<=0.)discard;` +

      `${useTint
        ? TINT_TYPE_SHADER(`v2.x`, `oCl`, `v4`)
        : ``}` +
    `}`;
  }
}