<|start_filename|>src/math.js<|end_filename|>
import { TILE_SIZE } from "./cfg";

/**
 * @param {Number} x
 * @return {Number}
 */
export function zoomScale(x) {
  return (
    x >= 0 ? x + 1 :
    x < 0 ? x + 1 :
    x + 1
  );
};

/**
 * @param {Number} x
 * @return {Boolean}
 */
export function isPowerOfTwo(x) {
  return ((x & (x - 1)) === 0);
};

/**
 * @param {Number} x
 * @param {Number} t
 * @return {Number}
 */
export function roundTo(x, t) {
  const i = 1 / t;
  return (Math.round(x * i) / i);
};

/**
 * @param {Number} value
 * @return {Number}
 */
export function alignToGrid(value) {
  return (roundTo(value, TILE_SIZE));
};

/**
 * @param {Number} x1
 * @param {Number} y1
 * @param {Number} x2
 * @param {Number} y2
 * @return {Number}
 */
export function pointDistance(x1, y1, x2, y2) {
  const xx = Math.pow(x2 - x1, 2);
  const yy = Math.pow(y2 - y1, 2);
  return (Math.sqrt(xx + yy));
};

/**
 * @param {Number} x1
 * @param {Number} y1
 * @param {Number} w1
 * @param {Number} h1
 * @param {Number} x2
 * @param {Number} y2
 * @param {Number} w2
 * @param {Number} h2
 * @return {Boolean}
 */
export function intersectRectangles(x1, y1, w1, h1, x2, y2, w2, h2) {
  x1 = x1 | 0; y1 = y1 | 0;
  w1 = w1 | 0; h1 = h1 | 0;
  x2 = x2 | 0; y2 = y2 | 0;
  w2 = w2 | 0; h2 = h2 | 0;
  const xx = Math.max(x1, x2);
  const ww = Math.min(x1 + w1, x2 + w2);
  const yy = Math.max(y1, y2);
  const hh = Math.min(y1 + h1, y2 + h2);
  return (ww >= xx && hh >= yy);
};


<|start_filename|>src/render/render.js<|end_filename|>
import {
  MODES,
  TILE_SIZE,
  HIDE_GRID,
  MAGIC_SCALE,
  GRID_LINE_WIDTH,
  SELECTION_COLOR,
  ERASE_TILE_COLOR,
  TILE_HOVER_COLOR,
  SELECTION_COLOR_ACTIVE
} from "../cfg";

import { createCanvasBuffer } from "../utils";
import { colorToRgbaString } from "../color";
import { intersectRectangles } from "../math";

export function update() {
  const containers = this.containers;
  for (let ii = 0; ii < containers.length; ++ii) {
    const container = containers[ii];
    const ww = container.bounds.w;
    const hh = container.bounds.h;
    if (this.frames % 32 === 0) {
      const frame = container.animation.frame;
      container.animation.frame = (frame >= (ww * hh) - 1) ? 0 : frame + 1;
    }
    this.redraw = true;
  };
};

export function updateGrid() {
  // only redraw texture if it's absolutely necessary
  if (this.last.cx !== this.cx || this.last.cy !== this.cy) {
    this.redrawGridBuffer();
    this.redraw = true;
  }
};

/** Main render method */
export function render() {
  this.frames++;
  const selection = this.sw !== -0 && this.sh !== -0;
  const cr = this.cr;
  const gl = this.gl;
  const glOpacity = gl.getUniformLocation(this.program, "vOpacity");
  gl.uniform1f(
    glOpacity, 1.0
  );
  // clear foreground buffer
  this.cache.fg.clearRect(0, 0, this.cw, this.ch);
  this.renderBackground();
  //if (this.cr > HIDE_GRID) this.renderGrid();
  // render cached version of our working area
  const cx = this.cx | 0;
  const cy = this.cy | 0;
  // draw global boundings
  if (MODES.DEV) {
    const bounds = this.bounds;
    const x = (cx + ((bounds.x * TILE_SIZE) * cr)) | 0;
    const y = (cy + ((bounds.y * TILE_SIZE) * cr)) | 0;
    const w = (bounds.w * TILE_SIZE) * cr;
    const h = (bounds.h * TILE_SIZE) * cr;
    this.drawRectangle(
      x, y,
      w, h,
      [0, 1, 0, 0.1]
    );
  }
  // render containers
  const containers = this.containers;
  for (let ii = 0; ii < containers.length; ++ii) {
    const container = containers[ii];
    if (!container.visible) continue;
    this.renderContainer(container);
  };
  // render layers
  const layers = this.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const idx = layers.length - 1 - ii;
    const layer = layers[idx];
    if (!layer.visible) continue;
    gl.uniform1f(
      glOpacity, layer.opacity
    );
    const bounds = layer.bounds;
    const ww = (bounds.w * TILE_SIZE) * cr;
    const hh = (bounds.h * TILE_SIZE) * cr;
    const xx = cx + ((layer.x + bounds.x) * TILE_SIZE) * cr;
    const yy = cy + ((layer.y + bounds.y) * TILE_SIZE) * cr;
    this.drawImage(
      layer.batch.texture,
      xx, yy,
      ww, hh
    );
    // don't forget to render live batches
    this.renderLayer(layer);
  };
  gl.uniform1f(
    glOpacity, 1.0
  );
  if (!this.states.drawing && (!this.states.select || !selection)) {
    this.renderHoveredTile();
  }
  if (this.shape !== null) this.renderShapeSelection();
  else if (selection) this.renderSelection();
  if (MODES.DEV) this.renderStats();
  // render foreground buffer
  this.renderForeground();
  this.redraw = false;
};

/**
 * @param {Layer} layer
 */
export function renderLayer(layer) {
  const cx = this.cx | 0;
  const cy = this.cy | 0;
  const cr = this.cr;
  const gl = this.gl;
  const glOpacity = gl.getUniformLocation(this.program, "vOpacity");
  const batches = layer.batches;
  const sindex = this.sindex;
  for (let ii = 0; ii < batches.length; ++ii) {
    const batch = batches[ii];
    const bounds = batch.bounds;
    if (!batch.forceRendering) continue;
    // batch index is higher than stack index, so ignore this batch
    if (sindex - batch.getStackIndex() < 0) {
      if (!batch.forceRendering) continue;
    }
    gl.uniform1f(
      glOpacity, layer.opacity
    );
    //if (!this.boundsInsideView(bounds)) continue;
    const x = (cx + (((layer.x + bounds.x) * TILE_SIZE) * cr)) | 0;
    const y = (cy + (((layer.y + bounds.y) * TILE_SIZE) * cr)) | 0;
    const w = (bounds.w * TILE_SIZE) * cr;
    const h = (bounds.h * TILE_SIZE) * cr;
    // draw batch boundings
    if (MODES.DEV) {
      this.drawRectangle(
        x, y,
        w, h,
        [1, 0, 0, 0.1]
      );
    }
    // erase by alpha blending
    if (batch.isEraser) {
      const gl = this.gl;
      gl.blendFuncSeparate(gl.ONE_MINUS_SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ZERO);
    }
    this.drawImage(
      batch.texture,
      x, y,
      w, h
    );
    // reset blending state
    if (batch.isEraser) {
      const gl = this.gl;
      gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    }
    gl.uniform1f(
      glOpacity, 1.0
    );
  };
  // draw a thin rectangle around active layers
  if (MODES.DEV && layer.isActive) {
    const bounds = layer.bounds;
    const x = (cx + (((layer.x + bounds.x) * TILE_SIZE) * cr));
    const y = (cy + (((layer.y + bounds.y) * TILE_SIZE) * cr));
    const w = (bounds.w * TILE_SIZE) * cr;
    const h = (bounds.h * TILE_SIZE) * cr;
    this.drawStrokedRect(x, y, w, h, "rgba(255,255,255,0.2)");
  }
};

/**
 * @param {Container} container
 */
export function renderContainer(container) {
  container.renderBoundings();
  const ww = container.bounds.w | 0;
  const hh = container.bounds.h | 0;
  for (let ii = 0; ii < ww * hh; ++ii) {
    container.renderFrame(ii);
  };
  container.renderAnimationPreview();
};

export function renderForeground() {
  const texture = this.cache.fgTexture;
  const fg = this.cache.fg;
  const view = fg.canvas;
  this.updateTextureByCanvas(texture, view);
  this.drawImage(
    texture,
    0, 0, view.width, view.height
  );
};

export function renderBackground() {
  this.drawImage(
    this.cache.bg,
    0, 0,
    this.cw, this.ch
  );
};

export function renderGrid() {
  this.drawImage(
    this.cache.gridTexture,
    0, 0,
    this.cw, this.ch
  );
};

export function renderHoveredTile() {
  const cx = this.cx | 0;
  const cy = this.cy | 0;
  const cr = this.cr;
  // apply empty tile hover color
  const mx = this.mx;
  const my = this.my;
  const relative = this.getRelativeTileOffset(mx, my);
  const rx = relative.x * TILE_SIZE;
  const ry = relative.y * TILE_SIZE;
  const x = ((cx + GRID_LINE_WIDTH/2) + (rx * cr)) | 0;
  const y = ((cy + GRID_LINE_WIDTH/2) + (ry * cr)) | 0;
  const ww = (TILE_SIZE * cr) | 0;
  const hh = (TILE_SIZE * cr) | 0;
  this.drawRectangle(
    x, y,
    ww, hh,
    TILE_HOVER_COLOR
  );
};

export function renderSelection() {
  const cx = this.cx | 0;
  const cy = this.cy | 0;
  const cr = this.cr;
  const xx = (cx + (this.sx * TILE_SIZE) * cr) | 0;
  const yy = (cy + (this.sy * TILE_SIZE) * cr) | 0;
  const ww = ((this.sw * TILE_SIZE) * cr) | 0;
  const hh = ((this.sh * TILE_SIZE) * cr) | 0;
  const color = (
    this.states.selecting ?
    SELECTION_COLOR_ACTIVE :
    SELECTION_COLOR
  );
  this.drawRectangle(
    xx, yy,
    ww, hh,
    color
  );
};

export function renderShapeSelection() {
  const cx = this.cx | 0;
  const cy = this.cy | 0;
  const cr = this.cr;
  const batch = this.shape;
  const bounds = batch.bounds;
  const xx = (cx + ((bounds.x * TILE_SIZE) * cr)) | 0;
  const yy = (cy + ((bounds.y * TILE_SIZE) * cr)) | 0;
  const ww = (bounds.w * TILE_SIZE) * cr;
  const hh = (bounds.h * TILE_SIZE) * cr;
  this.drawImage(
    batch.texture,
    xx, yy, ww, hh
  );
};

export function renderStats() {
  const buffer = this.cache.fg;
  const bounds = this.bounds;
  const mx = this.last.mx;
  const my = this.last.my;
  // font style
  buffer.font = "10px Verdana";
  buffer.fillStyle = "#fff";
  // stats
  buffer.fillText(`Mouse: x: ${mx}, y: ${my}`, 8, 16);
  buffer.fillText(`GPU textures: ${Object.keys(this.cache.gl.textures).length}`, 8, 28);
  buffer.fillText(`Boundings: x: ${bounds.x}, y: ${bounds.y}, w: ${bounds.w}, h: ${bounds.h}`, 8, 40);
  buffer.fillText(`Camera scale: ${this.cr}`, 8, 52);
  buffer.fillText(`Stack: ${this.sindex + 1}:${this.stack.length}`, 8, 64);
  // mouse color
  let color = this.getAbsolutePixelAt(mx, my);
  if (color !== null) {
    buffer.fillStyle = colorToRgbaString(color);
    buffer.fillRect(8, 70, 8, 8);
    buffer.fillStyle = "#fff";
    buffer.fillText(`${color[0]}, ${color[1]}, ${color[2]}, ${color[3]}`, 22, 77);
  }
  /*if (MODES.DEV) {
    const bounds = this.bounds;
    const cr = this.cr;
    const xx = ((this.cx | 0) + ((bounds.x * TILE_SIZE) * cr)) | 0;
    const yy = ((this.cy | 0) + ((bounds.y * TILE_SIZE) * cr)) | 0;
    const ww = (bounds.w * TILE_SIZE) * cr;
    const hh = (bounds.h * TILE_SIZE) * cr;
    this.drawResizeRectangle(xx, yy, ww, hh, "#313131");
  }*/
  if (this.sw !== 0 && this.sh !== 0) {
    this.drawSelectionShape();
  }
};

export function drawSelectionShape() {
  const cr = this.cr;
  const s = this.getSelection();
  const xx = ((this.cx | 0) + ((s.x * TILE_SIZE) * cr)) | 0;
  const yy = ((this.cy | 0) + ((s.y * TILE_SIZE) * cr)) | 0;
  const ww = (s.w * TILE_SIZE) * cr;
  const hh = (s.h * TILE_SIZE) * cr;
  const size = TILE_SIZE * cr;
  const buffer = this.cache.fg;
  buffer.strokeStyle = "rgba(255,255,255,0.7)";
  buffer.lineWidth = 0.45 * cr;
  buffer.setLineDash([size, size]);
  buffer.strokeRect(
    xx, yy,
    ww, hh
  );
  buffer.setLineDash([0, 0]);
};

/**
 * Draws a stroked rectangle
 * @param {Number} x
 * @param {Number} y
 * @param {Number} w
 * @param {Number} h
 * @param {String} color
 */
export function drawStrokedRect(x, y, w, h, color) {
  const cr = this.cr;
  const lw = Math.max(0.55, 0.55 * cr);
  const buffer = this.cache.fg;
  buffer.strokeStyle = color;
  buffer.lineWidth = lw;
  // main rectangle
  buffer.strokeRect(
    x - (lw / 2), y - (lw / 2),
    w + lw, h + lw
  );
};

/**
 * Draw resizable rectangle around given rectangle corners
 * @param {Number} x
 * @param {Number} y
 * @param {Number} w
 * @param {Number} h
 * @param {String} color
 */
export function drawResizeRectangle(x, y, w, h, color) {
  const cr = this.cr;
  const ww = 4 * cr;
  const hh = 4 * cr;
  const buffer = this.cache.fg;
  buffer.strokeStyle = color;
  buffer.lineWidth = Math.max(0.4, 0.45 * cr);
  // main rectangle
  buffer.strokeRect(
    x, y,
    w, h
  );
  buffer.lineWidth = Math.max(0.4, 0.3 * cr);
  // left rectangle
  buffer.strokeRect(
    x - ww, (y + (h / 2) - hh / 2),
    ww, hh
  );
  // right rectangle
  buffer.strokeRect(
    x + w, (y + (h / 2) - hh / 2),
    ww, hh
  );
  // top rectangle
  buffer.strokeRect(
    (x + (w / 2) - ww / 2), y - hh,
    ww, hh
  );
  // bottom rectangle
  buffer.strokeRect(
    (x + (w / 2) - ww / 2), (y + h),
    ww, hh
  );
};


<|start_filename|>src/batch/filter/smoothing.js<|end_filename|>

/**
 * Remove L shaped corners
 * http://deepnight.net/pixel-perfect-drawing/
 * @param {Batch} batch
 */
export function applyPixelSmoothing(batch) {
  const bw = batch.bounds.w;
  const bh = batch.bounds.h;
  const tiles = batch.data;
  for (let ii = 0; ii < tiles.length; ii += 4) {
    if (!(ii > 0 && ii + 1 < tiles.length)) continue;
    const x = (ii % bw);
    const y = (ii / bw) | 0;
    const px = (yy * bw + xx) * 4;
    if (
      (w.x === o.x  || w.y === o.y) &&
      (e.x === o.x  || e.y === o.y) &&
      (w.x !== e.x) && (w.y !== e.y)
    ) {
      tiles[ii + 0] = 0;
      tiles[ii + 1] = 0;
      tiles[ii + 2] = 0;
      tiles[ii + 3] = 0;
    }
  };
};


<|start_filename|>src/layer/index.js<|end_filename|>
import extend from "../extend";
import {
  uid,
  createCanvasBuffer
} from "../utils";
import { getRainbowColor } from "../color";

import Batch from "../batch/index";
import Boundings from "../bounds/index";

import * as _matrix from "./matrix";

/**
 * @class {Layer}
 */
class Layer {
  /**
   * @param {Poxi} instance
   * @constructor
   */
  constructor(instance) {
    this.id = uid();
    this.instance = instance;
    // position
    this.x = 0;
    this.y = 0;
    // references layers inference colors
    this.color = { value: null };
    // last boundings
    this.last = { x: 0, y: 0, w: 0, h: 0 };
    // we can name layers
    this.index = this.generateLayerNameIndex();
    this._name = "Layer " + this.index;
    // reference to ui node
    this.node = null;
    // reference (clone) to master layer
    this.reference = null;
    // opacity applied over local batches
    this._opacity = 1.0;
    // layer batch matrix
    this.batch = null;
    // batches we hold here
    this.batches = [];
    // relative boundings
    this.bounds = new Boundings();
    // layer states get/set base
    this._visible = true;
    this._locked = false;
    this.allocateLayerMatrix();
  }
  /**
   * @return {String}
   */
  get name() {
    return (this._name);
  }
  /**
   * @param {String}
   */
  set name(value) {
    this._name = value;
    const node = this.node.querySelector(".layer-text");
    node.value = value;
  }
  /**
   * @return {Number}
   */
  get opacity() {
    return (this._opacity);
  }
  /**
   * @param {Number}
   */
  set opacity(value) {
    this._opacity = value;
    this.instance.redraw = true;
  }
  /**
   * @return {Boolean}
   */
  get visible() {
    return (this._visible);
  }
  /**
   * @param {Boolean} state
   */
  set visible(state) {
    this._visible = state;
    this.instance.redraw = true;
    const node = this.node.querySelector(".layer-item-visible");
    node.src = state ? "assets/img/visible.png" : "assets/img/invisible.png";
  }
  /**
   * @return {Boolean}
   */
  get locked() {
    return (this._locked);
  }
  /**
   * @param {Boolean} state
   */
  set locked(state) {
    this._locked = state;
    this.instance.redraw = true;
    const node = this.node.querySelector(".layer-item-locked");
    node.src = state ? "assets/img/locked.png" : "assets/img/unlocked.png";
  }
  /**
   * Returns if layer is active
   * @return {Boolean}
   */
  get isActive() {
    const current = this.instance.getCurrentLayer();
    return (current === this);
  }
  /**
   * Indicates if layer is a reference (absolute or referenced)
   * @return {Boolean}
   */
  get isReference() {
    return (
      (this.getReferencedLayers().length > 0 || this.reference !== null)
    );
  }
};

/**
 * @return {Layer}
 */
Layer.prototype.clone = function() {
  const layer = new Layer(this.instance);
  const batch = this.batch.clone();
  layer.last = Object.assign(layer.last);
  layer.opacity = this.opacity;
  layer.bounds = this.bounds.clone();
  layer.x = this.x; layer.y = this.y;
  layer.batch = batch;
  layer.batches.push(batch);
  layer._visible = this.visible;
  layer._locked = this.locked;
  return (layer);
};

Layer.prototype.cloneByReference = function() {
  if (this.color.value === null) {
    this.color.value = getRainbowColor();
    this.removeUiReference();
    this.addUiReference();
  }
  const layer = new Layer(this.instance);
  layer.last = Object.assign(layer.last);
  layer.opacity = this.opacity;
  layer.bounds = this.bounds;
  layer.x = this.x; layer.y = this.y;
  layer.batch = this.batch;
  layer.batches = this.batches;
  //layer.reference = this.reference || this;
  layer.reference = this;
  layer.color = this.color;
  layer._visible = this.visible;
  layer._locked = this.locked;
  return (layer);
};

/**
 * Walks through referenced layers until
 * the absolute layer reference is found
 * @return {Layer}
 */
Layer.prototype.getAbsoluteReference = function() {
  let layer = this.reference;
  while (true) {
    if (layer.reference === null) {
      return (layer);
    }
    layer = layer.reference;
  };
  return (null);
};

/**
 * @return {Array}
 */
Layer.prototype.getReferencedLayers = function() {
  const layers = this.instance.layers;
  const references = [];
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    if (layer === this) continue;
    if (layer.reference !== null) {
      if (layer.reference === this) references.push(layer);
    }
  };
  return (references);
};

Layer.prototype.allocateLayerMatrix = function() {
  const instance = this.instance;
  this.batch = instance.createDynamicBatch(0, 0);
  // add reference to unused layer so we can use
  // the batch matrix logic for our layers too
  // but without including layer x,y in calculations
  this.batch.layer = instance.cache.layer;
};

/**
 * @return {Number}
 */
Layer.prototype.getIndex = function() {
  const layers = this.instance.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    if (this.id === layer.id) return (ii);
  };
  return (-1);
};

/**
 * @return {Boolean}
 */
Layer.prototype.isEmpty = function() {
  return (this.batch.isEmpty());
};

Layer.prototype.removeFromLayers = function() {
  const layers = this.instance.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    if (this.id === layer.id) layers.splice(ii, 1);
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Batch}
 */
Layer.prototype.createBatchAt = function(x, y) {
  const dx = (x - this.x) | 0;
  const dy = (y - this.y) | 0;
  const batch = new Batch(this.instance);
  batch.prepareMatrix(dx, dy);
  this.addBatch(batch);
  return (batch);
};

/**
 * Push batch and auto update layer boundings
 * @param {Batch} batch
 */
Layer.prototype.addBatch = function(batch) {
  batch.layer = this;
  this.batches.push(batch);
};

/**
 * @param {Number} id
 * @return {Number}
 */
Layer.prototype.getBatchById = function(id) {
  let result = null;
  const batches = this.batches;
  for (let ii = 0; ii < batches.length; ++ii) {
    const batch = batches[ii];
    if (batch.id === id) {
      result = batch;
      break;
    }
  };
  return (result);
};

/**
 * Returns the layer position
 * relative to our stage
 * @return {Object}
 */
Layer.prototype.getRelativePosition = function() {
  const sx = this.instance.bounds.x;
  const sy = this.instance.bounds.y;
  const x = (this.x + this.bounds.x) - sx;
  const y = (this.y + this.bounds.y) - sy;
  return ({ x, y });
};

/**
 * Fill imagedata with pixels then
 * put it into a canvas and return it
 * @return {CanvasRenderingContext2D}
 */
Layer.prototype.toCanvasBuffer = function() {
  // TODO: also draw batch[n].forceRendering for live previews
  const data = this.batch.data;
  const lw = this.bounds.w | 0;
  const lh = this.bounds.h | 0;
  const buffer = createCanvasBuffer(lw, lh);
  // prevent imagedata construction from failing
  if (lw <= 0 || lh <= 0) return (buffer);
  const img = new ImageData(lw, lh);
  const idata = img.data;
  for (let ii = 0; ii < data.length; ii += 4) {
    const alpha = data[ii + 3] | 0;
    if (alpha <= 0) continue;
    idata[ii + 0] = data[ii + 0] | 0;
    idata[ii + 1] = data[ii + 1] | 0;
    idata[ii + 2] = data[ii + 2] | 0;
    idata[ii + 3] = alpha;
  };
  buffer.putImageData(img, 0, 0);
  return (buffer);
};

/**
 * Auto generates a layer index name
 * and filles missing layer indices
 * @return {Number}
 */
Layer.prototype.generateLayerNameIndex = function() {
  const layers = this.instance.layers;
  // clone, take numeric index, sort ascending, es6 left its muck here
  const sorted = layers.concat().map((item) => item.index).sort((a, b) => a - b);
  for (let ii = 0; ii < sorted.length; ++ii) {
    if (sorted.indexOf(ii) < 0) return (ii);
  };
  return (layers.length);
};

/**
 * Returns the string version of the dom node
 * @return {String}
 */
Layer.prototype.generateUiNode = function() {
  const html = `
    <div class="layer-item">
      <img class="layer-item-visible" src="assets/img/visible.png">
      <img class="layer-item-locked" src="assets/img/unlocked.png">
      <input class="layer-text" value="${this.name}" readonly />
    </div>
  `;
  return (html);
};

/**
 * Returns color of the layer node
 * @return {String}
 */
Layer.prototype.getUiNodeColor = function() {
  // only attach color to layer if
  // layer is a absolute reference or is a reference
  const count = this.isReference;
  const cc = this.color.value;
  const color = (
    cc && count ? `rgba(${cc[0]},${cc[1]},${cc[2]},0.1)` : ""
  );
  return (color);
};

/**
 * yuck in here, yuck in here
 */
Layer.prototype.addUiReference = function() {
  const tmpl = this.generateUiNode();
  const parser = new DOMParser();
  const html = parser.parseFromString(tmpl, "text/html").querySelector(".layer-item");
  const index = this.getIndex();
  const ctx = window.layers;
  if (index >= ctx.children.length) {
    ctx.appendChild(html);
  } else {
    ctx.insertBefore(html, ctx.children[index]);
  }
  // save reference to inserted layer node
  this.node = html;
  html.style.backgroundColor = this.getUiNodeColor();
  if (this.isActive) {
    this.instance.setActiveLayer(this);
  }
  this.locked = this.locked;
  this.visible = this.visible;
};

Layer.prototype.removeUiReference = function() {
  this.node.parentNode.removeChild(this.node);
  this.node = null;
};

extend(Layer, _matrix);

export default Layer;


<|start_filename|>src/batch/tile.js<|end_filename|>
import { isPowerOfTwo } from "../math";

import {
  colorToRgbaString,
  rgbAlphaToAlphaByte
} from "../color";

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Number} size
 * @param {Array} color 
 */
export function drawAt(x, y, size, color) {
  const xpad = Math.floor(size / 2);
  const ypad = Math.floor(size / 2);
  this.fillRect(
    x - xpad, y - ypad,
    size + xpad, size + ypad,
    color
  );
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Number} w
 * @param {Number} h
 * @param {Array} color
 */
export function fillRect(x, y, w, h, color) {
  for (let ii = 0; ii < w * h; ++ii) {
    const xx = (ii % w) + x;
    const yy = ((ii / w) | 0) + y;
    this.drawPixel(xx, yy, color);
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Array} color
 */
export function drawPixel(x, y, color) {
  this.resizeByOffset(x, y);
  this.drawPixelFast(x, y, color);
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Array} color
 */
export function drawPixelFast(x, y, color) {
  const data = this.data;
  const rdata = this.reverse;
  const dx = x - this.layer.x;
  const dy = y - this.layer.y;
  const xx = dx - this.bounds.x;
  const yy = dy - this.bounds.y;
  const idx = 4 * (yy * this.bounds.w + xx);
  const pixel = this.layer.getPixelAt(x, y);
  // save earlier pixel state into reverse matrix
  if (rdata[idx + 3] <= 0 && pixel !== null) {
    rdata[idx + 0] = pixel[0];
    rdata[idx + 1] = pixel[1];
    rdata[idx + 2] = pixel[2];
    rdata[idx + 3] = rgbAlphaToAlphaByte(pixel[3]);
  }
  // overwrite pixel
  data[idx + 0] = color[0];
  data[idx + 1] = color[1];
  data[idx + 2] = color[2];
  data[idx + 3] = rgbAlphaToAlphaByte(color[3]);
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Number} size
 */
export function clearAt(x, y, size) {
  const xpad = Math.floor(size / 2);
  const ypad = Math.floor(size / 2);
  this.clearRect(
    x - xpad, y - ypad,
    size + xpad, size + ypad,
    color
  );
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Number} w
 * @param {Number} h
 */
export function clearRect(x, y, w, h) {
  for (let ii = 0; ii < w * h; ++ii) {
    const xx = (ii % w) + x;
    const yy = ((ii / w) | 0) + y;
    this.erasePixel(xx, yy);
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Void}
 */
export function erasePixel(x, y) {
  const pixel = this.layer.getPixelAt(x, y);
  // nothing to erase
  if (pixel === null) return;
  this.resizeByOffset(x, y);
  this.erasePixelFast(x, y, pixel);
  return;
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Array} pixel - Earlier pixel
 */
export function erasePixelFast(x, y, pixel) {
  const data = this.data;
  const rdata = this.reverse;
  const dx = x - this.layer.x;
  const dy = y - this.layer.y;
  const xx = dx - this.bounds.x;
  const yy = dy - this.bounds.y;
  const idx = 4 * (yy * this.bounds.w + xx);
  // save old pixel into reverse matrix if not set yet
  if (rdata[idx + 3] <= 0) {
    rdata[idx + 0] = pixel[0];
    rdata[idx + 1] = pixel[1];
    rdata[idx + 2] = pixel[2];
    rdata[idx + 3] = rgbAlphaToAlphaByte(pixel[3]);
  }
  // reset pixel data
  data[idx + 0] = 255;
  data[idx + 1] = 255;
  data[idx + 2] = 255;
  data[idx + 3] = 255;
};


<|start_filename|>src/env/insert.js<|end_filename|>
import { SETTINGS } from "../cfg";
import { alignToGrid } from "../math";

import {
  getRainbowColor,
  alphaByteToRgbAlpha
} from "../color";

import CommandKind from "../stack/kind";

/**
 * @param {CanvasRenderingContext2D} ctx
 * @param {Number} x
 * @param {Number} y
 * @return {Void}
 */
export function insertImage(ctx, x, y) {
  const layer = this.getCurrentLayer();
  if (layer === null) return;
  const batch = layer.createBatchAt(x, y);
  const view = ctx.canvas;
  const width = view.width; const height = view.height;
  const data = ctx.getImageData(0, 0, width, height).data;
  const ww = width - 1; const hh = height - 1;
  batch.resizeRectangular(
    x, y,
    width - 1, height - 1
  );
  let count = 0;
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % width) | 0;
    const yy = (idx / width) | 0;
    const px = (yy * width + xx) * 4;
    const r = data[px + 0];
    const g = data[px + 1];
    const b = data[px + 2];
    const a = data[px + 3];
    if (a <= 0) continue;
    batch.drawPixelFast(x + xx, y + yy, [r, g, b, alphaByteToRgbAlpha(a)]);
    count++;
  };
  // nothing changed
  if (count <= 0) {
    batch.kill();
    return;
  }
  batch.refreshTexture(true);
  batch.resizeByMatrixData();
  this.enqueue(CommandKind.INSERT_IMAGE, batch);
  return;
};

/**
 * @param {Number} x0
 * @param {Number} y0
 * @param {Number} x1
 * @param {Number} y1
 */
export function insertLine(x0, y0, x1, y1) {
  const base = 8 * this.cr;
  const batch = this.getCurrentDrawingBatch();
  const dx = Math.abs(x1 - x0); const dy = Math.abs(y1 - y0);
  const sx = (x0 < x1) ? 1 : -1; const sy = (y0 < y1) ? 1 : -1;
  let err = (dx - dy);
  while (true) {
    const relative = this.getRelativeTileOffset(x0, y0);
    // TODO: limit repeation rate on brush size (take modulo)
    if (this.states.drawing) {
      batch.drawAt(relative.x, relative.y, SETTINGS.PENCIL_SIZE, this.fillStyle);
    }
    else if (this.states.erasing) {
      batch.clearAt(relative.x, relative.y, SETTINGS.ERASER_SIZE);
    }
    else if (this.states.lighting) {
      batch.applyColorLightness(relative.x, relative.y, SETTINGS.LIGHTING_MODE);
    }
    else if (this.states.stroke) {
      batch.drawAt(x0, y0, SETTINGS.PENCIL_SIZE, this.fillStyle);
    }
    if (x0 === x1 && y0 === y1) break;
    const e2 = 2 * err;
    if (e2 > -dy) { err -= dy; x0 += sx; }
    if (e2 < dx) { err += dx; y0 += sy; }
  };
};

/**
 * Inserts filled arc at given position
 * @param {Batch} batch
 * @param {Number} x
 * @param {Number} y
 * @param {Number} radius
 * @param {Array} color
 */
export function fillArc(batch, x, y, radius, color) {
  radius = (radius || 1.0) | 0;
  if (!color) color = [255, 255, 255, 1];
  this.insertStrokedArc(batch, x, y, radius, color);
  // TODO: now fill the stroked circle (with fill?)
};

/**
 * Inserts stroked arc at given position
 * @param {Batch} batch
 * @param {Number} x
 * @param {Number} y
 * @param {Number} radius
 * @param {Array} color
 */
export function strokeArc(batch, x, y, radius, color) {
  radius = (radius || 1.0) | 0;
  if (!color) color = [255, 255, 255, 1];
  this.insertStrokedArc(batch, x, y, radius, color);
};

/**
 * Inserts filled arc at given position
 * @param {Batch} batch
 * @param {Number} x1
 * @param {Number} y1
 * @param {Number} radius
 * @param {Array} color
 */
export function insertStrokedArc(batch, x1, y1, radius, color) {
  let x2 = radius;
  let y2 = 0;
  let err = 0;
  const size = SETTINGS.PENCIL_SIZE;
  for (;x2 >= y2;) {
    batch.drawAt(x2 + x1, y2 + y1, size, color);
    batch.drawAt(y2 + x1, x2 + y1, size, color);
    batch.drawAt(-x2 + x1, y2 + y1, size, color);
    batch.drawAt(-y2 + x1, x2 + y1, size, color);
    batch.drawAt(-x2 + x1, -y2 + y1, size, color);
    batch.drawAt(-y2 + x1, -x2 + y1, size, color);
    batch.drawAt(x2 + x1, -y2 + y1, size, color);
    batch.drawAt(y2 + x1, -x2 + y1, size, color);
    if (err <= 0) {
      y2 += 1;
      err += 2 * y2 + 1;
    }
    if (err > 0) {
      x2 -= 1;
      err -= 2 * x2 + 1;
    }
  };
};

/**
 * Inserts filled rectangle at given position
 * @param {Batch} batch
 * @param {Number} x
 * @param {Number} y
 * @param {Number} width
 * @param {Number} height
 * @param {Array} color
 */
export function fillRect(batch, x, y, width, height, color) {
  if (!color) color = [255, 255, 255, 1];
  this.insertRectangleAt(
    batch,
    x | 0, y | 0,
    width | 0, height | 0,
    color, true
  );
};

/**
 * Inserts stroked rectangle at given position
 * @param {Batch} batch
 * @param {Number} x
 * @param {Number} y
 * @param {Number} width
 * @param {Number} height
 * @param {Array} color
 */
export function strokeRect(batch, x, y, width, height, color) {
  if (!color) color = [255, 255, 255, 1];
  this.insertRectangleAt(
    batch,
    x | 0, y | 0,
    width | 0, height | 0,
    color, false
  );
};

/**
 * Inserts rectangle at given position
 * @param {Batch} batch
 * @param {Number} x1
 * @param {Number} y1
 * @param {Number} x2
 * @param {Number} y2
 * @param {Array} color
 * @param {Boolean} filled
 */
export function insertRectangleAt(batch, x1, y1, x2, y2, color, filled) {
  const width = Math.abs(x2);
  const height = Math.abs(y2);
  const dx = (x2 < 0 ? -1 : 1);
  const dy = (y2 < 0 ? -1 : 1);
  const size = SETTINGS.PENCIL_SIZE;
  // stroke rectangle
  if (!filled) {
    for (let ii = 0; ii < width * height; ++ii) {
      const xx = (ii % width) | 0;
      const yy = (ii / width) | 0;
      if (!(
        (xx === 0 || xx >= width-1) ||
        (yy === 0 || yy >= height-1))
      ) continue;
      batch.drawAt(x1 + xx * dx, y1 + yy * dy, size, color);
    };
  // filled rectangle
  } else {
    for (let ii = 0; ii < width * height; ++ii) {
      const xx = (ii % width) | 0;
      const yy = (ii / width) | 0;
      batch.drawAt(x1 + xx * dx, y1 + yy * dy, size, color);
    };
  }
};


<|start_filename|>src/storage/read.js<|end_filename|>
import {
  STORAGE_KEY,
  STORAGE_OBJECT
} from "../cfg";

/**
 * @param {String} key 
 * @return {String}
 */
export function readStorage(key) {
  const access = `${STORAGE_KEY}::${key}`;
  const value = STORAGE_OBJECT.getItem(access);
  return (value || "");
};


<|start_filename|>src/transform/flip.js<|end_filename|>
/**
 * @param {Layer} layer
 */
export function flipHorizontally(layer) {
  const batch = layer.batch;
  const data = batch.data;
  const ww = batch.bounds.w;
  const hh = batch.bounds.h;
  const pixels = new Uint8Array(data.length);
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % ww) | 0;
    const yy = (idx / ww) | 0;
    const opx = 4 * (yy * ww + xx);
    const npx = 4 * ((ww - xx) + (yy * ww) - 1);
    pixels[opx + 0] = data[npx + 0];
    pixels[opx + 1] = data[npx + 1];
    pixels[opx + 2] = data[npx + 2];
    pixels[opx + 3] = data[npx + 3];
  };
  batch.data = pixels;
  batch.refreshTexture(true);
};

/**
 * @param {Layer} layer
 */
export function flipVertically(layer) {
  const batch = layer.batch;
  const data = batch.data;
  const ww = batch.bounds.w;
  const hh = batch.bounds.h;
  const pixels = new Uint8Array(data.length);
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % ww) | 0;
    const yy = (idx / ww) | 0;
    const opx = 4 * (yy * ww + xx);
    const npx = 4 * (((hh - yy - 1) * ww) + xx);
    pixels[opx + 0] = data[npx + 0];
    pixels[opx + 1] = data[npx + 1];
    pixels[opx + 2] = data[npx + 2];
    pixels[opx + 3] = data[npx + 3];
  };
  batch.data = pixels;
  batch.refreshTexture(true);
};


<|start_filename|>src/stack/undo.js<|end_filename|>
import CommandKind from "./kind";

/**
 * @return {Void}
 */
export function undo() {
  // prevent undo/redo when in e.g drawing state
  if (this.isInActiveState()) return;
  if (this.sindex >= 0) {
    const cmd = this.currentStackOperation();
    this.fire(cmd, false);
    this.sindex--;
  }
  this.refreshUiLayers();
  this.updateGlobalBoundings();
  this.redraw = true;
  return;
};

/**
 * Dequeue items from stack
 * @param {Number} from
 * @param {Number} to
 */
export function dequeue(from, to) {
  from = from + 1;
  const count = (to - (from - 1));
  for (let ii = count; ii > 0; --ii) {
    const idx = from + ii - 1;
    const cmd = this.stack[idx];
    const kind = this.getCommandKind(cmd);
    switch (kind) {
      case CommandKind.BATCH_OPERATION:
        cmd.batch.kill();
      break;
      case CommandKind.LAYER_OPERATION:
        if (cmd.kind === CommandKind.LAYER_MERGE) {
          cmd.batch.data.kill();
        }
      break;
    };
    this.stack.splice(idx, 1);
  };
};


<|start_filename|>src/storage/write.js<|end_filename|>
import {
  STORAGE_KEY,
  STORAGE_OBJECT
} from "../cfg";

/**
 * @param {String} key 
 * @param {String} value 
 */
export function writeStorage(key, value) {
  const access = `${STORAGE_KEY}::${key}`;
  STORAGE_OBJECT.setItem(access, value);
};

/**
 * @param {String} key 
 * @param {String} value 
 */
export function appendStorage(key, value) {
  const access = `${STORAGE_KEY}::${key}`;
  const base = this.readStorage(key);
  this.writeStorage(key, base + value);
};


<|start_filename|>src/color.js<|end_filename|>
import { MAGIC_RGB_A_BYTE } from "./cfg";

/**
 * 0-255 => 0-1 with precision 1
 * @param {Number} a
 * @return {Number}
 */
export function alphaByteToRgbAlpha(a) {
  return ((a * MAGIC_RGB_A_BYTE) * 10) / 10;
};

/**
 * 0-1 => 0-255
 * Derivative of alphaByteToRgbAlpha
 * @param {Number} a
 * @return {Number}
 */
export function rgbAlphaToAlphaByte(a) {
  return ((a / MAGIC_RGB_A_BYTE) * 10) / 10;
};

/**
 * Convert rgba to rgba byte color
 * @param {Array} rgba
 * @return {Array}
 */
export function rgbaToBytes(rgba) {
  const r = rgba[0] / 255;
  const g = rgba[1] / 255;
  const b = rgba[2] / 255;
  const a = rgba[3];
  return ([r, g, b, a]);
};

/**
 * Convert bytes to rgba color
 * @param {Array} bytes
 * @return {Array}
 */
export function bytesToRgba(bytes) {
  const r = bytes[0] * 255;
  const g = bytes[1] * 255;
  const b = bytes[2] * 255;
  const a = bytes[3];
  return ([r, g, b, a]);
};

/**
 * @param {Uint8Array} aa
 * @param {Uint8Array} bb
 * @param {Number} dir
 * @return {Uint8Array}
 */
export function rgbaDifference(aa, bb, dir) {
  const rgba = new Uint8Array(4);
  rgba[0] = aa[0] + bb[0] * dir;
  rgba[1] = aa[1] + bb[1] * dir;
  rgba[2] = aa[2] + bb[2] * dir;
  rgba[3] = aa[3] + bb[3] * dir;
  return (rgba);
};

/**
 * Additive color blending with alpha support
 * @param {Uint8Array} src
 * @param {Uint8Array} dst
 * @return {Uint8Array}
 */
export function additiveAlphaColorBlending(src, dst) {
  const a1 = ((src[3] * MAGIC_RGB_A_BYTE) * 10) / 10;
  const a2 = ((dst[3] * MAGIC_RGB_A_BYTE) * 10) / 10;
  const a = 1 - (1 - a2) * (1 - a1);
  src[0] = ((dst[0] * a2 / a) + (src[0] * a1 * (1 - a2) / a)) | 0;
  src[1] = ((dst[1] * a2 / a) + (src[1] * a1 * (1 - a2) / a)) | 0;
  src[2] = ((dst[2] * a2 / a) + (src[2] * a1 * (1 - a2) / a)) | 0;
  src[3] = (((a / MAGIC_RGB_A_BYTE) * 10) / 10) | 0;
  return (src);
};

/**
 * @param {Uint8Array} rgba
 * @return {Uint8Array}
 */
export function invertColors(rgba) {
  const data = new Uint8Array(rgba.length);
  for (let ii = 0; ii < rgba.length; ii += 4) {
    const alpha = rgba[ii + 3];
    data[ii + 0] = alpha - rgba[ii + 0];
    data[ii + 1] = alpha - rgba[ii + 1];
    data[ii + 2] = alpha - rgba[ii + 2];
    data[ii + 3] = alpha;
  };
  return (data);
};

/**
 * @param {Uint8Array} rgba
 * @return {Uint8Array}
 */
export function grayscaleColors(rgba) {
  const data = new Uint8Array(rgba.length);
  for (let ii = 0; ii < rgba.length; ii += 4) {
    const gr = (
      (rgba[ii + 0] * .3) + (rgba[ii + 1] * .59) + (rgba[ii + 2] * .11)
    ) | 0;
    data[ii + 0] = gr;
    data[ii + 1] = gr;
    data[ii + 2] = gr;
    data[ii + 3] = rgba[ii + 3];
  };
  return (data);
};

/**
 * @return {Array}
 */
export function randomRgbaColor() {
  const r = (Math.random() * 256) | 0;
  const g = (Math.random() * 256) | 0;
  const b = (Math.random() * 256) | 0;
  return ([r, g, b, 1]);
};

let velo = 64;
let rr = 127; let rrr = velo;
let rg = 12; let rrg = velo;
let rb = 108; let rrb = velo;
/**
 * @return {Array}
 */
export function getRainbowColor() {
  rr += rrr;
  if (rr >= 255) rrr = -velo;
  else if (rr <= 0) rrr = velo;
  rg += rrg;
  if (rg >= 255) rrg = -velo;
  else if (rg <= 0) rrg = velo;
  rb += rrb;
  if (rb >= 255) rrb = -velo;
  else if (rb <= 0) rrb = velo;
  return ([rr, rg, rb, velo]);
};

/**
 * @param {Array} color
 * @return {String}
 */
export function colorToRgbaString(color) {
  const r = color[0];
  const g = color[1];
  const b = color[2];
  const a = color[3];
  return (`rgba(${r},${g},${b},${a})`);
};

/**
 * @param {String} hex
 * @return {Array}
 */
export function hexToRgba(hex) {
  const r = parseInt(hex.substring(1,3), 16);
  const g = parseInt(hex.substring(3,5), 16);
  const b = parseInt(hex.substring(5,7), 16);
  return ([r, g, b, 1]);
};

/**
 * @param {Array} rgba
 * @return {String}
 */
export function rgbaToHex(rgba) {
  const r = rgba[0];
  const g = rgba[1];
  const b = rgba[2];
  const a = rgba[3];
  return (
    "#" +
    ("0" + parseInt(r, 10).toString(16)).slice(-2) +
    ("0" + parseInt(g, 10).toString(16)).slice(-2) +
    ("0" + parseInt(b, 10).toString(16)).slice(-2)
  );
};

/**
 * Do rgba color arrays match
 * @param {Array} a
 * @param {Array} a
 * @return {Boolean}
 */
export function colorsMatch(a, b) {
  return (
    a[0] === b[0] &&
    a[1] === b[1] &&
    a[2] === b[2] &&
    a[3] === b[3]
  );
};

/**
 * Checks if a color array is fully transparent
 * @param {Array} color
 * @return {Boolean}
 */
const transparent = [0, 0, 0, 0];
export function isGhostColor(color) {
  return (colorsMatch(color, transparent));
};


<|start_filename|>src/render/main.js<|end_filename|>
import { createCanvasBuffer } from "../utils";

/**
 * @return {String}
 */
export function exportAsDataUrl() {
  const layers = this.layers;
  const ww = this.bounds.w; const hh = this.bounds.h;
  const sx = this.bounds.x; const sy = this.bounds.y;
  const buffer = createCanvasBuffer(ww, hh);
  const view = buffer.canvas;
  for (let ii = 0; ii < layers.length; ++ii) {
    const idx = layers.length - 1 - ii;
    const layer = layers[idx];
    const pos = layer.getRelativePosition();
    const canvas = layer.toCanvas();
    buffer.drawImage(
      canvas,
      0, 0,
      canvas.width, canvas.height,
      pos.x, pos.y,
      canvas.width, canvas.height
    );
  };
  return (view.toDataURL("image/png"));
};


<|start_filename|>src/bounds/index.js<|end_filename|>
import { intersectRectangles } from "../math";

/**
 * @class Boundings
 */
class Boundings {
  /**
   * @param {Number} x
   * @param {Number} y
   * @param {Number} w
   * @param {Number} h
   */
  constructor(x = 0, y = 0, w = 0, h = 0) {
    this.x = 0;
    this.y = 0;
    this.w = 0;
    this.h = 0;
    this.update(x, y, w, h);
  };
};

/**
 * @return {Boundings}
 */
Boundings.prototype.clone = function() {
  const bounds = new Boundings(
    this.x, this.y, this.w, this.h
  );
  return (bounds);
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Number} w
 * @param {Number} h
 */
Boundings.prototype.update = function(x, y, w, h) {
  x = x | 0;
  y = y | 0;
  w = w | 0;
  h = h | 0;
  this.x = x;
  this.y = y;
  this.w = w;
  this.h = h;
};

/**
 * @param {Boundings} bounds
 */
Boundings.prototype.updateByBoundings = function(bounds) {
  this.x = bounds.x | 0;
  this.y = bounds.y | 0;
  this.w = bounds.w | 0;
  this.h = bounds.h | 0;
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Boolean}
 */
Boundings.prototype.isPointInside = function(x, y) {
  x = x | 0;
  y = y | 0;
  const state = intersectRectangles(
    this.x, this.y, this.w - 1, this.h - 1,
    x, y, 0, 0
  );
  return (state);
};

export default Boundings;


<|start_filename|>package.json<|end_filename|>
{
  "name": "poxi",
  "version": "0.5.3",
  "description": "A modern flat pixel editor",
  "license": "BSD-2-Clause",
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/maierfelix/poxi.git"
  },
  "main": "build.js",
  "homepage": "https://github.com/maierfelix/poxi#readme",
  "author": "<NAME> <<EMAIL>>",
  "contributors": [
    "<NAME> <<EMAIL>> (https://github.com/maierfelix)"
  ],
  "bugs": {
    "url": "https://github.com/maierfelix/poxi/issues"
  },
  "keywords": [],
  "scripts": {
    "start": "electron ."
  },
  "engines": {
    "node": ">= 6.x"
  },
  "devDependencies": {
    "ws": "^2.3.1",
    "electron": "^1.6.6",
    "rollup": "^0.41.6",
    "rollup-plugin-buble": "^0.15.0"
  },
  "dependencies": {}
}


<|start_filename|>src/batch/index.js<|end_filename|>
import { MAX_SAFE_INTEGER } from "../cfg";

import {
  uid,
  createCanvasBuffer
} from "../utils";

import { colorToRgbaString } from "../color";

import extend from "../extend";

import Boundings from "../bounds/index";

import * as _tile from "./tile";
import * as _matrix from "./matrix";

import * as _blend from "./filter/blend";
import * as _invert from "./filter/invert";
import * as _onion from "./filter/onion";
import * as _replace from "./filter/replace";
import * as _shading from "./filter/shading";
import * as _smoothing from "./filter/smoothing";

/**
 * @class Batch
 */
class Batch {
  /**
   * @param {Poxi} instance
   * @constructor
   */
  constructor(instance) {
    this.id = uid();
    // save reference to layer
    this.layer = null;
    // reference to stage
    this.instance = instance;
    // data related
    this.data = null;
    this.reverse = null;
    // buffer related
    // used for canvas batches
    this.buffer = null;
    // wgl texture
    // TODO: textures can possibly get from gpu as soon as mouseUp event passed
    this.texture = null;
    // relative boundings
    this.bounds = new Boundings();
    // we use this batch for erasing
    this.isEraser = false;
    // we use this batch for moving
    this.isMover = false;
    this.position = { x: 0, y: 0, mx: 0, my: 0 };
    // indicates if we should force to render this batch
    // even when it is not registered inside our stack yet
    this.forceRendering = false;
  }
};

/**
 * @return {Batch}
 */
Batch.prototype.clone = function() {
  const batch = new Batch(this.instance);
  batch.prepareMatrix(this.bounds.x, this.bounds.y);
  // clone props
  batch.layer = this.layer;
  batch.buffer = this.buffer;
  batch.bounds = this.bounds.clone();
  batch.isEraser = this.isEraser;
  batch.isMover = this.isMover;
  batch.position = Object.assign(this.position);
  batch.forceRendering = this.forceRendering;
  // clone matrices
  batch.data = new Uint8Array(this.data);
  batch.reverse = new Uint8Array(this.reverse);
  batch.refreshTexture(true);
  return (batch);
};

/**
 * @param {Number} x
 * @param {Number} y
 */
Batch.prototype.move = function(x, y) {
  const xx = x - this.position.mx;
  const yy = y - this.position.my;
  this.position.x += xx;
  this.position.y += yy;
  this.layer.x += xx;
  this.layer.y += yy;
  this.position.mx = x;
  this.position.my = y;
};

/**
 * @return {Number}
 */
Batch.prototype.getStackIndex = function() {
  const id = this.id;
  const commands = this.instance.stack;
  for (let ii = 0; ii < commands.length; ++ii) {
    const cmd = commands[ii];
    if (cmd.batch.id === id) return (ii);
  };
  return (-1);
};

Batch.prototype.kill = function() {
  const id = this.id;
  const instance = this.instance;
  const layers = instance.layers;
  // free batch from memory
  this.bounds = null;
  this.buffer = null;
  this.data = null;
  this.reverse = null;
  this.destroyTexture();
  // finally remove batch from layer
  let count = 0;
  for (let ii = 0; ii < layers.length; ++ii) {
    const batches = layers[ii].batches;
    for (let jj = 0; jj < batches.length; ++jj) {
      const batch = batches[jj];
      if (batch.id === id) {
        batches.splice(jj, 1);
        layers[ii].updateBoundings();
        count++;
      }
    };
  };
  /*if (count <= 0 && !this.isMover) {
    throw new Error(`Failed to kill batch:${this.id}`);
  }*/
};

/**
 * Frees the batch texture from the gpu
 */
Batch.prototype.destroyTexture = function() {
  const texture = this.texture;
  if (texture !== null) {
    this.instance.destroyTexture(texture);
    this.texture = null;
  }
};

/**
 * @param {Boolean} state
 */
Batch.prototype.refreshTexture = function(resized) {
  const bounds = this.bounds;
  const bw = bounds.w; const bh = bounds.h;
  const instance = this.instance;
  if (resized) {
    // free old texture from memory
    if (this.texture !== null) {
      instance.destroyTexture(this.texture);
    }
    this.texture = instance.bufferTexture(this.id, this.data, bw, bh);
  } else {
    instance.updateTexture(this.texture, this.data, bw, bh);
  }
  // trigger our stage to get redrawn
  this.instance.redraw = true;
};

extend(Batch, _tile);
extend(Batch, _matrix);

extend(Batch, _blend);
extend(Batch, _invert);
extend(Batch, _onion);
extend(Batch, _replace);
extend(Batch, _shading);
extend(Batch, _smoothing);

export default Batch;


<|start_filename|>src/stack/cmd.js<|end_filename|>
/**
 * @class Command
 */
class Command {
  /**
   * @param {Number} kind
   * @param {Batch} batch
   * @constructor
   */
  constructor(kind, batch) {
    this.kind = kind;
    this.batch = batch;
  }
};

export default Command;


<|start_filename|>src/render/buffer.js<|end_filename|>
/**
 * Create texture buffer from canvas
 * @param {String} name
 * @param {Uint8Array} data
 * @param {Number} width
 * @param {Number} height
 * @return {WebGLTexture}
 */
export function bufferTexture(name, data, width, height) {
  const gl = this.gl;
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  if (this.cache.gl.textures[name] === void 0) {
    this.cache.gl.textures[name] = texture;
  }
  gl.bindTexture(gl.TEXTURE_2D, null);
  return (this.cache.gl.textures[name]);
};

/**
 * Lookup for the texture inside our texture pool and free it from memory
 * @param {WebGLTexture} texture
 */
export function destroyTexture(texture) {
  const gl = this.gl;
  const textures = this.cache.gl.textures;
  for (let key in textures) {
    let txt = textures[key];
    if (txt !== texture) continue;
    gl.deleteTexture(txt);
    delete textures[key];
    txt = null;
    break;
  };
};

/**
 * @param {WebGLTexture} texture
 * @param {Uint8Array} data
 * @param {Number} width
 * @param {Number} height
 */
export function updateTexture(texture, data, width, height) {
  const gl = this.gl;
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data);
  gl.bindTexture(gl.TEXTURE_2D, null);
};

/**
 * Create texture buffer from canvas
 * @param {String} name
 * @param {HTMLCanvasElement} canvas
 * @return {WebGLTexture}
 */
export function bufferTextureByCanvas(name, canvas) {
  const gl = this.gl;
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  if (this.cache.gl.textures[name] === void 0) {
    this.cache.gl.textures[name] = texture;
  }
  gl.bindTexture(gl.TEXTURE_2D, null);
  return (this.cache.gl.textures[name]);
};

/**
 * @param {WebGLTexture} texture
 * @param {HTMLCanvasElement} canvas
 */
export function updateTextureByCanvas(texture, canvas) {
  const gl = this.gl;
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
  gl.bindTexture(gl.TEXTURE_2D, null);
};


<|start_filename|>src/index.js<|end_filename|>
import extend from "./extend";

import * as _select from "./area/select";
import * as _area_functions from "./area/functions";

import * as _camera from "./camera/functions";

import * as _emitter from "./event/emitter";
import * as _listener from "./event/listener";

import * as _env from "./env/functions";
import * as _fill from "./env/fill";
import * as _insert from "./env/insert";

import * as _buffer from "./render/buffer";
import * as _build from "./render/build";
import * as _draw from "./render/draw";
import * as _generate from "./render/generate";
import * as _main from "./render/main";
import * as _render from "./render/render";
import * as _resize from "./render/resize";
import * as _shaders from "./render/shaders";

import * as _redo from "./stack/redo";
import * as _state from "./stack/state";
import * as _undo from "./stack/undo";

import * as _read from "./storage/read";
import * as _write from "./storage/write";

import * as _transflip from "./transform/flip";
import * as _transrotate from "./transform/rotate";

import * as _ui from "./ui/index";

import * as _setup from "./setup";

import { BASE_SCALE } from "./cfg";

import Layer from "./Layer/index";
import Boundings from "./bounds/index";

/**
 * @class {Poxi}
 */
class Poxi {
  /**
   * @constructor
   */
  constructor() {
    // # webgl related
    // wgl context
    this.gl = null;
    // canvas reference
    this.view = null;
    // webgl program
    this.program = null;
    // global boundings
    this.bounds = new Boundings();
    // # camera related
    this.cx = 0;
    this.cy = 0;
    this.cw = 0;
    this.ch = 0;
    // camera render scale
    this.cr = BASE_SCALE;
    this.cs = BASE_SCALE;
    // camera drag related
    this.dx = 0;
    this.dy = 0;
    // camera zoom related
    this.lx = 0;
    this.ly = 0;
    // selection related
    this.sx = 0;
    this.sy = 0;
    this.sw = -0;
    this.sh = -0;
    this.shape = null;
    // mouse offset
    this.mx = 0;
    this.my = 0;
    // stack related
    this.stack = [];
    this.sindex = -1;
    // layer related
    this.layers = [];
    // container related
    this.containers = [];
    // general cache
    this.cache = {
      bg: null,
      fg: null,
      // unused base layer
      layer: null,
      fgTexture: null,
      grid: null,
      gridTexture: null,
      // wgl cache
      gl: {
        // empty texture
        empty: null,
        // general buffers
        buffers: {},
        // we use buffered uv coords
        vertices: {},
        // texture pool
        textures: {}
      }
    };
    // last things
    this.last = {
      cx: 1, cy: 1,
      // mouse move coordinates
      mx: 0, my: 0,
      // mouse down coordinates
      mdx: 0, mdy: 0,
      // mouse down relative coordinates
      mdrx: 0, mdry: 0
    };
    // shared buffer related
    this.buffers = {
      arc: null,
      rect: null,
      move: null,
      stroke: null,
      erasing: null,
      drawing: null
    };
    // keyboard related
    this.keys = {};
    // clipboard related
    this.clipboard = {
      copy: null
    };
    // stage stages
    this.states = {
      arc: false,
      rect: false,
      stroke: false,
      moving: false,
      drawing: false,
      pipette: false,
      lighting: false,
      dragging: false,
      select: false,
      selecting: false,
      fastColorMenu: false
    };
    // mode related
    this.modes = {
      arc: false,
      move: false,
      fill: false,
      rect: false,
      draw: false,
      shape: false,
      light: false,
      erase: false,
      flood: false,
      select: false,
      stroke: false,
      pipette: false
    };
    // how many frames we have drawn
    this.frames = 0;
    // indicates if we have to redraw our stage
    this.redraw = false;
    // global fill style
    this.fillStyle = [0, 0, 0, 0];
    // favorite used colors
    this.favoriteColors = [];
    // selected active layer
    this.activeLayer = null;
    this.setup();
  }
};

extend(Poxi, _select);
extend(Poxi, _area_functions);

extend(Poxi, _camera);

extend(Poxi, _emitter);
extend(Poxi, _listener);

extend(Poxi, _env);
extend(Poxi, _fill);
extend(Poxi, _insert);

extend(Poxi, _buffer);
extend(Poxi, _build);
extend(Poxi, _draw);
extend(Poxi, _generate);
extend(Poxi, _main);
extend(Poxi, _render);
extend(Poxi, _resize);
extend(Poxi, _shaders);

extend(Poxi, _redo);
extend(Poxi, _state);
extend(Poxi, _undo);

extend(Poxi, _read);
extend(Poxi, _write);

extend(Poxi, _transflip);
extend(Poxi, _transrotate);

extend(Poxi, _ui);

extend(Poxi, _setup);

if (typeof window !== "undefined") {
  window.Poxi = Poxi;
  window.stage = new Poxi();
} else {
  throw new Error("Poxi only runs inside the browser");
}


<|start_filename|>src/transform/rotate.js<|end_filename|>
/**
 * @param {Layer} layer
 */
export function rotateRight(layer) {
  const main = layer.batch;
  const batch = layer.createBatchAt(
    main.bounds.x, main.bounds.y
  );
  const data = main.data;
  const ww = main.bounds.w;
  const hh = main.bounds.h;
  const pixels = new Uint8Array(data.length);
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % ww) | 0;
    const yy = (idx / ww) | 0;
    const opx = 4 * (yy * ww + xx);
    const npx = 4 * ((xx * ww) + (hh - yy - 1));
    pixels[opx + 0] = data[npx + 0];
    pixels[opx + 1] = data[npx + 1];
    pixels[opx + 2] = data[npx + 2];
    pixels[opx + 3] = data[npx + 3];
  };
  batch.data = pixels;
  batch.bounds.w = hh;
  batch.bounds.h = ww;
  batch.refreshTexture(true);
  main.refreshTexture(true);
  layer.updateBoundings();
  layer.bounds.w = hh;
  layer.bounds.h = ww;
  main.bounds.w = hh;
  main.bounds.h = ww;
  main.data = pixels;
  this.updateGlobalBoundings();
};

export function rotateLeft() {

};


<|start_filename|>src/stack/redo.js<|end_filename|>
import Command from "./cmd";
import CommandKind from "./kind";

/**
 * @return {Void}
 */
export function redo() {
  // prevent undo/redo when in e.g drawing state
  if (this.isInActiveState()) return;
  if (this.sindex < this.stack.length - 1) {
    this.sindex++;
    const cmd = this.currentStackOperation();
    this.fire(cmd, true);
  }
  this.refreshUiLayers();
  this.updateGlobalBoundings();
  this.redraw = true;
  return;
};

/**
 * @param {Number} kind
 * @param {Batch} batch
 * @return {Void}
 */
export function enqueue(kind, batch) {
  // our stack index is out of position
  // => clean up all more recent batches
  this.refreshStack();
  const cmd = new Command(kind, batch);
  const type = this.getCommandKind(cmd);
  // free the texture from gpu, we dont need it anymore
  if (type === CommandKind.BATCH_OPERATION) {
    batch.destroyTexture();
  }
  const last = this.currentStackOperation();
  this.stack.push(cmd);
  this.redo();
  // delete last operation if it's mergable
  if (last !== null && last.kind === cmd.kind) {
    if (this.isMergeableOperation(cmd.kind)) {
      // delete last 2 commands
      this.stack.splice(this.sindex - 1, this.stack.length);
      this.sindex = this.stack.length - 1;
    }
  }
};


<|start_filename|>src/stack/kind.js<|end_filename|>
const CommandKind = {
  UNKNOWN: 0,
  LAYER_OPERATION: 1,
  BATCH_OPERATION: 2,
  CONTAINER_OPERATION: 3,
  DRAW: 4,
  ERASE: 5,
  FILL: 6,
  BACKGROUND: 7,
  PASTE: 8,
  CUT: 9,
  INSERT_IMAGE: 10,
  STROKE: 11,
  RECT_FILL: 12,
  RECT_STROKE: 13,
  ARC_FILL: 14,
  ARC_STROKE: 15,
  FLOOD_FILL: 16,
  LIGHTING: 17,
  LAYER_ADD: 18,
  LAYER_REMOVE: 19,
  LAYER_LOCK: 20,
  LAYER_FLIP_VERTICAL: 21,
  LAYER_FLIP_HORIZONTAL: 22,
  LAYER_MOVE: 23,
  LAYER_ORDER: 24,
  LAYER_RENAME: 25,
  LAYER_ROTATE_LEFT: 26,
  LAYER_ROTATE_RIGHT: 27,
  LAYER_VISIBILITY: 28,
  LAYER_OPACITY: 29,
  LAYER_CLONE: 30,
  LAYER_CLONE_REF: 31,
  LAYER_MERGE: 32,
  CONTAINER_ADD: 33,
  CONTAINER_REMOVE: 34
};

export default CommandKind;


<|start_filename|>src/render/generate.js<|end_filename|>
import {
  TILE_SIZE,
  HIDE_GRID,
  MAGIC_SCALE,
  GRID_LINE_WIDTH
} from "../cfg";

import {
  createCanvasBuffer,
  applyImageSmoothing
} from "../utils";

/**
 * @return {CanvasRenderingContext2D}
 */
export function createGridBuffer() {
  const cw = this.cw;
  const ch = this.ch;
  const buffer = createCanvasBuffer(cw, ch);
  if (this.cache.grid !== null) {
    this.cache.grid = null;
    this.destroyTexture(this.cache.gridTexture);
  }
  this.cache.grid = buffer;
  this.cache.gridTexture = this.bufferTextureByCanvas("grid", buffer.canvas);
  this.redrawGridBuffer();
  return (buffer);
};

/**
 * @return {Void}
 */
export function redrawGridBuffer() {
  if (this.cr <= HIDE_GRID) return;
  const buffer = this.cache.grid;
  const texture = this.cache.gridTexture;
  const cr = this.cr;
  const size = (TILE_SIZE * cr) | 0;
  const cx = this.cx;
  const cy = this.cy;
  const cw = this.cw;
  const ch = this.ch;
  buffer.clearRect(0, 0, cw, ch);
  buffer.lineWidth = GRID_LINE_WIDTH;
  buffer.strokeStyle = "rgba(51,51,51,0.5)";
  buffer.beginPath();
  for (let xx = (cx % size) | 0; xx < cw; xx += size) {
    buffer.moveTo(xx, 0);
    buffer.lineTo(xx, ch);
  };
  for (let yy = (cy % size) | 0; yy < ch; yy += size) {
    buffer.moveTo(0, yy);
    buffer.lineTo(cw, yy);
  };
  buffer.stroke();
  buffer.stroke();
  buffer.closePath();
  this.updateTextureByCanvas(texture, buffer.canvas);
  this.last.cx = this.cx;
  this.last.cy = this.cy;
  return;
};

/**
 * @return {WebGLTexture}
 */
export function createBackgroundBuffer() {
  if (this.cache.bg instanceof WebGLTexture) {
    this.destroyTexture(this.cache.bg);
  }
  const size = TILE_SIZE;
  const cw = this.cw;
  const ch = this.ch;
  const canvas = document.createElement("canvas");
  const buffer = canvas.getContext("2d");
  canvas.width = cw;
  canvas.height = ch;
  // dark rectangles
  buffer.fillStyle = "#1f1f1f";
  buffer.fillRect(0, 0, cw, ch);
  // bright rectangles
  buffer.fillStyle = "#212121";
  for (let yy = 0; yy < ch; yy += size*2) {
    for (let xx = 0; xx < cw; xx += size*2) {
      // applied 2 times to increase saturation
      buffer.fillRect(xx, yy, size, size);
      buffer.fillRect(xx, yy, size, size);
    };
  };
  for (let yy = size; yy < ch; yy += size*2) {
    for (let xx = size; xx < cw; xx += size*2) {
      buffer.fillRect(xx, yy, size, size);
    };
  };
  const texture = this.bufferTextureByCanvas("background", canvas);
  return (texture);
};

/**
 * @return {CanvasRenderingContext2D}
 */
export function createForegroundBuffer() {
  const cw = this.cw;
  const ch = this.ch;
  const buffer = createCanvasBuffer(cw, ch);
  applyImageSmoothing(buffer, false);
  if (this.cache.fg !== null) {
    this.cache.fg = null;
    this.destroyTexture(this.cache.fgTexture);
  }
  this.cache.fg = buffer;
  this.cache.fgTexture = this.bufferTextureByCanvas("foreground", buffer.canvas);
  return (buffer);
};


<|start_filename|>src/layer/matrix.js<|end_filename|>
import { MAX_SAFE_INTEGER } from "../cfg";
import { alphaByteToRgbAlpha } from "../color";

/**
 * @return {Boolean}
 */
export function hasResized() {
  const ox = this.bounds.x; const oy = this.bounds.y;
  const ow = this.bounds.w; const oh = this.bounds.h;
  const nx = this.last.x; const ny = this.last.y;
  const nw = this.last.w; const nh = this.last.h;
  return (
    ox !== nx || oy !== ny ||
    ow !== nw || oh !== nh
  );
};

/**
 * Updates the layer's boundings by calculating
 * min/max of x,y,w,h of all stored batches
 */
export function updateBoundings() {
  let x = MAX_SAFE_INTEGER; let y = MAX_SAFE_INTEGER;
  let w = -MAX_SAFE_INTEGER; let h = -MAX_SAFE_INTEGER;
  const batches = this.batches;
  let count = 0;
  for (let ii = 0; ii < batches.length; ++ii) {
    const batch = batches[ii];
    const bounds = batch.bounds;
    const bx = bounds.x; const by = bounds.y;
    const bw = bx + bounds.w; const bh = by + bounds.h;
    // ignore empty batches
    if (bounds.w === 0 && bounds.h === 0) continue;
    // calculate x
    if (x < 0 && bx < x) x = bx;
    else if (x >= 0 && (bx < 0 || bx < x)) x = bx;
    // calculate y
    if (y < 0 && by < y) y = by;
    else if (y >= 0 && (by < 0 || by < y)) y = by;
    // calculate width
    if (bw > w) w = bw;
    // calculate height
    if (bh > h) h = bh;
    count++;
  };
  // update our boundings
  if (count > 0) {
    const bounds = this.bounds;
    this.last.x = bounds.x; this.last.y = bounds.y;
    this.last.w = bounds.w; this.last.h = bounds.h;
    bounds.update(
      x, y,
      -x + w, -y + h
    );
  }
  if (this.hasResized()) {
    const main = this.batch;
    main.bounds.update(
      this.bounds.x, this.bounds.y,
      this.bounds.w, this.bounds.h
    );
    const xx = this.last.x; const yy = this.last.y;
    const ww = this.last.w; const hh = this.last.h;
    main.resizeMatrix(
      xx - this.bounds.x, yy - this.bounds.y,
      this.bounds.w - ww, this.bounds.h - hh
    );
  }
};

/**
 * Access raw pixel
 * @param {Number} x
 * @param {Number} y
 * @return {Array}
 */
export function getPixelAt(x, y) {
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  // normalize coordinates
  const dx = (x - this.x) | 0;
  const dy = (y - this.y) | 0;
  const xx = dx - this.bounds.x;
  const yy = dy - this.bounds.y;
  // check if point inside boundings
  if (
    (xx < 0 || yy < 0) ||
    (bw <= 0 || bh <= 0) ||
    (xx >= bw || yy >= bh)
  ) return (null);
  // now get the pixel from the layer matrix
  return (this.batch.getRawPixelAt(dx, dy));
};

/**
 * Access live pixel
 * @param {Number} x
 * @param {Number} y
 * @return {Array}
 */
export function getLivePixelAt(x, y) {
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  // normalize coordinates
  const dx = (x - this.x) | 0;
  const dy = (y - this.y) | 0;
  const xx = dx - this.bounds.x;
  const yy = dy - this.bounds.y;
  // check if point inside boundings
  if (
    (xx < 0 || yy < 0) ||
    (bw <= 0 || bh <= 0) ||
    (xx >= bw || yy >= bh)
  ) return (null);
  for (let ii = 0; ii < this.batches.length; ++ii) {
    const idx = (this.batches.length - 1) - ii;
    const batch = this.batches[idx];
    const pixel = batch.getRawPixelAt(dx, dy);
    if (pixel !== null) return (pixel);
  };
  return (null);
};

/**
 * Merges two layers
 * Resize this by layer<->this bounding diff
 * Inject this matrix into layer matrix at layer bound pos
 * @param {Layer} olayer
 * @return {Batch}
 */
export function mergeWithLayer(olayer) {
  const layer = olayer.clone();
  // TODO: fix merging referenced layers
  const main = this.batch;
  const opacity = this.opacity;
  const ldata = layer.batch.data;
  const lw = layer.bounds.w;
  const lh = layer.bounds.h;
  const lx = layer.x + layer.bounds.x;
  const ly = layer.y + layer.bounds.y;
  const sx = this.x + this.bounds.x;
  const sy = this.y + this.bounds.y;
  const dx = sx - lx; const dy = sy - ly;
  const bx = sx - dx; const by = sy - dy;
  const batch = this.createBatchAt(bx, by);
  // pre-resize batch
  batch.bounds.w = lw;
  batch.bounds.h = lh;
  // allocate pixel memory
  batch.data = new Uint8Array(ldata.length);
  batch.reverse = new Uint8Array(ldata.length);
  // draw batch matrix into layer matrix
  // and save earlier state
  for (let ii = 0; ii < ldata.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % lw) | 0;
    const yy = (idx / lw) | 0;
    const pixel = layer.getPixelAt(lx + xx, ly + yy);
    if (pixel === null) continue;
    //console.log(pixel[3], layer.oapcity);
    pixel[3] = pixel[3] * (opacity * layer.opacity);
    batch.drawPixelFast(lx + xx, ly + yy, pixel);
  };
  batch.refreshTexture(true);
  return (batch);
};


<|start_filename|>src/render/draw.js<|end_filename|>
/**
 * Clears the context
 */
export function clear() {
  const gl = this.gl;
  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
};

/**
 * Draw a texture
 * @param {Texture} tex
 * @param {Number} dx
 * @param {Number} dy
 * @param {Number} dw
 * @param {Number} dh
 */
export function drawImage(tex, dx, dy, dw, dh) {
  dx = dx | 0;
  dy = dy | 0;
  dw = dw | 0;
  dh = dh | 0;

  const gl = this.gl;
  const program = this.program;

  gl.uniform2f(
    gl.getUniformLocation(program, "uObjScale"),
    dw, dh
  );

  const pos = this.cache.gl.vertices.position;
  for (let ii = 0; ii < 6; ++ii) {
    pos[2 * ii + 0] = dx + (dw / 2);
    pos[2 * ii + 1] = dy + (dh / 2);
  };

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, tex);
  this.setGlAttribute(program, this.cache.gl.buffers.position, "aObjCen", 2, pos);
  gl.drawArrays(gl.TRIANGLES, 0, 6);

};

/**
 * Draw a rectangle
 * @param {Number} dx
 * @param {Number} dy
 * @param {Number} dw
 * @param {Number} dh
 * @param {Array} color
 */
export function drawRectangle(dx, dy, dw, dh, color) {
  dx = dx | 0;
  dy = dy | 0;
  dw = dw | 0;
  dh = dh | 0;

  const gl = this.gl;
  const program = this.program;

  gl.uniform2f(
    gl.getUniformLocation(program, "uObjScale"),
    dw, dh
  );
  gl.uniform1i(gl.getUniformLocation(program, "isRect"), 1);

  const pos = this.cache.gl.vertices.position;
  for (let ii = 0; ii < 6; ++ii) {
    pos[2 * ii + 0] = dx + (dw / 2);
    pos[2 * ii + 1] = dy + (dh / 2);
  };

  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, this.cache.gl.empty);
  gl.uniform4f(
    gl.getUniformLocation(program, "vColor"),
    color[0], color[1], color[2], color[3]
  );
  this.setGlAttribute(program, this.cache.gl.buffers.position, "aObjCen", 2, pos);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  gl.uniform1i(gl.getUniformLocation(program, "isRect"), 0);

};


<|start_filename|>src/render/shaders.js<|end_filename|>
export const SPRITE_VERTEX = `
  precision lowp float;
  uniform vec2 uScale;
  uniform vec2 uObjScale;
  attribute vec2 aObjCen;
  attribute float aIdx;
  varying vec2 uv;
  void main(void) {
    if (aIdx == 0.0) {
      uv = vec2(0.0,0.0);
    } else if (aIdx == 1.0) {
      uv = vec2(1.0,0.0);
    } else if (aIdx == 2.0) {
      uv = vec2(0.0,1.0);
    } else {
      uv = vec2(1.0,1.0);
    }
    gl_Position = vec4(
      -1.0 + 2.0 * (aObjCen.x + uObjScale.x * (-0.5 + uv.x)) / uScale.x,
      1.0 - 2.0 * (aObjCen.y + uObjScale.y * (-0.5 + uv.y)) / uScale.y,
      0.0, 1.0
    );
  }
`;

export const SPRITE_FRAGMENT = `
  precision lowp float;
  uniform sampler2D uSampler;
  varying vec2 uv;
  uniform int isRect;
  uniform float vOpacity;
  uniform vec4 vColor;
  void main(void) {
    if (isRect == 0) {
      gl_FragColor = texture2D(uSampler, uv);
    } else {
      gl_FragColor = vColor + texture2D(uSampler, uv);
    }
    gl_FragColor.a *=  vOpacity;
    if (gl_FragColor.a < 0.01) discard;
  }
`;


<|start_filename|>src/camera/functions.js<|end_filename|>
import {
  TILE_SIZE,
  MIN_SCALE,
  MAX_SCALE,
  ZOOM_SPEED,
  MAGIC_SCALE
} from "../cfg";

import {
  roundTo,
  zoomScale,
  alignToGrid
} from "../math";

/**
 * @param {Number} dir
 */
export function scale(dir) {
  const x = (dir * (ZOOM_SPEED / 1e2)) * zoomScale(this.cs);
  const oscale = this.cs;
  if (this.cs + x <= MIN_SCALE) this.cs = MIN_SCALE;
  else if (this.cs + x >= MAX_SCALE) this.cs = MAX_SCALE;
  else this.cs += x;
  this.cs = roundTo(this.cs, MAGIC_SCALE);
  if (this.cs >= (MAX_SCALE - 1) + .25) this.cs = (MAX_SCALE - 1) + .25;
  this.cx -= (this.lx) * (zoomScale(this.cs) - zoomScale(oscale));
  this.cy -= (this.ly) * (zoomScale(this.cs) - zoomScale(oscale));
  this.cr = roundTo(this.cs, MAGIC_SCALE);
  this.updateGrid();
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function hover(x, y) {
  x = x | 0;
  y = y | 0;
  this.mx = x;
  this.my = y;
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function click(x, y) {
  x = x | 0;
  y = y | 0;
  const position = this.getRelativeOffset(x, y);
  this.dx = x;
  this.dy = y;
  this.lx = position.x;
  this.ly = position.y;
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function drag(x, y) {
  x = x | 0;
  y = y | 0;
  this.cx += x - this.dx;
  this.cy += y - this.dy;
  this.dx = x;
  this.dy = y;
  this.updateGrid();
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Object}
 */
export function getRelativeOffset(x, y) {
  x = x | 0;
  y = y | 0;
  const xx = (x - this.cx) / this.cs;
  const yy = (y - this.cy) / this.cs;
  return ({
    x: xx,
    y: yy
  });
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Object}
 */
export function getRelativeTileOffset(x, y) {
  x = x | 0;
  y = y | 0;
  const rel = this.getRelativeOffset(x, y);
  return (
    this.getTileOffsetAt(rel.x, rel.y)
  );
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Object}
 */
export function getTileOffsetAt(x, y) {
  x = x | 0;  
  y = y | 0;
  const half = TILE_SIZE / 2;
  const xx = alignToGrid(x - half);
  const yy = alignToGrid(y - half);
  return ({
    x: (xx / TILE_SIZE) | 0,
    y: (yy / TILE_SIZE) | 0
  });
};

/**
 * @param {Boundings} bounds 
 */
export function boundsInsideView(bounds) {
  const cs = this.cs;
  const ww = (bounds.w * TILE_SIZE) * cs;
  const hh = (bounds.h * TILE_SIZE) * cs;
  const xx = ((bounds.x * TILE_SIZE) * cs) + this.cx;
  const yy = ((bounds.y * TILE_SIZE) * cs) + this.cy;
  return (
    (xx + ww >= 0 && xx <= this.cw) &&
    (yy + hh >= 0 && yy <= this.ch)
  );
};


<|start_filename|>src/render/build.js<|end_filename|>
import { getWGLContext } from "../utils";
import { WGL_TEXTURE_LIMIT } from "../cfg";

import {
  SPRITE_VERTEX,
  SPRITE_FRAGMENT
} from "./shaders";

/**
 * @param {HTMLCanvasElement} view 
 */
export function setupRenderer(view) {
  this.view = view;
  this.gl = getWGLContext(view);
  this.program = this.createSpriteProgram();
  this.gl.useProgram(this.program);
  this.cache.gl.empty = this.createEmptyTexture();
};

/**
 * @return {WebGLTexture}
 */
export function createEmptyTexture() {
  const gl = this.gl;
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(
    gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
    new Uint8Array([0, 0, 0, 0])
  );
  gl.bindTexture(gl.TEXTURE_2D, null);
  return (texture);
};

/**
 * @return {WebGLProgram}
 */
export function createSpriteProgram() {
  const gl = this.gl;
  const size = WGL_TEXTURE_LIMIT;
  const program = gl.createProgram();
  const vshader = gl.createShader(gl.VERTEX_SHADER);
  const fshader = gl.createShader(gl.FRAGMENT_SHADER);

  this.compileShader(vshader, SPRITE_VERTEX);
  this.compileShader(fshader, SPRITE_FRAGMENT);

  gl.attachShader(program, vshader);
  gl.attachShader(program, fshader);
  gl.linkProgram(program);

  const cache = this.cache.gl;
  const buffers = cache.buffers;
  const vertices = cache.vertices;
  const idxs = vertices.idx = new Float32Array(size * 6);
  vertices.position = new Float32Array(size * 12);

  buffers.idx = gl.createBuffer();
  buffers.position = gl.createBuffer();
  for (let ii = 0; ii < size; ii++) {
    idxs[6 * ii + 0] = 0;
    idxs[6 * ii + 1] = 1;
    idxs[6 * ii + 2] = 2;
    idxs[6 * ii + 3] = 1;
    idxs[6 * ii + 4] = 2;
    idxs[6 * ii + 5] = 3;
  };

  this.setGlAttribute(program, buffers.idx, "aIdx", 1, idxs);
  return (program);
};

export function compileShader(shader, shader_src) {
  const gl = this.gl;
  gl.shaderSource(shader, shader_src);
  gl.compileShader(shader);
};

export function setGlAttribute(program, buffer, name, size, values) {
  const gl = this.gl;
  const attribute = gl.getAttribLocation(program, name);
  gl.enableVertexAttribArray(attribute);
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  if (values.length > 0) {
    gl.bufferData(gl.ARRAY_BUFFER, values, gl.DYNAMIC_DRAW);
  }
  gl.vertexAttribPointer(attribute, size, gl.FLOAT, false, 0, 0);
};


<|start_filename|>src/ui/index.js<|end_filename|>
import {
  hexToRgba,
  rgbaToHex,
  rgbaToBytes
} from "../color";

import Layer from "../layer/index";
import Container from "../container/index";
import CommandKind from "../stack/kind";

export function resetModes() {
  for (let key in this.modes) {
    this.resetSelection();
    if (this.modes[key] === true) {
      this.resetActiveCursor(key);
    }
    this.modes[key] = false;
  };
  this.resetActiveUiButtons();
};

/**
 * @param {String} mode
 */
export function resetActiveCursor(mode) {
  const el = this.getCursorNodeByMode(mode);
  if (el !== null) {
    el.style.left = "0px";
    el.style.top = "0px";
    el.style.display = "none";
  }
};

/**
 * Returns the current active mode as a string
 * @return {String}
 */
export function getActiveMode() {
  for (let key in this.modes) {
    if (this.modes[key] === true) return (key);
  };
  return (null);
};

/**
 * @param {String} mode
 * @return {HTMLElement}
 */
export function getCursorNodeByMode(mode) {
  const value = "#c" + mode;
  const el = document.querySelector(value);
  return (el);
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Void}
 */
export function updateCursorPosition(x, y) {
  const mode = this.getActiveMode();
  if (mode === null) return;
  const el = this.getCursorNodeByMode(mode);
  if (el === null) return;
  el.style.left = x + "px";
  el.style.top = (y - 16) + "px";
  el.style.display = "block";
  return;
};

export function resetActiveUiButtons() {
  arc.style.removeProperty("opacity");
  move.style.removeProperty("opacity");
  shape.style.removeProperty("opacity");
  tiled.style.removeProperty("opacity");
  erase.style.removeProperty("opacity");
  bucket.style.removeProperty("opacity");
  select.style.removeProperty("opacity");
  stroke.style.removeProperty("opacity");
  pipette.style.removeProperty("opacity");
  lighting.style.removeProperty("opacity");
  rectangle.style.removeProperty("opacity");
  paint_all.style.removeProperty("opacity");
};

/**
 * @param {String} value
 * @return {Void}
 */
export function setUiColorByHex(value) {
  // close fast color picker menu
  if (this.states.fastColorMenu) {
    this.closeFastColorPickerMenu();
  }
  color_hex.innerHTML = String(value).toUpperCase();
  color_view.style.background = value;
  const rgba = hexToRgba(value);
  // prevent changing color if it didnt changed
  if (
    this.fillStyle[0] === rgba[0] &&
    this.fillStyle[1] === rgba[1] &&
    this.fillStyle[2] === rgba[2] &&
    this.fillStyle[3] === rgba[3]
  ) return;
  this.fillStyle = rgba;
  this.addCustomColor(rgba);
  return;
};

/**
 * @param {Array} rgba
 * @return {Void}
 */
export function setUiColorByRgba(rgba) {
  const r = rgba[0];
  const g = rgba[1];
  const b = rgba[2];
  const a = rgba[3];
  color_view.style.background = `rgba(${r},${g},${b},${a})`;
  const hex = rgbaToHex(rgba);
  // prevent changing color if it didnt changed
  if (
    this.fillStyle[0] === r &&
    this.fillStyle[1] === g &&
    this.fillStyle[2] === b &&
    this.fillStyle[3] === a
  ) return;
  this.fillStyle = rgba;
  return;
};

/**
 * @param {Array} rgba
 */
export function addCustomColor(rgba) {
  const colors = this.favoriteColors;
  let count = 0;
  for (let ii = 0; ii < colors.length; ++ii) {
    const color = colors[ii].color;
    const index = colors[ii].index;
    // color already saved, increase it's importance
    if (
      color[0] === rgba[0] &&
      color[1] === rgba[1] &&
      color[2] === rgba[2] &&
      color[3] === rgba[3]
    ) {
      colors[ii].index += 1;
      count++;
      //console.log("Found!", color, rgba);
    }
  };
  // color isn't saved yet
  if (count <= 0) {
    // color limit exceeded, replace less used color with this one
    if (colors.length >= 16) {
      colors[colors.length - 1].color = color;
      colors[colors.length - 1].index += 1;
    } else {
      // we have to replace the less used color
      colors.push({
        color: rgba,
        index: 0
      });
    }
  }
  // resort descending by most used color
  colors.sort((a, b) => { return (b.index - a.index); });
  // sync with storage
  this.writeStorage("favorite_colors", JSON.stringify(colors));
  // sync color menu with favorite colors
  this.updateFastColorPickMenu();
};

export function closeFastColorPickerMenu() {
  menu.style.visibility = "hidden";
  this.states.fastColorMenu = false;
};

export function openFastColorPickerMenu() {
  menu.style.visibility = "visible";
  this.states.fastColorMenu = true;
};

export function updateFastColorPickMenu() {
  // first remove all color nodes
  for (let ii = 0; ii < 16; ++ii) {
    const node = colors.children[0];
    if (!node) continue;
    node.parentNode.removeChild(node);
  };
  // now re-insert the updated ones
  for (let ii = 0; ii < 16; ++ii) {
    const color = this.favoriteColors[ii];
    const node = document.createElement("div");
    if (color) {
      node.setAttribute("color", "[" + color.color + "]");
      node.style.background = rgbaToHex(color.color);
    } else {
      node.setAttribute("color", "[0,0,0,1]");
      node.style.background = "#000000";
    }
    colors.appendChild(node);
  };
};

export function hoverLayer(e) {
  const el = e.target;
  const kind = el.classList.value;
  const parent = (
    kind !== "layer-item" ? el.parentNode : el
  );
  const layer = this.getLayerByNode(parent);
  if (layer === null) return;
  //const canvas = layer.toCanvas();
};

/**
 * @param {HTMLElement} e
 * @param {Boolean} dbl
 */
export function clickedLayer(e, dbl) {
  const el = e.target;
  const kind = el.classList.value;
  const parent = (
    kind !== "layer-item" ? el.parentNode : el
  );
  const layer = this.getLayerByNode(parent);
  if (layer === null) return;
  switch (kind) {
    // clicked on layer, set it active
    case "layer-text":
      if (!dbl) {
        this.setActiveLayer(layer);
      }
      else {
        el.removeAttribute("readonly");
        el.focus();
        el.onkeypress = (e) => {
          const code = (e.keyCode ? e.keyCode : e.which);
          if (code === 13) el.blur();
        };
        el.onblur = () => {
          const oname = layer.name;
          if (!el.value) {
            layer.name = oname;
          } else {
            if (String(oname) !== el.value) {
              this.enqueue(CommandKind.LAYER_RENAME, {
                oname, name: el.value, layer: layer
              });
            }
          }
          el.setAttribute("readonly", "readonly");
        };
      }
    break;
    // clicked a layer icon
    case "layer-item-visible":
      this.enqueue(CommandKind.LAYER_VISIBILITY, {
        layer: layer
      });
    break;
    // clicked lock icon
    case "layer-item-locked":
      this.enqueue(CommandKind.LAYER_LOCK, {
        layer: layer
      });
    break;
  };
};

export function setupUi() {

  // ui
  tiled.onclick = (e) => {
    this.resetModes();
    this.modes.draw = true;
    tiled.style.opacity = 1.0;
  };
  erase.onclick = (e) => {
    this.resetModes();
    this.modes.erase = true;
    erase.style.opacity = 1.0;
  };
  bucket.onclick = (e) => {
    this.resetModes();
    this.modes.fill = true;
    bucket.style.opacity = 1.0;
  };
  pipette.onclick = (e) => {
    this.resetModes();
    this.modes.pipette = true;
    pipette.style.opacity = 1.0;
  };
  select.onclick = (e) => {
    this.resetModes();
    this.modes.select = true;
    select.style.opacity = 1.0;
  };
  stroke.onclick = (e) => {
    this.resetModes();
    this.modes.stroke = true;
    stroke.style.opacity = 1.0;
  };
  arc.onclick = (e) => {
    this.resetModes();
    this.modes.arc = true;
    arc.style.opacity = 1.0;
  };
  rectangle.onclick = (e) => {
    this.resetModes();
    this.modes.rect = true;
    rectangle.style.opacity = 1.0;
  };
  paint_all.onclick = (e) => {
    this.resetModes();
    this.modes.flood = true;
    paint_all.style.opacity = 1.0;
  };
  shape.onclick = (e) => {
    this.resetModes();
    this.modes.shape = true;
    shape.style.opacity = 1.0;
  };
  lighting.onclick = (e) => {
    this.resetModes();
    this.modes.light = true;
    lighting.style.opacity = 1.0;
  };
  move.onclick = (e) => {
    this.resetModes();
    this.modes.move = true;
    move.style.opacity = 1.0;
  };
  color.onchange = (e) => {
    this.setUiColorByHex(color.value);
  };

  undo.onclick = (e) => {
    this.undo();
  };
  redo.onclick = (e) => {
    this.redo();
  };

  download.onclick = (e) => {
    const link = document.createElement("a");
    const data = this.exportAsDataUrl();
    link.href = data;
    link.download = 655321 + ".png";
    link.click();
  };

  // ## drag&drop images
  file.onclick = (e) => { e.preventDefault(); };
  file.onchange = (e) => {
    file.style.display = "none";
    let reader = new FileReader();
    reader.onload = (e) => {
      if (e.target.result.slice(11, 14) !== "png") {
        throw new Error("Invalid image type!");
      }
      let img = new Image();
      let canvas = document.createElement("canvas");
      let ctx = canvas.getContext("2d");
      img.onload = () => {
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(
          img,
          0, 0,
          img.width, img.height,
          0, 0,
          img.width, img.height
        );
        this.insertImage(ctx, this.last.mx, this.last.my);
        file.value = ""; // reassign to allow second files
      };
      img.src = e.target.result;
    };
    reader.readAsDataURL(e.target.files[0]);
  };
  // hidy things for drag & drop input
  this.view.addEventListener("dragenter", (e) => {
    file.style.display = "block";
  });
  file.addEventListener("dragleave", (e) => {
    file.style.display = "none";
  });

  layers.addEventListener("mouseover", (e) => this.hoverLayer(e));
  layers.addEventListener("click", (e) => this.clickedLayer(e, false));
  layers.addEventListener("dblclick", (e) => this.clickedLayer(e, true));

  add_layer.onclick = (e) => {
    const layer = this.getCurrentLayer();
    let index = layer ? layer.getIndex() : 0;
    index = index < 0 ? 0 : index;
    this.enqueue(CommandKind.LAYER_ADD, {
      layer: new Layer(this), index
    });
  };
  remove_layer.onclick = (e) => {
    const layer = this.getCurrentLayer();
    let index = layer ? layer.getIndex() : 0;
    index = index < 0 ? 0 : index;
    if (layer !== null) this.enqueue(CommandKind.LAYER_REMOVE, {
      layer, index
    });
    this.redraw = true;
  };

  move_layer_up.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && layer.getIndex() > 0) {
      this.enqueue(CommandKind.LAYER_ORDER, {
        layer, index: layer.getIndex() - 1, oindex: layer.getIndex()
      });
    }
    this.redraw = true;
  };
  move_layer_down.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && layer.getIndex() < this.layers.length - 1) {
      this.enqueue(CommandKind.LAYER_ORDER, {
        layer, index: layer.getIndex() + 1, oindex: layer.getIndex()
      });
    }
    this.redraw = true;
  };

  clone.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null) {
      let index = layer ? layer.getIndex() : 0;
      index = index < 0 ? 0 : index;
      this.enqueue(CommandKind.LAYER_CLONE, {
        layer: layer.clone(), index
      });
    }
  };

  clone_by_ref.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null) {
      let index = layer ? layer.getIndex() : 0;
      index = index < 0 ? 0 : index;
      this.enqueue(CommandKind.LAYER_CLONE_REF, {
        layer: layer.cloneByReference(), index
      });
    }
  };

  flip_horizontal.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && !layer.isEmpty()) {
      this.enqueue(CommandKind.LAYER_FLIP_HORIZONTAL, { layer });
    }
  };
  flip_vertical.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && !layer.isEmpty()) {
      this.enqueue(CommandKind.LAYER_FLIP_VERTICAL, { layer });
    }
  };

  rotate_right.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && !layer.isEmpty()) {
      this.enqueue(CommandKind.LAYER_ROTATE_RIGHT, { layer });
    }
  };
  rotate_left.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && !layer.isEmpty()) {
      this.enqueue(CommandKind.LAYER_ROTATE_LEFT, { layer });
    }
  };

  merge.onclick = (e) => {
    const layer = this.getCurrentLayer();
    if (layer !== null && this.layers.length > 1) {
      if (layer.getIndex() < this.layers.length - 1) {
        const merge = this.getLayerByIndex(layer.getIndex() + 1);
        const data = merge.mergeWithLayer(layer);
        this.enqueue(CommandKind.LAYER_MERGE, { data, layer, index: layer.getIndex() });
      }
    }
  };

  add_animation.onclick = (e) => {
    const container = new Container(this);
    const relative = this.getRelativeTileOffset(this.mx, this.my);
    container.bounds.x = relative.x; container.bounds.y = relative.y;
    this.enqueue(CommandKind.CONTAINER_ADD, {
      container: container
    });
  };

  const layer_opacity = (e) => {
    const opacity = 0.5;
    const oopacity = layer.opacity;
    if (oopacity !== opacity) {
      this.enqueue(CommandKind.LAYER_OPACITY, {
        oopacity, opacity, layer: layer
      });
    }
  };

  this.modes.draw = true;
  tiled.style.opacity = 1.0;

  // setup ui list button states
  /*this.processUIClick(document.querySelector("#light-size").children[0]);
  this.processUIClick(document.querySelector("#eraser-size").children[0]);
  this.processUIClick(document.querySelector("#pencil-size").children[0]);*/

};


<|start_filename|>src/render/resize.js<|end_filename|>
/**
 * Resize
 * @param {Number} width
 * @param {Number} height
 */
export function resize(width, height) {
  width = width | 0;
  height = height | 0;
  const gl = this.gl;
  const view = this.view;
  // first update camera size
  this.cw = width;
  this.ch = height;
  // update view
  view.width = width;
  view.height = height;
  // update viewport
  gl.viewport(0, 0, width, height);
  // update shader scales
  gl.uniform2f(
    gl.getUniformLocation(this.program, "uScale"),
    width, height
  );
  gl.enable(gl.BLEND);
  gl.disable(gl.CULL_FACE);
  gl.disable(gl.DEPTH_TEST);
  gl.disable(gl.STENCIL_TEST);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  // re-generate our bg and fg
  this.cache.bg = this.createBackgroundBuffer();
  this.cache.fg = this.createForegroundBuffer();
  // re-generate our grid
  this.cache.grid = this.createGridBuffer();
  this.redrawGridBuffer();
  this.redraw = true;
};


<|start_filename|>src/env/fill.js<|end_filename|>
import { BASE_TILE_COLOR } from "../cfg";

import {
  createCanvasBuffer
} from "../utils";

import {
  colorsMatch,
  rgbAlphaToAlphaByte
} from "../color";

import CommandKind from "../stack/kind";

/**
 * Fill enclosed tile area
 * @param {Number} x
 * @param {Number} y
 * @param {Array} color
 */
export function fillBucket(x, y, color) {
  color = color || [255, 255, 255, 1];
  if (color[3] > 1) throw new Error("Invalid alpha color!");
  // differentiate between empty and colored tiles
  const layer = this.getCurrentLayer();
  const bounds = layer.bounds;
  const base = layer.getPixelAt(x, y) || BASE_TILE_COLOR;
  // clicked tile color and fill colors matches, abort
  if (colorsMatch(base, color)) return;
  // save the current stack index
  const batch = layer.createBatchAt(x, y);
  // flood fill
  let shape = this.getBinaryShape(x, y, base);
  // ups, we filled infinite
  if (shape === null) return;
  // now fill a buffer by our grid data
  const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
  const bw = bounds.w; const bh = bounds.h;
  const bcolor = [color[0], color[1], color[2], color[3]];
  batch.resizeRectangular(
    bx, by,
    bw, bh
  );
  let count = 0;
  // flood fill pixels
  for (let ii = 0; ii < bw * bh; ++ii) {
    const xx = (ii % bw) | 0;
    const yy = (ii / bw) | 0;
    const px = (yy * bw + xx) | 0;
    // only fill active grid pixels
    if (shape[px] !== 2) continue;
    batch.drawPixelFast(bx + xx, by + yy, bcolor);
    count++;
  };
  // nothing changed
  if (count <= 0) {
    batch.kill();
    return;
  }
  // auto resize batch's size by the used pixel data
  batch.resizeByMatrixData();
  this.enqueue(CommandKind.FILL, batch);
  // free grid from memory
  shape = null;
  return;
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Void}
 */
export function floodPaint(x, y) {
  const color = this.fillStyle;
  const layer = this.getCurrentLayer();
  const bounds = layer.bounds;
  const base = layer.getPixelAt(x, y);
  // empty base tile or colors to fill are the same
  if (base === null || colorsMatch(base, color)) return;
  const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
  const bw = bounds.w; const bh = bounds.h;
  const batch = layer.createBatchAt(bx, by);
  batch.resizeRectangular(
    bx, by,
    bw, bh
  );
  let count = 0;
  // flood paint
  for (let ii = 0; ii < bw * bh; ++ii) {
    const xx = (ii % bw);
    const yy = (ii / bw) | 0;
    const pixel = layer.getPixelAt(bx + xx, by + yy);
    if (pixel === null) continue;
    if (!colorsMatch(base, pixel)) continue;
    batch.drawPixelFast(bx + xx, by + yy, color);
    count++;
  };
  // nothing changed
  if (count <= 0) {
    batch.kill();
    return;
  }
  batch.resizeByMatrixData();
  this.enqueue(CommandKind.FLOOD_FILL, batch);
  return;
};


<|start_filename|>src/cfg.js<|end_filename|>
// default view size
export const DEFAULT_WIDTH = 480;
export const DEFAULT_HEIGHT = 320;
// default grid hidden or not
export const DEFAULT_GRID_HIDDEN = false;

export const TILE_SIZE = 8;
export const MIN_SCALE = 0.1;
export const MAX_SCALE = 32;
export const BASE_SCALE = 1;
export const MAGIC_SCALE = .125;
// trace ghost tiles by alpha=^2
export const UNSET_TILE_COLOR = 2;
export const ERASE_TILE_COLOR = [0, 1, 0, 0.1];
export const BASE_TILE_COLOR = [0, 0, 0, 0];
export const SELECTION_COLOR = [1, 1, 1, 0.1];
export const SELECTION_COLOR_ACTIVE = [1, 1, 1, 0.2];
export const TILE_HOVER_COLOR = [1, 1, 1, 0.2];

// 32-bit ints are allowed at maximum
export const MAX_SAFE_INTEGER = (2 ** 31) - 1;

// alpha byte to rgb-alpha conversion
export const MAGIC_RGB_A_BYTE = 0.00392;

// factor when to hide the grid
export const HIDE_GRID = 1.0;
export const GRID_LINE_WIDTH = 0.25;

// how fast we can scale with our mouse wheel
export const ZOOM_SPEED = 15;

// base step size we jump when resizing batches
export const BATCH_JUMP_RESIZE = 64;

// Maximum allowed items inside stack
export const STACK_LIMIT = 128;

// WebGL texture limit
export const WGL_TEXTURE_LIMIT = STACK_LIMIT * 2;

// WebGL supported or not
export const WGL_SUPPORTED = (
  typeof WebGLRenderingContext !== "undefined"
);

// WebAssembly supported or not
export const WASM_SUPPORTED = (
  typeof WebAssembly !== "undefined"
);

// dev mode state
export let MODES = {
  DEV: false
};

// different settings
export let SETTINGS = {
  LIGHT_SIZE: 1,
  PENCIL_SIZE: 1,
  ERASER_SIZE: 1,
  LIGHTING_MODE: 0.05
};

export const STORAGE_KEY = "poxi";
export const STORAGE_OBJECT = window.localStorage;

// asset path
export const ASSET_PATH = "./assets/";

// light bulb icon res
export const LIGHT_DARKEN_IMG_PATH = ASSET_PATH + "img/light_off.png";
export const LIGHT_LIGHTEN_IMG_PATH = ASSET_PATH + "img/light_on.png";


<|start_filename|>src/stack/state.js<|end_filename|>
import { MODES } from "../cfg";
import { additiveAlphaColorBlending } from "../color";

import CommandKind from "./kind";

/**
 * Manually refresh the stack,
 * clear future operations etc.
 */
export function refreshStack() {
  if (this.sindex < this.stack.length - 1) {
    this.dequeue(this.sindex, this.stack.length - 1);
  } else {
    this.stack.splice(this.sindex + 1, this.stack.length);
  }
  //this.updateGlobalBoundings();
};

/**
 * Returns the latest stack operation
 * @return {Command}
 */
export function currentStackOperation() {
  return (this.stack[this.sindex] || null);
};

/**
 * Indicates if an operation is mergeable
 * with an older same kind but opposite operation
 * @param {Number} kind
 * @return {Boolean}
 */
export function isMergeableOperation(kind) {
  return (
    kind === CommandKind.LAYER_LOCK ||
    kind === CommandKind.LAYER_FLIP_VERTICAL ||
    kind === CommandKind.LAYER_FLIP_HORIZONTAL ||
    kind === CommandKind.LAYER_VISIBILITY
  );
};

/**
 * @param {Command} cmd
 * @param {Boolean} state
 */
export function fire(cmd, state) {
  const kind = this.getCommandKind(cmd);
  switch (kind) {
    case CommandKind.LAYER_OPERATION:
      this.fireLayerOperation(cmd, state);
    break;
    case CommandKind.BATCH_OPERATION:
      this.fireBatchOperation(cmd, state);
    break;
    case CommandKind.CONTAINER_OPERATION:
      this.fireContainerOperation(cmd, state);
    break;
  };
};

/**
 * @param {Command} cmd
 * @param {Boolean} state
 */
export function fireContainerOperation(cmd, state) {
  const batch = cmd.batch;
  const container = batch.container;
  if (state) {
    this.containers.push(container);
  } else {
    for (let ii = 0; ii < this.containers.length; ++ii) {
      if (this.containers[ii].id === container.id) {
        this.containers.splice(ii, 1);
        break;
      }
    };
  }
};

/**
 * @param {Command} cmd
 * @param {Boolean} state
 */
export function fireLayerOperation(cmd, state) {
  const kind = cmd.kind;
  const batch = cmd.batch;
  const layer = batch.layer;
  const main = layer.batch;
  switch (kind) {
    case CommandKind.LAYER_MERGE:
      layer.updateBoundings();
      const data = cmd.batch.data;
      if (state) {
        layer.removeUiReference();
        this.layers.splice(batch.index, 1);
        let index = batch.index < 0 ? 0 : batch.index;
        index = index === this.layers.length ? index - 1 : index;
        const merge = this.getLayerByIndex(index);
        this.setActiveLayer(merge);
        merge.updateBoundings();
        merge.batch.injectMatrix(data, true);
        merge.batch.refreshTexture(true);
      } else {
        const merge = this.getLayerByIndex(batch.index);
        this.layers.splice(batch.index, 0, layer);
        layer.addUiReference();
        this.setActiveLayer(layer);
        merge.batch.injectMatrix(data, false);
        merge.batch.refreshTexture(true);
      }
    break;
    case CommandKind.LAYER_CLONE:
    case CommandKind.LAYER_CLONE_REF:
    case CommandKind.LAYER_ADD:
      if (state) {
        this.layers.splice(batch.index, 0, layer);
        layer.addUiReference();
        this.setActiveLayer(layer);
      } else {
        layer.removeUiReference();
        this.layers.splice(batch.index, 1);
        const index = batch.index < 0 ? 0 : batch.index;
        this.setActiveLayer(this.getLayerByIndex(index));
      }
    break;
    case CommandKind.LAYER_REMOVE:
      if (!state) {
        this.layers.splice(batch.index, 0, layer);
        layer.addUiReference();
        this.setActiveLayer(layer);
      } else {
        layer.removeUiReference();
        this.layers.splice(batch.index, 1);
        let index = batch.index < 0 ? 0 : batch.index;
        index = index === this.layers.length ? index - 1 : index;
        this.setActiveLayer(this.getLayerByIndex(index));
      }
    break;
    case CommandKind.LAYER_RENAME:
      layer.name = batch[state ? "name": "oname"];
    break;
    case CommandKind.LAYER_LOCK:
      layer.locked = !layer.locked;
    break;
    case CommandKind.LAYER_VISIBILITY:
      layer.visible = !layer.visible;
    break;
    case CommandKind.LAYER_OPACITY:
      layer.opacity = batch[state ? "opacity" : "oopacity"];
    break;
    case CommandKind.LAYER_ORDER:
      if (state) {
        const tmp = this.layers[batch.oindex];
        this.layers[batch.oindex] = this.layers[batch.index];
        this.layers[batch.index] = tmp;
        tmp.removeUiReference(); tmp.addUiReference();
        this.setActiveLayer(tmp);
      } else {
        const tmp = this.layers[batch.index];
        this.layers[batch.index] = this.layers[batch.oindex];
        this.layers[batch.oindex] = tmp;
        tmp.removeUiReference(); tmp.addUiReference();
        this.setActiveLayer(tmp);
      }
    break;
    case CommandKind.LAYER_MOVE:
      layer.updateBoundings();
      const dir = state ? 1 : -1;
      layer.x += (batch.position.x * dir);
      layer.y += (batch.position.y * dir);
    break;
    case CommandKind.LAYER_FLIP_VERTICAL:
      this.flipVertically(layer);
    break;
    case CommandKind.LAYER_FLIP_HORIZONTAL:
      this.flipHorizontally(layer);
    break;
    case CommandKind.LAYER_ROTATE_LEFT:
      if (state) this.rotateLeft(layer);
      else this.rotateRight(layer);
    break;
    case CommandKind.LAYER_ROTATE_RIGHT:
      if (state) this.rotateRight(layer);
      else this.rotateLeft(layer);
    break;
  };
};

/**
 * @param {Command} cmd
 * @param {Boolean} state
 */
export function fireBatchOperation(cmd, state) {
  const batch = cmd.batch;
  const layer = batch.layer;
  const main = layer.batch;
  const kind = cmd.kind;
  layer.updateBoundings();
  main.injectMatrix(batch, state);
  main.refreshTexture(true);
};

/**
 * @param {Command} cmd
 * @return {Number}
 */
export function getCommandKind(cmd) {
  const kind = cmd.kind;
  switch (kind) {
    case CommandKind.LAYER_LOCK:
    case CommandKind.LAYER_MOVE:
    case CommandKind.LAYER_ORDER:
    case CommandKind.LAYER_RENAME:
    case CommandKind.LAYER_ROTATE_LEFT:
    case CommandKind.LAYER_ROTATE_RIGHT:
    case CommandKind.LAYER_VISIBILITY:
    case CommandKind.LAYER_OPACITY:
    case CommandKind.LAYER_ADD:
    case CommandKind.LAYER_REMOVE:
    case CommandKind.LAYER_MERGE:
    case CommandKind.LAYER_CLONE:
    case CommandKind.LAYER_CLONE_REF:
    case CommandKind.LAYER_FLIP_VERTICAL:
    case CommandKind.LAYER_FLIP_HORIZONTAL:
      return (CommandKind.LAYER_OPERATION);
    break;
    case CommandKind.DRAW:
    case CommandKind.ERASE:
    case CommandKind.FILL:
    case CommandKind.BACKGROUND:
    case CommandKind.PASTE:
    case CommandKind.CUT:
    case CommandKind.INSERT_IMAGE:
    case CommandKind.STROKE:
    case CommandKind.RECT_FILL:
    case CommandKind.RECT_STROKE:
    case CommandKind.ARC_FILL:
    case CommandKind.ARC_STROKE:
    case CommandKind.FLOOD_FILL:
    case CommandKind.LIGHTING:
      return (CommandKind.BATCH_OPERATION);
    break;
    case CommandKind.CONTAINER_ADD:
    case CommandKind.CONTAINER_REMOVE:
      return (CommandKind.CONTAINER_OPERATION);
    break;
  };
  return (CommandKind.UNKNOWN);
};


<|start_filename|>src/event/listener.js<|end_filename|>
import {
  hexToRgba,
  rgbaToHex,
  rgbaToBytes,
  getRainbowColor
} from "../color";

import {
  MODES,
  SETTINGS,
  TILE_SIZE,
  LIGHT_DARKEN_IMG_PATH,
  LIGHT_LIGHTEN_IMG_PATH
} from "../cfg";

import { pointDistance } from "../math";

import CommandKind from "../stack/kind";

export function initListeners() {

  window.addEventListener("resize", (e) => this.onResize(e));

  window.addEventListener("mousedown", (e) => this.onMouseDown(e));
  window.addEventListener("mouseup", (e) => this.onMouseUp(e));

  window.addEventListener("mousemove", (e) => this.onMouseMove(e));

  window.addEventListener("keydown", (e) => this.onKeyDown(e));
  window.addEventListener("keyup", (e) => this.onKeyUp(e));

  window.addEventListener("contextmenu", (e) => this.onContextmenu(e));

  window.addEventListener("wheel", (e) => this.onMouseWheel(e));
  window.addEventListener("mousewheel", (e) => this.onMouseWheel(e));

  this.view.addEventListener("mouseout", (e) => this.onMouseOut(e));
  this.view.addEventListener("mouseleave", (e) => this.onMouseLeave(e));

  menu.addEventListener("click", (e) => this.onColorMenuClick(e));

};

/**
 * @param {Event} e
 */
export function onResize(e) {
  this.resize(
    window.innerWidth, window.innerHeight
  );
};

/**
 * @param {Event} e
 */
export function onMouseOut(e) {
  e.preventDefault();
  //this.onMouseUp(e);
};

/**
 * @param {Event} e
 */
export function onMouseLeave(e) {
  e.preventDefault();
  //this.onMouseUp(e);
};

export function resetListElementsActiveState(el) {
  for (let ii = 0; ii < el.children.length; ii++) {
    el.children[ii].classList.remove("active");
  };
};

/**
 * @param {HTMLElement} el
 * @return {Void}
 */
export function onColorMenuClick(el) {
  const element = el.target;
  if (element.id) return;
  const value = element.getAttribute("color");
  const rgba = JSON.parse(value);
  this.setUiColorByRgba(rgba);
  return;
};

/**
 * @param {HTMLElement} el
 */
export function processUIClick(el) {
  const parent = el.parentNode;
  if (!parent) return;
  const id = parent.id;
  if (id === "pencil-size") {
    const value = el.innerHTML;
    SETTINGS.PENCIL_SIZE = parseInt(value);
    this.resetModes();
    this.modes.draw = true;
    tiled.style.opacity = 1.0;
  }
  else if (id === "eraser-size") {
    const value = el.innerHTML;
    SETTINGS.ERASER_SIZE = parseInt(value);
    this.resetModes();
    this.modes.erase = true;
    erase.style.opacity = 1.0;
  }
  else if (id === "light-size") {
    const value = el.innerHTML;
    SETTINGS.LIGHT_SIZE = parseInt(value);
    this.resetModes();
    this.modes.light = true;
    lighting.style.opacity = 1.0;
  }
  else return;
  this.resetListElementsActiveState(el.parentNode);
  el.classList.add("active");
};

/**
 * @param {Event} e
 */
export function onMouseDown(e) {
  // only allow clicking on canvas
  if (!(e.target instanceof HTMLCanvasElement)) {
    this.processUIClick(e.target);
    return;
  }
  // finalize earlier operations and abort
  if (this.isInActiveState() && e.which === 1) {
    this.onMouseUp(e);
    return;
  }
  const x = e.clientX;
  const y = e.clientY;
  const relative = this.getRelativeTileOffset(x, y);
  const rx = relative.x; const ry = relative.y;
  if (e.which === 1) {
    const layer = this.getCurrentLayer();
    if (layer === null) return;
    this.resetSelection();
    if (this.modes.move) {
      const layer = this.getLayerByPoint(rx, ry);
      if (layer !== null) {
        this.buffers.mLayer = this.getCurrentLayer();
        this.setActiveLayer(layer);
        this.states.moving = true;
        const batch = this.createDynamicBatch(rx, ry);
        batch.position.mx = rx;
        batch.position.my = ry;
        batch.layer = layer;
        batch.isMover = true;
        this.buffers.move = batch;
      }
    }
    else if (this.modes.select) {
      this.states.selecting = true;
      this.selectFrom(x, y);
      this.selectTo(x, y);
    }
    else if (this.modes.arc) {
      this.states.arc = true;
      this.buffers.arc = layer.createBatchAt(rx, ry);
      const batch = this.buffers.arc;
      batch.forceRendering = true;
      batch.refreshTexture(false);
    }
    else if (this.modes.rect) {
      this.states.rect = true;
      this.buffers.rect = layer.createBatchAt(rx, ry);
      const batch = this.buffers.rect;
      batch.forceRendering = true;
      batch.refreshTexture(false);
    }
    else if (this.modes.draw) {
      this.states.drawing = true;
      this.buffers.drawing = layer.createBatchAt(rx, ry);
      const batch = this.buffers.drawing;
      batch.forceRendering = true;
      batch.drawAt(rx, ry, SETTINGS.PENCIL_SIZE, this.fillStyle);
      batch.refreshTexture(false);
    }
    else if (this.modes.erase) {
      this.states.erasing = true;
      this.buffers.erasing = layer.createBatchAt(rx, ry);
      const batch = this.buffers.erasing;
      batch.forceRendering = true;
      batch.clearRect(rx, ry, SETTINGS.ERASER_SIZE, SETTINGS.ERASER_SIZE);
      batch.refreshTexture(false);
      batch.isEraser = true;
    }
    else if (this.modes.light) {
      this.states.lighting = true;
      this.buffers.lighting = layer.createBatchAt(rx, ry);
      const batch = this.buffers.lighting;
      batch.forceRendering = true;
      batch.applyColorLightness(rx, ry, SETTINGS.LIGHTING_MODE);
      batch.refreshTexture(false);
    }
    else if (this.modes.stroke) {
      this.states.stroke = true;
      this.buffers.stroke = layer.createBatchAt(rx, ry);
      const batch = this.buffers.stroke;
      batch.forceRendering = true;
      batch.refreshTexture(false);
    }
    else if (this.modes.flood) {
      this.floodPaint(rx, ry);
    }
    else if (this.modes.fill) {
      this.fillBucket(rx, ry, this.fillStyle);
    }
    else if (this.modes.shape) {
      const batch = this.getShapeAt(rx, ry);
      this.shape = batch;
    }
    else if (this.modes.pipette) {
      this.states.pipette = true;
      const color = this.getAbsolutePixelAt(rx, ry);
      if (color !== null) {
        this.fillStyle = color;
        color_view.style.background = color.value = rgbaToHex(color);
      }
    }
  }
  else if (e.which === 2 || e.which === 3) {
    e.preventDefault();
    this.states.dragging = true;
    this.click(x, y);
  }
  if (e.which === 1) {
    this.last.mdx = x; this.last.mdy = y;
    const start = this.getRelativeTileOffset(this.last.mdx, this.last.mdy);
    this.last.mdrx = start.x; this.last.mdry = start.y;
  }
};

let lastx = -0;
let lasty = -0;
/**
 * @param {Event} e
 */
export function onMouseMove(e) {
  e.preventDefault();
  const x = e.clientX; const y = e.clientY;
  if (!(e.target instanceof HTMLCanvasElement)) return;
  const last = this.last;
  const layer = this.getCurrentLayer();
  const relative = this.getRelativeTileOffset(x, y);
  const rx = relative.x; const ry = relative.y;
  // mouse polling rate isn't 'per-pixel'
  // so we try to interpolate missed offsets
  this.updateCursorPosition(x, y);
  if (this.modes.move) {
    this.redraw = true;
  }
  if (this.states.dragging) {
    this.drag(x, y);
    this.hover(x, y);
    lastx = x; lasty = y;
    last.mx = rx; last.my = ry;
    return;
  }
  if (last.mx === rx && last.my === ry) return;
  this.hover(x, y);
  this.redraw = true;
  if (this.states.moving) {
    const batch = this.buffers.move;
    batch.move(rx, ry);
    batch.refreshTexture(false);
  }
  else if (this.states.arc) {
    const batch = this.buffers.arc;
    batch.clear();
    const sx = this.last.mdrx;
    const sy = this.last.mdry;
    const radius = pointDistance(sx, sy, rx, ry);
    this.strokeArc(batch, sx, sy, radius, this.fillStyle);
    batch.refreshTexture(false);
  }
  else if (this.states.rect) {
    const batch = this.buffers.rect;
    batch.clear();
    const sx = this.last.mdrx;
    const sy = this.last.mdry;
    const ww = rx - sx;
    const hh = ry - sy;
    this.strokeRect(batch, sx, sy, ww, hh, this.fillStyle);
    batch.refreshTexture(false);
  }
  else if (this.states.stroke) {
    const batch = this.buffers.stroke;
    batch.clear();
    this.insertLine(this.last.mdrx, this.last.mdry, rx, ry);
    batch.refreshTexture(false);
  }
  else if (this.states.drawing) {
    const batch = this.buffers.drawing;
    this.insertLine(x, y, lastx, lasty);
    batch.refreshTexture(false);
  }
  else if (this.states.erasing) {
    const batch = this.buffers.erasing;
    this.insertLine(x, y, lastx, lasty);
    batch.refreshTexture(false);
  }
  else if (this.states.lighting) {
    const batch = this.buffers.lighting;
    this.insertLine(x, y, lastx, lasty);
    batch.refreshTexture(false);
  }
  else if (this.states.dragging) {
    this.drag(x, y);
  }
  else if (this.states.selecting) {
    this.selectTo(x, y);
  }
  else if (this.states.pipette) {
    const color = this.getAbsolutePixelAt(rx, ry);
    if (color !== null) {
      this.fillStyle = color;
      color_view.style.background = color.value = rgbaToHex(color);
    }
  }
  lastx = x; lasty = y;
  last.mx = rx; last.my = ry;
};

/**
 * @param {Event} e
 */
export function onMouseUp(e) {
  e.preventDefault();
  if (!(e.target instanceof HTMLCanvasElement)) return;
  if (e.which === 1) {
    if (this.getCurrentLayer() === null) return;
    if (this.modes.move && this.buffers.move) {
      const batch = this.buffers.move;
      const layer = batch.layer;
      this.states.move = false;
      this.states.moving = false;
      // only enqueue if batch not empty
      if (batch.position.x !== 0 || batch.position.y !== 0) {
        layer.x -= batch.position.x;
        layer.y -= batch.position.y;
        this.enqueue(CommandKind.LAYER_MOVE, batch);
      } else {
        batch.kill();
      }
      this.setActiveLayer(this.buffers.mLayer);
      this.buffers.move = null;
    }
    else if (this.states.arc) {
      const batch = this.buffers.arc;
      batch.forceRendering = false;
      this.states.arc = false;
      batch.resizeByMatrixData();
      batch.refreshTexture(false);
      if (batch.isEmpty()) batch.kill();
      else this.enqueue(CommandKind.ARC_FILL, batch);
      this.buffers.arc = null;
    }
    else if (this.states.rect) {
      const batch = this.buffers.rect;
      batch.forceRendering = false;
      this.states.rect = false;
      batch.resizeByMatrixData();
      batch.refreshTexture(false);
      if (batch.isEmpty()) batch.kill();
      else this.enqueue(CommandKind.RECT_FILL, batch);
      this.buffers.rect = null;
    }
    else if (this.states.stroke) {
      const batch = this.buffers.stroke;
      batch.forceRendering = false;
      this.states.stroke = false;
      batch.resizeByMatrixData();
      batch.refreshTexture(false);
      if (batch.isEmpty()) batch.kill();
      else this.enqueue(CommandKind.STROKE, batch);
      this.buffers.stroke = null;
    }
    else if (this.states.select) {
      this.states.selecting = false;
      this.redraw = true;
    }
    else if (this.states.drawing) {
      const batch = this.buffers.drawing;
      batch.forceRendering = false;
      batch.resizeByMatrixData();
      this.states.drawing = false;
      this.enqueue(CommandKind.DRAW, batch);
      this.buffers.drawing = null;
    }
    else if (this.states.erasing) {
      const batch = this.buffers.erasing;
      batch.forceRendering = false;
      batch.resizeByMatrixData();
      this.states.erasing = false;
      if (batch.isEmpty()) batch.kill();
      else this.enqueue(CommandKind.ERASE, batch);
      this.buffers.erasing = null;
    }
    else if (this.states.lighting) {
      const batch = this.buffers.lighting;
      batch.forceRendering = false;
      batch.resizeByMatrixData();
      this.states.lighting = false;
      if (batch.isEmpty()) batch.kill();
      else this.enqueue(CommandKind.LIGHTING, batch);
      this.buffers.lighting = null;
    }
    else if (this.states.pipette) {
      this.states.pipette = false;
    }
  }
  if (e.which === 2 || e.which === 3) {
    this.states.dragging = false;
  }
};

/**
 * @param {Event} e
 */
export function onKeyDown(e) {
  const code = e.keyCode;
  const target = e.target;
  if (target !== document.body) {
    return;
  }
  e.preventDefault();
  this.keys[code] = 1;
  switch (code) {
    // ctrl
    case 17:
      if (this.modes.light) {
        // lighting mode is darken
        SETTINGS.LIGHTING_MODE = -(Math.abs(SETTINGS.LIGHTING_MODE));
        lighting.src = LIGHT_DARKEN_IMG_PATH;
      }
    break;
    // del
    case 46:
      this.clearSelection(this.getSelection());
      this.resetSelection();
    break;
    // c | ctrl+c
    case 67:
      if (this.keys[17]) {
        this.copy(this.getSelection());
      }
    break;
    // x | ctrl+x
    case 88:
      if (this.keys[17]) {
        this.cut(this.getSelection());
        this.resetSelection();
      }
    break;
    // v + ctrl+v
    case 86:
      if (this.keys[17]) {
        this.pasteAt(this.last.mx, this.last.my, this.getSelection());
        this.resetSelection();
      }
    break;
    // z | ctr+z
    case 90:
      if (this.keys[17]) {
        this.undo();
      }
    break;
    // y | ctrl+y
    case 89:
      if (this.keys[17]) {
        this.redo();
      }
    break;
    // f2
    case 113:
      MODES.DEV = !MODES.DEV;
      this.redraw = true;
    break;
    // f5
    case 116:
      location.reload();
    break;
    // space
    case 32:
      // already open, close so
      if (this.states.fastColorMenu) {
        this.closeFastColorPickerMenu();
        return;
      }
      const width = menu.clientWidth;
      const height = menu.clientHeight;
      const btmWidth = document.querySelector(".bottom-menu").clientHeight;
      let yy = lasty;
      let xx = (lastx - (width / 2) | 0);
      // invert menu position since we are out of window view
      if (yy + height > stage.ch - btmWidth) {
        yy = yy - height;
      }
      menu.style.top = yy + "px";
      menu.style.left = xx + "px";
      this.openFastColorPickerMenu();
    break;
    default:
      return;
    break;
  };
};

/**
 * @param {Event} e
 */
export function onKeyUp(e) {
  e.preventDefault();
  const code = e.keyCode;
  this.keys[code] = 0;
  switch (code) {
    // ctrl
    case 17:
      // lighting mode is lighten
      if (this.modes.light) {
        SETTINGS.LIGHTING_MODE = Math.abs(SETTINGS.LIGHTING_MODE);
        lighting.src = LIGHT_LIGHTEN_IMG_PATH;
      }
    break;
  };
};

/**
 * @param {Event} e
 */
export function onContextmenu(e) {
  e.preventDefault();
};

/**
 * @param {Event} e
 */
export function onMouseWheel(e) {
  if (!(e.target instanceof HTMLCanvasElement)) return;
  e.preventDefault();
  const x = e.clientX;
  const y = e.clientY;
  const value = e.deltaY > 0 ? -1 : 1;
  this.click(x, y);
  this.scale(value);
  this.hover(x, y);
};


<|start_filename|>src/env/functions.js<|end_filename|>
import {
  SETTINGS,
  MAX_SAFE_INTEGER
} from "../cfg";

import {
  colorToRgbaString,
  alphaByteToRgbAlpha
} from "../color";

import Layer from "../layer/index";
import Batch from "../batch/index";
import CommandKind from "../stack/kind";

/**
 * @return {Boolean}
 */
export function isInActiveState() {
  const states = this.states;
  for (let key in states) {
    // ignore dragging state
    if (key === "dragging") continue;
    if (states[key]) return (true);
  };
  return (false);
};

/**
 * @param {Number} id
 * @return {Batch}
 */
export function getBatchById(id) {
  let result = null;
  const layers = this.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const idx = layers.length - 1 - ii;
    const layer = layers[idx];
    let batch = layer.getBatchById(id);
    if (batch !== null) {
      result = batch;
      break;
    }
  };
  return (result);
};

/**
 * @return {Layer}
 */
export function addLayer() {
  const layer = new Layer(this);
  layer.addUiReference();
  this.layers.push(layer);
  return (layer);
};

/**
 * @return {Layer}
 */
export function getCurrentLayer() {
  return (this.activeLayer || null);
};

/**
 * @param {HTMLElement} node
 * @return {Layer}
 */
export function getLayerByNode(node) {
  for (let ii = 0; ii < this.layers.length; ++ii) {
    const layer = this.layers[ii];
    if (layer.node === node) return (layer);
  };
  return (null);
};

/**
 * @param {Number} index
 * @return {Layer}
 */
export function getLayerByIndex(index) {
  return (this.layers[index] || null);
};

/**
 * @param {Layer} layer
 */
export function setActiveLayer(layer) {
  const old = this.getCurrentLayer();
  if (old && old.node) {
    old.node.classList.remove("selected");
  }
  if (layer) layer.node.classList.add("selected");
  this.activeLayer = layer;
  this.redraw = true;
};

export function refreshUiLayers() {
  const layers = this.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    layer.removeUiReference();
    layer.addUiReference();
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Layer}
 */
export function getLayerByPoint(x, y) {
  const layers = this.layers;
  // search by active pixel
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    const xx = x - layer.x;
    const yy = y - layer.y;
    if (layer.locked) continue;
    if (layer.bounds.isPointInside(xx, yy)) {
      if (layer.getPixelAt(x, y)) {
        return (layer);
      }
    }
  };
  // active pixel search failed
  // so now search by point inside
  for (let ii = 0; ii < layers.length; ++ii) {
    const idx = layers.length - 1 - ii;
    const layer = layers[ii];
    const xx = x - layer.x;
    const yy = y - layer.y;
    if (layer.locked) continue;
    if (layer.bounds.isPointInside(xx, yy)) {
      return (layer);
    }
  };
  return (null);
};

/**
 * Get batch to insert at by current active state
 * @return {Batch}
 */
export function getCurrentDrawingBatch() {
  for (let key in this.states) {
    const state = this.states[key];
    if (state === true && this.buffers[key]) {
      return (this.buffers[key]);
    }
  };
  return (null);
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Batch}
 */
export function createDynamicBatch(x, y) {
  const batch = new Batch(this);
  batch.prepareMatrix(x, y);
  return (batch);
};

/**
 * Get absolute pixel
 * @param {Number} x
 * @param {Number} y
 * @return {Array}
 */
export function getAbsolutePixelAt(x, y) {
  // normalize coordinates
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  const xx = x - this.bounds.x;
  const yy = y - this.bounds.y;
  // check if point inside boundings
  if (
    (xx < 0 || yy < 0) ||
    (bw <= 0 || bh <= 0) ||
    (xx >= bw || yy >= bh)
  ) return (null);
  // go through each layer reversed
  // and search for the given pixel
  const layers = this.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    if (!layer.visible) continue;
    const pixel = layer.getPixelAt(x, y);
    if (pixel !== null) return (pixel);
  };
  return (null);
};

/**
 * Get layer relative pixel
 * @param {Number} x
 * @param {Number} y
 * @return {Array}
 */
export function getRelativePixelAt(x, y) {
  // normalize coordinates
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  const xx = x - this.bounds.x;
  const yy = y - this.bounds.y;
  // check if point inside boundings
  if (
    (xx < 0 || yy < 0) ||
    (bw <= 0 || bh <= 0) ||
    (xx >= bw || yy >= bh)
  ) return (null);
  // search for the pixel at given layer
  const layer = this.getCurrentLayer();
  if (layer !== null) {
    return (layer.getPixelAt(x, y));
  }
  return (null);
};
/**
 * Get absolute live pixel
 * @param {Number} x
 * @param {Number} y
 * @return {Array}
 */
export function getLivePixelAt(x, y) {
  // normalize coordinates
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  const xx = x - this.bounds.x;
  const yy = y - this.bounds.y;
  // check if point inside boundings
  if (
    (xx < 0 || yy < 0) ||
    (bw <= 0 || bh <= 0) ||
    (xx >= bw || yy >= bh)
  ) return (null);
  // go through each layer reversed
  // and search for the given pixel
  const layers = this.layers;
  for (let ii = 0; ii < layers.length; ++ii) {
    const pixel = layers[ii].getLivePixelAt(x, y);
    if (pixel !== null) return (pixel);
  };
  return (null);
};

export function updateGlobalBoundings() {
  const layers = this.layers;
  const bounds = this.bounds;
  let x = MAX_SAFE_INTEGER;  let y = MAX_SAFE_INTEGER;
  let w = -MAX_SAFE_INTEGER; let h = -MAX_SAFE_INTEGER;
  let count = 0;
  for (let ii = 0; ii < layers.length; ++ii) {
    const layer = layers[ii];
    layer.updateBoundings();
    const bounds = layer.bounds;
    const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
    const bw = bx + bounds.w; const bh = by + bounds.h;
    // ignore empty layers
    if (bounds.w === 0 && bounds.h === 0) continue;
    // calculate x
    if (x < 0 && bx < x) x = bx;
    else if (x >= 0 && (bx < 0 || bx < x)) x = bx;
    // calculate y
    if (y < 0 && by < y) y = by;
    else if (y >= 0 && (by < 0 || by < y)) y = by;
    // calculate width
    if (bw > w) w = bw;
    // calculate height
    if (bh > h) h = bh;
    count++;
  };
  // update our boundings
  if (count > 0) {
    //this.updateSelectionMatrix();
    this.bounds.update(x, y, -x + w, -y + h);
  }
};

/**
 * Uses preallocated binary grid with the size of the absolute boundings
 * of our working area. In the next step we trace "alive cells" in the grid,
 * then we take the boundings of the used area of our grid and crop out
 * the relevant part. Next we can process each tile=^2 traced as inside shape
 * @param {Number} x
 * @param {Number} y
 * @param {Array} base
 * @return {Uint8Array}
 */
export function getBinaryShape(x, y, base) {
  const layer = this.getCurrentLayer();
  const bounds = layer.bounds;
  const bx = layer.x + bounds.x; const by = layer.y + bounds.y;
  const bw = bounds.w; const bh = bounds.h;
  const isEmpty = base[3] === 0;
  const gridl = bw * bh;
  // allocate and do a basic fill onto the grid
  let grid = new Uint8Array(bw * bh);
  for (let ii = 0; ii < gridl; ++ii) {
    const xx = ii % bw;
    const yy = (ii / bw) | 0;
    const color = layer.getPixelAt(bx + xx, by + yy);
    // empty tile based
    if (isEmpty) { if (color !== null) continue; }
    // color based
    else {
      if (color === null) continue;
      if (!(
        base[0] === color[0] &&
        base[1] === color[1] &&
        base[2] === color[2] &&
        base[3] === color[3]
      )) continue;
    }
    // fill tiles with 1's if we got a color match
    grid[yy * bw + xx] = 1;
  };
  // trace connected tiles by [x,y]=2
  let queue = [{x: x - bx, y: y - by}];
  while (queue.length > 0) {
    const point = queue.pop();
    const x = point.x; const y = point.y;
    const idx = y * bw + x;
    // set this grid tile to 2, if it got traced earlier as a color match
    if (grid[idx] === 1) grid[idx] = 2;
    const nn = (y-1) * bw + x;
    const ee = y * bw + (x+1);
    const ss = (y+1) * bw + x;
    const ww = y * bw + (x-1);
    if (grid[nn] === 1) queue.push({x, y:y-1});
    if (grid[ee] === 1) queue.push({x:x+1, y});
    if (grid[ss] === 1) queue.push({x, y:y+1});
    if (grid[ww] === 1) queue.push({x:x-1, y});
  };
  return (grid);
};

/**
 * @return {Number}
 */
export function getCursorSize() {
  for (let key in this.modes) {
    if (!this.modes[key]) continue;
    switch (key) {
      case "arc":
      case "draw":
      case "rect":
      case "stroke":
        return (SETTINGS.PENCIL_SIZE);
      break;
      case "erase":
        return (SETTINGS.ERASER_SIZE);
      break;
      case "light":
        return (SETTINGS.LIGHT_SIZE);
      break;
      case "move":
      case "fill":
      case "shape":
      case "flood":
      case "select":
        return (1);
      break;
      case "pipette":
        return (1);
      break;
    };
  };
  return (1);
};


<|start_filename|>src/setup.js<|end_filename|>
import { rgbaToHex } from "./color";
import { getWGLContext } from "./utils";

import Batch from "./batch/index";
import Layer from "./layer/index";

export function setup() {
  const view = document.createElement("canvas");
  const width = window.innerWidth;
  const height = window.innerHeight;
  view.width = width;
  view.height = height;
  // sync storage colors with stage colors
  /*const colors = this.readStorage("favorite_colors");
  if (colors && colors.length > 2) {
    this.favoriteColors = JSON.parse(colors);
    this.updateFastColorPickMenu();
    this.setUiColorByRgba(this.favoriteColors[0].color);
  } else {
    this.setUiColorByHex([255, 0, 0, 1]);
  }*/
  this.setUiColorByRgba([255, 0, 0, 1]);
  this.setupRenderer(view);
  this.initListeners();
  this.resize(width, height);
  this.scale(0);
  const draw = () => {
    requestAnimationFrame(() => draw());
    this.update();
    if (this.redraw) {
      this.clear();
      this.render();
    }
  };
  // add some things manually
  (() => {
    this.cache.layer = new Layer(this);
    const layer = this.addLayer();
    this.setActiveLayer(layer);
  })();
  requestAnimationFrame(() => draw());
  this.setupUi();
  document.body.appendChild(view);
};


<|start_filename|>src/container/index.js<|end_filename|>
import { TILE_SIZE } from "../cfg";
import {
  uid,
  createCanvasBuffer
} from "../utils";

import { intersectRectangles } from "../math";

import {
  colorToRgbaString,
  rgbAlphaToAlphaByte
} from "../color";

import extend from "../extend";
import Layer from "../layer/index";
import Boundings from "../bounds/index";

/**
 * @class Container
 * @extends Layer
 */
class Container extends Layer {
  /**
   * @param {Poxi} instance
   * @constructor
   */
  constructor(instance) {
    super(instance);
    this.animation = {
      w: 32, h: 32,
      frame: 0, speed: 0
    };
    // tile amount x
    this.bounds.w = 3;
    // tile amount y
    this.bounds.h = 4;
  }
};

/**
 * @param {Number} index
 * @return {CanvasRenderingContext2D}
 */
Container.prototype.getFrameData = function(index) {
  const instance = this.instance;
  const animation = this.animation;
  const fw = animation.w; const fh = animation.h;
  const fx = this.bounds.x + (fw * ((index % this.bounds.w) | 0));
  const fy = this.bounds.y + (fh * ((index / this.bounds.w) | 0));
  const layers = this.instance.layers;
  const size = fw * fh;
  const buffer = createCanvasBuffer(fw, fh);
  for (let ii = 0; ii < size; ++ii) {
    const xx = (ii % fw) | 0;
    const yy = (ii / fw) | 0;
    const pixel = instance.getAbsolutePixelAt(fx + xx, fy + yy);
    if (pixel === null) continue;
    buffer.fillStyle = colorToRgbaString(pixel);
    buffer.fillRect(xx, yy, 1, 1);
  };
  return (buffer);
};

/**
 * @return {CanvasRenderingContext2D}
 */
Container.prototype.getAnimationTemplate = function() {
  const instance = this.instance;
  const animation = this.animation;
  const fw = animation.w; const fh = animation.h;
  const fx = this.bounds.x - fw;
  const fy = this.bounds.y;
  const layers = this.instance.layers;
  const size = fw * fh;
  const buffer = createCanvasBuffer(fw, fh);
  for (let ii = 0; ii < size; ++ii) {
    const xx = (ii % fw) | 0;
    const yy = (ii / fw) | 0;
    const pixel = instance.getAbsolutePixelAt(fx + xx, fy + yy);
    if (pixel === null) continue;
    buffer.fillStyle = colorToRgbaString(pixel);
    buffer.fillRect(xx, yy, 1, 1);
  };
  return (buffer);
};

Container.prototype.renderAnimationPreview = function() {
  const instance = this.instance;
  const cx = instance.cx | 0;
  const cy = instance.cy | 0;
  const cr = instance.cr;
  const buffer = instance.cache.fg;
  const animation = this.animation;
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  const tilew = ((animation.w * TILE_SIZE) * cr) | 0;
  const tileh = ((animation.h * TILE_SIZE) * cr) | 0;
  const ww = (((bw * animation.w) * TILE_SIZE) * cr) | 0;
  const hh = (((bh * animation.h) * TILE_SIZE) * cr) | 0;
  const xx = (cx + (this.bounds.x * TILE_SIZE) * cr) | 0;
  const yy = (cy + (this.bounds.y * TILE_SIZE) * cr) | 0;
  const ctx = this.getFrameData(animation.frame);
  const canvas = ctx.canvas;
  // draw preview border
  const preview = this.getAnimationTemplate();
  buffer.globalAlpha = 0.25;
  const size = ((bw * bh) * animation.w);
  for (let ii = 0; ii < size; ii += animation.w) {
    const px = (ii % bw) | 0;
    const py = ((ii / bw) / animation.w) | 0;
    buffer.drawImage(
      preview.canvas,
      0, 0,
      animation.w, animation.h,
      xx + (px * tilew), yy + (py * tileh),
      tilew, tileh
    );
  };
  buffer.globalAlpha = 1.0;
};

Container.prototype.renderFrame = function() {
  const instance = this.instance;
  const cx = instance.cx | 0;
  const cy = instance.cy | 0;
  const cr = instance.cr;
  const buffer = instance.cache.fg;
  const animation = this.animation;
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  const tilew = ((animation.w * TILE_SIZE) * cr) | 0;
  const tileh = ((animation.h * TILE_SIZE) * cr) | 0;
  const ww = (((bw * animation.w) * TILE_SIZE) * cr) | 0;
  const hh = (((bh * animation.h) * TILE_SIZE) * cr) | 0;
  const xx = (cx + (this.bounds.x * TILE_SIZE) * cr) | 0;
  const yy = (cy + (this.bounds.y * TILE_SIZE) * cr) | 0;
  const ax = xx - tilew | 0;
  const ay = yy | 0;
  const ctx = this.getFrameData(animation.frame);
  const canvas = ctx.canvas;
  buffer.drawImage(
    canvas,
    0, 0,
    canvas.width | 0, canvas.height | 0,
    ax | 0, ay | 0,
    tilew | 0, tileh | 0
  );
};

Container.prototype.renderBoundings = function() {
  const instance = this.instance;
  const cx = instance.cx | 0;
  const cy = instance.cy | 0;
  const cr = instance.cr;
  const buffer = instance.cache.fg;
  const lw = Math.max(0.55, 0.55 * cr);
  const animation = this.animation;
  const bw = this.bounds.w;
  const bh = this.bounds.h;
  const tilew = ((animation.w * TILE_SIZE) * cr) | 0;
  const tileh = ((animation.h * TILE_SIZE) * cr) | 0;
  const ww = (((bw * animation.w) * TILE_SIZE) * cr) | 0;
  const hh = (((bh * animation.h) * TILE_SIZE) * cr) | 0;
  const xx = (cx + (this.bounds.x * TILE_SIZE) * cr) | 0;
  const yy = (cy + (this.bounds.y * TILE_SIZE) * cr) | 0;
  const ax = xx - tilew | 0;
  const ay = yy | 0;
  // draw grid
  buffer.strokeStyle = "rgba(255,255,255,0.375)";
  buffer.lineWidth = lw;
  buffer.beginPath();
  for (let ii = tilew; ii < ww; ii += tilew) {
    buffer.moveTo(xx + ii, yy);
    buffer.lineTo(xx + ii, yy + hh);
  };
  for (let ii = tileh; ii < hh; ii += tileh) {
    buffer.moveTo(xx, yy + ii);
    buffer.lineTo(xx + ww, yy + ii);
  };
  buffer.stroke();
  buffer.closePath();
  // draw border
  instance.drawStrokedRect(xx, yy, ww, hh, "rgba(255,255,255,0.375)");
  instance.drawRectangle(ax, ay, tilew - lw, tileh, [1,1,1,0.1]);
  instance.drawStrokedRect(ax, ay, tilew - lw, tileh, "rgba(255,255,255,0.375)");
};

export default Container;


<|start_filename|>src/batch/matrix.js<|end_filename|>
import {
  MAX_SAFE_INTEGER,
  BATCH_JUMP_RESIZE
} from "../cfg";

import {
  alphaByteToRgbAlpha,
  additiveAlphaColorBlending
} from "../color";

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Number} w
 * @param {Number} h
 */
export function resizeRectangular(x, y, w, h) {
  this.resizeByOffset(x, y);
  this.resizeByOffset(x + w, y + h);
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function resizeByOffset(x, y) {
  const bounds = this.bounds;
  let bx = bounds.x; let by = bounds.y;
  const dx = x - this.layer.x;
  const dy = y - this.layer.y;
  const w = (Math.abs(bx - dx) | 0) + 1;
  const h = (Math.abs(by - dy) | 0) + 1;
  const ox = bx; const oy = by;
  const ow = bounds.w; const oh = bounds.h;
  const xx = -(bx - dx) | 0;
  const yy = -(by - dy) | 0;
  // resize bound rect to left, top
  if (xx < 0) {
    bx += xx - BATCH_JUMP_RESIZE;
    bounds.w += Math.abs(xx) + BATCH_JUMP_RESIZE;
  }
  if (yy < 0) {
    by += yy - BATCH_JUMP_RESIZE;
    bounds.h += Math.abs(yy) + BATCH_JUMP_RESIZE;
  }
  // resize bound to right, bottom
  if (w > bounds.w) bounds.w = w + BATCH_JUMP_RESIZE;
  if (h > bounds.h) bounds.h = h + BATCH_JUMP_RESIZE;
  bounds.x = bx; bounds.y = by;
  // make sure we only resize if necessary
  if (ow !== bounds.w || oh !== bounds.h) {
    this.resizeMatrix(
      ox - bx, oy - by,
      bounds.w - ow, bounds.h - oh
    );
  }
};

/**
 * Resizes matrix by calculating min/max of x,y,w,h
 * @return {Void}
 */
export function resizeByMatrixData() {
  const data = this.data;
  const bounds = this.bounds;
  const bx = bounds.x; const by = bounds.y;
  const bw = bounds.w; const bh = bounds.h;
  const ox = bounds.x; const oy = bounds.y;
  const ow = bounds.w; const oh = bounds.h;
  let x = MAX_SAFE_INTEGER; let y = MAX_SAFE_INTEGER;
  let w = -MAX_SAFE_INTEGER; let h = -MAX_SAFE_INTEGER;
  let count = 0;
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % bw) | 0;
    const yy = (idx / bw) | 0;
    const px = 4 * (yy * bw + xx);
    const r = data[px + 0];
    const g = data[px + 1];
    const b = data[px + 2];
    const a = data[px + 3];
    // ignore empty tiles
    if (a <= 0) continue;
    // x, y
    if (xx >= 0 && xx <= x) x = xx;
    if (yy >= 0 && yy <= y) y = yy;
    // width, height
    if (xx >= 0 && xx >= w) w = xx;
    if (yy >= 0 && yy >= h) h = yy;
    count++;
  };
  const nx = (w - (-x + w));
  const ny = (h - (-y + h));
  const nbx = bounds.x + nx; const nby = bounds.y + ny;
  const nbw = (-x + w) + 1; const nbh = (-y + h) + 1;
  // abort if nothing has changed
  if (count <= 0) return;
  if (ox === nbx && oy === nby && ow === nbw && oh === nbh) return;
  bounds.x = nbx; bounds.y = nby;
  bounds.w = nbw; bounds.h = nbh;
  this.resizeMatrix(
    ox - nbx, oy - nby,
    nbw - ow, nbh - oh
  );
  return;
};

/**
 * Resize internal array buffers
 * and join old matrix with new one
 * @param {Number} x - Resize left
 * @param {Number} y - Resize top
 * @param {Number} w - Resize right
 * @param {Number} h - Resize bottom
 * @return {Void}
 */
export function resizeMatrix(x, y, w, h) {
  const data = this.data;
  const rdata = this.reverse;
  const nw = this.bounds.w; const nh = this.bounds.h;
  const ow = nw - w; const oh = nh - h;
  const size = 4 * (nw * nh);
  const buffer = new Uint8Array(size);
  const reverse = new Uint8Array(size);
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % ow) | 0;
    const yy = (idx / ow) | 0;
    const opx = 4 * (yy * ow + xx);
    // black magic 🦄
    const npx = opx + (yy * (nw - ow) * 4) + (x * 4) + ((y * 4) * nw);
    if (data[opx + 3] <= 0) continue;
    // refill data
    buffer[npx + 0] = data[opx + 0];
    buffer[npx + 1] = data[opx + 1];
    buffer[npx + 2] = data[opx + 2];
    buffer[npx + 3] = data[opx + 3];
    if (rdata[opx + 3] <= 0) continue;
    // refill reverse data
    reverse[npx + 0] = rdata[opx + 0];
    reverse[npx + 1] = rdata[opx + 1];
    reverse[npx + 2] = rdata[opx + 2];
    reverse[npx + 3] = rdata[opx + 3];
  };
  this.data = buffer;
  this.reverse = reverse;
  this.refreshTexture(true);
  return;
};

/**
 * Merges given matrix
 * @param {Batch} batch
 * @param {Number} px - X-offset to merge at
 * @param {Number} py - Y-offset to merge at
 * @param {Boolean} state - Add or reverse
 */
export function injectMatrix(batch, state) {
  const buffer = this.data;
  const isEraser = batch.isEraser;
  const data = state ? batch.data : batch.reverse;
  const bw = batch.bounds.w | 0;
  const bx = (this.bounds.x) | 0;
  const by = (this.bounds.y) | 0;
  const dx = (batch.bounds.x - bx) | 0;
  const dy = (batch.bounds.y - by) | 0;
  const w = this.bounds.w | 0; const h = this.bounds.h | 0;
  const x = bx | 0; const y = by | 0;
  // loop given batch data and merge it with our main matrix
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % bw) | 0;
    const yy = (idx / bw) | 0;
    const opx = ((yy * bw + xx) * 4) | 0;
    const npx = (opx + (yy * (w - bw) * 4) + (dx * 4) + ((dy * 4) * w)) | 0;
    const alpha = data[opx + 3] | 0;
    // erase pixel
    if (isEraser === true) {
      if (state === true && alpha > 0) {
        buffer[npx + 0] = buffer[npx + 1] = buffer[npx + 2] = buffer[npx + 3] = 0;
        continue;
      }
    }
    // ignore empty data pixels
    if (alpha <= 0 && state === true) continue;
    // only overwrite the reverse batch's used pixels
    if (state === false && alpha <= 0 && batch.data[opx + 3] <= 0) continue;
    // manual color blending
    if (buffer[npx + 3] > 0 && alpha < 255 && alpha > 0) {
      const src = buffer.subarray(npx, npx + 4);
      const dst = data.subarray(opx, opx + 4);
      // manual color blending
      if (state) {
        const src = buffer.subarray(npx, npx + 4);
        const dst = data.subarray(opx, opx + 4);
        const color = additiveAlphaColorBlending(src, dst);
        buffer[npx + 0] = color[0];
        buffer[npx + 1] = color[1];
        buffer[npx + 2] = color[2];
        buffer[npx + 3] = color[3];
        continue;
      // simply fill with reverse data
      } else {
        buffer[npx + 0] = data[opx + 0];
        buffer[npx + 1] = data[opx + 1];
        buffer[npx + 2] = data[opx + 2];
        buffer[npx + 3] = data[opx + 3];
        continue;
      }
    }
    // just fill colors with given batch data kind
    buffer[npx + 0] = data[opx + 0];
    buffer[npx + 1] = data[opx + 1];
    buffer[npx + 2] = data[opx + 2];
    buffer[npx + 3] = data[opx + 3];
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Array}
 */
export function getRawPixelAt(x, y) {
  // normalize coordinates
  const xx = x - (this.bounds.x);
  const yy = y - (this.bounds.y);
  // now extract the data
  const data = this.data;
  // imagedata array is 1d
  const idx = 4 * (yy * this.bounds.w + xx);
  // pixel index out of bounds
  if (idx < 0 || idx >= data.length) return (null);
  // get each color value
  const r = data[idx + 0];
  const g = data[idx + 1];
  const b = data[idx + 2];
  const a = data[idx + 3];
  // dont return anything if we got no valid color
  if (a <= 0) return (null);
  const color = [r, g, b, alphaByteToRgbAlpha(a)];
  // finally return the color array
  return (color);
};

/**
 * Clears all pixels non-reversible
 */
export function clear() {
  const data = this.data;
  for (let ii = 0; ii < data.length; ++ii) {
    data[ii] = 0;
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function prepareMatrix(x, y) {
  const bounds = this.bounds;
  // we don't have a buffer to store data at yet
  if (this.data === null) {
    bounds.x = x;
    bounds.y = y;
    bounds.w = 1;
    bounds.h = 1;
    const size = 4 * (bounds.w * bounds.h);
    this.data = new Uint8Array(size);
    this.reverse = new Uint8Array(size);
    this.texture = this.instance.bufferTexture(this.id, this.data, bounds.w, bounds.h);
  }
};

/**
 * Returns the very first found pixel color
 * *We expect that the batch is single colored*
 * @ignore
 * @return {Uint8Array}
 */
export function getBatchColor() {
  const data = this.data;
  const bounds = this.bounds;
  const bw = bounds.w; const bh = bounds.h;
  // calculate batch color
  for (let ii = 0; ii < bw * bh; ++ii) {
    const xx = (ii % bw) | 0;
    const yy = (ii / bw) | 0;
    const px = 4 * (yy * bw + xx);
    if (data[px + 3] <= 0) continue;
    return (data.subarray(px, px + 4));
  };
  return (null);
};

/**
 * @return {Boolean}
 */
export function isEmpty() {
  const data = this.data;
  const bw = this.bounds.w;
  let count = 0;
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % bw) | 0;
    const yy = (idx / bw) | 0;
    const px = (yy * bw + xx) * 4;
    const a = data[px + 3];
    // ignore empty tiles
    if (a <= 0) continue;
    count++;
  };
  return (count <= 0);
};


<|start_filename|>src/utils.js<|end_filename|>
import {
  WGL_SUPPORTED,
  MAGIC_RGB_A_BYTE
} from "./cfg";

/**
 * Returns unique integer
 * @return {Number}
 */
let uidx = 0;
export function uid() {
  return (++uidx);
};

/**
 * @param {Number} width
 * @param {Number} height
 * @return {CanvasRenderingContext2D}
 */
export function createCanvasBuffer(width, height) {
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext("2d");
  applyImageSmoothing(ctx, false);
  return (ctx);
};

/**
 * @param {CanvasRenderingContext2D} ctx
 * @param {Boolean} state
 */
export function applyImageSmoothing(ctx, state) {
  ctx.imageSmoothingEnabled = state;
  ctx.oImageSmoothingEnabled = state;
  ctx.msImageSmoothingEnabled = state;
  ctx.webkitImageSmoothingEnabled = state;
};

/**
 * @param {String} path
 * @param {Function} resolve
 */
export function loadImage(path, resolve) {
  const img = new Image();
  img.addEventListener("load", () => {
    resolve(img);
  });
  img.addEventListener("error", () => {
    throw new Error("Failed to load image ressource " + path);
  });
  img.src = path;
};

/**
 * Creates and returns an webgl context
 * @param {HTMLCanvasElement} canvas
 * @return {WebGLRenderingContext}
 */
export function getWGLContext(canvas) {
  if (!WGL_SUPPORTED) {
    throw new Error("Your browser doesn't support WebGL.");
  }
  const opts = {
    alpha: false,
    antialias: false,
    premultipliedAlpha: false,
    stencil: false,
    preserveDrawingBuffer: false
  };
  return (
    canvas.getContext("webgl", opts) ||
    canvas.getContext("experimental-webgl", opts)
  );
};


<|start_filename|>build.js<|end_filename|>
const fs = require("fs");
const url = require("url");
const path = require("path");
const buble = require("rollup-plugin-buble");
const rollup = require("rollup");
const electron = require("electron");

const bundleSource = () => {
  return new Promise((resolve, reject) => {
    rollup.rollup({
      entry: __dirname + "/src/index.js",
      plugins: [ buble() ],
    }).then((bundle) => {
      const result = bundle.generate({
        format: "cjs"
      });
      const code = "(function() { " + result.code + "})();";
      fs.writeFileSync("static/bundle.js", code);
      resolve();
    }).catch((e) => {
      reject(e);
    });
  });
};

// now open up electron with our fresh rolluped bundle
const initElectron = () => {
  return new Promise((resolve) => {

    const app = electron.app;
    const BrowserWindow = electron.BrowserWindow;

    let win = null;
    const createWindow = () => {
      win = new BrowserWindow({
        width: 980,
        height: 680,
        titleBarStyle: "hidden",
        icon: path.join(__dirname, "/static/assets/img/tree.png")
      });

      win.loadURL(url.format({
        pathname: path.join(__dirname, "/static/index.html"),
        protocol: "file:",
        slashes: true
      }));
      win.setMenu(null);

      //win.setFullScreen(true);
      win.webContents.openDevTools();
      win.on("closed", () => {
        win = null;
      });
      resolve({ win, app });
    };

    app.on("ready", createWindow);
    app.on("window-all-closed", () => {
      if (process.platform !== "darwin") app.quit();
    });
    app.on("activate", () => {
      if (win === null) createWindow();
    });

  });
};

const initializeStage = () => {
  return new Promise((resolve) => {
    bundleSource().then(() => {
      initElectron().then(resolve);
    }).catch((e) => {
      throw new Error(e);
    });
  });
};

// simple live reload system
initializeStage().then((old) => {
  const onRefresh = (e, file) => {
    // prevent circular tracking
    if (file === "bundle.js") return;
    bundleSource().then(() => {
      old.win.reload();
      old.win.webContents.reloadIgnoringCache();
      old.win.webContents.openDevTools();
      console.log("Refreshed!", "#" + Date.now());
    });
  };
  fs.watch("./src/", {recursive: true}, onRefresh);
  fs.watch("./static/", {recursive: true}, onRefresh);
});


<|start_filename|>src/area/functions.js<|end_filename|>
import { SELECTION_COLOR } from "../cfg";

import {
  createCanvasBuffer
} from "../utils";

import {
  bytesToRgba,
  colorToRgbaString
} from "../color";

import CommandKind from "../stack/kind";

/**
 * @param {Object} selection
 */
export function copy(selection) {
  this.clipboard.copy = null;
  // shape based selection
  if (selection.shape !== null) {
    this.copyByShape(selection);
  } else {
    this.copyBySelection(selection);
  }
};

/**
 * Shape-based copying
 * @param {Object} selection
 */
export function copyByShape(selection) {
  const shape = selection.shape;
  const data = shape.data;
  const bx = shape.bounds.x; const by = shape.bounds.y;
  const bw = shape.bounds.w; const bh = shape.bounds.h;
  let pixels = [];
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = ii / 4;
    const xx = idx % bw;
    const yy = (idx / bw) | 0;
    const px = (yy * bw + xx) * 4;
    const alpha = data[px + 3];
    // ignore shape pixels that aren't used
    if (alpha <= 0) continue;
    const pixel = this.getAbsolutePixelAt(bx + xx, by + yy);
    if (pixel === null) continue;
    pixels.push({
      x: xx, y: yy, color: pixel
    });
  };
  this.clipboard.copy = {
    pixels: pixels,
    selection: selection
  };
};

/**
 * Rectangle-based copying
 * @param {Object} selection
 */
export function copyBySelection(selection) {
  const x = selection.x; const y = selection.y;
  const w = selection.w; const h = selection.h;
  let pixels = [];
  for (let ii = 0; ii < w * h; ++ii) {
    const xx = ii % w;
    const yy = (ii / w) | 0;
    const pixel = this.getAbsolutePixelAt(x + xx, y + yy);
    if (pixel === null) continue;
    pixels.push({
      x: xx, y: yy, color: pixel
    });
  };
  this.clipboard.copy = {
    pixels: pixels,
    selection: selection
  };
};

/**
 * @param {Number} x
 * @param {Number} y
 * @param {Object} sel
 * @return {Void}
 */
export function pasteAt(x, y, sel) {
  const pixels = sel.pixels;
  if (pixels === null || !pixels.length) return;
  const layer = this.getCurrentLayer();
  const batch = layer.createBatchAt(x, y);
  batch.resizeRectangular(
    x, y,
    sel.w - 1, sel.h - 1
  );
  for (let ii = 0; ii < pixels.length; ++ii) {
    const pixel = pixels[ii];
    const color = pixel.color;
    batch.drawPixelFast(x + pixel.x, y + pixel.y, color);
  };
  batch.refreshTexture(false);
  this.enqueue(CommandKind.PASTE, batch);
  return;
};

/**
 * @param {Object} selection
 * @return {Void}
 */
export function cut(selection) {
  this.copy(selection);
  const pixels = this.clipboard.copy.pixels;
  if (pixels === null || !pixels.length) return;
  this.clearSelection(selection);
  return;
};

/**
 * Shape-based clearing
 * @param {Object} selection
 * @return {Void}
 */
export function clearSelection(selection) {
  const shape = selection.shape;
  const bounds = shape.bounds;
  const data = shape.data;
  const x = selection.x; const y = selection.y;
  const w = selection.w; const h = selection.h;
  const layer = this.getCurrentLayer();
  layer.createBatchAt(x, y);
  batch.isEraser = true;
  const bw = bounds.w; const bh = bounds.h;
  batch.resizeRectangular(
    x, y,
    w - 1, h - 1
  );
  let count = 0;
  for (let ii = 0; ii < data.length; ii += 4) {
    const idx = (ii / 4) | 0;
    const xx = (idx % bw) | 0;
    const yy = (idx / bw) | 0;
    const px = (yy * bw + xx) * 4;
    if (data[px + 3] <= 0) continue;
    const pixel = this.getAbsolutePixelAt(x + xx, y + yy);
    // only erase if we have sth to erase
    if (pixel === null) continue;
    batch.erasePixelFast(x + xx, y + yy, pixel);
    count++;
  };
  // nothing to change
  if (count <= 0) {
    batch.kill();
    return;
  }
  batch.refreshTexture(false);
  this.enqueue(CommandKind.CLEAR, batch);
  return;
};

/**
 * @param {Number} x
 * @param {Number} y
 * @return {Batch}
 */
export function getShapeAt(x, y) {
  const color = this.getAbsolutePixelAt(x, y);
  if (color === null) return (null);
  const shape = this.getBinaryShape(x, y, color);
  if (shape === null) return (null);
  const batch = this.createDynamicBatch(x, y);
  const bounds = this.bounds;
  const bx = bounds.x;
  const by = bounds.y;
  const bw = bounds.w;
  const bh = bounds.h;
  // create buffer to draw a fake shape into
  const buffer = createCanvasBuffer(bw, bh);
  const rgba = bytesToRgba(SELECTION_COLOR);
  rgba[3] = 0.45;
  buffer.fillStyle = colorToRgbaString(rgba);
  for (let ii = 0; ii < shape.length; ++ii) {
    const xx = (ii % bw);
    const yy = (ii / bw) | 0;
    if (shape[yy * bw + xx] !== 2) continue;
    buffer.fillRect(
      xx, yy,
      1, 1
    );
  };
  batch.buffer = buffer;
  batch.data = new Uint8Array(buffer.getImageData(0, 0, bw, bh).data);
  batch.bounds.update(bx, by, bw, bh);
  batch.resizeByMatrixData();
  batch.refreshTexture(true);
  return (batch);
};


<|start_filename|>src/area/select.js<|end_filename|>
/**
 * @return {Object}
 */
export function getSelection() {
  // active shape selection
  if (this.shape !== null) {
    const bounds = this.shape.bounds;
    return ({
      shape: this.shape,
      x: bounds.x, y: bounds.y,
      w: bounds.w, h: bounds.h
    });
  }
  let x = this.sx; let y = this.sy;
  let w = this.sw; let h = this.sh;
  if (w < 0) x += w;
  if (h < 0) y += h;
  w = w < 0 ? -w : w;
  h = h < 0 ? -h : h;
  return ({
    shape: null,
    x: x, y: y,
    w: w, h: h
  });
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function selectFrom(x, y) {
  x = x | 0;
  y = y | 0;
  const relative = this.getRelativeTileOffset(x, y);
  this.sx = relative.x;
  this.sy = relative.y;
  this.sw = this.sh = 0;
  this.redraw = true;
};

/**
 * @param {Number} x
 * @param {Number} y
 */
export function selectTo(x, y) {
  x = x | 0;
  y = y | 0;
  const relative = this.getRelativeTileOffset(x, y);
  let w = relative.x - this.sx;
  let h = relative.y - this.sy;
  w = w + (w >= 0 ? 1 : 0);
  h = h + (h >= 0 ? 1 : 0);
  this.sw = w;
  this.sh = h;
  this.redraw = true;
};

export function resetSelection() {
  this.sx = this.sy = 0;
  this.sw = this.sh = -0;
  if (this.shape !== null) {
    this.destroyTexture(this.shape.texture);
    this.shape = null;
  }
  this.redraw = true;
};


<|start_filename|>src/batch/filter/blend.js<|end_filename|>
import { SETTINGS } from "../../cfg";

import CommandKind from "../../stack/kind";

/**
 * Shade or tint
 * @param {Number} x
 * @param {Number} y
 * @param {Number} factor
 */
export function applyColorLightness(x, y, factor) {
  const instance = this.instance;
  const bounds = this.bounds;
  const layer = this.layer;
  const w = SETTINGS.LIGHT_SIZE;
  const h = SETTINGS.LIGHT_SIZE;
  for (let ii = 0; ii < w * h; ++ii) {
    const xx = x + (ii % w) | 0;
    const yy = y + (ii / w) | 0;
    const pixel = layer.getLivePixelAt(xx, yy);
    if (pixel === null) continue;
    const t = factor < 0 ? 0 : 255;
    const p = factor < 0 ? -factor : factor;
    const r = (Math.round((t - pixel[0]) * p) + pixel[0]);
    const g = (Math.round((t - pixel[1]) * p) + pixel[1]);
    const b = (Math.round((t - pixel[2]) * p) + pixel[2]);
    const a = pixel[3];
    this.drawPixel(xx, yy, [r, g, b, a]);
  };
};
