Extensions

Source: renderers/Stage2D.js

import { noop } from "../../common/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 { getCanvasPointerPosition } from "../utils/pointerUtils";
import { BatchRenderer } from "./BatchRenderer";
import { LightRenderer } from "./LightRenderer";

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

// 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;
    config.useInteraction = config.useInteraction ?? true;
    const useDistortion = (config.useDistortion = config.useDistortion ?? true);

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

    super(config);

    const { $MAX_RENDER_COUNT } = this;

    this.container = new StageContainer(this);
    this.$renderingTypes = Object.create(null);
    this.setRenderingType(Item.RENDERING_TYPE, noop);
    this.setRenderingType(Light.RENDERING_TYPE, noop);
    this.setRenderingType(Image.RENDERING_TYPE, this.$drawImage);
    this.setRenderingType(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
    );

    this.$checkMousePositionFv = this.$addDistortionDataFv = this.$uploadDistortionFv = this.$createDistortionFv = noop;

    if (useDistortion) {
      // prettier-ignore
      this.$distortionBuffer = new Buffer(
        "aE",
        $MAX_RENDER_COUNT,
        4,
        2
      );

      this.$addDistortionDataFv = this.$addDistortionData;
      this.$uploadDistortionFv = this.$uploadDistortion;
      this.$createDistortionFv = this.$createDistortion;
    }

    if (config.useInteraction) {
      this.$checkMousePositionFv = this.$checkMousePosition;
      const { canvas } = this.context;

      this.$onMouseEventHandler = this.$onMouseEventHandler.bind(this);
      _INTERACTION_EVENT_TYPES.forEach((type) => canvas.addEventListener(type, this.$onMouseEventHandler));
    }

    const { lightRenderer } = config;
    lightRenderer && this.attachLightRenderer(lightRenderer);
  }

  /**
   * Sets the draw function for a rendering type.
   * @param {string} type
   * @param {function} drawFunction
   * @param {*} scope
   */
  setRenderingType(type, drawFunction, scope = this) {
    this.$renderingTypes[type] = drawFunction.bind(scope);
  }

  /**
   * Attach LightRenderer
   * Recommended to set LightRenderer when using Light
   * @param {LightRenderer} v
   */
  attachLightRenderer(v) {
    this.setRenderingType(Light.RENDERING_TYPE, v.addLightForRender, v);
  }

  /**
   * Detach LightRenderer
   */
  detachLightRenderer() {
    this.setRenderingType(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];
  }

  /**
   * Draws an item: callbackBeforeRender, then update and render, then callbackAfterRender.
   * A non-renderable item (and so its subtree) or one no longer under `parent` (removed or moved
   * in a callback) is neither updated nor rendered; its callbacks still run.
   * @param {Item} item
   * @param {Container} [parent]
   * @ignore
   */
  $drawItem(item, parent) {
    const { $renderTime } = this;

    item.callbackBeforeRender(item, $renderTime);

    if (item.parent === parent && item.renderable) {
      item.update($renderTime);
      this.$renderingTypes[item.RENDERING_TYPE](item);
    }

    item.callbackAfterRender(item, $renderTime);
  }

  /**
   * @param {Container} container
   * @ignore
   */
  $drawContainer(container) {
    const { children } = container;
    let i = -1;

    while (++i < children.length) {
      this.$drawItem(children[i], container);
    }
  }

  /**
   * @param {Image} image
   * @ignore
   */
  $drawImage(image) {
    const { context } = this;

    context.setBlendMode(image.blendMode, this.$batchDraw);

    // Resolved before the slot index: a texture that needs a unit while all are taken flushes the batch.
    const textureId = context.useTexture(image.texture, this.$renderTime, false, this.$batchDraw);
    const { $batchItems } = this;
    const matrixBufferId = $batchItems * 16;
    const matrixBufferData = this.$matrixBuffer.data;
    const { matrixCache, textureMatrixCache, textureCropCache } = image;

    // Fixed-index writes: cheaper than a generic copy loop on the per-image hot path.
    matrixBufferData[matrixBufferId] = matrixCache[0];
    matrixBufferData[matrixBufferId + 1] = matrixCache[1];
    matrixBufferData[matrixBufferId + 2] = matrixCache[2];
    matrixBufferData[matrixBufferId + 3] = matrixCache[3];
    matrixBufferData[matrixBufferId + 4] = matrixCache[4];
    matrixBufferData[matrixBufferId + 5] = matrixCache[5];
    matrixBufferData[matrixBufferId + 6] = textureMatrixCache[0];
    matrixBufferData[matrixBufferId + 7] = textureMatrixCache[1];
    matrixBufferData[matrixBufferId + 8] = textureMatrixCache[2];
    matrixBufferData[matrixBufferId + 9] = textureMatrixCache[3];
    matrixBufferData[matrixBufferId + 10] = textureMatrixCache[4];
    matrixBufferData[matrixBufferId + 11] = textureMatrixCache[5];
    matrixBufferData[matrixBufferId + 12] = textureCropCache[0];
    matrixBufferData[matrixBufferId + 13] = textureCropCache[1];
    matrixBufferData[matrixBufferId + 14] = textureCropCache[2];
    matrixBufferData[matrixBufferId + 15] = textureCropCache[3];
    this.$addImageData(image, $batchItems, textureId);

    ++this.$batchItems === this.$MAX_RENDER_COUNT && this.$batchDraw();
  }

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

    if ($batchItems) {
      // Uniforms belong to the program; switching programs clears the textures and marks them changed.
      if (context.$texturesChanged) {
        context.$texturesChanged = false;
        const textureIdCount = context.textureIds.length;
        if (textureIdCount) {
          $gl.uniform1iv($locations.uB, context.$textureIdData, 0, textureIdCount);
          $gl.uniform2fv($locations.uF, context.$textureSizeData, 0, context.textureSizes.length);
        }
      }
      this.$uploadBuffers();
      this.$drawInstanced($batchItems);
      this.$batchItems = 0;
    }
  }

  /**
   * @ignore
   */
  $onMouseEventHandler(event) {
    this.$currentEvent = event;
    const { canvas } = this.context;
    const pointerPosition = getCanvasPointerPosition(canvas, event);

    this.$setMousePosition(pointerPosition.x, pointerPosition.y);
  }

  /**
   * @param {Image} image
   * @param {number} index
   * @param {number} [textureId] - The texture unit of the image, resolved by `$drawImage`
   * @ignore
   */
  $addImageData(image, index, textureId = this.context.useTexture(image.texture, this.$renderTime, false)) {
    const dataBufferId = index * 12;
    const itemParent = image.parent;
    const dataBufferData = this.$dataBuffer.data;

    this.$checkMousePositionFv(image);

    const { colorCache, textureRepeatRandomCache } = image;

    dataBufferData[dataBufferId] = colorCache[0];
    dataBufferData[dataBufferId + 1] = colorCache[1];
    dataBufferData[dataBufferId + 2] = colorCache[2];
    dataBufferData[dataBufferId + 3] = colorCache[3];
    dataBufferData[dataBufferId + 8] = textureRepeatRandomCache[0];
    dataBufferData[dataBufferId + 9] = textureRepeatRandomCache[1];
    dataBufferData[dataBufferId + 10] = textureRepeatRandomCache[2];
    dataBufferData[dataBufferId + 11] = textureRepeatRandomCache[3];
    dataBufferData[dataBufferId + 4] = image.alpha * itemParent.$premultipliedAlpha;
    dataBufferData[dataBufferId + 5] = image.tintType * itemParent.$premultipliedUseTint;
    dataBufferData[dataBufferId + 6] = textureId;

    this.$addDistortionDataFv(dataBufferData, dataBufferId + 7, image, index);
  }

  $addDistortionData(dataBufferData, id, image, index) {
    dataBufferData[id] = image.distortion.distortTexture;
    // Always written: batch slots are reused, so a skipped write would keep the previous image's distortion.
    const distortionBufferData = this.$distortionBuffer.data;
    const { distortionCache } = image;
    const distortionBufferId = index * 8;

    distortionBufferData[distortionBufferId] = distortionCache[0];
    distortionBufferData[distortionBufferId + 1] = distortionCache[1];
    distortionBufferData[distortionBufferId + 2] = distortionCache[2];
    distortionBufferData[distortionBufferId + 3] = distortionCache[3];
    distortionBufferData[distortionBufferId + 4] = distortionCache[4];
    distortionBufferData[distortionBufferId + 5] = distortionCache[5];
    distortionBufferData[distortionBufferId + 6] = distortionCache[6];
    distortionBufferData[distortionBufferId + 7] = distortionCache[7];
  }

  $checkMousePosition(image) {
    if (this.$isMousePositionSet && image.interactive && image.isContainsPoint(this.$mousePosition)) {
      this.$eventTarget = image;
    }
  }

  /**
   * @ignore
   */
  $render() {
    this.$eventTarget = null;
    this.$drawItem(this.container);
    this.$batchDraw();
    this.$isMousePositionSet = false;
    this.$handleMouseEvent();
  }

  /**
   * @ignore
   */
  $uploadBuffers() {
    const { $gl, $batchItems } = this;

    this.$dataBuffer.uploadElements($gl, $batchItems);
    this.$uploadDistortionFv($gl, $batchItems);
    super.$uploadBuffers();
  }

  $uploadDistortion($gl, $batchItems) {
    this.$distortionBuffer.uploadElements($gl, $batchItems);
  }

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

  $createDistortion($gl, $locations) {
    this.$distortionBuffer.create($gl, $locations);
  }

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

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

    `${Utils.GLSL.BASE_VERTEX_SHADER_ATTRIBUTES}` +
    `${useDistortion
      ? `in mat4x2 ` +
        `aE;`
      : ``}` +
    `in mat3x4 ` +
      `aD;` +
    `in mat4 ` +
      `aB;` +

    `${Utils.GLSL.BASE_VERTEX_SHADER_UNIFORMS}` +

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

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

    `${useDistortion
      ? // Projective weight of corner p (o: the opposite corner): the diagonals split at c,
        // q=(|p-c|+|o-c|)/|o-c|. Parallelograms (also the undistorted quad) are exactly affine
        // with q=1; concave, self-intersecting or degenerate quads fall back to it.
        `float gQ(vec2 p,vec2 o){` +
          `vec2 ` +
            `d0=aE[2]-aE[0],` +
            `d1=aE[3]-aE[1],` +
            `e=aE[1]-aE[0];` +
          `float ` +
            `dn=d0.x*d1.y-d0.y*d1.x;` +
          `if(abs(dn)<1e-6||aE[0]+aE[2]==aE[1]+aE[3])return 1.;` +
          `vec2 ` +
            `tu=vec2(e.x*d1.y-e.y*d1.x,e.x*d0.y-e.y*d0.x)/dn;` +
          `if(any(lessThanEqual(tu,Z.xx))||any(greaterThanEqual(tu,Z.yy)))return 1.;` +
          `vec2 ` +
            `c=aE[0]+tu.x*d0;` +
          `float ` +
            `l=length(o-c);` +
          `return (length(p-c)+l)/l;` +
        `}`
      : ``}` +

    `void main(){` +
      // aA is a unit-quad corner: (0,0) TL, (1,0) TR, (1,1) BR, (0,1) BL are aE[0..3].
      `${useDistortion
        ? `int ` +
            `cI=int(aA.y*3.+aA.x*(1.-2.*aA.y));`
        : ``}` +
      `vec2 ` +
        `tPs=${useDistortion ? `aE[cI]` : `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.);` +

      `${useDistortion ? `vec2 tUv` : `v0`}=(` +
          `mat3(aB[1].zw,0,aB[2].xy,0,aB[2].zw,1)*` +
          `vec3(` +
            `${useDistortion
              ? `mix(` +
                  `tPs,` +
                  `aA,` +
                  `aD[1].w` +
                `)`
              : `aA`},` +
            `1.` +
          `)` +
        `).xy;` +
      `${useDistortion
        ? `float ` +
            `q=mix(1.,gQ(tPs,aE[(cI+2)&3]),aD[1].w);` +
          `v0=vec3(tUv*q,q);`
        : ``}` +
      `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, useDistortion } = $config;
    // With distortion the varying is projective (uv*q,q), divided once in main into tC.
    const uvName = useDistortion ? "tC" : "v0";

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

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

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

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

    `out vec4 ` +
      `oCl;` +

    `${useDistortion
      ? `vec2 ` +
          `tC;`
      : ``}` +

    `vec4 gTxC(float i,vec4 s,vec2 m){` +
      `vec2 cp=${Utils.GLSL.CREATE_ATLAS_CROP({ crop: "s", uv: "m", bias: "v2.zw" })};` +
      `${Utils.GLSL.CREATE_TEXTURE_UNIT_LOOKUP({
        index: "i",
        uv: "cp",
        samplerArray: "uB",
        count: maxTextureImageUnits,
        miss: "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=${uvName};` +

          `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.){` +
        `${useDistortion ? `tC=v0.xy/v0.z;` : ``}` +
        `vec2 ` +
          `uv=fract(${uvName});` +

        `${useRepeatTextures
          ? `if(v5.x>0.||v5.y>0.){` +
              `vec2 ` +
                `c0=floor(${uvName});` +
              `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
        ? Utils.GLSL.TINT_TYPE_SHADER(`v2.x`, `oCl`, `v4`)
        : ``}` +
    `}`;
  }
}