perfect-freehand
Advanced tools
Comparing version 0.3.5 to 0.4.0
@@ -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 @@ |
@@ -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": { |
312
src/index.ts
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
140704
19
1546
1