perfect-freehand
Advanced tools
Comparing version 0.4.2 to 0.4.3
@@ -0,1 +1,6 @@ | ||
## 0.4.3 | ||
- Improves caps, corners. | ||
- Re-writes most comments. | ||
## 0.4.2 | ||
@@ -2,0 +7,0 @@ |
@@ -7,2 +7,3 @@ import { StrokeOptions, StrokePoint } from './types'; | ||
* @param streamline How much to streamline the stroke. | ||
* @param size The stroke's size. | ||
*/ | ||
@@ -24,8 +25,10 @@ export declare function getStrokePoints<T extends number[], K extends { | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
export declare function getStrokeOutlinePoints(points: StrokePoint[], options?: StrokeOptions): number[][]; | ||
export declare function getStrokeOutlinePoints(points: StrokePoint[], options?: Partial<StrokeOptions>): number[][]; | ||
/** | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @description Returns a stroke as an array of outline points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
@@ -36,4 +39,7 @@ * @param options An (optional) object with options. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
@@ -40,0 +46,0 @@ export default function getStroke<T extends number[], K extends { |
@@ -5,44 +5,2 @@ 'use strict'; | ||
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 _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 _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."); | ||
} | ||
it = o[Symbol.iterator](); | ||
return it.next.bind(it); | ||
} | ||
function lerp(y1, y2, mu) { | ||
@@ -54,2 +12,8 @@ return y1 * (1 - mu) + y2 * mu; | ||
} | ||
/** | ||
* Convert an array of points to the correct format ([x, y, radius]) | ||
* @param points | ||
* @returns | ||
*/ | ||
function toPointsArray(points) { | ||
@@ -74,3 +38,21 @@ if (Array.isArray(points[0])) { | ||
} | ||
/** | ||
* Compute a radius based on the pressure. | ||
* @param size | ||
* @param thinning | ||
* @param easing | ||
* @param pressure | ||
* @returns | ||
*/ | ||
function getStrokeRadius(size, thinning, easing, pressure) { | ||
if (pressure === void 0) { | ||
pressure = 0.5; | ||
} | ||
if (!thinning) return size / 2; | ||
pressure = clamp(easing(pressure), 0, 1); | ||
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; | ||
} | ||
/** | ||
@@ -80,5 +62,2 @@ * Negate a vector. | ||
*/ | ||
function neg(A) { | ||
return [-A[0], -A[1]]; | ||
} | ||
/** | ||
@@ -173,11 +152,2 @@ * Add vectors. | ||
/** | ||
* 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) | ||
@@ -189,5 +159,5 @@ * @param A vector | ||
function rotAround(A, C, rx, ry) { | ||
var s = Math.sin(rx); | ||
var c = Math.cos(ry); | ||
function rotAround(A, C, r) { | ||
var s = Math.sin(r); | ||
var c = Math.cos(r); | ||
var px = A[0] - C[0]; | ||
@@ -212,12 +182,2 @@ var py = A[1] - C[1]; | ||
PI = Math.PI; | ||
function getStrokeRadius(size, thinning, easing, pressure) { | ||
if (pressure === void 0) { | ||
pressure = 0.5; | ||
} | ||
if (!thinning) return size / 2; | ||
pressure = clamp(easing(pressure), 0, 1); | ||
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; | ||
} | ||
/** | ||
@@ -228,5 +188,5 @@ * ## getStrokePoints | ||
* @param streamline How much to streamline the stroke. | ||
* @param size The stroke's size. | ||
*/ | ||
function getStrokePoints(points, streamline, size) { | ||
@@ -242,5 +202,5 @@ if (streamline === void 0) { | ||
var pts = toPointsArray(points); | ||
var _short = true; | ||
if (pts.length === 0) return []; | ||
if (pts.length === 1) pts.push(add(pts[0], [1, 0])); | ||
var len = pts.length; | ||
if (len === 0) return []; | ||
if (len === 1) pts.push(add(pts[0], [1, 0])); | ||
var strokePoints = [{ | ||
@@ -255,3 +215,3 @@ point: [pts[0][0], pts[0][1]], | ||
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), | ||
var point = lrp(prev.point, curr, 1 - streamline), | ||
pressure = curr[2], | ||
@@ -261,3 +221,3 @@ vector = uni(vec(point, prev.point)), | ||
runningLength = prev.runningLength + distance; | ||
var strokePoint = { | ||
strokePoints.push({ | ||
point: point, | ||
@@ -268,30 +228,49 @@ pressure: pressure, | ||
runningLength: runningLength | ||
}; | ||
strokePoints.push(strokePoint); | ||
}); | ||
} | ||
/* | ||
Align vectors at the start of the line | ||
Find the first stroke point past the size and then set all preceding points' | ||
vectors to match this point's vector. This aligns the start cap and reduces | ||
noise at the start of the line. | ||
*/ | ||
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; | ||
for (var _i = 0; _i < len; _i++) { | ||
var _strokePoints$_i = strokePoints[_i], | ||
_runningLength = _strokePoints$_i.runningLength, | ||
_vector = _strokePoints$_i.vector; | ||
if (_runningLength > size || _i === len - 1) { | ||
for (var j = 0; j < _i; j++) { | ||
strokePoints[j].vector = _vector; | ||
} | ||
break; | ||
} | ||
} | ||
/* | ||
Align vectors at the end of the line | ||
Starting from the last point, work back until we've traveled more than | ||
half of the line's size (width). Take the current point's vector and then | ||
work forward, setting all remaining points' vectors to this vector. This | ||
removes the "noise" at the end of the line and allows for a better-facing | ||
end cap. | ||
*/ | ||
if (i === pts.length - 1) { | ||
var rlen = 0; | ||
for (var k = i; k > 1; k--) { | ||
var _strokePoint = strokePoints[k]; | ||
var totalLength = strokePoints[len - 1].runningLength; | ||
if (rlen > size) { | ||
for (var j = k; j < pts.length; j++) { | ||
strokePoints[j].vector = _strokePoint.vector; | ||
} | ||
for (var _i2 = len - 1; _i2 > 1; _i2--) { | ||
var _strokePoints$_i2 = strokePoints[_i2], | ||
_runningLength2 = _strokePoints$_i2.runningLength, | ||
_vector2 = _strokePoints$_i2.vector; | ||
break; | ||
} | ||
if (totalLength - _runningLength2 > size / 2) { | ||
for (var _j = _i2; _j < len; _j++) { | ||
strokePoints[_j].vector = _vector2; | ||
} | ||
rlen += _strokePoint.distance; | ||
} | ||
break; | ||
} | ||
@@ -312,2 +291,4 @@ } | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
@@ -339,7 +320,7 @@ */ | ||
_options$last = _options.last, | ||
last = _options$last === void 0 ? false : _options$last; | ||
isComplete = _options$last === void 0 ? false : _options$last; | ||
var _start$taper = start.taper, | ||
taperStart = _start$taper === void 0 ? 0 : _start$taper, | ||
_start$easing = start.easing, | ||
taperStartCurve = _start$easing === void 0 ? function (t) { | ||
taperStartEase = _start$easing === void 0 ? function (t) { | ||
return t * (2 - t); | ||
@@ -350,158 +331,193 @@ } : _start$easing; | ||
_end$easing = end.easing, | ||
taperEndCurve = _end$easing === void 0 ? function (t) { | ||
taperEndEase = _end$easing === void 0 ? function (t) { | ||
return --t * t * t + 1; | ||
} : _end$easing; | ||
var len = points.length; // The number of points in the array | ||
} : _end$easing; // The number of points in the array | ||
var totalLength = points[len - 1].runningLength; // The total length of the line | ||
var len = points.length; // We can't do anything with an empty array. | ||
var minDist = size * smoothing; // The minimum distance for measurements | ||
if (len === 0) return []; // The total length of the line | ||
var leftPts = []; // Our collected left and right points | ||
var totalLength = points[len - 1].runningLength; // Our collected left and right points | ||
var rightPts = []; | ||
var pl = points[0].point; // Previous left and right points | ||
var leftPts = []; | ||
var rightPts = []; // Previous pressure (start with average of first five pressures) | ||
var pr = points[0].point; | ||
var tl = pl; // Points to test distance from | ||
var prevPressure = points.slice(0, 5).reduce(function (acc, cur) { | ||
return (acc + cur.pressure) / 2; | ||
}, points[0].pressure); // The current radius | ||
var tr = pr; | ||
var pa = points[0].vector; | ||
var pp = 1; // Previous (maybe simulated) pressure | ||
var radius = getStrokeRadius(size, thinning, easing, points[len - 1].pressure); // Previous vector | ||
var ir = 0; // The initial radius | ||
var prevVector = points[0].vector; // Previous left and right points | ||
var r = size; // The current radius | ||
var pl = points[0].point; | ||
var pr = pl; // Temporary left and right points | ||
var _short2 = true; // Whether the line is drawn far enough | ||
// We can't do anything with an empty array. | ||
var tl = pl; | ||
var tr = pr; | ||
/* | ||
Find the outline's left and right points | ||
Iterating through the points and populate the rightPts and leftPts arrays, | ||
skipping the first and last pointsm, which will get caps later on. | ||
*/ | ||
if (len === 0) return []; // Set initial radius | ||
for (var i = 0; i < len - 1; i++) { | ||
for (var i = 1; i < len - 1; i++) { | ||
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; | ||
/* | ||
Calculate the radius | ||
If not thinning, the current point's radius will be half the size; or | ||
otherwise, the size will be based on the current (real or simulated) | ||
pressure. | ||
*/ | ||
if (runningLength > size) { | ||
ir = getStrokeRadius(size, thinning, easing, pressure); | ||
break; | ||
} | ||
} // Set radius for last point | ||
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; | ||
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 - 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. | ||
var rp = min(1, 1 - distance / size); | ||
var sp = min(1, distance / size); | ||
pressure = min(1, prevPressure + (rp - prevPressure) * (sp / 2)); | ||
} | ||
r = getStrokeRadius(size, thinning, easing, _pressure); | ||
radius = getStrokeRadius(size, thinning, easing, pressure); | ||
} else { | ||
r = size / 2; | ||
} // 2. Apply tapering to start and end pressures | ||
radius = size / 2; | ||
} | ||
/* | ||
Apply tapering | ||
If the current length is within the taper distance at either the | ||
start or the end, calculate the taper strengths. Apply the smaller | ||
of the two taper strengths to the radius. | ||
*/ | ||
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 ts = runningLength < taperStart ? taperStartEase(runningLength / taperStart) : 1; | ||
var te = totalLength - runningLength < taperEnd ? taperEndEase((totalLength - runningLength) / taperEnd) : 1; | ||
radius *= Math.min(ts, te); | ||
/* | ||
Handle sharp corners | ||
Find the difference (dot product) between the current and next vector. | ||
If the next vector is at more than a right angle to the current vector, | ||
draw a cap at the current point. | ||
*/ | ||
var dpr$1 = dpr(vector, next.vector); | ||
var nextVector = points[i + 1].vector; | ||
var dpr$1 = dpr(vector, nextVector); | ||
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)); | ||
var _offset = mul(per(prevVector), radius); | ||
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); | ||
var la = add(point, _offset); | ||
var ra = sub(point, _offset); | ||
for (var t = 0.2; t < 1; t += 0.2) { | ||
tr = rotAround(la, point, PI * -t); | ||
tl = rotAround(ra, point, PI * t); | ||
rightPts.push(tr); | ||
leftPts.push(tl); | ||
rightPts.push(tr); | ||
} | ||
pl = tl; | ||
pr = tr; | ||
continue; | ||
} // 4. Add regular point. | ||
} | ||
/* | ||
Add regular points | ||
Project points to either side of the current point, using the | ||
calculated size as a distance. If a point's distance to the | ||
previous point on that side greater than the minimum distance | ||
(or if the corner is kinda sharp), add the points to the side's | ||
points array. | ||
*/ | ||
pl = add(point, mul(per(vector), r)); | ||
pr = add(point, mul(neg(per(vector)), r)); | ||
var offset = mul(per(lrp(nextVector, vector, dpr$1)), radius); | ||
tl = sub(point, offset); | ||
tr = add(point, offset); | ||
var tlu = uni(vec(tr, pr)); | ||
var tru = uni(vec(tl, pl)); | ||
var alwaysAdd = i === 1 || dpr$1 < 0.25; | ||
var minDistance = (runningLength > size ? size : size / 2) * smoothing; | ||
if (_i == 1 || dpr$1 < 0.25 || dist(pl, tl) > (_short2 ? minDist / 2 : minDist)) { | ||
leftPts.push(med(tl, pl)); | ||
tl = pl; | ||
if (alwaysAdd || dist(pr, tr) > minDistance && dpr(tlu, vector) > 0) { | ||
rightPts.push(tr); | ||
pr = tr; | ||
} | ||
if (_i == 1 || dpr$1 < 0.25 || dist(pr, tr) > (_short2 ? minDist / 2 : minDist)) { | ||
rightPts.push(med(tr, pr)); | ||
tr = pr; | ||
} | ||
if (alwaysAdd || dist(pl, tl) > minDistance && dpr(tru, vector) > 0) { | ||
leftPts.push(tl); | ||
pl = tl; | ||
} // Set variables for next iteration | ||
pp = _pressure; | ||
pa = vector; | ||
} // 4. Draw caps | ||
prevPressure = pressure; | ||
prevVector = vector; | ||
} | ||
/* | ||
Drawing caps | ||
Now that we have our points on either side of the line, we need to | ||
draw caps at the start and end. Tapered lines don't have caps, but | ||
may have dots for very short lines. | ||
*/ | ||
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 | ||
var lastPoint = points[len - 1]; | ||
var isVeryShort = rightPts.length < 2 || leftPts.length < 2; | ||
/* | ||
Draw a dot for very short or completed strokes | ||
If the line is too short to gather left or right points and if the line is | ||
not tapered on either side, draw a dot. If the line is tapered, then only | ||
draw a dot if the line is both very short and complete. If we draw a dot, | ||
we can just return those points. | ||
*/ | ||
if (veryShort) { | ||
if (!isTapering || veryShort && last) { | ||
// Backup: draw an inverse cap for the end cap | ||
lpv = uni(vec(lastPoint.point, firstPoint.point)); | ||
if (isVeryShort && (!(taperStart || taperEnd) || isComplete)) { | ||
var ir = 0; | ||
var _start = add(firstPoint.point, mul(per(neg(lpv)), ir || r)); | ||
for (var _i3 = 0; _i3 < len; _i3++) { | ||
var _points$_i = points[_i3], | ||
_pressure = _points$_i.pressure, | ||
_runningLength3 = _points$_i.runningLength; | ||
for (var _t = 0, step = 0.1; _t <= 1; _t += step) { | ||
var _rx = PI * -_t; | ||
if (_runningLength3 > size) { | ||
ir = getStrokeRadius(size, thinning, easing, _pressure); | ||
break; | ||
} | ||
} | ||
var _ry = PI * -_t; | ||
var _start = sub(firstPoint.point, mul(per(uni(vec(lastPoint.point, firstPoint.point))), ir || radius)); | ||
startCap.push(rotAround(_start, firstPoint.point, _rx, _ry)); | ||
} | ||
var dotPts = []; | ||
leftPts.shift(); | ||
rightPts.shift(); | ||
for (var _t = 0, step = 0.1; _t <= 1; _t += step) { | ||
dotPts.push(rotAround(_start, firstPoint.point, PI * 2 * _t)); | ||
} | ||
} 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)); | ||
return dotPts; | ||
} | ||
/* | ||
Draw a start cap | ||
Unless the line has a tapered start, or unless the line has a tapered end | ||
and the line is very short, draw a start cap around the first point. Use | ||
the distance between the second left and right point for the cap's radius. | ||
Finallym remove the first left and right points. :psyduck: | ||
*/ | ||
for (var _t2 = 0, _step2 = 0.1; _t2 <= 1; _t2 += _step2) { | ||
var _rx2 = PI * -_t2; | ||
var _ry2 = PI * -_t2; | ||
var startCap = []; | ||
startCap.push(rotAround(_start2, firstPoint.point, _rx2, _ry2)); | ||
if (!taperStart && !(taperEnd && isVeryShort)) { | ||
tr = rightPts[1]; | ||
tl = leftPts[1]; | ||
var _start2 = sub(firstPoint.point, mul(uni(vec(tr, tl)), dist(tr, tl) / 2)); | ||
for (var _t2 = 0, _step = 0.2; _t2 <= 1; _t2 += _step) { | ||
startCap.push(rotAround(_start2, firstPoint.point, PI * _t2)); | ||
} | ||
@@ -511,27 +527,39 @@ | ||
rightPts.shift(); | ||
} else if (points[1]) { | ||
startCap.push(points[1].point); | ||
} // Draw end cap if taper end is set to zero | ||
} | ||
/* | ||
Draw an end cap | ||
If the line does not have a tapered end, and unless the line has a tapered | ||
start and the line is very short, draw a cap around the last point. Finally, | ||
remove the last left and right points. Otherwise, add the last point. Note | ||
that This cap is a full-turn-and-a-half: this prevents incorrect caps on | ||
sharp end turns. | ||
*/ | ||
if (!isTapering || taperEnd === 0 && !veryShort || veryShort && last) { | ||
var _start3 = add(lastPoint.point, mul(neg(per(lpv)), r)); | ||
var endCap = []; | ||
for (var _t3 = 0, _step3 = 0.1; _t3 <= 1; _t3 += _step3) { | ||
var _rx3 = PI * _t3; | ||
if (!taperEnd && !(taperStart && isVeryShort)) { | ||
var _start3 = sub(lastPoint.point, mul(per(lastPoint.vector), radius)); | ||
var _ry3 = PI * _t3; | ||
for (var _t3 = 0, _step2 = 0.1; _t3 <= 1; _t3 += _step2) { | ||
endCap.push(rotAround(_start3, lastPoint.point, PI * 3 * _t3)); | ||
} | ||
endCap.push(rotAround(_start3, lastPoint.point, _rx3, _ry3)); | ||
} | ||
leftPts.pop(); | ||
rightPts.pop(); | ||
} else { | ||
endCap.push(lastPoint.point); | ||
} | ||
/* | ||
Return the points in the correct windind order: begin on the left side, then | ||
continue around the end cap, then come back along the right side, and finally | ||
complete the start cap. | ||
*/ | ||
var results = [].concat(startCap, leftPts, endCap.reverse(), rightPts.reverse()); | ||
return results; | ||
return leftPts.concat(endCap, rightPts.reverse(), startCap); | ||
} | ||
/** | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @description Returns a stroke as an array of outline points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
@@ -542,4 +570,7 @@ * @param options An (optional) object with options. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
@@ -552,4 +583,3 @@ | ||
var results = getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options); | ||
return results; | ||
return getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options); | ||
} | ||
@@ -556,0 +586,0 @@ |
@@ -1,2 +0,2 @@ | ||
"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?0:z,E=k.easing,T=void 0===E?function(n){return n*(2-n)}:E,U=O.taper,$=void 0===U?0: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||Ln&&_){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(!Pn||0===$&&!Ln||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; | ||
"use strict";function r(r,n,t){return r*(1-t)+n*t}function n(r,n,t){return Math.max(n,Math.min(t,r))}function t(t,e,i,o){return void 0===o&&(o=.5),e?(o=n(i(o),0,1),(e<0?r(t,t+t*n(e,-.95,-.05),o):r(t-t*n(e,.05,.95),t,o))/2):t/2}function e(r,n){return[r[0]+n[0],r[1]+n[1]]}function i(r,n){return[r[0]-n[0],r[1]-n[1]]}function o(r,n){return[n[0]-r[0],n[1]-r[1]]}function u(r,n){return[r[0]*n,r[1]*n]}function a(r){return[r[1],-r[0]]}function v(r,n){return r[0]*n[0]+r[1]*n[1]}function s(r){return function(r,n){return[r[0]/n,r[1]/n]}(r,function(r){return Math.hypot(r[0],r[1])}(r))}function f(r,n){return Math.hypot(r[1]-n[1],r[0]-n[0])}function p(r,n,t){var e=Math.sin(t),i=Math.cos(t),o=r[0]-n[0],u=r[1]-n[1];return[o*i-u*e+n[0],o*e+u*i+n[1]]}function c(r,n,t){return e(r,u(o(r,n),t))}Object.defineProperty(exports,"__esModule",{value:!0});var h=Math.min,g=Math.PI;function d(r,n,t){void 0===n&&(n=.5),void 0===t&&(t=8);var i=function(r){return Array.isArray(r[0])?r.map((function(r){var n=r[2];return[r[0],r[1],void 0===n?.5:n]})):r.map((function(r){var n=r.pressure;return[r.x,r.y,void 0===n?.5:n]}))}(r),u=i.length;if(0===u)return[];1===u&&i.push(e(i[0],[1,0]));for(var a=[{point:[i[0][0],i[0][1]],pressure:i[0][2],vector:[0,0],distance:0,runningLength:0}],v=1,p=i[v],h=a[0];v<i.length;p=i[++v],h=a[v-1]){var g=c(h.point,p,1-n),d=p[2],l=s(o(g,h.point)),M=f(g,h.point);a.push({point:g,pressure:d,vector:l,distance:M,runningLength:h.runningLength+M})}for(var m=0;m<u;m++){var L=a[m],x=L.vector;if(L.runningLength>t||m===u-1){for(var y=0;y<m;y++)a[y].vector=x;break}}for(var k=a[u-1].runningLength,P=u-1;P>1;P--){var b=a[P],A=b.vector;if(k-b.runningLength>t/2){for(var O=P;O<u;O++)a[O].vector=A;break}}return a}function l(r,n){void 0===n&&(n={});var d=n.size,l=void 0===d?8:d,M=n.thinning,m=void 0===M?.5:M,L=n.smoothing,x=void 0===L?.5:L,y=n.simulatePressure,k=void 0===y||y,P=n.easing,b=void 0===P?function(r){return r}:P,A=n.start,O=void 0===A?{}:A,S=n.end,_=void 0===S?{}:S,j=n.last,z=void 0!==j&&j,I=O.taper,q=void 0===I?0:I,w=O.easing,B=void 0===w?function(r){return r*(2-r)}:w,C=_.taper,D=void 0===C?0:C,E=_.easing,F=void 0===E?function(r){return--r*r*r+1}:E,G=r.length;if(0===G)return[];for(var H=r[G-1].runningLength,J=[],K=[],N=r.slice(0,5).reduce((function(r,n){return(r+n.pressure)/2}),r[0].pressure),Q=t(l,m,b,r[G-1].pressure),R=r[0].vector,T=r[0].point,U=T,V=T,W=U,X=1;X<G-1;X++){var Y=r[X],Z=Y.point,$=Y.pressure,rr=Y.vector,nr=Y.distance,tr=Y.runningLength;if(m){if(k){var er=h(1,1-nr/l),ir=h(1,nr/l);$=h(1,N+ir/2*(er-N))}Q=t(l,m,b,$)}else Q=l/2;var or=tr<q?B(tr/q):1,ur=H-tr<D?F((H-tr)/D):1;Q*=Math.min(or,ur);var ar=r[X+1].vector,vr=v(rr,ar);if(vr<0){for(var sr=u(a(R),Q),fr=e(Z,sr),pr=i(Z,sr),cr=.2;cr<1;cr+=.2)W=p(fr,Z,g*-cr),V=p(pr,Z,g*cr),K.push(W),J.push(V);T=V,U=W}else{var hr=u(a(c(ar,rr,vr)),Q);V=i(Z,hr);var gr=s(o(W=e(Z,hr),U)),dr=s(o(V,T)),lr=1===X||vr<.25,Mr=(tr>l?l:l/2)*x;(lr||f(U,W)>Mr&&v(gr,rr)>0)&&(K.push(W),U=W),(lr||f(T,V)>Mr&&v(dr,rr)>0)&&(J.push(V),T=V),N=$,R=rr}}var mr=r[0],Lr=r[G-1],xr=K.length<2||J.length<2;if(xr&&(!q&&!D||z)){for(var yr=0,kr=0;kr<G;kr++){var Pr=r[kr];if(Pr.runningLength>l){yr=t(l,m,b,Pr.pressure);break}}for(var br=i(mr.point,u(a(s(o(Lr.point,mr.point))),yr||Q)),Ar=[],Or=0;Or<=1;Or+=.1)Ar.push(p(br,mr.point,2*g*Or));return Ar}var Sr=[];if(!(q||D&&xr)){for(var _r=i(mr.point,u(s(o(W=K[1],V=J[1])),f(W,V)/2)),jr=0;jr<=1;jr+=.2)Sr.push(p(_r,mr.point,g*jr));J.shift(),K.shift()}var zr=[];if(D||q&&xr)zr.push(Lr.point);else{for(var Ir=i(Lr.point,u(a(Lr.vector),Q)),qr=0;qr<=1;qr+=.1)zr.push(p(Ir,Lr.point,3*g*qr));J.pop(),K.pop()}return J.concat(zr,K.reverse(),Sr)}exports.default=function(r,n){return void 0===n&&(n={}),l(d(r,n.streamline),n)},exports.getStrokeOutlinePoints=l,exports.getStrokePoints=d; | ||
//# sourceMappingURL=perfect-freehand.cjs.production.min.js.map |
@@ -1,43 +0,1 @@ | ||
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 _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 _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."); | ||
} | ||
it = o[Symbol.iterator](); | ||
return it.next.bind(it); | ||
} | ||
function lerp(y1, y2, mu) { | ||
@@ -49,2 +7,8 @@ return y1 * (1 - mu) + y2 * mu; | ||
} | ||
/** | ||
* Convert an array of points to the correct format ([x, y, radius]) | ||
* @param points | ||
* @returns | ||
*/ | ||
function toPointsArray(points) { | ||
@@ -69,3 +33,21 @@ if (Array.isArray(points[0])) { | ||
} | ||
/** | ||
* Compute a radius based on the pressure. | ||
* @param size | ||
* @param thinning | ||
* @param easing | ||
* @param pressure | ||
* @returns | ||
*/ | ||
function getStrokeRadius(size, thinning, easing, pressure) { | ||
if (pressure === void 0) { | ||
pressure = 0.5; | ||
} | ||
if (!thinning) return size / 2; | ||
pressure = clamp(easing(pressure), 0, 1); | ||
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; | ||
} | ||
/** | ||
@@ -75,5 +57,2 @@ * Negate a vector. | ||
*/ | ||
function neg(A) { | ||
return [-A[0], -A[1]]; | ||
} | ||
/** | ||
@@ -168,11 +147,2 @@ * Add vectors. | ||
/** | ||
* 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) | ||
@@ -184,5 +154,5 @@ * @param A vector | ||
function rotAround(A, C, rx, ry) { | ||
var s = Math.sin(rx); | ||
var c = Math.cos(ry); | ||
function rotAround(A, C, r) { | ||
var s = Math.sin(r); | ||
var c = Math.cos(r); | ||
var px = A[0] - C[0]; | ||
@@ -207,12 +177,2 @@ var py = A[1] - C[1]; | ||
PI = Math.PI; | ||
function getStrokeRadius(size, thinning, easing, pressure) { | ||
if (pressure === void 0) { | ||
pressure = 0.5; | ||
} | ||
if (!thinning) return size / 2; | ||
pressure = clamp(easing(pressure), 0, 1); | ||
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; | ||
} | ||
/** | ||
@@ -223,5 +183,5 @@ * ## getStrokePoints | ||
* @param streamline How much to streamline the stroke. | ||
* @param size The stroke's size. | ||
*/ | ||
function getStrokePoints(points, streamline, size) { | ||
@@ -237,5 +197,5 @@ if (streamline === void 0) { | ||
var pts = toPointsArray(points); | ||
var _short = true; | ||
if (pts.length === 0) return []; | ||
if (pts.length === 1) pts.push(add(pts[0], [1, 0])); | ||
var len = pts.length; | ||
if (len === 0) return []; | ||
if (len === 1) pts.push(add(pts[0], [1, 0])); | ||
var strokePoints = [{ | ||
@@ -250,3 +210,3 @@ point: [pts[0][0], pts[0][1]], | ||
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), | ||
var point = lrp(prev.point, curr, 1 - streamline), | ||
pressure = curr[2], | ||
@@ -256,3 +216,3 @@ vector = uni(vec(point, prev.point)), | ||
runningLength = prev.runningLength + distance; | ||
var strokePoint = { | ||
strokePoints.push({ | ||
point: point, | ||
@@ -263,30 +223,49 @@ pressure: pressure, | ||
runningLength: runningLength | ||
}; | ||
strokePoints.push(strokePoint); | ||
}); | ||
} | ||
/* | ||
Align vectors at the start of the line | ||
Find the first stroke point past the size and then set all preceding points' | ||
vectors to match this point's vector. This aligns the start cap and reduces | ||
noise at the start of the line. | ||
*/ | ||
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; | ||
for (var _i = 0; _i < len; _i++) { | ||
var _strokePoints$_i = strokePoints[_i], | ||
_runningLength = _strokePoints$_i.runningLength, | ||
_vector = _strokePoints$_i.vector; | ||
if (_runningLength > size || _i === len - 1) { | ||
for (var j = 0; j < _i; j++) { | ||
strokePoints[j].vector = _vector; | ||
} | ||
break; | ||
} | ||
} | ||
/* | ||
Align vectors at the end of the line | ||
Starting from the last point, work back until we've traveled more than | ||
half of the line's size (width). Take the current point's vector and then | ||
work forward, setting all remaining points' vectors to this vector. This | ||
removes the "noise" at the end of the line and allows for a better-facing | ||
end cap. | ||
*/ | ||
if (i === pts.length - 1) { | ||
var rlen = 0; | ||
for (var k = i; k > 1; k--) { | ||
var _strokePoint = strokePoints[k]; | ||
var totalLength = strokePoints[len - 1].runningLength; | ||
if (rlen > size) { | ||
for (var j = k; j < pts.length; j++) { | ||
strokePoints[j].vector = _strokePoint.vector; | ||
} | ||
for (var _i2 = len - 1; _i2 > 1; _i2--) { | ||
var _strokePoints$_i2 = strokePoints[_i2], | ||
_runningLength2 = _strokePoints$_i2.runningLength, | ||
_vector2 = _strokePoints$_i2.vector; | ||
break; | ||
} | ||
if (totalLength - _runningLength2 > size / 2) { | ||
for (var _j = _i2; _j < len; _j++) { | ||
strokePoints[_j].vector = _vector2; | ||
} | ||
rlen += _strokePoint.distance; | ||
} | ||
break; | ||
} | ||
@@ -307,2 +286,4 @@ } | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
@@ -334,7 +315,7 @@ */ | ||
_options$last = _options.last, | ||
last = _options$last === void 0 ? false : _options$last; | ||
isComplete = _options$last === void 0 ? false : _options$last; | ||
var _start$taper = start.taper, | ||
taperStart = _start$taper === void 0 ? 0 : _start$taper, | ||
_start$easing = start.easing, | ||
taperStartCurve = _start$easing === void 0 ? function (t) { | ||
taperStartEase = _start$easing === void 0 ? function (t) { | ||
return t * (2 - t); | ||
@@ -345,158 +326,193 @@ } : _start$easing; | ||
_end$easing = end.easing, | ||
taperEndCurve = _end$easing === void 0 ? function (t) { | ||
taperEndEase = _end$easing === void 0 ? function (t) { | ||
return --t * t * t + 1; | ||
} : _end$easing; | ||
var len = points.length; // The number of points in the array | ||
} : _end$easing; // The number of points in the array | ||
var totalLength = points[len - 1].runningLength; // The total length of the line | ||
var len = points.length; // We can't do anything with an empty array. | ||
var minDist = size * smoothing; // The minimum distance for measurements | ||
if (len === 0) return []; // The total length of the line | ||
var leftPts = []; // Our collected left and right points | ||
var totalLength = points[len - 1].runningLength; // Our collected left and right points | ||
var rightPts = []; | ||
var pl = points[0].point; // Previous left and right points | ||
var leftPts = []; | ||
var rightPts = []; // Previous pressure (start with average of first five pressures) | ||
var pr = points[0].point; | ||
var tl = pl; // Points to test distance from | ||
var prevPressure = points.slice(0, 5).reduce(function (acc, cur) { | ||
return (acc + cur.pressure) / 2; | ||
}, points[0].pressure); // The current radius | ||
var tr = pr; | ||
var pa = points[0].vector; | ||
var pp = 1; // Previous (maybe simulated) pressure | ||
var radius = getStrokeRadius(size, thinning, easing, points[len - 1].pressure); // Previous vector | ||
var ir = 0; // The initial radius | ||
var prevVector = points[0].vector; // Previous left and right points | ||
var r = size; // The current radius | ||
var pl = points[0].point; | ||
var pr = pl; // Temporary left and right points | ||
var _short2 = true; // Whether the line is drawn far enough | ||
// We can't do anything with an empty array. | ||
var tl = pl; | ||
var tr = pr; | ||
/* | ||
Find the outline's left and right points | ||
Iterating through the points and populate the rightPts and leftPts arrays, | ||
skipping the first and last pointsm, which will get caps later on. | ||
*/ | ||
if (len === 0) return []; // Set initial radius | ||
for (var i = 0; i < len - 1; i++) { | ||
for (var i = 1; i < len - 1; i++) { | ||
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; | ||
/* | ||
Calculate the radius | ||
If not thinning, the current point's radius will be half the size; or | ||
otherwise, the size will be based on the current (real or simulated) | ||
pressure. | ||
*/ | ||
if (runningLength > size) { | ||
ir = getStrokeRadius(size, thinning, easing, pressure); | ||
break; | ||
} | ||
} // Set radius for last point | ||
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; | ||
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 - 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. | ||
var rp = min(1, 1 - distance / size); | ||
var sp = min(1, distance / size); | ||
pressure = min(1, prevPressure + (rp - prevPressure) * (sp / 2)); | ||
} | ||
r = getStrokeRadius(size, thinning, easing, _pressure); | ||
radius = getStrokeRadius(size, thinning, easing, pressure); | ||
} else { | ||
r = size / 2; | ||
} // 2. Apply tapering to start and end pressures | ||
radius = size / 2; | ||
} | ||
/* | ||
Apply tapering | ||
If the current length is within the taper distance at either the | ||
start or the end, calculate the taper strengths. Apply the smaller | ||
of the two taper strengths to the radius. | ||
*/ | ||
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 ts = runningLength < taperStart ? taperStartEase(runningLength / taperStart) : 1; | ||
var te = totalLength - runningLength < taperEnd ? taperEndEase((totalLength - runningLength) / taperEnd) : 1; | ||
radius *= Math.min(ts, te); | ||
/* | ||
Handle sharp corners | ||
Find the difference (dot product) between the current and next vector. | ||
If the next vector is at more than a right angle to the current vector, | ||
draw a cap at the current point. | ||
*/ | ||
var dpr$1 = dpr(vector, next.vector); | ||
var nextVector = points[i + 1].vector; | ||
var dpr$1 = dpr(vector, nextVector); | ||
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)); | ||
var _offset = mul(per(prevVector), radius); | ||
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); | ||
var la = add(point, _offset); | ||
var ra = sub(point, _offset); | ||
for (var t = 0.2; t < 1; t += 0.2) { | ||
tr = rotAround(la, point, PI * -t); | ||
tl = rotAround(ra, point, PI * t); | ||
rightPts.push(tr); | ||
leftPts.push(tl); | ||
rightPts.push(tr); | ||
} | ||
pl = tl; | ||
pr = tr; | ||
continue; | ||
} // 4. Add regular point. | ||
} | ||
/* | ||
Add regular points | ||
Project points to either side of the current point, using the | ||
calculated size as a distance. If a point's distance to the | ||
previous point on that side greater than the minimum distance | ||
(or if the corner is kinda sharp), add the points to the side's | ||
points array. | ||
*/ | ||
pl = add(point, mul(per(vector), r)); | ||
pr = add(point, mul(neg(per(vector)), r)); | ||
var offset = mul(per(lrp(nextVector, vector, dpr$1)), radius); | ||
tl = sub(point, offset); | ||
tr = add(point, offset); | ||
var tlu = uni(vec(tr, pr)); | ||
var tru = uni(vec(tl, pl)); | ||
var alwaysAdd = i === 1 || dpr$1 < 0.25; | ||
var minDistance = (runningLength > size ? size : size / 2) * smoothing; | ||
if (_i == 1 || dpr$1 < 0.25 || dist(pl, tl) > (_short2 ? minDist / 2 : minDist)) { | ||
leftPts.push(med(tl, pl)); | ||
tl = pl; | ||
if (alwaysAdd || dist(pr, tr) > minDistance && dpr(tlu, vector) > 0) { | ||
rightPts.push(tr); | ||
pr = tr; | ||
} | ||
if (_i == 1 || dpr$1 < 0.25 || dist(pr, tr) > (_short2 ? minDist / 2 : minDist)) { | ||
rightPts.push(med(tr, pr)); | ||
tr = pr; | ||
} | ||
if (alwaysAdd || dist(pl, tl) > minDistance && dpr(tru, vector) > 0) { | ||
leftPts.push(tl); | ||
pl = tl; | ||
} // Set variables for next iteration | ||
pp = _pressure; | ||
pa = vector; | ||
} // 4. Draw caps | ||
prevPressure = pressure; | ||
prevVector = vector; | ||
} | ||
/* | ||
Drawing caps | ||
Now that we have our points on either side of the line, we need to | ||
draw caps at the start and end. Tapered lines don't have caps, but | ||
may have dots for very short lines. | ||
*/ | ||
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 | ||
var lastPoint = points[len - 1]; | ||
var isVeryShort = rightPts.length < 2 || leftPts.length < 2; | ||
/* | ||
Draw a dot for very short or completed strokes | ||
If the line is too short to gather left or right points and if the line is | ||
not tapered on either side, draw a dot. If the line is tapered, then only | ||
draw a dot if the line is both very short and complete. If we draw a dot, | ||
we can just return those points. | ||
*/ | ||
if (veryShort) { | ||
if (!isTapering || veryShort && last) { | ||
// Backup: draw an inverse cap for the end cap | ||
lpv = uni(vec(lastPoint.point, firstPoint.point)); | ||
if (isVeryShort && (!(taperStart || taperEnd) || isComplete)) { | ||
var ir = 0; | ||
var _start = add(firstPoint.point, mul(per(neg(lpv)), ir || r)); | ||
for (var _i3 = 0; _i3 < len; _i3++) { | ||
var _points$_i = points[_i3], | ||
_pressure = _points$_i.pressure, | ||
_runningLength3 = _points$_i.runningLength; | ||
for (var _t = 0, step = 0.1; _t <= 1; _t += step) { | ||
var _rx = PI * -_t; | ||
if (_runningLength3 > size) { | ||
ir = getStrokeRadius(size, thinning, easing, _pressure); | ||
break; | ||
} | ||
} | ||
var _ry = PI * -_t; | ||
var _start = sub(firstPoint.point, mul(per(uni(vec(lastPoint.point, firstPoint.point))), ir || radius)); | ||
startCap.push(rotAround(_start, firstPoint.point, _rx, _ry)); | ||
} | ||
var dotPts = []; | ||
leftPts.shift(); | ||
rightPts.shift(); | ||
for (var _t = 0, step = 0.1; _t <= 1; _t += step) { | ||
dotPts.push(rotAround(_start, firstPoint.point, PI * 2 * _t)); | ||
} | ||
} 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)); | ||
return dotPts; | ||
} | ||
/* | ||
Draw a start cap | ||
Unless the line has a tapered start, or unless the line has a tapered end | ||
and the line is very short, draw a start cap around the first point. Use | ||
the distance between the second left and right point for the cap's radius. | ||
Finallym remove the first left and right points. :psyduck: | ||
*/ | ||
for (var _t2 = 0, _step2 = 0.1; _t2 <= 1; _t2 += _step2) { | ||
var _rx2 = PI * -_t2; | ||
var _ry2 = PI * -_t2; | ||
var startCap = []; | ||
startCap.push(rotAround(_start2, firstPoint.point, _rx2, _ry2)); | ||
if (!taperStart && !(taperEnd && isVeryShort)) { | ||
tr = rightPts[1]; | ||
tl = leftPts[1]; | ||
var _start2 = sub(firstPoint.point, mul(uni(vec(tr, tl)), dist(tr, tl) / 2)); | ||
for (var _t2 = 0, _step = 0.2; _t2 <= 1; _t2 += _step) { | ||
startCap.push(rotAround(_start2, firstPoint.point, PI * _t2)); | ||
} | ||
@@ -506,27 +522,39 @@ | ||
rightPts.shift(); | ||
} else if (points[1]) { | ||
startCap.push(points[1].point); | ||
} // Draw end cap if taper end is set to zero | ||
} | ||
/* | ||
Draw an end cap | ||
If the line does not have a tapered end, and unless the line has a tapered | ||
start and the line is very short, draw a cap around the last point. Finally, | ||
remove the last left and right points. Otherwise, add the last point. Note | ||
that This cap is a full-turn-and-a-half: this prevents incorrect caps on | ||
sharp end turns. | ||
*/ | ||
if (!isTapering || taperEnd === 0 && !veryShort || veryShort && last) { | ||
var _start3 = add(lastPoint.point, mul(neg(per(lpv)), r)); | ||
var endCap = []; | ||
for (var _t3 = 0, _step3 = 0.1; _t3 <= 1; _t3 += _step3) { | ||
var _rx3 = PI * _t3; | ||
if (!taperEnd && !(taperStart && isVeryShort)) { | ||
var _start3 = sub(lastPoint.point, mul(per(lastPoint.vector), radius)); | ||
var _ry3 = PI * _t3; | ||
for (var _t3 = 0, _step2 = 0.1; _t3 <= 1; _t3 += _step2) { | ||
endCap.push(rotAround(_start3, lastPoint.point, PI * 3 * _t3)); | ||
} | ||
endCap.push(rotAround(_start3, lastPoint.point, _rx3, _ry3)); | ||
} | ||
leftPts.pop(); | ||
rightPts.pop(); | ||
} else { | ||
endCap.push(lastPoint.point); | ||
} | ||
/* | ||
Return the points in the correct windind order: begin on the left side, then | ||
continue around the end cap, then come back along the right side, and finally | ||
complete the start cap. | ||
*/ | ||
var results = [].concat(startCap, leftPts, endCap.reverse(), rightPts.reverse()); | ||
return results; | ||
return leftPts.concat(endCap, rightPts.reverse(), startCap); | ||
} | ||
/** | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @description Returns a stroke as an array of outline points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
@@ -537,4 +565,7 @@ * @param options An (optional) object with options. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
@@ -547,4 +578,3 @@ | ||
var results = getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options); | ||
return results; | ||
return getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options); | ||
} | ||
@@ -551,0 +581,0 @@ |
export interface StrokeOptions { | ||
size?: number; | ||
thinning?: number; | ||
smoothing?: number; | ||
streamline?: number; | ||
easing?: (pressure: number) => number; | ||
simulatePressure?: boolean; | ||
start?: { | ||
taper?: number; | ||
easing?: (distance: number) => number; | ||
size: number; | ||
thinning: number; | ||
smoothing: number; | ||
streamline: number; | ||
easing: (pressure: number) => number; | ||
simulatePressure: boolean; | ||
start: { | ||
taper: number; | ||
easing: (distance: number) => number; | ||
}; | ||
end?: { | ||
taper?: number; | ||
easing?: (distance: number) => number; | ||
end: { | ||
taper: number; | ||
easing: (distance: number) => number; | ||
}; | ||
last?: boolean; | ||
last: boolean; | ||
} | ||
@@ -18,0 +18,0 @@ export interface StrokePoint { |
export declare function lerp(y1: number, y2: number, mu: number): number; | ||
export declare function clamp(n: number, a: number, b: number): number; | ||
/** | ||
* Convert an array of points to the correct format ([x, y, radius]) | ||
* @param points | ||
* @returns | ||
*/ | ||
export declare function toPointsArray<T extends number[], K extends { | ||
@@ -8,1 +13,10 @@ x: number; | ||
}>(points: (T | K)[]): number[][]; | ||
/** | ||
* Compute a radius based on the pressure. | ||
* @param size | ||
* @param thinning | ||
* @param easing | ||
* @param pressure | ||
* @returns | ||
*/ | ||
export declare function getStrokeRadius(size: number, thinning: number, easing: (t: number) => number, pressure?: number): number; |
@@ -58,7 +58,2 @@ /** | ||
/** | ||
* Get normalized / unit vector. | ||
* @param A | ||
*/ | ||
export declare function normalize(A: number[]): number[]; | ||
/** | ||
* Dist length from A to B | ||
@@ -81,3 +76,3 @@ * @param A | ||
*/ | ||
export declare function rotAround(A: number[], C: number[], rx: number, ry: number): number[]; | ||
export declare function rotAround(A: number[], C: number[], r: number): number[]; | ||
/** | ||
@@ -84,0 +79,0 @@ * Interpolate vector A to B with a scalar t |
{ | ||
"version": "0.4.2", | ||
"version": "0.4.3", | ||
"name": "perfect-freehand", | ||
@@ -4,0 +4,0 @@ "author": { |
417
src/index.ts
@@ -1,2 +0,2 @@ | ||
import { toPointsArray, clamp, lerp } from './utils' | ||
import { toPointsArray, getStrokeRadius } from './utils' | ||
import { StrokeOptions, StrokePoint } from './types' | ||
@@ -7,17 +7,2 @@ import * as vec from './vec' | ||
function getStrokeRadius( | ||
size: number, | ||
thinning: number, | ||
easing: (t: number) => number, | ||
pressure = 0.5 | ||
) { | ||
if (!thinning) return size / 2 | ||
pressure = clamp(easing(pressure), 0, 1) | ||
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 | ||
) | ||
} | ||
/** | ||
@@ -28,2 +13,3 @@ * ## getStrokePoints | ||
* @param streamline How much to streamline the stroke. | ||
* @param size The stroke's size. | ||
*/ | ||
@@ -35,9 +21,8 @@ export function getStrokePoints< | ||
const pts = toPointsArray(points) | ||
const len = pts.length | ||
let short = true | ||
if (len === 0) return [] | ||
if (pts.length === 0) return [] | ||
if (len === 1) pts.push(vec.add(pts[0], [1, 0])) | ||
if (pts.length === 1) pts.push(vec.add(pts[0], [1, 0])) | ||
const strokePoints: StrokePoint[] = [ | ||
@@ -58,3 +43,3 @@ { | ||
) { | ||
const point = vec.lrp(prev.point, [curr[0], curr[1]], 1 - streamline), | ||
const point = vec.lrp(prev.point, curr, 1 - streamline), | ||
pressure = curr[2], | ||
@@ -65,3 +50,3 @@ vector = vec.uni(vec.vec(point, prev.point)), | ||
const strokePoint = { | ||
strokePoints.push({ | ||
point, | ||
@@ -72,25 +57,40 @@ pressure, | ||
runningLength, | ||
} | ||
}) | ||
} | ||
strokePoints.push(strokePoint) | ||
if (short && (runningLength > size || i === pts.length - 1)) { | ||
short = false | ||
for (let pt of strokePoints) { | ||
pt.vector = strokePoint.vector | ||
/* | ||
Align vectors at the start of the line | ||
Find the first stroke point past the size and then set all preceding points' | ||
vectors to match this point's vector. This aligns the start cap and reduces | ||
noise at the start of the line. | ||
*/ | ||
for (let i = 0; i < len; i++) { | ||
const { runningLength, vector } = strokePoints[i] | ||
if (runningLength > size || i === len - 1) { | ||
for (let j = 0; j < i; j++) { | ||
strokePoints[j].vector = vector | ||
} | ||
break | ||
} | ||
} | ||
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 | ||
/* | ||
Align vectors at the end of the line | ||
Starting from the last point, work back until we've traveled more than | ||
half of the line's size (width). Take the current point's vector and then | ||
work forward, setting all remaining points' vectors to this vector. This | ||
removes the "noise" at the end of the line and allows for a better-facing | ||
end cap. | ||
*/ | ||
const totalLength = strokePoints[len - 1].runningLength | ||
for (let i = len - 1; i > 1; i--) { | ||
const { runningLength, vector } = strokePoints[i] | ||
if (totalLength - runningLength > size / 2) { | ||
for (let j = i; j < len; j++) { | ||
strokePoints[j].vector = vector | ||
} | ||
break | ||
} | ||
@@ -112,2 +112,4 @@ } | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
@@ -117,3 +119,3 @@ */ | ||
points: StrokePoint[], | ||
options: StrokeOptions = {} as StrokeOptions | ||
options: Partial<StrokeOptions> = {} as Partial<StrokeOptions> | ||
): number[][] { | ||
@@ -126,5 +128,5 @@ const { | ||
easing = t => t, | ||
start = {}, | ||
end = {}, | ||
last = false, | ||
start = {} as Partial<StrokeOptions['start']>, | ||
end = {} as Partial<StrokeOptions['end']>, | ||
last: isComplete = false, | ||
} = options | ||
@@ -134,3 +136,3 @@ | ||
taper: taperStart = 0, | ||
easing: taperStartCurve = t => t * (2 - t), | ||
easing: taperStartEase = t => t * (2 - t), | ||
} = start | ||
@@ -140,67 +142,78 @@ | ||
taper: taperEnd = 0, | ||
easing: taperEndCurve = t => --t * t * t + 1, | ||
easing: taperEndEase = 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 | ||
// The number of points in the array | ||
const len = points.length | ||
// We can't do anything with an empty array. | ||
if (len === 0) return [] | ||
// The total length of the line | ||
const totalLength = points[len - 1].runningLength | ||
// Our collected left and right points | ||
const leftPts: number[][] = [] | ||
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 | ||
// Previous pressure (start with average of first five pressures) | ||
let prevPressure = points | ||
.slice(0, 5) | ||
.reduce((acc, cur) => (acc + cur.pressure) / 2, points[0].pressure) | ||
// The current radius | ||
let radius = getStrokeRadius(size, thinning, easing, points[len - 1].pressure) | ||
// Previous vector | ||
let prevVector = points[0].vector | ||
// Previous left and right points | ||
let pl = points[0].point | ||
let pr = pl | ||
// Temporary left and right points | ||
let tl = pl | ||
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 [] | ||
/* | ||
Find the outline's left and right points | ||
// 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 | ||
} | ||
} | ||
Iterating through the points and populate the rightPts and leftPts arrays, | ||
skipping the first and last pointsm, which will get caps later on. | ||
*/ | ||
// Set radius for last point | ||
r = getStrokeRadius(size, thinning, easing, points[len - 1].pressure) | ||
// For a point with more than one point, create an outline shape. | ||
for (let i = 1; i < len - 1; i++) { | ||
const next = points[i + 1] | ||
let { point, pressure, vector, distance, runningLength } = points[i] | ||
if (short && runningLength > minDist) { | ||
short = false | ||
} | ||
/* | ||
Calculate the radius | ||
// 1. Calculate the size of the current point. | ||
If not thinning, the current point's radius will be half the size; or | ||
otherwise, the size will be based on the current (real or simulated) | ||
pressure. | ||
*/ | ||
if (thinning) { | ||
if (simulatePressure) { | ||
// Simulate pressure by accellerating the reported pressure. | ||
const rp = min(1 - distance / size, 1) | ||
const sp = min(distance / size, 1) | ||
pressure = min(1, pp + (rp - pp) * (sp / 2)) | ||
const rp = min(1, 1 - distance / size) | ||
const sp = min(1, distance / size) | ||
pressure = min(1, prevPressure + (rp - prevPressure) * (sp / 2)) | ||
} | ||
// Compute the stroke radius based on the pressure, easing and thinning. | ||
r = getStrokeRadius(size, thinning, easing, pressure) | ||
radius = getStrokeRadius(size, thinning, easing, pressure) | ||
} else { | ||
r = size / 2 | ||
radius = size / 2 | ||
} | ||
// 2. Apply tapering to start and end pressures | ||
/* | ||
Apply tapering | ||
If the current length is within the taper distance at either the | ||
start or the end, calculate the taper strengths. Apply the smaller | ||
of the two taper strengths to the radius. | ||
*/ | ||
const ts = | ||
runningLength < taperStart | ||
? taperStartCurve(runningLength / taperStart) | ||
? taperStartEase(runningLength / taperStart) | ||
: 1 | ||
@@ -210,114 +223,182 @@ | ||
totalLength - runningLength < taperEnd | ||
? taperEndCurve((totalLength - runningLength) / taperEnd) | ||
? taperEndEase((totalLength - runningLength) / taperEnd) | ||
: 1 | ||
r = r * Math.min(ts, te) | ||
radius *= Math.min(ts, te) | ||
// 3. Handle sharp corners | ||
/* | ||
Handle sharp corners | ||
// Find the delta between the current and next angle. | ||
const dpr = vec.dpr(vector, next.vector) | ||
Find the difference (dot product) between the current and next vector. | ||
If the next vector is at more than a right angle to the current vector, | ||
draw a cap at the current point. | ||
*/ | ||
const nextVector = points[i + 1].vector | ||
const dpr = vec.dpr(vector, nextVector) | ||
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)) | ||
const offset = vec.mul(vec.per(prevVector), radius) | ||
const la = vec.add(point, offset) | ||
const ra = vec.sub(point, offset) | ||
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) | ||
for (let t = 0.2; t < 1; t += 0.2) { | ||
tr = vec.rotAround(la, point, PI * -t) | ||
tl = vec.rotAround(ra, point, PI * t) | ||
rightPts.push(tr) | ||
leftPts.push(tl) | ||
rightPts.push(tr) | ||
} | ||
pl = tl | ||
pr = tr | ||
continue | ||
} | ||
// 4. Add regular point. | ||
/* | ||
Add regular points | ||
pl = vec.add(point, vec.mul(vec.per(vector), r)) | ||
pr = vec.add(point, vec.mul(vec.neg(vec.per(vector)), r)) | ||
Project points to either side of the current point, using the | ||
calculated size as a distance. If a point's distance to the | ||
previous point on that side greater than the minimum distance | ||
(or if the corner is kinda sharp), add the points to the side's | ||
points array. | ||
*/ | ||
const offset = vec.mul(vec.per(vec.lrp(nextVector, vector, dpr)), radius) | ||
tl = vec.sub(point, offset) | ||
tr = vec.add(point, offset) | ||
const tlu = vec.uni(vec.vec(tr, pr)) | ||
const tru = vec.uni(vec.vec(tl, pl)) | ||
const alwaysAdd = i === 1 || dpr < 0.25 | ||
const minDistance = (runningLength > size ? size : size / 2) * smoothing | ||
if ( | ||
i == 1 || | ||
dpr < 0.25 || | ||
vec.dist(pl, tl) > (short ? minDist / 2 : minDist) | ||
alwaysAdd || | ||
(vec.dist(pr, tr) > minDistance && vec.dpr(tlu, vector) > 0) | ||
) { | ||
leftPts.push(vec.med(tl, pl)) | ||
tl = pl | ||
rightPts.push(tr) | ||
pr = tr | ||
} | ||
if ( | ||
i == 1 || | ||
dpr < 0.25 || | ||
vec.dist(pr, tr) > (short ? minDist / 2 : minDist) | ||
alwaysAdd || | ||
(vec.dist(pl, tl) > minDistance && vec.dpr(tru, vector) > 0) | ||
) { | ||
rightPts.push(vec.med(tr, pr)) | ||
tr = pr | ||
leftPts.push(tl) | ||
pl = tl | ||
} | ||
pp = pressure | ||
pa = vector | ||
// Set variables for next iteration | ||
prevPressure = pressure | ||
prevVector = vector | ||
} | ||
// 4. Draw caps | ||
/* | ||
Drawing caps | ||
Now that we have our points on either side of the line, we need to | ||
draw caps at the start and end. Tapered lines don't have caps, but | ||
may have dots for very short lines. | ||
*/ | ||
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 lastPoint = points[len - 1] | ||
const isVeryShort = rightPts.length < 2 || leftPts.length < 2 | ||
const startCap: number[][] = [] | ||
const endCap: number[][] = [] | ||
/* | ||
Draw a dot for very short or completed strokes | ||
If the line is too short to gather left or right points and if the line is | ||
not tapered on either side, draw a dot. If the line is tapered, then only | ||
draw a dot if the line is both very short and complete. If we draw a dot, | ||
we can just return those points. | ||
*/ | ||
// Draw start cap if the end taper is set to zero | ||
if (isVeryShort && (!(taperStart || taperEnd) || isComplete)) { | ||
let ir = 0 | ||
if (veryShort) { | ||
if (!isTapering || (veryShort && last)) { | ||
// 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)) | ||
for (let i = 0; i < len; i++) { | ||
const { pressure, runningLength } = points[i] | ||
if (runningLength > size) { | ||
ir = getStrokeRadius(size, thinning, easing, pressure) | ||
break | ||
} | ||
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( | ||
const start = vec.sub( | ||
firstPoint.point, | ||
vec.mul(vec.uni(vec.vec(lp0, rp0)), vec.dist(lp0, rp0) / 2) | ||
vec.mul( | ||
vec.per(vec.uni(vec.vec(lastPoint.point, firstPoint.point))), | ||
ir || radius | ||
) | ||
) | ||
const dotPts: number[][] = [] | ||
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)) | ||
dotPts.push(vec.rotAround(start, firstPoint.point, PI * 2 * t)) | ||
} | ||
return dotPts | ||
} | ||
/* | ||
Draw a start cap | ||
Unless the line has a tapered start, or unless the line has a tapered end | ||
and the line is very short, draw a start cap around the first point. Use | ||
the distance between the second left and right point for the cap's radius. | ||
Finallym remove the first left and right points. :psyduck: | ||
*/ | ||
const startCap: number[][] = [] | ||
if (!taperStart && !(taperEnd && isVeryShort)) { | ||
tr = rightPts[1] | ||
tl = leftPts[1] | ||
const start = vec.sub( | ||
firstPoint.point, | ||
vec.mul(vec.uni(vec.vec(tr, tl)), vec.dist(tr, tl) / 2) | ||
) | ||
for (let t = 0, step = 0.2; t <= 1; t += step) { | ||
startCap.push(vec.rotAround(start, firstPoint.point, PI * t)) | ||
} | ||
leftPts.shift() | ||
rightPts.shift() | ||
} else if (points[1]) { | ||
startCap.push(points[1].point) | ||
} | ||
// Draw end cap if taper end is set to zero | ||
/* | ||
Draw an end cap | ||
if (!isTapering || (taperEnd === 0 && !veryShort) || (veryShort && last)) { | ||
const start = vec.add(lastPoint.point, vec.mul(vec.neg(vec.per(lpv)), r)) | ||
If the line does not have a tapered end, and unless the line has a tapered | ||
start and the line is very short, draw a cap around the last point. Finally, | ||
remove the last left and right points. Otherwise, add the last point. Note | ||
that This cap is a full-turn-and-a-half: this prevents incorrect caps on | ||
sharp end turns. | ||
*/ | ||
const endCap: number[][] = [] | ||
if (!taperEnd && !(taperStart && isVeryShort)) { | ||
const start = vec.sub( | ||
lastPoint.point, | ||
vec.mul(vec.per(lastPoint.vector), radius) | ||
) | ||
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)) | ||
endCap.push(vec.rotAround(start, lastPoint.point, PI * 3 * t)) | ||
} | ||
leftPts.pop() | ||
rightPts.pop() | ||
} else { | ||
@@ -327,10 +408,9 @@ endCap.push(lastPoint.point) | ||
const results = [ | ||
...startCap, | ||
...leftPts, | ||
...endCap.reverse(), | ||
...rightPts.reverse(), | ||
] | ||
/* | ||
Return the points in the correct windind order: begin on the left side, then | ||
continue around the end cap, then come back along the right side, and finally | ||
complete the start cap. | ||
*/ | ||
return results | ||
return leftPts.concat(endCap, rightPts.reverse(), startCap) | ||
} | ||
@@ -340,3 +420,3 @@ | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @description Returns a stroke as an array of outline points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
@@ -347,4 +427,7 @@ * @param options An (optional) object with options. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.easing An easing function to apply to each point's pressure. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
* @param options.start Tapering and easing function for the start of the line. | ||
* @param options.end Tapering and easing function for the end of the line. | ||
* @param options.last Whether to handle the points as a completed stroke. | ||
*/ | ||
@@ -355,10 +438,8 @@ export default function getStroke< | ||
>(points: (T | K)[], options: StrokeOptions = {} as StrokeOptions): number[][] { | ||
const results = getStrokeOutlinePoints( | ||
return getStrokeOutlinePoints( | ||
getStrokePoints(points, options.streamline), | ||
options | ||
) | ||
return results | ||
} | ||
export { StrokeOptions } |
export interface StrokeOptions { | ||
size?: number | ||
thinning?: number | ||
smoothing?: number | ||
streamline?: number | ||
easing?: (pressure: number) => number | ||
simulatePressure?: boolean | ||
start?: { | ||
taper?: number | ||
easing?: (distance: number) => number | ||
size: number | ||
thinning: number | ||
smoothing: number | ||
streamline: number | ||
easing: (pressure: number) => number | ||
simulatePressure: boolean | ||
start: { | ||
taper: number | ||
easing: (distance: number) => number | ||
} | ||
end?: { | ||
taper?: number | ||
easing?: (distance: number) => number | ||
end: { | ||
taper: number | ||
easing: (distance: number) => number | ||
} | ||
last?: boolean | ||
last: boolean | ||
} | ||
@@ -18,0 +18,0 @@ |
@@ -9,2 +9,7 @@ export function lerp(y1: number, y2: number, mu: number) { | ||
/** | ||
* Convert an array of points to the correct format ([x, y, radius]) | ||
* @param points | ||
* @returns | ||
*/ | ||
export function toPointsArray< | ||
@@ -28,1 +33,24 @@ T extends number[], | ||
} | ||
/** | ||
* Compute a radius based on the pressure. | ||
* @param size | ||
* @param thinning | ||
* @param easing | ||
* @param pressure | ||
* @returns | ||
*/ | ||
export function getStrokeRadius( | ||
size: number, | ||
thinning: number, | ||
easing: (t: number) => number, | ||
pressure = 0.5 | ||
) { | ||
if (!thinning) return size / 2 | ||
pressure = clamp(easing(pressure), 0, 1) | ||
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 | ||
) | ||
} |
@@ -89,10 +89,2 @@ /** | ||
/** | ||
* Get normalized / unit vector. | ||
* @param A | ||
*/ | ||
export function normalize(A: number[]) { | ||
return uni(A) | ||
} | ||
/** | ||
* Dist length from A to B | ||
@@ -121,5 +113,5 @@ * @param A | ||
*/ | ||
export function rotAround(A: number[], C: number[], rx: number, ry: number) { | ||
const s = Math.sin(rx) | ||
const c = Math.cos(ry) | ||
export function rotAround(A: number[], C: number[], r: number) { | ||
const s = Math.sin(r) | ||
const c = Math.cos(r) | ||
@@ -126,0 +118,0 @@ const px = A[0] - C[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
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
156149
1699