Source: renderer/Stage2D.js

import { BatchRenderer } from "./BatchRenderer";
import { LightRenderer } from "./LightRenderer";
import { Item } from "../display/Item";
import { Image } from "../display/Image";
import { Light } from "../display/Light";
import { Container } from "../display/Container";
import { StageContainer } from "../display/StageContainer";
import { arraySet, noop } from "../utils/helpers";
import { Buffer } from "../utils/Buffer";
import { Utils } from "../utils/Utils";
import "../geom/PointType";

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

// Prefix for rendering functions.
const _RENDERING_TYPE_PREFIX = "_draw",
  // Mouse move event type.
  _INTERACTION_EVENT_TYPE_MOUSE_MOVE = "mousemove",
  // Mouse down event type.
  _INTERACTION_EVENT_TYPE_MOUSE_DOWN = "mousedown",
  // Mouse up event type.
  _INTERACTION_EVENT_TYPE_MOUSE_UP = "mouseup",
  // Mouse click event type.
  _INTERACTION_EVENT_TYPE_CLICK = "click",
  // Touch start event type.
  _INTERACTION_EVENT_TYPE_TOUCH_START = "touchstart",
  // Touch move event type.
  _INTERACTION_EVENT_TYPE_TOUCH_MOVE = "touchmove",
  // Touch end event type.
  _INTERACTION_EVENT_TYPE_TOUCH_END = "touchend",
  // Mapped pointer move event type.
  _INTERACTION_EVENT_MAPPED_TYPE_POINTER_CLICK = "PointerClick",
  // Mapped pointer move event type.
  _INTERACTION_EVENT_MAPPED_TYPE_POINTER_DOWN = "PointerDown",
  // Mapped pointer move event type.
  _INTERACTION_EVENT_MAPPED_TYPE_POINTER_MOVE = "PointerMove",
  // Mapped pointer move event type.
  _INTERACTION_EVENT_MAPPED_TYPE_POINTER_UP = "PointerUp",
  // Mapped pointer move event type.
  _INTERACTION_EVENT_MAPPED_TYPE_POINTER_OUT = "PointerOut",
  // Mapped pointer move event type.
  _INTERACTION_EVENT_MAPPED_TYPE_POINTER_OVER = "PointerOver",
  // List of interaction event types.
  _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.
  _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}
 * @property {StageContainer} container
 */
export class Stage2D extends BatchRenderer {
  /**
   * Creates an instance of Stage2D.
   * @constructor
   * @param {Stage2DConfig} options
   */
  constructor(options = {}) {
    options.config = Utils.initRendererConfig(options.config);
    options.useTint = options.useTint ?? true;

    // prettier-ignore
    options.config.locations = [
      "aDt",
      "aDst"
    ];

    super(options);

    const maxRenderCount = this.$MAX_RENDER_COUNT;

    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 };

    this._dataBuffer = new Buffer("aDt", maxRenderCount, 3, 4);
    this._distortionBuffer = new Buffer("aDst", maxRenderCount, 4, 2);

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

    const lightRenderer = options.lightRenderer;
    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.canvas;
    _INTERACTION_EVENT_TYPES.forEach((type) =>
      canvas.removeEventListener(type, this._onMouseEventHandler)
    );
  }

  /**
   * @ignore
   */
  _handleMouseEvent() {
    const latestEvent = this._latestEvent,
      eventTarget = this._eventTarget,
      previousEventTarget = this._previousEventTarget;

    if (latestEvent) {
      if (eventTarget !== previousEventTarget) {
        const newEvent = { ...latestEvent };

        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, {
          ...latestEvent,
          type: _INTERACTION_EVENT_MAPPED_TYPES[latestEvent.type],
        });

      this._previousEventTarget = eventTarget;
    }

    this._latestEvent = null;
  }

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

    const matrixCache = this.container.matrixCache,
      mousePosition = this._mousePosition;

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

  /**
   * @param {Item} item
   * @ignore
   */
  _drawItem(item) {
    const renderTime = this.$renderTime;
    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.children,
      l = children.length;

    for (let i = 0; i < l; i++) this._drawItem(children[i]);
  }

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

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

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

    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.distortionProps.distortTexture;

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

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

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

  /**
   * @ignore
   */
  _batchDraw() {
    if (this._batchItems) {
      this.$uploadBuffers();
      this.$gl.uniform1iv(this.$locations.uTex, this.context.textureIds);
      this.$drawInstanced(this._batchItems);
      this._batchItems = 0;
    }
  }

  /**
   * @ignore
   */
  _onMouseEventHandler(event) {
    this._latestEvent = event;
    const canvas = this.context.canvas;
    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.$gl,
      enableBuffers = this.$enableBuffers;
    this._dataBuffer.upload(gl, enableBuffers);
    this._distortionBuffer.upload(gl, enableBuffers);
    super.$uploadBuffers();
  }

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

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

    return "" +
    Utils.GLSL.DEFINE.Z +

    "in vec2 " +
      "aPos;" +
    "in mat4x2 " +
      "aDst;" +
    "in mat3x4 " +
      "aDt;" +
    "in mat4 " +
      "aMt;" +

    "uniform float " +
      "uFlpY;" +
    "uniform sampler2D " +
      "uTex[" + maxTextureImageUnits + "];" +

    "out float " +
      "vACl," +
      "vTId," +
      "vTTp;" +
    "out vec2 " +
      "vTUv," +
      "vTsh;" +
    "out vec4 " +
      "vTCrop," +
      "vCl" +
    (useRepeatTextures
      ? ",vRR;"
      : ";") +

    "vec2 gtTexS(float i){" +
      Array(maxTextureImageUnits).fill().map((v, i) => 
      "if(i<" + (i + 1) + ".)" + 
        "return .5/vec2(textureSize(uTex[" + i + "],0));").join("") +
      "return Z.yy;" +
    "}" +

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

    "void main(void){" +
      "mat3 " +
        "mt=mat3(aMt[0].xy,0,aMt[0].zw,0,aMt[1].xy,1)," +
        "tMt=mat3(aMt[1].zw,0,aMt[2].xy,0,aMt[2].zw,1);" +

      "vec3 " +
        "tPos=vec3(" +
          "clcQd(aPos)," +
          "1" +
        ");" +
      "gl_Position=vec4(mt*tPos,1);" +
      "gl_Position.y*=uFlpY;" +
      "float dt=aDt[1].w;" +
      "vTUv=(tMt*((dt*vec3(aPos,1))+((1.-dt)*tPos))).xy;" + 

      "vTCrop=aMt[3];" +

      "vCl=aDt[0];" +
      "vACl=aDt[1].x;" +

      "vTTp=aDt[1].y;" +
      "vTId=aDt[1].z;" +

      "vTsh=gtTexS(vTId);" +

      (useRepeatTextures
        ? "vRR=aDt[2].xyzw;" +
          "vRR.w=vRR.x+vRR.y;"
        : "") +
    "}";
  }

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

    const getSimpleTexColor = (modCoordName) =>
      "gtTexCl(vTId,vTCrop," + modCoordName + ")";

    return "" +
    Utils.GLSL.DEFINE.Z +
    Utils.GLSL.DEFINE.RADIANS_360 +

    "in float " +
      "vACl," +
      "vTId," +
      "vTTp;" +
    "in vec2 " +
      "vTUv," + 
      "vTsh;" +
    "in vec4 " +
      "vTCrop," +
      "vCl" +
      (useRepeatTextures
        ? ",vRR;"
        : ";") +

    "uniform sampler2D " +
      "uTex[" + maxTextureImageUnits + "];" +

    "out vec4 " +
      "oCl;" +

    "vec4 gtTexCl(float i,vec4 s,vec2 m){" +
      Array(maxTextureImageUnits).fill().map((v, i) => 
      "if(i<" + (i + 1) + ".)" + 
        "return texture(uTex[" + i + "],clamp(s.xy+s.zw*m,s.xy+vTsh,s.xy+s.zw-vTsh));").join("") +
      "return Z.yyyy;" +
    "}" +

    "float cosine(float a,float b,float v){" +
      "v=abs(v);" +
      "v=v<.5?2.*v*v:1.-pow(-2.*v+2.,2.)/2.;" +
      "return a*(1.-v)+b*v;" +
    "}" +

    (useRepeatTextures
      ? Utils.GLSL.RANDOM +
        "vec4 gtClBUv(vec2 st){" +
          "vec2 " +
            "uv=vTUv;" +

          "float " +
            "rnd=rand(floor(uv+st))," +
            "rndDg=rnd*RADIANS_360*vRR.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("mod(uv,Z.yy)") + ";" +
        "}" +

        "float gtRClBUv(vec2 st,vec2 uv){" +
          "float " +
            "rnd=rand(floor(vTUv+st));" +
          "return (1.-(2.*rnd-1.)*vRR.y)*" +
            "cosine(0.,1.,1.-st.x-uv.x)*cosine(0.,1.,1.-st.y-uv.y);" +
        "}"
      : "") +

    "void main(void){" +
      "if(vTId>-1.){" +
        "vec2 " +
          "uv=mod(vTUv,Z.yy);" +

        (useRepeatTextures
          ? "if(vRR.w>0.){" +
              "float " +
                "rca," +
                "rcb," +
                "rcc," +
                "rcd;" +
              "if(vRR.y+vRR.z>0.){" +
                "rca=gtRClBUv(Z.xx,uv);" +
                "rcb=gtRClBUv(Z.yx,uv);" +
                "rcc=gtRClBUv(Z.xy,uv);" +
                "rcd=gtRClBUv(Z.yy,uv);" +
              "}" +
              "oCl=vRR.z>0." +
                "?clamp(" +
                  "gtClBUv(Z.xx)*rca+" +
                  "gtClBUv(Z.yx)*rcb+" +
                  "gtClBUv(Z.xy)*rcc+" +
                  "gtClBUv(Z.yy)*rcd" +
                ",0.,1.)" +
                ":gtClBUv(Z.xy);" +
              "if(vRR.y>0.)" +
                "oCl.a=clamp(" +
                  "oCl.a*(" +
                    "rca+" +
                    "rcb+" +
                    "rcc+" +
                    "rcd" +
                  "),0.,1.);" +
            "}else oCl=" + getSimpleTexColor("uv") + ";"
          : "oCl=" + getSimpleTexColor("uv") + ";") +
      "}else oCl+=1.;" +

      "oCl.a*=vACl;" +

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

      (useTint
        ? "if(vTTp>0.)" +
          "if(vTTp==1.||(vTTp==2.&&oCl.r==oCl.g&&oCl.r==oCl.b))" +
            "oCl*=vCl;" +
          "else if(vTTp==3.)" +
            "oCl=vCl;" +
          "else if(vTTp==4.)" +
            "oCl+=vCl;" 
        : "") +
    "}";
  }
}