Source code of plot #034 back to plot
Download full working sketch as 034.tar.gz.
Unzip, then start a local web server and load the page in a browser.
Unless otherwise noted, code published here is © Gábor L Ugray, shared under the Creative Commons
BY-NC-SA license (Attribution, Non-Commercial, Share-Alike). Files in lib/thirdparty
, and additional
libraries in the downloadable archive, are shared under their respective open-source licenses, attributed
to their authors.
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
};