Source: aftereffect/shapes/CVShapeElement.js

import ShapeTransformManager from './ShapeTransformManager';
import PropertyFactory from './PropertyFactory';
import CVShapeData from './CVShapeData';
import TransformPropertyFactory from './TransformProperty';
import getModifier from './shapes/ShapeModifiers';
import Matrix from './lib/transformation-matrix';

const degToRads = Math.PI/180;

/**
 * a
 * @param {*} element a
 * @param {*} position a
 */
function ProcessedElement(element, position) {
  this.elem = element;
  this.pos = position;
}

/**
 * a
 */
export default class CVShapeElement {
  /**
   * a
   * @param {*} data a
   */
  constructor(data) {
    this.data = data;
    this.shapes = [];
    this.shapesData = data.shapes;
    this.stylesList = [];
    this.itemsData = [];
    this.prevViewData = [];
    this.shapeModifiers = [];
    this.processedElements = [];
    this.transformsManager = new ShapeTransformManager();
    this.lcEnum = {
      '1': 'butt',
      '2': 'round',
      '3': 'square',
    };
    this.ljEnum = {
      '1': 'miter',
      '2': 'round',
      '3': 'bevel',
    };

    // set to true when inpoint is rendered
    this._isFirstFrame = true;
    // list of animated properties
    this.dynamicProperties = [];
    // If layer has been modified in current tick this will be true
    this._mdf = false;

    this.finalTransform = {
      mProp: this.data.ks ? TransformPropertyFactory.getTransformProperty(this, this.data.ks, this) : {o: 0},
      _matMdf: false,
      _opMdf: false,
      mat: new Matrix(),
    };
    if (this.data.ao) {
      this.finalTransform.mProp.autoOriented = true;
    }

    this.transformHelper = { opacity: 1, _opMdf: false };
    this.createContent();
  }

  /**
   * a
   * @param {*} elem a
   */
  searchProcessedElement(elem) {
    let elements = this.processedElements;
    let i = 0;
    const len = elements.length;
    while (i < len) {
      if (elements[i].elem === elem) {
        return elements[i].pos;
      }
      i += 1;
    }
    return 0;
  }


  /**
   * a
   * @param {*} elem a
   * @param {*} pos a
   */
  addProcessedElement(elem, pos) {
    let elements = this.processedElements;
    let i = elements.length;
    while (i) {
      i -= 1;
      if (elements[i].elem === elem) {
        elements[i].pos = pos;
        return;
      }
    }
    elements.push(new ProcessedElement(elem, pos));
  }

  /**
   * a
   * @param {*} data a
   */
  addShapeToModifiers(data) {
    let i;
    const len = this.shapeModifiers.length;
    for (i=0; i<len; i+=1) {
      this.shapeModifiers[i].addShape(data);
    }
  }

  /**
   * a
   * @param {*} num a
   */
  prepareFrame(num) {
    this.prepareProperties(num);
  }

  /**
   * @function
   * Calculates all dynamic values
   *
   * @param {number} num current frame number in Layer's time
   * @param {boolean} isVisible if layers is currently in range
   *
   */
  prepareProperties(num) {
    let i;
    const len = this.dynamicProperties.length;
    for (i = 0; i < len; i += 1) {
      this.dynamicProperties[i].getValue(num);
      if (this.dynamicProperties[i]._mdf) {
        this._mdf = true;
      }
    }
  }

  /**
   * a
   * @param {*} prop a
   */
  addDynamicProperty(prop) {
    if (this.dynamicProperties.indexOf(prop) === -1) {
      this.dynamicProperties.push(prop);
    }
  }

  /**
   * a
   * @param {*} ctx a
   */
  renderFrame(ctx) {
    if (this.data.hd) {
      return;
    }
    this.renderInnerContent(ctx);
    if (this._isFirstFrame) {
      this._isFirstFrame = false;
    }
  }

  /**
   * a
   */
  createContent() {
    this.searchShapes(this.shapesData, this.itemsData, this.prevViewData, true, []);
  }

  /**
   * a
   * @param {*} data a
   * @param {*} transforms a
   * @return {*}
   */
  createStyleElement(data, transforms) {
    const styleElem = {
      data: data,
      type: data.ty,
      preTransforms: this.transformsManager.addTransformSequence(transforms),
      transforms: [],
      elements: [],
      closed: data.hd === true,
    };
    const elementData = {};
    if (data.ty == 'fl' || data.ty == 'st') {
      elementData.c = PropertyFactory(this, data.c, 1, 255, this);
      if (!elementData.c.k) {
        styleElem.co = 'rgb('+Math.floor(elementData.c.v[0])+','+Math.floor(elementData.c.v[1])+','+Math.floor(elementData.c.v[2])+')';
      }
    } else if (data.ty === 'gf' || data.ty === 'gs') {
      elementData.s = PropertyFactory(this, data.s, 1, null, this);
      elementData.e = PropertyFactory(this, data.e, 1, null, this);
      elementData.h = PropertyFactory(this, data.h||{ k: 0 }, 0, 0.01, this);
      elementData.a = PropertyFactory(this, data.a||{ k: 0 }, 0, degToRads, this);
      // elementData.g = new GradientProperty(this, data.g, this);
    }
    elementData.o = PropertyFactory(this, data.o, 0, 0.01, this);
    if (data.ty == 'st' || data.ty == 'gs') {
      styleElem.lc = this.lcEnum[data.lc] || 'round';
      styleElem.lj = this.ljEnum[data.lj] || 'round';
      if (data.lj == 1) {
        styleElem.ml = data.ml;
      }
      elementData.w = PropertyFactory(this, data.w, 0, null, this);
      if (!elementData.w.k) {
        styleElem.wi = elementData.w.v;
      }
      // if (data.d) {
      //   const d = new DashProperty(this, data.d, 'canvas', this);
      //   elementData.d = d;
      //   if (!elementData.d.k) {
      //     styleElem.da = elementData.d.dashArray;
      //     styleElem.do = elementData.d.dashoffset[0];
      //   }
      // }
    } else {
      styleElem.r = data.r === 2 ? 'evenodd' : 'nonzero';
    }
    this.stylesList.push(styleElem);
    elementData.style = styleElem;
    return elementData;
  }

  /**
   * a
   * @param {*} data a
   * @return {*}
   */
  createGroupElement() {
    return {
      it: [],
      prevViewData: [],
    };
  }

  /**
   * a
   * @param {*} data a
   * @return {*}
   */
  createTransformElement(data) {
    return {
      transform: {
        opacity: 1,
        _opMdf: false,
        key: this.transformsManager.getNewKey(),
        op: PropertyFactory(this, data.o, 0, 0.01, this),
        mProps: TransformPropertyFactory.getTransformProperty(this, data, this),
      },
    };
  }

  /**
   * a
   * @param {*} data a
   * @return {*}
   */
  createShapeElement(data) {
    const elementData = new CVShapeData(this, data, this.stylesList, this.transformsManager);

    this.shapes.push(elementData);
    this.addShapeToModifiers(elementData);
    return elementData;
  }

  /**
   * a
   * @param {*} transform a
   */
  addTransformToStyleList(transform) {
    const len = this.stylesList.length;
    for (let i = 0; i < len; i += 1) {
      if (!this.stylesList[i].closed) {
        this.stylesList[i].transforms.push(transform);
      }
    }
  }

  /**
   * a
   */
  removeTransformFromStyleList() {
    const len = this.stylesList.length;
    for (let i = 0; i < len; i += 1) {
      if (!this.stylesList[i].closed) {
        this.stylesList[i].transforms.pop();
      }
    }
  }

  /**
   * a
   * @param {*} styles a
   */
  closeStyles(styles) {
    const len = styles.length;
    for (let i = 0; i < len; i += 1) {
      styles[i].closed = true;
    }
  }

  /**
   * a
   * @param {*} arr a
   * @param {*} itemsData a
   * @param {*} prevViewData a
   * @param {*} shouldRender a
   * @param {*} transforms a
   */
  searchShapes(arr, itemsData, prevViewData, shouldRender, transforms) {
    const len = arr.length - 1;
    const ownStyles = [];
    const ownModifiers = [];
    let processedPos;
    let modifier;
    const ownTransforms = [].concat(transforms);
    for (let i = len; i >= 0; i -= 1) {
      processedPos = this.searchProcessedElement(arr[i]);
      if (!processedPos) {
        arr[i]._shouldRender = shouldRender;
      } else {
        itemsData[i] = prevViewData[processedPos - 1];
      }
      if (arr[i].ty == 'fl' || arr[i].ty == 'st'|| arr[i].ty == 'gf'|| arr[i].ty == 'gs') {
        if (!processedPos) {
          itemsData[i] = this.createStyleElement(arr[i], ownTransforms);
        } else {
          itemsData[i].style.closed = false;
        }
        ownStyles.push(itemsData[i].style);
      } else if (arr[i].ty == 'gr') {
        if (!processedPos) {
          itemsData[i] = this.createGroupElement(arr[i]);
        } else {
          const jLen = itemsData[i].it.length;
          for (let j = 0; j < jLen; j += 1) {
            itemsData[i].prevViewData[j] = itemsData[i].it[j];
          }
        }
        this.searchShapes(arr[i].it, itemsData[i].it, itemsData[i].prevViewData, shouldRender, ownTransforms);
      } else if (arr[i].ty == 'tr') {
        if (!processedPos) {
          const currentTransform = this.createTransformElement(arr[i]);
          itemsData[i] = currentTransform;
        }
        ownTransforms.push(itemsData[i]);
        this.addTransformToStyleList(itemsData[i]);
      } else if (arr[i].ty == 'sh' || arr[i].ty == 'rc' || arr[i].ty == 'el' || arr[i].ty == 'sr') {
        if (!processedPos) {
          itemsData[i] = this.createShapeElement(arr[i]);
        }
      } else if (arr[i].ty == 'tm' || arr[i].ty == 'rd') {
        if (!processedPos) {
          modifier = getModifier(arr[i].ty);
          modifier.init(this, arr[i]);
          itemsData[i] = modifier;
          this.shapeModifiers.push(modifier);
        } else {
          modifier = itemsData[i];
          modifier.closed = false;
        }
        ownModifiers.push(modifier);
      } else if (arr[i].ty == 'rp') {
        if (!processedPos) {
          modifier = getModifier(arr[i].ty);
          itemsData[i] = modifier;
          modifier.init(this, arr, i, itemsData);
          this.shapeModifiers.push(modifier);
          shouldRender = false;
        } else {
          modifier = itemsData[i];
          modifier.closed = true;
        }
        ownModifiers.push(modifier);
      }
      this.addProcessedElement(arr[i], i + 1);
    }
    this.removeTransformFromStyleList();
    this.closeStyles(ownStyles);
    const olen = ownModifiers.length;
    for (let i = 0; i < olen; i += 1) {
      ownModifiers[i].closed = true;
    }
  }

  /**
   * a
   */
  renderModifiers() {
    if (!this.shapeModifiers.length) return;
    const len = this.shapes.length;
    for (let i = 0; i < len; i += 1) {
      this.shapes[i].sh.reset();
    }

    let i = this.shapeModifiers.length - 1;
    for (;i >= 0; i -= 1) {
      this.shapeModifiers[i].processShapes(this._isFirstFrame);
    }
  }

  /**
   * a
   */
  renderInnerContent(ctx) {
    this.transformHelper.opacity = 1;
    this.transformHelper._opMdf = false;
    this.renderModifiers();
    this.transformsManager.processSequences(this._isFirstFrame);
    this.renderShape(this.transformHelper, this.shapesData, this.itemsData, ctx);
  }

  /**
   * a
   * @param {*} parentTransform a
   * @param {*} groupTransform a
   */
  renderShapeTransform(parentTransform, groupTransform) {
    if (parentTransform._opMdf || groupTransform.op._mdf || this._isFirstFrame) {
      groupTransform.opacity = parentTransform.opacity;
      groupTransform.opacity *= groupTransform.op.v;
      groupTransform._opMdf = true;
    }
  }

  /**
   * a
   * @param {*} parentTransform a
   * @param {*} items a
   * @param {*} data a
   * @param {*} ctx a
   */
  renderShape(parentTransform, items, data, ctx) {
    const len = items.length - 1;
    let groupTransform = parentTransform;
    for (let i = len; i >= 0; i -= 1) {
      if (items[i].ty == 'tr') {
        groupTransform = data[i].transform;
        this.renderShapeTransform(parentTransform, groupTransform);
      } else if (items[i].ty == 'sh' || items[i].ty == 'el' || items[i].ty == 'rc' || items[i].ty == 'sr') {
        this.renderPath(items[i], data[i]);
      } else if (items[i].ty == 'fl') {
        this.renderFill(items[i], data[i], groupTransform);
      } else if (items[i].ty == 'st') {
        this.renderStroke(items[i], data[i], groupTransform);
      } else if (items[i].ty == 'gf' || items[i].ty == 'gs') {
        this.renderGradientFill(items[i], data[i], groupTransform);
      } else if (items[i].ty == 'gr') {
        this.renderShape(groupTransform, items[i].it, data[i].it);
      }
    }
    if (ctx) {
      this.drawLayer(ctx);
    }
  }

  /**
   * a
   */
  drawLayer(ctx) {
    const len = this.stylesList.length;
    for (let i = 0; i < len; i+=1) {
      const currentStyle = this.stylesList[i];
      const type = currentStyle.type;

      // Skipping style when
      // Stroke width equals 0
      // style should not be rendered (extra unused repeaters)
      // current opacity equals 0
      // global opacity equals 0
      if (((type === 'st' || type === 'gs') && currentStyle.wi === 0) || !currentStyle.data._shouldRender || currentStyle.coOp === 0) {
        continue;
      }
      ctx.save();
      const elems = currentStyle.elements;
      if (type === 'st' || type === 'gs') {
        ctx.strokeStyle = type === 'st' ? currentStyle.co : currentStyle.grd;
        ctx.lineWidth = currentStyle.wi;
        ctx.lineCap = currentStyle.lc;
        ctx.lineJoin = currentStyle.lj;
        ctx.miterLimit = currentStyle.ml || 0;
      } else {
        ctx.fillStyle = type === 'fl' ? currentStyle.co : currentStyle.grd;
      }
      ctx.globalAlpha = currentStyle.coOp * this.finalTransform.mProp.o.v;
      if (type !== 'st' && type !== 'gs') {
        ctx.beginPath();
      }
      const trProps = currentStyle.preTransforms.finalTransform.props;
      ctx.transform(trProps[0], trProps[1], trProps[4], trProps[5], trProps[12], trProps[13]);
      const jLen = elems.length;
      for (let j = 0; j < jLen; j += 1) {
        if (type === 'st' || type === 'gs') {
          ctx.beginPath();
          if (currentStyle.da) {
            ctx.setLineDash(currentStyle.da);
            ctx.lineDashOffset = currentStyle.do;
          }
        }
        const nodes = elems[j].trNodes;
        const kLen = nodes.length;

        for (let k = 0; k < kLen; k++) {
          if (nodes[k].t == 'm') {
            ctx.moveTo(nodes[k].p[0], nodes[k].p[1]);
          } else if (nodes[k].t == 'c') {
            ctx.bezierCurveTo(nodes[k].pts[0], nodes[k].pts[1], nodes[k].pts[2], nodes[k].pts[3], nodes[k].pts[4], nodes[k].pts[5]);
          } else {
            ctx.closePath();
          }
        }
        if (type === 'st' || type === 'gs') {
          ctx.stroke();
          if (currentStyle.da) {
            ctx.setLineDash(this.dashResetter);
          }
        }
      }
      if (type !== 'st' && type !== 'gs') {
        ctx.fill(currentStyle.r);
      }
      ctx.restore();
    }
  }

  /**
   * a
   * @param {*} pathData a
   * @param {*} itemData a
   */
  renderPath(pathData, itemData) {
    if (pathData.hd !== true && pathData._shouldRender) {
      const len = itemData.styledShapes.length;
      for (let i = 0; i < len; i += 1) {
        this.renderStyledShape(itemData.styledShapes[i], itemData.sh);
      }
    }
  }

  /**
   * a
   * @param {*} styledShape a
   * @param {*} shape a
   */
  renderStyledShape(styledShape, shape) {
    if (this._isFirstFrame || shape._mdf || styledShape.transforms._mdf) {
      const shapeNodes = styledShape.trNodes;
      const paths = shape.paths;
      const jLen = paths._length;
      shapeNodes.length = 0;
      const groupTransformMat = styledShape.transforms.finalTransform;
      for (let j = 0; j < jLen; j += 1) {
        const pathNodes = paths.shapes[j];
        if (pathNodes && pathNodes.v) {
          let i = 1;
          const len = pathNodes._length;
          for (; i < len; i += 1) {
            if (i === 1) {
              shapeNodes.push({
                t: 'm',
                p: groupTransformMat.applyToPointArray(pathNodes.v[0][0], pathNodes.v[0][1], 0),
              });
            }
            shapeNodes.push({
              t: 'c',
              pts: groupTransformMat.applyToTriplePoints(pathNodes.o[i - 1], pathNodes.i[i], pathNodes.v[i]),
            });
          }
          if (len === 1) {
            shapeNodes.push({
              t: 'm',
              p: groupTransformMat.applyToPointArray(pathNodes.v[0][0], pathNodes.v[0][1], 0),
            });
          }
          if (pathNodes.c && len) {
            shapeNodes.push({
              t: 'c',
              pts: groupTransformMat.applyToTriplePoints(pathNodes.o[i - 1], pathNodes.i[0], pathNodes.v[0]),
            });
            shapeNodes.push({
              t: 'z',
            });
          }
        }
      }
      styledShape.trNodes = shapeNodes;
    }
  }

  /**
   * a
   * @param {*} styleData a
   * @param {*} itemData a
   * @param {*} groupTransform a
   */
  renderFill(styleData, itemData, groupTransform) {
    const styleElem = itemData.style;

    if (itemData.c._mdf || this._isFirstFrame) {
      styleElem.co = 'rgb('
      + Math.floor(itemData.c.v[0]) + ','
      + Math.floor(itemData.c.v[1]) + ','
      + Math.floor(itemData.c.v[2]) + ')';
    }
    if (itemData.o._mdf || groupTransform._opMdf || this._isFirstFrame) {
      styleElem.coOp = itemData.o.v * groupTransform.opacity;
    }
  }

  /**
   * a
   * @param {*} styleData a
   * @param {*} itemData a
   * @param {*} groupTransform a
   */
  renderGradientFill(styleData, itemData, groupTransform) {
    const styleElem = itemData.style;
    if (!styleElem.grd || itemData.g._mdf || itemData.s._mdf || itemData.e._mdf || (styleData.t !== 1 && (itemData.h._mdf || itemData.a._mdf))) {
      const ctx = this.renderer;
      let grd;
      const pt1 = itemData.s.v;
      const pt2 = itemData.e.v;
      if (styleData.t === 1) {
        grd = ctx.createLinearGradient(pt1[0], pt1[1], pt2[0], pt2[1]);
      } else {
        const rad = Math.sqrt(Math.pow(pt1[0] - pt2[0], 2) + Math.pow(pt1[1] - pt2[1], 2));
        const ang = Math.atan2(pt2[1] - pt1[1], pt2[0] - pt1[0]);

        const percent = itemData.h.v >= 1 ? 0.99 : itemData.h.v <= -1 ? -0.99: itemData.h.v;
        const dist = rad * percent;
        const x = Math.cos(ang + itemData.a.v) * dist + pt1[0];
        const y = Math.sin(ang + itemData.a.v) * dist + pt1[1];
        grd = ctx.createRadialGradient(x, y, 0, pt1[0], pt1[1], rad);
      }

      const len = styleData.g.p;
      const cValues = itemData.g.c;
      let opacity = 1;

      for (let i = 0; i < len; i += 1) {
        if (itemData.g._hasOpacity && itemData.g._collapsable) {
          opacity = itemData.g.o[i*2 + 1];
        }
        grd.addColorStop(cValues[i * 4] / 100, 'rgba('+ cValues[i * 4 + 1] + ',' + cValues[i * 4 + 2] + ','+cValues[i * 4 + 3] + ',' + opacity + ')');
      }
      styleElem.grd = grd;
    }
    styleElem.coOp = itemData.o.v*groupTransform.opacity;
  }

  /**
   * a
   * @param {*} styleData a
   * @param {*} itemData a
   * @param {*} groupTransform a
   */
  renderStroke(styleData, itemData, groupTransform) {
    const styleElem = itemData.style;
    const d = itemData.d;
    if (d && (d._mdf || this._isFirstFrame)) {
      styleElem.da = d.dashArray;
      styleElem.do = d.dashoffset[0];
    }
    if (itemData.c._mdf || this._isFirstFrame) {
      styleElem.co = 'rgb('+Math.floor(itemData.c.v[0])+','+Math.floor(itemData.c.v[1])+','+Math.floor(itemData.c.v[2])+')';
    }
    if (itemData.o._mdf || groupTransform._opMdf || this._isFirstFrame) {
      styleElem.coOp = itemData.o.v*groupTransform.opacity;
    }
    if (itemData.w._mdf || this._isFirstFrame) {
      styleElem.wi = itemData.w.v;
    }
  }
}