Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

perfect-freehand

Package Overview
Dependencies
Maintainers
1
Versions
56
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

perfect-freehand - npm Package Compare versions

Comparing version 0.3.5 to 0.4.0

dist/vec.d.ts

6

CHANGELOG.md

@@ -0,1 +1,7 @@

## 0.4.0
- Adds `last` option.
- Adds `start` and `end` option, each with `taper` and `easing`.
- Improves cap handling.
## 0.3.5

@@ -2,0 +8,0 @@

7

dist/index.d.ts

@@ -1,2 +0,2 @@

import { StrokeOptions } from './types';
import { StrokeOptions, StrokePoint } from './types';
/**

@@ -12,3 +12,3 @@ * ## getStrokePoints

pressure?: number;
}>(points: (T | K)[], streamline?: number): number[][];
}>(points: (T | K)[], streamline?: number, size?: number): StrokePoint[];
/**

@@ -24,4 +24,5 @@ * ## getStrokeOutlinePoints

* @param options.simulatePressure Whether to simulate pressure based on velocity.
* @param options.last Whether to handle the points as a completed stroke.
*/
export declare function getStrokeOutlinePoints(points: number[][], options?: StrokeOptions): number[][];
export declare function getStrokeOutlinePoints(points: StrokePoint[], options?: StrokeOptions): number[][];
/**

@@ -28,0 +29,0 @@ * ## getStroke

@@ -5,41 +5,49 @@ 'use strict';

var hypot = Math.hypot,
cos = Math.cos,
max = Math.max,
min = Math.min,
sin = Math.sin,
atan2 = Math.atan2,
PI = Math.PI,
PI2 = PI * 2;
function lerp(y1, y2, mu) {
return y1 * (1 - mu) + y2 * mu;
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function projectPoint(p0, a, d) {
return [cos(a) * d + p0[0], sin(a) * d + p0[1]];
}
function shortAngleDist(a0, a1) {
var max = PI2;
var da = (a1 - a0) % max;
return 2 * da % max - da;
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function getAngleDelta(a0, a1) {
return shortAngleDist(a0, a1);
}
function getPointBetween(p0, p1, d) {
if (d === void 0) {
d = 0.5;
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it;
if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
return [p0[0] + (p1[0] - p0[0]) * d, p0[1] + (p1[1] - p0[1]) * d];
it = o[Symbol.iterator]();
return it.next.bind(it);
}
function getAngle(p0, p1) {
return atan2(p1[1] - p0[1], p1[0] - p0[0]);
function lerp(y1, y2, mu) {
return y1 * (1 - mu) + y2 * mu;
}
function getDistance(p0, p1) {
return hypot(p1[1] - p0[1], p1[0] - p0[0]);
}
function clamp(n, a, b) {
return max(a, min(b, n));
return Math.max(a, Math.min(b, n));
}

@@ -66,9 +74,136 @@ function toPointsArray(points) {

var abs = Math.abs,
min$1 = Math.min,
PI$1 = Math.PI,
TAU = PI$1 / 2,
SHARP = TAU,
DULL = SHARP / 2;
/**
* Negate a vector.
* @param A
*/
function neg(A) {
return [-A[0], -A[1]];
}
/**
* Add vectors.
* @param A
* @param B
*/
function add(A, B) {
return [A[0] + B[0], A[1] + B[1]];
}
/**
* Subtract vectors.
* @param A
* @param B
*/
function sub(A, B) {
return [A[0] - B[0], A[1] - B[1]];
}
/**
* Get the vector from vectors A to B.
* @param A
* @param B
*/
function vec(A, B) {
// A, B as vectors get the vector from A to B
return [B[0] - A[0], B[1] - A[1]];
}
/**
* Vector multiplication by scalar
* @param A
* @param n
*/
function mul(A, n) {
return [A[0] * n, A[1] * n];
}
/**
* Vector division by scalar.
* @param A
* @param n
*/
function div(A, n) {
return [A[0] / n, A[1] / n];
}
/**
* Perpendicular rotation of a vector A
* @param A
*/
function per(A) {
return [A[1], -A[0]];
}
/**
* Dot product
* @param A
* @param B
*/
function dpr(A, B) {
return A[0] * B[0] + A[1] * B[1];
}
/**
* Length of the vector
* @param A
*/
function len(A) {
return Math.hypot(A[0], A[1]);
}
/**
* Get normalized / unit vector.
* @param A
*/
function uni(A) {
return div(A, len(A));
}
/**
* Dist length from A to B
* @param A
* @param B
*/
function dist(A, B) {
return Math.hypot(A[1] - B[1], A[0] - B[0]);
}
/**
* Mean between two vectors or mid vector between two vectors
* @param A
* @param B
*/
function med(A, B) {
return mul(add(A, B), 0.5);
}
/**
* Rotate a vector around another vector by r (radians)
* @param A vector
* @param C center
* @param r rotation in radians
*/
function rotAround(A, C, rx, ry) {
var s = Math.sin(rx);
var c = Math.cos(ry);
var px = A[0] - C[0];
var py = A[1] - C[1];
var nx = px * c - py * s;
var ny = px * s + py * c;
return [nx + C[0], ny + C[1]];
}
/**
* Interpolate vector A to B with a scalar t
* @param A
* @param B
* @param t scalar
*/
function lrp(A, B, t) {
return add(A, mul(vec(A, B), t));
}
var min = Math.min,
PI = Math.PI;
function getStrokeRadius(size, thinning, easing, pressure) {

@@ -79,3 +214,3 @@ if (pressure === void 0) {

if (thinning === undefined) return size / 2;
if (!thinning) return size / 2;
pressure = clamp(easing(pressure), 0, 1);

@@ -92,3 +227,3 @@ return (thinning < 0 ? lerp(size, size + size * clamp(thinning, -0.95, -0.05), pressure) : lerp(size - size * clamp(thinning, 0.05, 0.95), size, pressure)) / 2;

function getStrokePoints(points, streamline) {
function getStrokePoints(points, streamline, size) {
if (streamline === void 0) {

@@ -98,15 +233,62 @@ streamline = 0.5;

if (size === void 0) {
size = 8;
}
var pts = toPointsArray(points);
var _short = true;
if (pts.length === 0) return [];
pts[0] = [pts[0][0], pts[0][1], pts[0][2] || 0.5, 0, 0, 0];
if (pts.length === 1) pts.push(add(pts[0], [1, 0]));
var strokePoints = [{
point: [pts[0][0], pts[0][1]],
pressure: pts[0][2],
vector: [0, 0],
distance: 0,
runningLength: 0
}];
for (var i = 1, curr = pts[i], prev = pts[0]; i < pts.length; i++, curr = pts[i], prev = pts[i - 1]) {
curr[0] = lerp(prev[0], curr[0], 1 - streamline);
curr[1] = lerp(prev[1], curr[1], 1 - streamline);
curr[3] = getAngle(curr, prev);
curr[4] = getDistance(curr, prev);
curr[5] = prev[5] + curr[4];
for (var i = 1, curr = pts[i], prev = strokePoints[0]; i < pts.length; i++, curr = pts[i], prev = strokePoints[i - 1]) {
var point = lrp(prev.point, [curr[0], curr[1]], 1 - streamline),
pressure = curr[2],
vector = uni(vec(point, prev.point)),
distance = dist(point, prev.point),
runningLength = prev.runningLength + distance;
var strokePoint = {
point: point,
pressure: pressure,
vector: vector,
distance: distance,
runningLength: runningLength
};
strokePoints.push(strokePoint);
if (_short && (runningLength > size || i === pts.length - 1)) {
_short = false;
for (var _iterator = _createForOfIteratorHelperLoose(strokePoints), _step; !(_step = _iterator()).done;) {
var pt = _step.value;
pt.vector = strokePoint.vector;
}
}
if (i === pts.length - 1) {
var rlen = 0;
for (var k = i; k > 1; k--) {
var _strokePoint = strokePoints[k];
if (rlen > size) {
for (var j = k; j < pts.length; j++) {
strokePoints[j].vector = _strokePoint.vector;
}
break;
}
rlen += _strokePoint.distance;
}
}
}
return pts;
return strokePoints;
}

@@ -123,2 +305,3 @@ /**

* @param options.simulatePressure Whether to simulate pressure based on velocity.
* @param options.last Whether to handle the points as a completed stroke.
*/

@@ -143,98 +326,109 @@

return t;
} : _options$easing;
var len = points.length,
totalLength = points[len - 1][5],
// The total length of the line
minDist = size * smoothing,
// The minimum distance for measurements
leftPts = [],
// Our collected left and right points
rightPts = [];
var pl = points[0],
// Previous left and right points
pr = points[0],
tl = pl,
// Points to test distance from
tr = pr,
pa = pr[3],
pp = 0,
// Previous (maybe simulated) pressure
r = size / 2,
// The current point radius
_short = true; // Whether the line is drawn far enough
} : _options$easing,
_options$start = _options.start,
start = _options$start === void 0 ? {} : _options$start,
_options$end = _options.end,
end = _options$end === void 0 ? {} : _options$end,
_options$last = _options.last,
last = _options$last === void 0 ? false : _options$last;
var _start$taper = start.taper,
taperStart = _start$taper === void 0 ? size : _start$taper,
_start$easing = start.easing,
taperStartCurve = _start$easing === void 0 ? function (t) {
return t * (2 - t);
} : _start$easing;
var _end$taper = end.taper,
taperEnd = _end$taper === void 0 ? size : _end$taper,
_end$easing = end.easing,
taperEndCurve = _end$easing === void 0 ? function (t) {
return --t * t * t + 1;
} : _end$easing;
var len = points.length; // The number of points in the array
var totalLength = points[len - 1].runningLength; // The total length of the line
var minDist = size * smoothing; // The minimum distance for measurements
var leftPts = []; // Our collected left and right points
var rightPts = [];
var pl = points[0].point; // Previous left and right points
var pr = points[0].point;
var tl = pl; // Points to test distance from
var tr = pr;
var pa = points[0].vector;
var pp = 1; // Previous (maybe simulated) pressure
var ir = 0; // The initial radius
var r = size; // The current radius
var _short2 = true; // Whether the line is drawn far enough
// We can't do anything with an empty array.
if (len === 0) return []; // If the point is only one point long, draw two caps at either end.
if (len === 0) return []; // Set initial radius
if (len === 1 || totalLength <= size / 4) {
var first = points[0],
_last = points[len - 1],
angle = getAngle(first, _last);
for (var i = 0; i < len - 1; i++) {
var _points$i = points[i],
pressure = _points$i.pressure,
runningLength = _points$i.runningLength;
if (thinning) {
r = getStrokeRadius(size, thinning, easing, _last[2]);
if (runningLength > size) {
ir = getStrokeRadius(size, thinning, easing, pressure);
break;
}
} // Set radius for last point
for (var t = 0, step = 0.1; t <= 1; t += step) {
tl = projectPoint(first, angle + PI$1 + TAU - t * PI$1, r);
tr = projectPoint(_last, angle + TAU - t * PI$1, r);
leftPts.push(tl);
rightPts.push(tr);
}
return leftPts.concat(rightPts);
} // For a point with more than one point, create an outline shape.
r = getStrokeRadius(size, thinning, easing, points[len - 1].pressure); // For a point with more than one point, create an outline shape.
for (var _i = 1; _i < len - 1; _i++) {
var next = points[_i + 1];
var _points$_i = points[_i],
point = _points$_i.point,
_pressure = _points$_i.pressure,
vector = _points$_i.vector,
distance = _points$_i.distance,
_runningLength = _points$_i.runningLength;
for (var i = 1; i < len - 1; i++) {
var next = points[i + 1];
var _points$i = points[i],
x = _points$i[0],
y = _points$i[1],
pressure = _points$i[2],
_angle = _points$i[3],
distance = _points$i[4],
clen = _points$i[5]; // 1.
// Calculate the size of the current point.
if (_short2 && _runningLength > minDist) {
_short2 = false;
} // 1. Calculate the size of the current point.
if (thinning) {
if (simulatePressure) {
// Simulate pressure by accellerating the reported pressure.
var rp = min$1(1 - distance / size, 1);
var sp = min$1(distance / size, 1);
pressure = min$1(1, pp + (rp - pp) * (sp / 2));
var rp = min(1 - distance / size, 1);
var sp = min(distance / size, 1);
_pressure = min(1, pp + (rp - pp) * (sp / 2));
} // Compute the stroke radius based on the pressure, easing and thinning.
r = getStrokeRadius(size, thinning, easing, pressure);
} // 2.
// Draw a cap once we've reached the minimum length.
r = getStrokeRadius(size, thinning, easing, _pressure);
} else {
r = size / 2;
} // 2. Apply tapering to start and end pressures
if (_short) {
if (clen < size / 4) continue; // The first point after we've reached the minimum length.
// Draw a cap at the first point angled toward the current point.
_short = false;
for (var _t = 0, _step = 0.1; _t <= 1; _t += _step) {
tl = projectPoint(points[0], _angle + TAU - _t * PI$1, r);
leftPts.push(tl);
}
tr = projectPoint(points[0], _angle + TAU, r);
rightPts.push(tr);
} // 3.
// Handle sharp corners
var ts = _runningLength < taperStart ? taperStartCurve(_runningLength / taperStart) : 1;
var te = totalLength - _runningLength < taperEnd ? taperEndCurve((totalLength - _runningLength) / taperEnd) : 1;
r = r * Math.min(ts, te); // 3. Handle sharp corners
// Find the delta between the current and next angle.
var dpr$1 = dpr(vector, next.vector);
var absDelta = abs(getAngleDelta(next[3], _angle));
if (dpr$1 < 0) {
// Draw a cap at the sharp corner.
var v = per(pa);
var pushedA = add(point, mul(v, r));
var pushedB = sub(point, mul(v, r));
if (absDelta > SHARP) {
// A sharp corner.
// Project points (left and right) for a cap.
for (var _t2 = 0, _step2 = 0.25; _t2 <= 1; _t2 += _step2) {
tl = projectPoint([x, y], pa - TAU + _t2 * -PI$1, r);
tr = projectPoint([x, y], pa + TAU + _t2 * PI$1, r);
for (var t = 0; t <= 1; t += 0.25) {
var rx = PI * t;
var ry = PI * t;
tl = rotAround(pushedA, point, -rx, -ry);
tr = rotAround(pushedB, point, rx, ry);
leftPts.push(tl);

@@ -248,27 +442,84 @@ rightPts.push(tr);

pl = projectPoint([x, y], _angle - TAU, r);
pr = projectPoint([x, y], _angle + TAU, r);
pl = add(point, mul(per(vector), r));
pr = add(point, mul(neg(per(vector)), r));
if (absDelta > DULL || getDistance(pl, tl) > minDist) {
leftPts.push(getPointBetween(tl, pl));
if (_i == 1 || dpr$1 < 0.25 || dist(pl, tl) > (_short2 ? minDist / 2 : minDist)) {
leftPts.push(med(tl, pl));
tl = pl;
}
if (absDelta > DULL || getDistance(pr, tr) > minDist) {
rightPts.push(getPointBetween(tr, pr));
if (_i == 1 || dpr$1 < 0.25 || dist(pr, tr) > (_short2 ? minDist / 2 : minDist)) {
rightPts.push(med(tr, pr));
tr = pr;
}
pp = pressure;
pa = _angle;
} // Add the end cap. This is tricky because some lines end with sharp angles.
pp = _pressure;
pa = vector;
} // 4. Draw caps
var last = points[points.length - 1];
var firstPoint = points[0];
var lastPoint = points[points.length - 1];
var veryShort = leftPts.length < 2 || rightPts.length < 2;
var isTapering = taperStart + taperEnd > 0;
var lpv = lastPoint.vector;
var startCap = [];
var endCap = []; // Draw start cap if the end taper is set to zero
for (var _t3 = 0, _step3 = 0.1; _t3 <= 1; _t3 += _step3) {
rightPts.push(projectPoint(last, last[3] + TAU + _t3 * PI$1, r));
if (veryShort) {
if (last || !isTapering) {
// Backup: draw an inverse cap for the end cap
lpv = uni(vec(lastPoint.point, firstPoint.point));
var _start = add(firstPoint.point, mul(per(neg(lpv)), ir || r));
for (var _t = 0, step = 0.1; _t <= 1; _t += step) {
var _rx = PI * -_t;
var _ry = PI * -_t;
startCap.push(rotAround(_start, firstPoint.point, _rx, _ry));
}
leftPts.shift();
rightPts.shift();
}
} else if (taperStart === 0) {
// Draw a cap between second left / right points
var lp0 = leftPts[1];
var rp0 = rightPts[1];
var _start2 = add(firstPoint.point, mul(uni(vec(lp0, rp0)), dist(lp0, rp0) / 2));
for (var _t2 = 0, _step2 = 0.1; _t2 <= 1; _t2 += _step2) {
var _rx2 = PI * -_t2;
var _ry2 = PI * -_t2;
startCap.push(rotAround(_start2, firstPoint.point, _rx2, _ry2));
}
leftPts.shift();
rightPts.shift();
} else if (points[1]) {
startCap.push(points[1].point);
} // Draw end cap if taper end is set to zero
if (taperEnd === 0 && (!veryShort || last && veryShort || !isTapering && veryShort)) {
var _start3 = add(lastPoint.point, mul(neg(per(lpv)), r));
for (var _t3 = 0, _step3 = 0.1; _t3 <= 1; _t3 += _step3) {
var _rx3 = PI * _t3;
var _ry3 = PI * _t3;
endCap.push(rotAround(_start3, lastPoint.point, _rx3, _ry3));
}
} else {
endCap.push(lastPoint.point);
}
return leftPts.concat(rightPts.reverse());
var results = [].concat(startCap, leftPts, endCap.reverse(), rightPts.reverse());
return results;
}

@@ -292,3 +543,4 @@ /**

return getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options);
var results = getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options);
return results;
}

@@ -295,0 +547,0 @@

@@ -1,2 +0,2 @@

"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var r=Math.hypot,t=Math.cos,n=Math.max,e=Math.min,i=Math.sin,o=Math.atan2,u=2*Math.PI;function a(r,t,n){return r*(1-n)+t*n}function s(r,n,e){return[t(n)*e+r[0],i(n)*e+r[1]]}function v(r,t,n){return void 0===n&&(n=.5),[r[0]+(t[0]-r[0])*n,r[1]+(t[1]-r[1])*n]}function f(r,t){return o(t[1]-r[1],t[0]-r[0])}function h(t,n){return r(n[1]-t[1],n[0]-t[0])}function c(r,t,i){return n(t,e(i,r))}var p=Math.abs,d=Math.min,l=Math.PI,M=l/2,g=M,m=g/2;function x(r,t,n,e){return void 0===e&&(e=.5),void 0===t?r/2:(e=c(n(e),0,1),(t<0?a(r,r+r*c(t,-.95,-.05),e):a(r-r*c(t,.05,.95),r,e))/2)}function P(r,t){void 0===t&&(t=.5);var n=function(r){return Array.isArray(r[0])?r.map((function(r){var t=r[2];return[r[0],r[1],void 0===t?.5:t]})):r.map((function(r){var t=r.pressure;return[r.x,r.y,void 0===t?.5:t]}))}(r);if(0===n.length)return[];n[0]=[n[0][0],n[0][1],n[0][2]||.5,0,0,0];for(var e=1,i=n[e],o=n[0];e<n.length;i=n[++e],o=n[e-1])i[0]=a(o[0],i[0],1-t),i[1]=a(o[1],i[1],1-t),i[3]=f(i,o),i[4]=h(i,o),i[5]=o[5]+i[4];return n}function y(r,t){void 0===t&&(t={});var n=t.size,e=void 0===n?8:n,i=t.thinning,o=void 0===i?.5:i,a=t.smoothing,c=t.simulatePressure,P=void 0===c||c,y=t.easing,b=void 0===y?function(r){return r}:y,k=r.length,A=e*(void 0===a?.5:a),I=[],O=[],S=r[0],_=r[0],j=S,z=_,q=_[3],w=0,B=e/2,C=!0;if(0===k)return[];if(1===k||r[k-1][5]<=e/4){var D=r[0],E=r[k-1],F=f(D,E);o&&(B=x(e,o,b,E[2]));for(var G=0;G<=1;G+=.1)j=s(D,F+l+M-G*l,B),z=s(E,F+M-G*l,B),I.push(j),O.push(z);return I.concat(O)}for(var H=1;H<k-1;H++){var J=r[H+1],K=r[H],L=K[0],N=K[1],Q=K[2],R=K[3],T=K[4],U=K[5];if(o){if(P){var V=d(1-T/e,1),W=d(T/e,1);Q=d(1,w+W/2*(V-w))}B=x(e,o,b,Q)}if(C){if(U<e/4)continue;C=!1;for(var X=0;X<=1;X+=.1)j=s(r[0],R+M-X*l,B),I.push(j);z=s(r[0],R+M,B),O.push(z)}var Y=p(function(r,t){var n=(t-r)%u;return 2*n%u-n}(J[3],R));if(Y>g)for(var Z=0;Z<=1;Z+=.25)j=s([L,N],q-M+Z*-l,B),z=s([L,N],q+M+Z*l,B),I.push(j),O.push(z);else S=s([L,N],R-M,B),_=s([L,N],R+M,B),(Y>m||h(S,j)>A)&&(I.push(v(j,S)),j=S),(Y>m||h(_,z)>A)&&(O.push(v(z,_)),z=_),w=Q,q=R}for(var $=r[r.length-1],rr=0;rr<=1;rr+=.1)O.push(s($,$[3]+M+rr*l,B));return I.concat(O.reverse())}exports.default=function(r,t){return void 0===t&&(t={}),y(P(r,t.streamline),t)},exports.getStrokeOutlinePoints=y,exports.getStrokePoints=P;
"use strict";function n(n,r){(null==r||r>n.length)&&(r=n.length);for(var t=0,e=new Array(r);t<r;t++)e[t]=n[t];return e}function r(r,t){var e;if("undefined"==typeof Symbol||null==r[Symbol.iterator]){if(Array.isArray(r)||(e=function(r,t){if(r){if("string"==typeof r)return n(r,void 0);var e=Object.prototype.toString.call(r).slice(8,-1);return"Object"===e&&r.constructor&&(e=r.constructor.name),"Map"===e||"Set"===e?Array.from(r):"Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e)?n(r,void 0):void 0}}(r))||t&&r&&"number"==typeof r.length){e&&(r=e);var o=0;return function(){return o>=r.length?{done:!0}:{done:!1,value:r[o++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}return(e=r[Symbol.iterator]()).next.bind(e)}function t(n,r,t){return n*(1-t)+r*t}function e(n,r,t){return Math.max(r,Math.min(t,n))}function o(n){return[-n[0],-n[1]]}function i(n,r){return[n[0]+r[0],n[1]+r[1]]}function u(n,r){return[n[0]-r[0],n[1]-r[1]]}function a(n,r){return[r[0]-n[0],r[1]-n[1]]}function s(n,r){return[n[0]*r,n[1]*r]}function v(n){return[n[1],-n[0]]}function f(n){return function(n,r){return[n[0]/r,n[1]/r]}(n,function(n){return Math.hypot(n[0],n[1])}(n))}function c(n,r){return Math.hypot(n[1]-r[1],n[0]-r[0])}function p(n,r){return s(i(n,r),.5)}function h(n,r,t,e){var o=Math.sin(t),i=Math.cos(e),u=n[0]-r[0],a=n[1]-r[1];return[u*i-a*o+r[0],u*o+a*i+r[1]]}Object.defineProperty(exports,"__esModule",{value:!0});var l=Math.min,d=Math.PI;function g(n,r,o,i){return void 0===i&&(i=.5),r?(i=e(o(i),0,1),(r<0?t(n,n+n*e(r,-.95,-.05),i):t(n-n*e(r,.05,.95),n,i))/2):n/2}function m(n,t,e){void 0===t&&(t=.5),void 0===e&&(e=8);var o=function(n){return Array.isArray(n[0])?n.map((function(n){var r=n[2];return[n[0],n[1],void 0===r?.5:r]})):n.map((function(n){var r=n.pressure;return[n.x,n.y,void 0===r?.5:r]}))}(n),u=!0;if(0===o.length)return[];1===o.length&&o.push(i(o[0],[1,0]));for(var v,p,h=[{point:[o[0][0],o[0][1]],pressure:o[0][2],vector:[0,0],distance:0,runningLength:0}],l=1,d=o[l],g=h[0];l<o.length;d=o[++l],g=h[l-1]){var m=(p=1-t,i(v=g.point,s(a(v,[d[0],d[1]]),p))),y=d[2],b=f(a(m,g.point)),M=c(m,g.point),A=g.runningLength+M,S={point:m,pressure:y,vector:b,distance:M,runningLength:A};if(h.push(S),u&&(A>e||l===o.length-1)){u=!1;for(var x,L=r(h);!(x=L()).done;)x.value.vector=S.vector}if(l===o.length-1)for(var P=0,j=l;j>1;j--){var k=h[j];if(P>e){for(var I=j;I<o.length;I++)h[I].vector=k.vector;break}P+=k.distance}}return h}function y(n,r){void 0===r&&(r={});var t,e,m=r.size,y=void 0===m?8:m,b=r.thinning,M=void 0===b?.5:b,A=r.smoothing,S=r.simulatePressure,x=void 0===S||S,L=r.easing,P=void 0===L?function(n){return n}:L,j=r.start,k=void 0===j?{}:j,I=r.end,O=void 0===I?{}:I,w=r.last,_=void 0!==w&&w,z=k.taper,C=void 0===z?y:z,E=k.easing,T=void 0===E?function(n){return n*(2-n)}:E,U=O.taper,$=void 0===U?y:U,q=O.easing,B=void 0===q?function(n){return--n*n*n+1}:q,D=n.length,F=n[D-1].runningLength,G=y*(void 0===A?.5:A),H=[],J=[],K=n[0].point,N=n[0].point,Q=K,R=N,V=n[0].vector,W=1,X=0,Y=y,Z=!0;if(0===D)return[];for(var nn=0;nn<D-1;nn++){var rn=n[nn];if(rn.runningLength>y){X=g(y,M,P,rn.pressure);break}}Y=g(y,M,P,n[D-1].pressure);for(var tn=1;tn<D-1;tn++){var en=n[tn+1],on=n[tn],un=on.point,an=on.pressure,sn=on.vector,vn=on.distance,fn=on.runningLength;if(Z&&fn>G&&(Z=!1),M){if(x){var cn=l(1-vn/y,1),pn=l(vn/y,1);an=l(1,W+pn/2*(cn-W))}Y=g(y,M,P,an)}else Y=y/2;var hn=fn<C?T(fn/C):1,ln=F-fn<$?B((F-fn)/$):1;Y*=Math.min(hn,ln);var dn=(t=sn)[0]*(e=en.vector)[0]+t[1]*e[1];if(dn<0)for(var gn=v(V),mn=i(un,s(gn,Y)),yn=u(un,s(gn,Y)),bn=0;bn<=1;bn+=.25){var Mn=d*bn,An=d*bn;Q=h(mn,un,-Mn,-An),R=h(yn,un,Mn,An),H.push(Q),J.push(R)}else K=i(un,s(v(sn),Y)),N=i(un,s(o(v(sn)),Y)),(1==tn||dn<.25||c(K,Q)>(Z?G/2:G))&&(H.push(p(Q,K)),Q=K),(1==tn||dn<.25||c(N,R)>(Z?G/2:G))&&(J.push(p(R,N)),R=N),W=an,V=sn}var Sn=n[0],xn=n[n.length-1],Ln=H.length<2||J.length<2,Pn=C+$>0,jn=xn.vector,kn=[],In=[];if(Ln){if(_||!Pn){jn=f(a(xn.point,Sn.point));for(var On=i(Sn.point,s(v(o(jn)),X||Y)),wn=0;wn<=1;wn+=.1)kn.push(h(On,Sn.point,d*-wn,d*-wn));H.shift(),J.shift()}}else if(0===C){for(var _n=H[1],zn=J[1],Cn=i(Sn.point,s(f(a(_n,zn)),c(_n,zn)/2)),En=0;En<=1;En+=.1)kn.push(h(Cn,Sn.point,d*-En,d*-En));H.shift(),J.shift()}else n[1]&&kn.push(n[1].point);if(0===$&&(!Ln||_&&Ln||!Pn&&Ln))for(var Tn=i(xn.point,s(o(v(jn)),Y)),Un=0;Un<=1;Un+=.1)In.push(h(Tn,xn.point,d*Un,d*Un));else In.push(xn.point);return[].concat(kn,H,In.reverse(),J.reverse())}exports.default=function(n,r){return void 0===r&&(r={}),y(m(n,r.streamline),r)},exports.getStrokeOutlinePoints=y,exports.getStrokePoints=m;
//# sourceMappingURL=perfect-freehand.cjs.production.min.js.map

@@ -1,40 +0,48 @@

var hypot = Math.hypot,
cos = Math.cos,
max = Math.max,
min = Math.min,
sin = Math.sin,
atan2 = Math.atan2,
PI = Math.PI,
PI2 = PI * 2;
function lerp(y1, y2, mu) {
return y1 * (1 - mu) + y2 * mu;
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function projectPoint(p0, a, d) {
return [cos(a) * d + p0[0], sin(a) * d + p0[1]];
}
function shortAngleDist(a0, a1) {
var max = PI2;
var da = (a1 - a0) % max;
return 2 * da % max - da;
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function getAngleDelta(a0, a1) {
return shortAngleDist(a0, a1);
}
function getPointBetween(p0, p1, d) {
if (d === void 0) {
d = 0.5;
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it;
if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) {
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
return [p0[0] + (p1[0] - p0[0]) * d, p0[1] + (p1[1] - p0[1]) * d];
it = o[Symbol.iterator]();
return it.next.bind(it);
}
function getAngle(p0, p1) {
return atan2(p1[1] - p0[1], p1[0] - p0[0]);
function lerp(y1, y2, mu) {
return y1 * (1 - mu) + y2 * mu;
}
function getDistance(p0, p1) {
return hypot(p1[1] - p0[1], p1[0] - p0[0]);
}
function clamp(n, a, b) {
return max(a, min(b, n));
return Math.max(a, Math.min(b, n));
}

@@ -61,9 +69,136 @@ function toPointsArray(points) {

var abs = Math.abs,
min$1 = Math.min,
PI$1 = Math.PI,
TAU = PI$1 / 2,
SHARP = TAU,
DULL = SHARP / 2;
/**
* Negate a vector.
* @param A
*/
function neg(A) {
return [-A[0], -A[1]];
}
/**
* Add vectors.
* @param A
* @param B
*/
function add(A, B) {
return [A[0] + B[0], A[1] + B[1]];
}
/**
* Subtract vectors.
* @param A
* @param B
*/
function sub(A, B) {
return [A[0] - B[0], A[1] - B[1]];
}
/**
* Get the vector from vectors A to B.
* @param A
* @param B
*/
function vec(A, B) {
// A, B as vectors get the vector from A to B
return [B[0] - A[0], B[1] - A[1]];
}
/**
* Vector multiplication by scalar
* @param A
* @param n
*/
function mul(A, n) {
return [A[0] * n, A[1] * n];
}
/**
* Vector division by scalar.
* @param A
* @param n
*/
function div(A, n) {
return [A[0] / n, A[1] / n];
}
/**
* Perpendicular rotation of a vector A
* @param A
*/
function per(A) {
return [A[1], -A[0]];
}
/**
* Dot product
* @param A
* @param B
*/
function dpr(A, B) {
return A[0] * B[0] + A[1] * B[1];
}
/**
* Length of the vector
* @param A
*/
function len(A) {
return Math.hypot(A[0], A[1]);
}
/**
* Get normalized / unit vector.
* @param A
*/
function uni(A) {
return div(A, len(A));
}
/**
* Dist length from A to B
* @param A
* @param B
*/
function dist(A, B) {
return Math.hypot(A[1] - B[1], A[0] - B[0]);
}
/**
* Mean between two vectors or mid vector between two vectors
* @param A
* @param B
*/
function med(A, B) {
return mul(add(A, B), 0.5);
}
/**
* Rotate a vector around another vector by r (radians)
* @param A vector
* @param C center
* @param r rotation in radians
*/
function rotAround(A, C, rx, ry) {
var s = Math.sin(rx);
var c = Math.cos(ry);
var px = A[0] - C[0];
var py = A[1] - C[1];
var nx = px * c - py * s;
var ny = px * s + py * c;
return [nx + C[0], ny + C[1]];
}
/**
* Interpolate vector A to B with a scalar t
* @param A
* @param B
* @param t scalar
*/
function lrp(A, B, t) {
return add(A, mul(vec(A, B), t));
}
var min = Math.min,
PI = Math.PI;
function getStrokeRadius(size, thinning, easing, pressure) {

@@ -74,3 +209,3 @@ if (pressure === void 0) {

if (thinning === undefined) return size / 2;
if (!thinning) return size / 2;
pressure = clamp(easing(pressure), 0, 1);

@@ -87,3 +222,3 @@ return (thinning < 0 ? lerp(size, size + size * clamp(thinning, -0.95, -0.05), pressure) : lerp(size - size * clamp(thinning, 0.05, 0.95), size, pressure)) / 2;

function getStrokePoints(points, streamline) {
function getStrokePoints(points, streamline, size) {
if (streamline === void 0) {

@@ -93,15 +228,62 @@ streamline = 0.5;

if (size === void 0) {
size = 8;
}
var pts = toPointsArray(points);
var _short = true;
if (pts.length === 0) return [];
pts[0] = [pts[0][0], pts[0][1], pts[0][2] || 0.5, 0, 0, 0];
if (pts.length === 1) pts.push(add(pts[0], [1, 0]));
var strokePoints = [{
point: [pts[0][0], pts[0][1]],
pressure: pts[0][2],
vector: [0, 0],
distance: 0,
runningLength: 0
}];
for (var i = 1, curr = pts[i], prev = pts[0]; i < pts.length; i++, curr = pts[i], prev = pts[i - 1]) {
curr[0] = lerp(prev[0], curr[0], 1 - streamline);
curr[1] = lerp(prev[1], curr[1], 1 - streamline);
curr[3] = getAngle(curr, prev);
curr[4] = getDistance(curr, prev);
curr[5] = prev[5] + curr[4];
for (var i = 1, curr = pts[i], prev = strokePoints[0]; i < pts.length; i++, curr = pts[i], prev = strokePoints[i - 1]) {
var point = lrp(prev.point, [curr[0], curr[1]], 1 - streamline),
pressure = curr[2],
vector = uni(vec(point, prev.point)),
distance = dist(point, prev.point),
runningLength = prev.runningLength + distance;
var strokePoint = {
point: point,
pressure: pressure,
vector: vector,
distance: distance,
runningLength: runningLength
};
strokePoints.push(strokePoint);
if (_short && (runningLength > size || i === pts.length - 1)) {
_short = false;
for (var _iterator = _createForOfIteratorHelperLoose(strokePoints), _step; !(_step = _iterator()).done;) {
var pt = _step.value;
pt.vector = strokePoint.vector;
}
}
if (i === pts.length - 1) {
var rlen = 0;
for (var k = i; k > 1; k--) {
var _strokePoint = strokePoints[k];
if (rlen > size) {
for (var j = k; j < pts.length; j++) {
strokePoints[j].vector = _strokePoint.vector;
}
break;
}
rlen += _strokePoint.distance;
}
}
}
return pts;
return strokePoints;
}

@@ -118,2 +300,3 @@ /**

* @param options.simulatePressure Whether to simulate pressure based on velocity.
* @param options.last Whether to handle the points as a completed stroke.
*/

@@ -138,98 +321,109 @@

return t;
} : _options$easing;
var len = points.length,
totalLength = points[len - 1][5],
// The total length of the line
minDist = size * smoothing,
// The minimum distance for measurements
leftPts = [],
// Our collected left and right points
rightPts = [];
var pl = points[0],
// Previous left and right points
pr = points[0],
tl = pl,
// Points to test distance from
tr = pr,
pa = pr[3],
pp = 0,
// Previous (maybe simulated) pressure
r = size / 2,
// The current point radius
_short = true; // Whether the line is drawn far enough
} : _options$easing,
_options$start = _options.start,
start = _options$start === void 0 ? {} : _options$start,
_options$end = _options.end,
end = _options$end === void 0 ? {} : _options$end,
_options$last = _options.last,
last = _options$last === void 0 ? false : _options$last;
var _start$taper = start.taper,
taperStart = _start$taper === void 0 ? size : _start$taper,
_start$easing = start.easing,
taperStartCurve = _start$easing === void 0 ? function (t) {
return t * (2 - t);
} : _start$easing;
var _end$taper = end.taper,
taperEnd = _end$taper === void 0 ? size : _end$taper,
_end$easing = end.easing,
taperEndCurve = _end$easing === void 0 ? function (t) {
return --t * t * t + 1;
} : _end$easing;
var len = points.length; // The number of points in the array
var totalLength = points[len - 1].runningLength; // The total length of the line
var minDist = size * smoothing; // The minimum distance for measurements
var leftPts = []; // Our collected left and right points
var rightPts = [];
var pl = points[0].point; // Previous left and right points
var pr = points[0].point;
var tl = pl; // Points to test distance from
var tr = pr;
var pa = points[0].vector;
var pp = 1; // Previous (maybe simulated) pressure
var ir = 0; // The initial radius
var r = size; // The current radius
var _short2 = true; // Whether the line is drawn far enough
// We can't do anything with an empty array.
if (len === 0) return []; // If the point is only one point long, draw two caps at either end.
if (len === 0) return []; // Set initial radius
if (len === 1 || totalLength <= size / 4) {
var first = points[0],
_last = points[len - 1],
angle = getAngle(first, _last);
for (var i = 0; i < len - 1; i++) {
var _points$i = points[i],
pressure = _points$i.pressure,
runningLength = _points$i.runningLength;
if (thinning) {
r = getStrokeRadius(size, thinning, easing, _last[2]);
if (runningLength > size) {
ir = getStrokeRadius(size, thinning, easing, pressure);
break;
}
} // Set radius for last point
for (var t = 0, step = 0.1; t <= 1; t += step) {
tl = projectPoint(first, angle + PI$1 + TAU - t * PI$1, r);
tr = projectPoint(_last, angle + TAU - t * PI$1, r);
leftPts.push(tl);
rightPts.push(tr);
}
return leftPts.concat(rightPts);
} // For a point with more than one point, create an outline shape.
r = getStrokeRadius(size, thinning, easing, points[len - 1].pressure); // For a point with more than one point, create an outline shape.
for (var _i = 1; _i < len - 1; _i++) {
var next = points[_i + 1];
var _points$_i = points[_i],
point = _points$_i.point,
_pressure = _points$_i.pressure,
vector = _points$_i.vector,
distance = _points$_i.distance,
_runningLength = _points$_i.runningLength;
for (var i = 1; i < len - 1; i++) {
var next = points[i + 1];
var _points$i = points[i],
x = _points$i[0],
y = _points$i[1],
pressure = _points$i[2],
_angle = _points$i[3],
distance = _points$i[4],
clen = _points$i[5]; // 1.
// Calculate the size of the current point.
if (_short2 && _runningLength > minDist) {
_short2 = false;
} // 1. Calculate the size of the current point.
if (thinning) {
if (simulatePressure) {
// Simulate pressure by accellerating the reported pressure.
var rp = min$1(1 - distance / size, 1);
var sp = min$1(distance / size, 1);
pressure = min$1(1, pp + (rp - pp) * (sp / 2));
var rp = min(1 - distance / size, 1);
var sp = min(distance / size, 1);
_pressure = min(1, pp + (rp - pp) * (sp / 2));
} // Compute the stroke radius based on the pressure, easing and thinning.
r = getStrokeRadius(size, thinning, easing, pressure);
} // 2.
// Draw a cap once we've reached the minimum length.
r = getStrokeRadius(size, thinning, easing, _pressure);
} else {
r = size / 2;
} // 2. Apply tapering to start and end pressures
if (_short) {
if (clen < size / 4) continue; // The first point after we've reached the minimum length.
// Draw a cap at the first point angled toward the current point.
_short = false;
for (var _t = 0, _step = 0.1; _t <= 1; _t += _step) {
tl = projectPoint(points[0], _angle + TAU - _t * PI$1, r);
leftPts.push(tl);
}
tr = projectPoint(points[0], _angle + TAU, r);
rightPts.push(tr);
} // 3.
// Handle sharp corners
var ts = _runningLength < taperStart ? taperStartCurve(_runningLength / taperStart) : 1;
var te = totalLength - _runningLength < taperEnd ? taperEndCurve((totalLength - _runningLength) / taperEnd) : 1;
r = r * Math.min(ts, te); // 3. Handle sharp corners
// Find the delta between the current and next angle.
var dpr$1 = dpr(vector, next.vector);
var absDelta = abs(getAngleDelta(next[3], _angle));
if (dpr$1 < 0) {
// Draw a cap at the sharp corner.
var v = per(pa);
var pushedA = add(point, mul(v, r));
var pushedB = sub(point, mul(v, r));
if (absDelta > SHARP) {
// A sharp corner.
// Project points (left and right) for a cap.
for (var _t2 = 0, _step2 = 0.25; _t2 <= 1; _t2 += _step2) {
tl = projectPoint([x, y], pa - TAU + _t2 * -PI$1, r);
tr = projectPoint([x, y], pa + TAU + _t2 * PI$1, r);
for (var t = 0; t <= 1; t += 0.25) {
var rx = PI * t;
var ry = PI * t;
tl = rotAround(pushedA, point, -rx, -ry);
tr = rotAround(pushedB, point, rx, ry);
leftPts.push(tl);

@@ -243,27 +437,84 @@ rightPts.push(tr);

pl = projectPoint([x, y], _angle - TAU, r);
pr = projectPoint([x, y], _angle + TAU, r);
pl = add(point, mul(per(vector), r));
pr = add(point, mul(neg(per(vector)), r));
if (absDelta > DULL || getDistance(pl, tl) > minDist) {
leftPts.push(getPointBetween(tl, pl));
if (_i == 1 || dpr$1 < 0.25 || dist(pl, tl) > (_short2 ? minDist / 2 : minDist)) {
leftPts.push(med(tl, pl));
tl = pl;
}
if (absDelta > DULL || getDistance(pr, tr) > minDist) {
rightPts.push(getPointBetween(tr, pr));
if (_i == 1 || dpr$1 < 0.25 || dist(pr, tr) > (_short2 ? minDist / 2 : minDist)) {
rightPts.push(med(tr, pr));
tr = pr;
}
pp = pressure;
pa = _angle;
} // Add the end cap. This is tricky because some lines end with sharp angles.
pp = _pressure;
pa = vector;
} // 4. Draw caps
var last = points[points.length - 1];
var firstPoint = points[0];
var lastPoint = points[points.length - 1];
var veryShort = leftPts.length < 2 || rightPts.length < 2;
var isTapering = taperStart + taperEnd > 0;
var lpv = lastPoint.vector;
var startCap = [];
var endCap = []; // Draw start cap if the end taper is set to zero
for (var _t3 = 0, _step3 = 0.1; _t3 <= 1; _t3 += _step3) {
rightPts.push(projectPoint(last, last[3] + TAU + _t3 * PI$1, r));
if (veryShort) {
if (last || !isTapering) {
// Backup: draw an inverse cap for the end cap
lpv = uni(vec(lastPoint.point, firstPoint.point));
var _start = add(firstPoint.point, mul(per(neg(lpv)), ir || r));
for (var _t = 0, step = 0.1; _t <= 1; _t += step) {
var _rx = PI * -_t;
var _ry = PI * -_t;
startCap.push(rotAround(_start, firstPoint.point, _rx, _ry));
}
leftPts.shift();
rightPts.shift();
}
} else if (taperStart === 0) {
// Draw a cap between second left / right points
var lp0 = leftPts[1];
var rp0 = rightPts[1];
var _start2 = add(firstPoint.point, mul(uni(vec(lp0, rp0)), dist(lp0, rp0) / 2));
for (var _t2 = 0, _step2 = 0.1; _t2 <= 1; _t2 += _step2) {
var _rx2 = PI * -_t2;
var _ry2 = PI * -_t2;
startCap.push(rotAround(_start2, firstPoint.point, _rx2, _ry2));
}
leftPts.shift();
rightPts.shift();
} else if (points[1]) {
startCap.push(points[1].point);
} // Draw end cap if taper end is set to zero
if (taperEnd === 0 && (!veryShort || last && veryShort || !isTapering && veryShort)) {
var _start3 = add(lastPoint.point, mul(neg(per(lpv)), r));
for (var _t3 = 0, _step3 = 0.1; _t3 <= 1; _t3 += _step3) {
var _rx3 = PI * _t3;
var _ry3 = PI * _t3;
endCap.push(rotAround(_start3, lastPoint.point, _rx3, _ry3));
}
} else {
endCap.push(lastPoint.point);
}
return leftPts.concat(rightPts.reverse());
var results = [].concat(startCap, leftPts, endCap.reverse(), rightPts.reverse());
return results;
}

@@ -287,3 +538,4 @@ /**

return getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options);
var results = getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options);
return results;
}

@@ -290,0 +542,0 @@

@@ -8,2 +8,18 @@ export interface StrokeOptions {

simulatePressure?: boolean;
start?: {
taper?: number;
easing?: (distance: number) => number;
};
end?: {
taper?: number;
easing?: (distance: number) => number;
};
last?: boolean;
}
export interface StrokePoint {
point: number[];
pressure: number;
vector: number[];
distance: number;
runningLength: number;
}
export declare function lerp(y1: number, y2: number, mu: number): number;
export declare function projectPoint(p0: number[], a: number, d: number): number[];
export declare function getAngleDelta(a0: number, a1: number): number;
export declare function lerpAngles(a0: number, a1: number, t: number): number;
export declare function getPointBetween(p0: number[], p1: number[], d?: number): number[];
export declare function getAngle(p0: number[], p1: number[]): number;
export declare function getDistance(p0: number[], p1: number[]): number;
export declare function clamp(n: number, a: number, b: number): number;

@@ -9,0 +3,0 @@ export declare function toPointsArray<T extends number[], K extends {

{
"version": "0.3.5",
"version": "0.4.0",
"name": "perfect-freehand",

@@ -4,0 +4,0 @@ "author": {

@@ -1,17 +0,6 @@

import {
toPointsArray,
clamp,
getAngle,
getAngleDelta,
getDistance,
getPointBetween,
projectPoint,
lerp,
} from './utils'
import { StrokeOptions } from './types'
import { toPointsArray, clamp, lerp } from './utils'
import { StrokeOptions, StrokePoint } from './types'
import * as vec from './vec'
const { abs, min, PI } = Math,
TAU = PI / 2,
SHARP = TAU,
DULL = SHARP / 2
const { min, PI } = Math

@@ -24,3 +13,3 @@ function getStrokeRadius(

) {
if (thinning === undefined) return size / 2
if (!thinning) return size / 2
pressure = clamp(easing(pressure), 0, 1)

@@ -43,22 +32,65 @@ return (

K extends { x: number; y: number; pressure?: number }
>(points: (T | K)[], streamline = 0.5): number[][] {
>(points: (T | K)[], streamline = 0.5, size = 8): StrokePoint[] {
const pts = toPointsArray(points)
let short = true
if (pts.length === 0) return []
pts[0] = [pts[0][0], pts[0][1], pts[0][2] || 0.5, 0, 0, 0]
if (pts.length === 1) pts.push(vec.add(pts[0], [1, 0]))
const strokePoints: StrokePoint[] = [
{
point: [pts[0][0], pts[0][1]],
pressure: pts[0][2],
vector: [0, 0],
distance: 0,
runningLength: 0,
},
]
for (
let i = 1, curr = pts[i], prev = pts[0];
let i = 1, curr = pts[i], prev = strokePoints[0];
i < pts.length;
i++, curr = pts[i], prev = pts[i - 1]
i++, curr = pts[i], prev = strokePoints[i - 1]
) {
curr[0] = lerp(prev[0], curr[0], 1 - streamline)
curr[1] = lerp(prev[1], curr[1], 1 - streamline)
curr[3] = getAngle(curr, prev)
curr[4] = getDistance(curr, prev)
curr[5] = prev[5] + curr[4]
const point = vec.lrp(prev.point, [curr[0], curr[1]], 1 - streamline),
pressure = curr[2],
vector = vec.uni(vec.vec(point, prev.point)),
distance = vec.dist(point, prev.point),
runningLength = prev.runningLength + distance
const strokePoint = {
point,
pressure,
vector,
distance,
runningLength,
}
strokePoints.push(strokePoint)
if (short && (runningLength > size || i === pts.length - 1)) {
short = false
for (let pt of strokePoints) {
pt.vector = strokePoint.vector
}
}
if (i === pts.length - 1) {
let rlen = 0
for (let k = i; k > 1; k--) {
const strokePoint = strokePoints[k]
if (rlen > size) {
for (let j = k; j < pts.length; j++) {
strokePoints[j].vector = strokePoint.vector
}
break
}
rlen += strokePoint.distance
}
}
}
return pts
return strokePoints
}

@@ -76,5 +108,6 @@

* @param options.simulatePressure Whether to simulate pressure based on velocity.
* @param options.last Whether to handle the points as a completed stroke.
*/
export function getStrokeOutlinePoints(
points: number[][],
points: StrokePoint[],
options: StrokeOptions = {} as StrokeOptions

@@ -88,42 +121,48 @@ ): number[][] {

easing = t => t,
start = {},
end = {},
last = false,
} = options
const len = points.length,
totalLength = points[len - 1][5], // The total length of the line
minDist = size * smoothing, // The minimum distance for measurements
leftPts: number[][] = [], // Our collected left and right points
rightPts: number[][] = []
const {
taper: taperStart = size,
easing: taperStartCurve = t => t * (2 - t),
} = start
let pl = points[0], // Previous left and right points
pr = points[0],
tl = pl, // Points to test distance from
tr = pr,
pa = pr[3],
pp = 0, // Previous (maybe simulated) pressure
r = size / 2, // The current point radius
short = true // Whether the line is drawn far enough
const {
taper: taperEnd = size,
easing: taperEndCurve = t => --t * t * t + 1,
} = end
const len = points.length // The number of points in the array
const totalLength = points[len - 1].runningLength // The total length of the line
const minDist = size * smoothing // The minimum distance for measurements
const leftPts: number[][] = [] // Our collected left and right points
const rightPts: number[][] = []
let pl = points[0].point // Previous left and right points
let pr = points[0].point
let tl = pl // Points to test distance from
let tr = pr
let pa = points[0].vector
let pp = 1 // Previous (maybe simulated) pressure
let ir = 0 // The initial radius
let r = size // The current radius
let short = true // Whether the line is drawn far enough
// We can't do anything with an empty array.
if (len === 0) return []
// If the point is only one point long, draw two caps at either end.
if (len === 1 || totalLength <= size / 4) {
let first = points[0],
last = points[len - 1],
angle = getAngle(first, last)
if (thinning) {
r = getStrokeRadius(size, thinning, easing, last[2])
// Set initial radius
for (let i = 0; i < len - 1; i++) {
let { pressure, runningLength } = points[i]
if (runningLength > size) {
ir = getStrokeRadius(size, thinning, easing, pressure)
break
}
}
for (let t = 0, step = 0.1; t <= 1; t += step) {
tl = projectPoint(first, angle + PI + TAU - t * PI, r)
tr = projectPoint(last, angle + TAU - t * PI, r)
leftPts.push(tl)
rightPts.push(tr)
}
// Set radius for last point
r = getStrokeRadius(size, thinning, easing, points[len - 1].pressure)
return leftPts.concat(rightPts)
}
// For a point with more than one point, create an outline shape.

@@ -133,7 +172,10 @@ for (let i = 1; i < len - 1; i++) {

let [x, y, pressure, angle, distance, clen] = points[i]
let { point, pressure, vector, distance, runningLength } = points[i]
// 1.
// Calculate the size of the current point.
if (short && runningLength > minDist) {
short = false
}
// 1. Calculate the size of the current point.
if (thinning) {

@@ -149,37 +191,36 @@ if (simulatePressure) {

r = getStrokeRadius(size, thinning, easing, pressure)
} else {
r = size / 2
}
// 2.
// Draw a cap once we've reached the minimum length.
// 2. Apply tapering to start and end pressures
if (short) {
if (clen < size / 4) continue
const ts =
runningLength < taperStart
? taperStartCurve(runningLength / taperStart)
: 1
// The first point after we've reached the minimum length.
// Draw a cap at the first point angled toward the current point.
const te =
totalLength - runningLength < taperEnd
? taperEndCurve((totalLength - runningLength) / taperEnd)
: 1
short = false
r = r * Math.min(ts, te)
for (let t = 0, step = 0.1; t <= 1; t += step) {
tl = projectPoint(points[0], angle + TAU - t * PI, r)
leftPts.push(tl)
}
// 3. Handle sharp corners
tr = projectPoint(points[0], angle + TAU, r)
rightPts.push(tr)
}
// 3.
// Handle sharp corners
// Find the delta between the current and next angle.
const absDelta = abs(getAngleDelta(next[3], angle))
const dpr = vec.dpr(vector, next.vector)
if (absDelta > SHARP) {
// A sharp corner.
// Project points (left and right) for a cap.
if (dpr < 0) {
// Draw a cap at the sharp corner.
const v = vec.per(pa)
const pushedA = vec.add(point, vec.mul(v, r))
const pushedB = vec.sub(point, vec.mul(v, r))
for (let t = 0, step = 0.25; t <= 1; t += step) {
tl = projectPoint([x, y], pa - TAU + t * -PI, r)
tr = projectPoint([x, y], pa + TAU + t * PI, r)
for (let t = 0; t <= 1; t += 0.25) {
const rx = PI * t
const ry = PI * t
tl = vec.rotAround(pushedA, point, -rx, -ry)
tr = vec.rotAround(pushedB, point, rx, ry)

@@ -195,12 +236,19 @@ leftPts.push(tl)

pl = projectPoint([x, y], angle - TAU, r)
pr = projectPoint([x, y], angle + TAU, r)
pl = vec.add(point, vec.mul(vec.per(vector), r))
pr = vec.add(point, vec.mul(vec.neg(vec.per(vector)), r))
if (absDelta > DULL || getDistance(pl, tl) > minDist) {
leftPts.push(getPointBetween(tl, pl))
if (
i == 1 ||
dpr < 0.25 ||
vec.dist(pl, tl) > (short ? minDist / 2 : minDist)
) {
leftPts.push(vec.med(tl, pl))
tl = pl
}
if (absDelta > DULL || getDistance(pr, tr) > minDist) {
rightPts.push(getPointBetween(tr, pr))
if (
i == 1 ||
dpr < 0.25 ||
vec.dist(pr, tr) > (short ? minDist / 2 : minDist)
) {
rightPts.push(vec.med(tr, pr))
tr = pr

@@ -210,13 +258,77 @@ }

pp = pressure
pa = angle
pa = vector
}
// Add the end cap. This is tricky because some lines end with sharp angles.
const last = points[points.length - 1]
// 4. Draw caps
for (let t = 0, step = 0.1; t <= 1; t += step) {
rightPts.push(projectPoint(last, last[3] + TAU + t * PI, r))
const firstPoint = points[0]
const lastPoint = points[points.length - 1]
const veryShort = leftPts.length < 2 || rightPts.length < 2
const isTapering = taperStart + taperEnd > 0
let lpv = lastPoint.vector
const startCap: number[][] = []
const endCap: number[][] = []
// Draw start cap if the end taper is set to zero
if (veryShort) {
if (last || !isTapering) {
// Backup: draw an inverse cap for the end cap
lpv = vec.uni(vec.vec(lastPoint.point, firstPoint.point))
const start = vec.add(
firstPoint.point,
vec.mul(vec.per(vec.neg(lpv)), ir || r)
)
for (let t = 0, step = 0.1; t <= 1; t += step) {
const rx = PI * -t
const ry = PI * -t
startCap.push(vec.rotAround(start, firstPoint.point, rx, ry))
}
leftPts.shift()
rightPts.shift()
}
} else if (taperStart === 0) {
// Draw a cap between second left / right points
const lp0 = leftPts[1]
const rp0 = rightPts[1]
const start = vec.add(
firstPoint.point,
vec.mul(vec.uni(vec.vec(lp0, rp0)), vec.dist(lp0, rp0) / 2)
)
for (let t = 0, step = 0.1; t <= 1; t += step) {
const rx = PI * -t
const ry = PI * -t
startCap.push(vec.rotAround(start, firstPoint.point, rx, ry))
}
leftPts.shift()
rightPts.shift()
} else if (points[1]) {
startCap.push(points[1].point)
}
return leftPts.concat(rightPts.reverse())
// Draw end cap if taper end is set to zero
if (
taperEnd === 0 &&
(!veryShort || (last && veryShort) || (!isTapering && veryShort))
) {
const start = vec.add(lastPoint.point, vec.mul(vec.neg(vec.per(lpv)), r))
for (let t = 0, step = 0.1; t <= 1; t += step) {
const rx = PI * t
const ry = PI * t
endCap.push(vec.rotAround(start, lastPoint.point, rx, ry))
}
} else {
endCap.push(lastPoint.point)
}
const results = [
...startCap,
...leftPts,
...endCap.reverse(),
...rightPts.reverse(),
]
return results
}

@@ -239,8 +351,10 @@

>(points: (T | K)[], options: StrokeOptions = {} as StrokeOptions): number[][] {
return getStrokeOutlinePoints(
const results = getStrokeOutlinePoints(
getStrokePoints(points, options.streamline),
options
)
return results
}
export { StrokeOptions }

@@ -8,2 +8,19 @@ export interface StrokeOptions {

simulatePressure?: boolean
start?: {
taper?: number
easing?: (distance: number) => number
}
end?: {
taper?: number
easing?: (distance: number) => number
}
last?: boolean
}
export interface StrokePoint {
point: number[]
pressure: number
vector: number[]
distance: number
runningLength: number
}

@@ -1,4 +0,1 @@

const { hypot, cos, max, min, sin, atan2, PI } = Math,
PI2 = PI * 2
export function lerp(y1: number, y2: number, mu: number) {

@@ -8,34 +5,4 @@ return y1 * (1 - mu) + y2 * mu

export function projectPoint(p0: number[], a: number, d: number) {
return [cos(a) * d + p0[0], sin(a) * d + p0[1]]
}
function shortAngleDist(a0: number, a1: number) {
var max = PI2
var da = (a1 - a0) % max
return ((2 * da) % max) - da
}
export function getAngleDelta(a0: number, a1: number) {
return shortAngleDist(a0, a1)
}
export function lerpAngles(a0: number, a1: number, t: number) {
return a0 + shortAngleDist(a0, a1) * t
}
export function getPointBetween(p0: number[], p1: number[], d = 0.5) {
return [p0[0] + (p1[0] - p0[0]) * d, p0[1] + (p1[1] - p0[1]) * d]
}
export function getAngle(p0: number[], p1: number[]) {
return atan2(p1[1] - p0[1], p1[0] - p0[0])
}
export function getDistance(p0: number[], p1: number[]) {
return hypot(p1[1] - p0[1], p1[0] - p0[0])
}
export function clamp(n: number, a: number, b: number) {
return max(a, min(b, n))
return Math.max(a, Math.min(b, n))
}

@@ -42,0 +9,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc