Source code of plot #033 back to plot

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


class Vec2 {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  clone() {
    return new Vec2(this.x, this.y);
  }
  equals(other) {
    return this.x == other.x && this.y == other.y;
  }
}

class Vec3 {
  constructor(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
  }
  rotx(angle) {
    const sin = Math.sin(angle);
    const cos = Math.cos(angle);
    return new Vec3(this.x, cos * this.y - sin * this.z, sin * this.y + cos * this.z);
  }
  roty(angle) {
    const sin = Math.sin(angle);
    const cos = Math.cos(angle);
    return new Vec3(this.x * cos + this.z * sin, this.y, this.x * (-sin) + this.z * cos);
  }
}


function makeHatch(x, y, angle, length, interval, count, startHalf) {
  let res = [];
  const sin = Math.sin(angle);
  const cos = Math.cos(angle);
  const rsin = Math.sin(angle - Math.PI / 2);
  const rcos = Math.cos(angle - Math.PI / 2);
  for (let i = 0; i < count; ++i) {
    const midOfs = startHalf ? interval * (i + 0.5) : interval * i;
    const midX = x + midOfs * rcos;
    const midY = y - midOfs * rsin;
    const x1 = midX + length / 2 * cos;
    const y1 = midY - length / 2 * sin;
    const x2 = midX - length / 2 * cos;
    const y2 = midY + length / 2 * sin;
    res.push([new Vec2(x1, y1), new Vec2(x2, y2)]);
  }
  return res;
}

function makePaperHatchLines(angle, length, interval, count, startHalf, midPt) {

  let ox = 0, oy = 0;
  if (midPt) { ox = midPt.x; oy = midPt.y; }

  let res = [];
  const sin = Math.sin(angle / 180 * Math.PI);
  const cos = Math.cos(angle / 180 * Math.PI);
  const rsin = Math.sin(angle / 180 * Math.PI - Math.PI / 2);
  const rcos = Math.cos(angle / 180 * Math.PI - Math.PI / 2);
  for (let i = -count; i < count; ++i) {
    const midOfs = startHalf ? interval * (i + 0.5) : interval * i;
    const midX = ox + midOfs * rcos;
    const midY = oy - midOfs * rsin;
    const x1 = midX + length / 2 * cos;
    const y1 = midY - length / 2 * sin;
    const x2 = midX - length / 2 * cos;
    const y2 = midY + length / 2 * sin;
    const line = paper.Path.Line(new paper.Point(x1, y1), new paper.Point(x2, y2));
    res.push(line);
    // DBG
    //line.strokeColor = "red";
    //paper.project.activeLayer.addChild(line);
    //paper.view.draw();
  }
  return res;
}

function shortenLine(line, val) {
  const a = line[0], b = line[1];
  const mid = new Vec2((a.x + b.x) / 2, (a.y + b.y) / 2);
  const len = Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y));
  if (len - val < 2) return null;
  const prop = (len - val) / len;
  const newA = new Vec2(mid.x + (a.x - mid.x) * prop, mid.y + (a.y - mid.y) * prop);
  const newB = new Vec2(mid.x + (b.x - mid.x) * prop, mid.y + (b.y - mid.y) * prop);
  return [newA, newB];
}

function shortenPaperLine(line, val) {
  if (val == 0) return line;
  const a = line.segments[0].point, b = line.segments[1].point;
  const mid = new Vec2((a.x + b.x) / 2, (a.y + b.y) / 2);
  const len = Math.sqrt((b.x - a.x) * (b.x - a.x) + (b.y - a.y) * (b.y - a.y));
  if (len - val < 2) return null;
  const prop = (len - val) / len;
  const newA = new Point(mid.x + (a.x - mid.x) * prop, mid.y + (a.y - mid.y) * prop);
  const newB = new Point(mid.x + (b.x - mid.x) * prop, mid.y + (b.y - mid.y) * prop);
  return Path.Line(newA, newB);
}

function lerpPt(pta, ptb, f) {
  return new paper.Point(pta.x + (ptb.x - pta.x) * f, pta.y + (ptb.y - pta.y) * f);
}

function lnPtDist(ln1, ln2, pt) {
  let B = pt.y - ln1.y;
  let A = pt.x - ln1.x;
  let C = ln2.x - ln1.x;
  let D = ln2.y - ln1.y;

  let dot = A * C + B * D;
  let len_sq = C * C + D * D;
  let param = -1;
  if (len_sq != 0) //in case of 0 length line
    param = dot / len_sq;

  let xx, yy;

  if (param < 0) {
    xx = ln1.x;
    yy = ln1.y;
  } else if (param > 1) {
    xx = ln2.x;
    yy = ln2.y;
  } else {
    xx = ln1.x + param * C;
    yy = ln1.y + param * D;
  }

  let dx = pt.x - xx;
  let dy = pt.y - yy;
  return Math.sqrt(dx * dx + dy * dy);
}

function getMaskedPoly(pts, negMasks, posMasks, goInside = false) {

  // Result: array of array of points.
  const polys = [];

  // Bounding rectangle: for quick test before invoking expensive "contains"
  const negRects = [];
  negMasks.forEach(x => negRects.push(x.bounds));
  const posRects = [];
  posMasks.forEach(x => posRects.push(x.bounds));

  let currPts = [];
  let lastPt = null;

  for (const pt of pts) {
    let visible = true;
    for (let i = 0; i < posRects.length && visible; ++i)
      visible &= posRects[i].contains(pt);
    for (let i = 0; i < negRects.length && visible; ++i) {
      if (negRects[i].contains(pt) && negMasks[i].contains(pt))
        visible = false;
    }
    for (let i = 0; i < posMasks.length && visible; ++i)
      visible &= posMasks[i].contains(pt);
    if (!visible) {
      if (goInside && currPts.length > 0) currPts.push(pt);
      addCurrentPoints();
    }
    else {
      if (goInside && currPts.length == 0 && lastPt != null)
        currPts.push(lastPt);
      currPts.push(pt);
    }
    lastPt = pt;
  }
  addCurrentPoints();
  return polys;

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


function getMaskedLine(pt1, pt2, negMasks, posMasks, segLength = 2, goInside = false) {

  // 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 = getMaskedPoly(pts, negMasks, posMasks, goInside);
  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;
}

/**
 * Interpolates provided points for a smooth curve that goes through them.
 * Expects and returns paper.Point arrays.
 * @param {Array<paper.Point>} points The points to interpolate.
 * @param {Number} nSegs Number of segments to add between every two interpolated point.
 * @param {Number} tension Magic number.
 */
function calcSplinePaper(points, nSegs = 16, tension = 0.5) {
  // Cardinal spline interpolation, via:
  // stackoverflow.com/questions/7054272/how-to-draw-smooth-curve-through-n-points-using-javascript-html5-canvas

  // Make a copy, because we'll mess with array
  const pts = points.slice(0);

  // Duplicate points at start and end
  pts.unshift(points[0].clone());
  pts.push(points[points.length - 1].clone());

  // Fill in vector of interpolated spline points
  // This will be our result
  const splinePoints = [];
  for (let i = 0; i < (points.length - 1) * nSegs; ++i)
    splinePoints.push(new paper.Point(0, 0));

  // Preliminary calculations: step, then cardinals
  const c1 = [];
  const c2 = [];
  const c3 = [];
  const c4 = [];
  for (let t = 0; t < nSegs; ++t) {
    const st = t / nSegs;
    c1.push(2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1);
    c2.push(-(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2));
    c3.push(Math.pow(st, 3) - 2 * Math.pow(st, 2) + st);
    c4.push(Math.pow(st, 3) - Math.pow(st, 2));
  }

  // Calculate segments
  for (let i = 1; i < pts.length - 2; ++i) {
    for (let t = 0; t < nSegs; t++) {
      // calc tension vectors
      const t1x = (pts[i + 1].x - pts[i - 1].x) * tension;
      const t2x = (pts[i + 2].x - pts[i].x) * tension;
      const t1y = (pts[i + 1].y - pts[i - 1].y) * tension;
      const t2y = (pts[i + 2].y - pts[i].y) * tension;
      // calc x and y cords with common control vectors
      const x = c1[t] * pts[i].x + c2[t] * pts[i + 1].x + c3[t] * t1x + c4[t] * t2x;
      const y = c1[t] * pts[i].y + c2[t] * pts[i + 1].y + c3[t] * t1y + c4[t] * t2y;
      // store points in array
      splinePoints[(i - 1) * nSegs + t].x = x;
      splinePoints[(i - 1) * nSegs + t].y = y;
    }
  }

  // Add final point
  splinePoints.push(pts[pts.length - 1]);

  // Done
  return splinePoints;
}


export {
  Vec2, Vec3, shortenLine, shortenPaperLine, lerpPt, lnPtDist,
  makeHatch, makePaperHatchLines,
  getMaskedLine, getMaskedPoly,
  calcSplinePaper
};