Source code of plot #053 back to plot

Download full working sketch as 053.tar.gz.
Unzip, then start a local web server and load the page in a browser.


class CanvasMasker {

  /**
   * Initializes canvas-based masker.
   * @param w Pixel width of modeled area
   * @param h Pixel height of modeled area
   * @param rf Raster factor: Canvas width and height is w * rf and h * rf
   * @param elmHost Host element to append canvas to. If missing, canvas will not be part of DOM.
   * @param id ID of canvas element. Can be omitted.
   */
  constructor(w, h, rf = 5, elmHost, id) {
    this.reqPosCount = 1;
    this.w = w;
    this.h = h;
    this.rf = rf;
    this.canvasW = w * rf;
    this.elm = document.createElement("canvas");
    this.elm.style.backgroundColor = "black";
    this.elm.width = w * rf;
    this.elm.height = h * rf;
    if (id) this.elm.id = "canvas-calc";
    if (elmHost) elmHost.appendChild(this.elm);
    this.ctx = this.elm.getContext("2d");

    // Full imageData from canvas: created when a snapshot is requested
    this.imgData = null;
  }

  clear() {
    this.imgData = null;
    this.ctx.globalCompositeOperation = "source-over";
    this.ctx.fillStyle = "#000000";
    this.ctx.fillRect(0, 0, this.w * this.rf, this.h * this.rf);
  }

  /**
   * Marks a margin of specified breadth as a blocked area.
   * @param margin Breadth of margin
   */
  blockMargin(margin) {
    this.imgData = null;
    this.ctx.globalCompositeOperation = "source-over";
    this.ctx.fillStyle = "#400000";
    this.ctx.fillRect(0, 0, this.elm.width, margin * this.rf);
    this.ctx.fillRect(0, 0, margin * this.rf, this.elm.height);
    this.ctx.fillRect(0, (this.h - margin) * this.rf, this.elm.width, margin * this.rf);
    this.ctx.fillRect((this.w - margin) * this.rf, 0, margin * this.rf, this.elm.height);
  }

  blockMarginXY(mx, my) {
    this.imgData = null;
    this.ctx.globalCompositeOperation = "source-over";
    this.ctx.fillStyle = "#400000";
    this.ctx.fillRect(0, 0, this.elm.width, my * this.rf);
    this.ctx.fillRect(0, 0, mx * this.rf, this.elm.height);
    this.ctx.fillRect(0, (this.h - my) * this.rf, this.elm.width, my * this.rf);
    this.ctx.fillRect((this.w - mx) * this.rf, 0, mx * this.rf, this.elm.height);
  }

  includePoly(vertices) {
    markPoly(this, vertices, false);
  }

  blockPoly(vertices) {
    markPoly(this, vertices, true);
  }

  blockPath(vertices, lineWidth) {
    markPath(this, vertices, lineWidth, true);
  }

  inlcudeCircle(center, radius) {
    markCircle(this, center, radius, false);
  }

  blockCircle(center, radius) {
    markCircle(this, center, radius, true);
  }

  includeRect(x, y, w, h) {
    markRect(this, x, y, w, h, false);
  }

  blockRect(x, y, w, h) {
    markRect(this, x, y, w, h, true);
  }

  /**
   * Retrieves and caches full image data. Cache gets invalidated when you next block or include something.
   * Do this when you know you'll be checking a lot of points before drawing again.
   */
  takeSnapshot() {
    this.imgData = this.ctx.getImageData(0, 0, this.elm.width, this.elm.height);
  }

  extendLine(midPt, angleDeg, mustBeMarkedVisible = false, segLength = 2) {

    let rgb = [0, 0, 0];
    const isVisible = pt => {
      this.getPixel(pt.x, pt.y, rgb);
      let isBlue = rgb[2] >= this.reqPosCount * 0x40;
      let isRed = rgb[0] >= 0x40;
      return (isBlue || !mustBeMarkedVisible) && !isRed;
    }

    if (!isVisible(midPt)) return null;

    let step = new Point(0, -segLength);
    step = step.rotate(angleDeg);

    // Walk one way
    const currPt = midPt.clone();
    const endPt = midPt.clone();
    while (true) {
      currPt.x += step.x;
      currPt.y += step.y;
      if (!isVisible(currPt)) break;
      endPt.x = currPt.x;
      endPt.y = currPt.y;
    }
    if (endPt.equals(midPt)) return null;

    // Walk the other way
    const startPt = midPt.clone();
    while (true) {
      currPt.x -= step.x;
      currPt.y -= step.y;
      if (!isVisible(currPt)) break;
      startPt.x = currPt.x;
      startPt.y = currPt.y;
    }

    return [startPt, endPt];
  }

  genSeq(midPt, next, maxPts, mustBeMarkedVisible = false) {

    let rgb = [0, 0, 0];
    const isVisible = pt => {
      this.getPixel(pt.x, pt.y, rgb);
      let isBlue = rgb[2] >= this.reqPosCount * 0x40;
      let isRed = rgb[0] >= 0x40;
      return (isBlue || !mustBeMarkedVisible) && !isRed;
    }

    if (!isVisible(midPt)) return null;

    // Walk one way
    const pts = [midPt.clone()];
    let currPt = midPt.clone();
    while (pts.length < maxPts) {
      if (!next(currPt, 1)) break;
      if (!isVisible(currPt)) break;
      pts.push(currPt.clone());
    }

    // Walk the other way
    currPt = midPt.clone();
    while (pts.length < maxPts) {
      if (!next(currPt, -1)) break;
      if (!isVisible(currPt)) break;
      pts.unshift(currPt.clone())
    }

    if (pts.length < 3) return null;
    else return pts;
  }

  getMaskedPoly(pts, mustBeMarkedVisible = false) {

    // Result: array of array of points.
    const polys = [];
    let currPts = [];
    let rgb = [0, 0, 0];

    for (const pt of pts) {
      this.getPixel(pt.x, pt.y, rgb);
      let isBlue = rgb[2] >= this.reqPosCount * 0x40;
      let isRed = rgb[0] >= 0x40;
      let visible = (isBlue || !mustBeMarkedVisible) && !isRed;
      if (!visible) addCurrentPoints();
      else currPts.push(pt);
    }
    addCurrentPoints();
    return polys;

    function addCurrentPoints() {
      if (currPts.length == 0) return;
      if (currPts.length >= 2) {
        polys.push(currPts);
      }
      currPts = [];
    }
  }

  getMaskedLine(pt1, pt2, mustBeMarkedVisible = false, segLength = 2) {

    // Build points: short segments of the requested length
    const lineVect = pt2.subtract(pt1);
    const lineLength = lineVect.length;
    const nSegs = Math.max(2, Math.round(lineLength / segLength));
    const segVect = lineVect.divide(nSegs);
    const pts = [];
    for (let i = 0; i <= nSegs; ++i) {
      pts.push(pt1.add(segVect.multiply(i)));
    }

    // Get polylines
    const polys = this.getMaskedPoly(pts, mustBeMarkedVisible);

    const res = [];

    // Simplify: just keep first and last point of each polyline.
    // These are all straight lines.
    polys.forEach(poly => {
      const pta = poly[0];
      const ptb = poly[poly.length - 1];
      res.push([pta, ptb]);
    });
    return res;
  }

  getPixel(x, y, rgb) {

    rgb[0] = rgb[1] = rgb[2] = 0;
    if (x < 0 || y < 0) return;
    if (x >= this.w || y >= this.h) return;
    x = Math.round(x * this.rf);
    y = Math.round(y * this.rf);

    // If we have an image data snapshot of full canvas, query that
    if (this.imgData) {
      let pxOfs = ((y * (this.canvasW * 4)) + (x * 4));
      rgb[0] = this.imgData.data[pxOfs];
      rgb[1] = this.imgData.data[pxOfs + 1];
      rgb[2] = this.imgData.data[pxOfs + 2];
      return;
    }

    // Retrieve image data for a single pixel
    let spid = this.ctx.getImageData(x, y, 1, 1);
    rgb[0] = spid.data[0];
    rgb[1] = spid.data[1];
    rgb[2] = spid.data[2];
  }

}

function markPath(cm, vertices, lineWidth, blocked) {
  cm.imgData = null;
  let clr = "#000040";
  if (blocked) clr = "#400000";
  cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
  cm.ctx.lineWidth = lineWidth;
  cm.ctx.lineCap = 'round';
  cm.ctx.globalCompositeOperation = "lighter";
  cm.ctx.beginPath();
  vertices.forEach((pt, ix) => {
    if (ix == 0) cm.ctx.moveTo(pt.x * cm.rf, pt.y * cm.rf);
    else cm.ctx.lineTo(pt.x * cm.rf, pt.y * cm.rf);
  });
  cm.ctx.stroke();
}

function markPoly(cm, vertices, blocked) {
  cm.imgData = null;
  let clr = "#000040";
  if (blocked) clr = "#400000";
  cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
  cm.ctx.lineWidth = 1;
  cm.ctx.globalCompositeOperation = "lighter";
  cm.ctx.beginPath();
  vertices.forEach((pt, ix) => {
    if (ix == 0) cm.ctx.moveTo(pt.x * cm.rf, pt.y * cm.rf);
    else cm.ctx.lineTo(pt.x * cm.rf, pt.y * cm.rf);
  });
  cm.ctx.fill();
}

function markCircle(cm, center, radius, blocked) {
  cm.imgData = null;
  let clr = "#000040";
  if (blocked) clr = "#400000";
  cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
  cm.ctx.lineWidth = 1;
  cm.ctx.globalCompositeOperation = "lighter";
  cm.ctx.beginPath();
  // Arc: Top is -Math.PI/2; false means clockwise
  cm.ctx.arc(center.x * cm.rf, center.y * cm.rf, radius * cm.rf, 0, 2 * Math.PI, false);
  cm.ctx.arc(center.x * cm.rf, center.y * cm.rf, 0, 0, 2 * Math.PI, true);
  cm.ctx.fill();

}

function markRect(cm, x, y, w, h, blocked) {
  cm.imgData = null;
  let clr = "#000040";
  if (blocked) clr = "#400000";
  cm.ctx.fillStyle = cm.ctx.strokeStyle = clr;
  cm.ctx.lineWidth = 1;
  cm.ctx.globalCompositeOperation = "lighter";
  cm.ctx.fillRect(x * cm.rf, y * cm.rf, w * cm.rf, h * cm.rf);
}

export {CanvasMasker}