Home Reference Source

src/components/TextRenderer.js

import VerticesRenderer from './VerticesRenderer';
import System from '../systems/System';
import { vec4 } from '../utils/gl-matrix';
import { stringToRGBA } from '../utils';

export default class TextRenderer extends VerticesRenderer {

  static factory() {
    return new TextRenderer();
  }

  static get propsTypes() {
    return {
      visible: VerticesRenderer.propsTypes.visible,
      shader: VerticesRenderer.propsTypes.shader,
      overrideUniforms: VerticesRenderer.propsTypes.overrideUniforms,
      overrideSamplers: VerticesRenderer.propsTypes.overrideSamplers,
      layers: VerticesRenderer.propsTypes.layers,
      text: 'string',
      font: 'asset(font)',
      color: 'rgba',
      colorOutline: 'rgba',
      halign: 'enum(left, center, right)',
      valign: 'enum(top, middle, bottom)',
      filtering: 'enum(nearest, linear)'
    };
  }

  get text() {
    return this._text;
  }

  set text(value) {
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._text = value;
    this._rebuild = true;
  }

  get font() {
    return this._font;
  }

  set font(value) {
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    const assets = System.get('AssetSystem');
    if (!assets) {
      throw new Error('There is no registered AssetSystem!');
    }

    const font = assets.get(`font://${value}`);
    if (!font) {
      throw new Error(`There is no font asset loaded: ${value}`);
    }

    this._font = value;
    this._fontData = font.data.descriptor;
    this._rebuild = true;
  }

  get color() {
    return this._color;
  }

  set color(value) {
    if (typeof value === 'string') {
      value = stringToRGBA(value);
    }
    if (!(value instanceof Array) && !(value instanceof Float32Array)) {
      throw new Error('`value` is not type of either Array or Float32Array!');
    }
    if (value.length < 4) {
      throw new Error('`value` array must have at least 4 items!');
    }

    vec4.copy(this._color, value);
    const { overrideUniforms } = this;
    overrideUniforms.uColor = this._color;
    this.overrideUniforms = overrideUniforms;
  }

  get colorOutline() {
    return this._colorOutline;
  }

  set colorOutline(value) {
    if (typeof value === 'string') {
      value = stringToRGBA(value);
    }
    if (!(value instanceof Array) && !(value instanceof Float32Array)) {
      throw new Error('`value` is not type of either Array or Float32Array!');
    }
    if (value.length < 4) {
      throw new Error('`value` array must have at least 4 items!');
    }

    vec4.copy(this._colorOutline, value);
    const { overrideUniforms } = this;
    overrideUniforms.uColorOutline = this._colorOutline;
    this.overrideUniforms = overrideUniforms;
  }

  get halign() {
    return this._halign;
  }

  set halign(value) {
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._halign = value;
    this._rebuild = true;
  }

  get valign() {
    return this._valign;
  }

  set valign(value) {
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._valign = value;
    this._rebuild = true;
  }

  get filtering() {
    return this._filtering;
  }

  set filtering(value) {
    if (typeof value !== 'string') {
      throw new Error('`value` is not type of String!');
    }

    this._filtering = value;

    const { _fontData } = this;
    if (!_fontData) {
      return;
    }

    const { pages } = _fontData;
    const samplers = {};

    for (let i = 0, c = pages.size; i < c; ++i) {
      samplers[`sPage${i}`] = {
        texture: pages.get(i).file,
        filtering: value
      };
    }

    this.overrideSamplers = samplers;
  }

  constructor() {
    super();

    this._text = null;
    this._font = null;
    this._color = vec4.fromValues(1, 1, 1, 1);
    this._colorOutline = vec4.fromValues(0, 0, 0, 1);
    this._halign = 'left';
    this._valign = 'top';
    this._filtering = 'linear';
    this._fontData = null;
    this._rebuild = true;
  }

  dispose() {
    super.dispose();

    this._text = null;
    this._font = null;
    this._fontData = null;
  }

  onAttach() {
    const { overrideUniforms } = this;
    overrideUniforms.uColor = this._color;
    overrideUniforms.uColorOutline = this._colorOutline;
    this.overrideUniforms = overrideUniforms;
  }

  onRender(gl, renderer, deltaTime, layer) {
    this.ensureVertices(renderer);

    const { _text, _fontData } = this;
    if (!!_text && _text !== '' && !!_fontData) {
      super.onRender(gl, renderer, deltaTime);
    }
  }

  onPropertySerialize(name, value) {
    if (name === 'color' || name === 'colorOutline') {
      return [ ...value ];
    } else if (name === 'overrideUniforms') {
      const result = super.onPropertySerialize(name, value);

      if (!result) {
        return null;
      }

      delete result.uColor;
      delete result.uColorOutline;
      return Object.keys(result).length > 0 ? result : null;
    } else if (name === 'overrideSamplers') {
      const result = super.onPropertySerialize(name, value);

      if (!result) {
        return null;
      }

      for (const key in result) {
        if (key.indexOf('sPage') === 0) {
          delete result[key];
        }
      }
      return Object.keys(result).length > 0 ? result : null;
    } else {
      return super.onPropertySerialize(name, value);
    }
  }

  ensureVertices(renderer) {
    if (!this._rebuild) {
      return;
    }

    this._rebuild = false;

    const { _text, _fontData, _halign, _valign, _filtering } = this;
    if (!_text || _text === '' || !_fontData) {
      this.vertices = [0];
      this.indices = [0, 0, 0];
      return;
    }

    const { common, pages, chars } = _fontData;
    const { lineHeight, scaleW, scaleH } = common;
    const vertices = [];
    const indices = [];
    const samplers = {};

    for (let i = 0, c = pages.size; i < c; ++i) {
      samplers[`sPage${i}`] = {
        texture: pages.get(i).file,
        filtering: _filtering
      };
    }

    let x = 0;
    let y = 0;
    let lineStartPos = 0;
    for (let i = 0, c = _text.length, pos = 0; i < c; ++i) {
      const code = _text.charCodeAt(i);
      if (code === 10 || code === 13) {
        const { length } = vertices;

        if (_halign === 'right') {
          for (let j = lineStartPos; j < length; j += 5) {
            vertices[j] -= x;
          }
        } else if (_halign === 'center') {
          for (let j = lineStartPos; j < length; j += 5) {
            vertices[j] -= x * 0.5;
          }
        }

        x = 0;
        y += lineHeight;
        lineStartPos = vertices.length;
        continue;
      }

      const char = chars.get(code);
      if (!char) {
        continue;
      }

      const xs = x + char.xoffset;
      const ys = y + char.yoffset;
      const xe = xs + char.width;
      const ye = ys + char.height;
      const txs = char.x / scaleW;
      const tys = char.y / scaleH;
      const txe = (char.x + char.width) / scaleW;
      const tye = (char.y + char.height) / scaleH;

      vertices.push(
        xs, ys, txs, tys, char.page | 0,
        xe, ys, txe, tys, char.page | 0,
        xe, ye, txe, tye, char.page | 0,
        xs, ye, txs, tye, char.page | 0
      );
      indices.push(
        pos, pos + 1, pos + 2,
        pos + 2, pos + 3, pos
      );
      pos += 4;
      x += char.xadvance;
    }

    const { length } = vertices;

    if (_halign === 'right') {
      for (let j = lineStartPos; j < length; j += 5) {
        vertices[j] -= x;
      }
    } else if (_halign === 'center') {
      for (let j = lineStartPos; j < length; j += 5) {
        vertices[j] -= x * 0.5;
      }
    }

    if (_valign === 'bottom') {
      for (let j = 1; j < length; j += 5) {
        vertices[j] -= y + lineHeight;
      }
    } else if (_valign === 'middle') {
      for (let j = 1; j < length; j += 5) {
        vertices[j] -= (y + lineHeight) * 0.5;
      }
    }

    this.vertices = vertices;
    this.indices = indices;
    this.overrideSamplers = samplers;
  }

}