perfect-freehand
Advanced tools
Comparing version 0.2.5 to 0.3.0
@@ -0,1 +1,22 @@ | ||
# 0.3.0 | ||
This version has breaking changes. | ||
- Removes polygon-clipping as a dependency. The problems it solved are no longer problems but a developer might still use it separately for aesthetic reasons. | ||
- Removes `clipPath`. | ||
- Removes options types other than `StrokeOptions`. | ||
- Removes `getShortStrokeOutlinePoints`. | ||
- Removes `pressure` option. | ||
- Removes `minSize` and `maxSize` options. | ||
- Adds `size` and `thinning` options. | ||
- Renames `smooth` to `smoothing`. | ||
- Improves caps. | ||
- Improves dots and short strokes. | ||
- You can now use `thinning` to create strokes that shink at high pressure as well as at low pressure. This is a normalized value based on the `size` option: | ||
- at `0` the `thinning` property will have no effect on a stroke's width. | ||
- at `1` a stroke will reach zero width at the lowest pressure and its full width (`size`) at the highest pressure | ||
- at `-1` a stroke will reach zero width at the highest pressure and its full width at the lowest pressure. | ||
- Setting `thinning` to zero has the same effect as had setting the now removed `pressure` option to `false`. | ||
- Improves code organization and comments. | ||
# 0.2.5 | ||
@@ -2,0 +23,0 @@ |
@@ -1,16 +0,2 @@ | ||
import polygonClipping from 'polygon-clipping'; | ||
export declare function lerpAngles(a0: number, a1: number, t: number): number; | ||
export interface StrokePointsOptions { | ||
streamline?: number; | ||
} | ||
export interface StrokeOutlineOptions extends StrokePointsOptions { | ||
simulatePressure?: boolean; | ||
pressure?: boolean; | ||
minSize?: number; | ||
maxSize?: number; | ||
smooth?: number; | ||
} | ||
export interface StrokeOptions extends StrokeOutlineOptions { | ||
clip?: boolean; | ||
} | ||
import { StrokeOptions } from './types'; | ||
/** | ||
@@ -20,3 +6,3 @@ * ## getStrokePoints | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param streamline How much to streamline the stroke. | ||
*/ | ||
@@ -27,11 +13,4 @@ export declare function getStrokePoints<T extends number[], K extends { | ||
pressure?: number; | ||
}>(points: (T | K)[], options?: StrokePointsOptions): number[][]; | ||
}>(points: (T | K)[], streamline?: number): number[][]; | ||
/** | ||
* ## getShortStrokeOutlinePoints | ||
* @description Draw an outline around a short stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
*/ | ||
export declare function getShortStrokeOutlinePoints(points: number[][], options?: StrokeOutlineOptions): number[][]; | ||
/** | ||
* ## getStrokeOutlinePoints | ||
@@ -41,20 +20,24 @@ * @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
export declare function getStrokeOutlinePoints(points: number[][], options?: StrokeOutlineOptions): number[][]; | ||
export declare function getStrokeOutlinePoints(points: number[][], options?: StrokeOptions): number[][]; | ||
/** | ||
* ## clipPath | ||
* @description Returns a clipped polygon of the provided points. | ||
* @param points An array of points (as number[]), the output of getStrokeOutlinePoints. | ||
*/ | ||
export declare function clipPath(points: number[][]): polygonClipping.MultiPolygon; | ||
/** | ||
* ## getPath | ||
* @description Returns a pressure sensitive stroke SVG data | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
export default function getPath<T extends number[], K extends { | ||
export default function getStroke<T extends number[], K extends { | ||
x: number; | ||
y: number; | ||
pressure?: number; | ||
}>(points: (T | K)[], options?: StrokeOptions): string; | ||
}>(points: (T | K)[], options?: StrokeOptions): number[][]; | ||
export { StrokeOptions }; |
@@ -5,52 +5,3 @@ 'use strict'; | ||
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } | ||
var polygonClipping = _interopDefault(require('polygon-clipping')); | ||
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); | ||
} | ||
/* --------------------- Helpers -------------------- */ | ||
var abs = Math.abs, | ||
hypot = Math.hypot, | ||
var hypot = Math.hypot, | ||
cos = Math.cos, | ||
@@ -61,12 +12,28 @@ max = Math.max, | ||
atan2 = Math.atan2, | ||
PI = Math.PI, | ||
TAU = PI / 2, | ||
PI2 = PI * 2; | ||
PI = Math.PI; | ||
/** | ||
* Linear interpolation betwen two numbers. | ||
* @param y1 | ||
* @param y2 | ||
* @param mu | ||
*/ | ||
function projectPoint(x0, y0, a, d) { | ||
return [cos(a) * d + x0, sin(a) * d + y0]; | ||
function lerp(y1, y2, mu) { | ||
return y1 * (1 - mu) + y2 * mu; | ||
} | ||
/** | ||
* Project a point in a direction, by an angle. | ||
* @param x0 | ||
* @param y0 | ||
* @param a | ||
* @param d | ||
* @returns | ||
*/ | ||
function projectPoint(p0, a, d) { | ||
return [cos(a) * d + p0[0], sin(a) * d + p0[1]]; | ||
} | ||
function shortAngleDist(a0, a1) { | ||
var max = PI2; | ||
var max = PI * 2; | ||
var da = (a1 - a0) % max; | ||
@@ -76,11 +43,6 @@ return 2 * da % max - da; | ||
function lerpAngles(a0, a1, t) { | ||
return a0 + shortAngleDist(a0, a1) * t; | ||
} | ||
function angleDelta(a0, a1) { | ||
function getAngleDelta(a0, a1) { | ||
return shortAngleDist(a0, a1); | ||
} | ||
function getPointBetween(x0, y0, x1, y1, d) { | ||
function getPointBetween(p0, p1, d) { | ||
if (d === void 0) { | ||
@@ -90,17 +52,13 @@ d = 0.5; | ||
return [x0 + (x1 - x0) * d, y0 + (y1 - y0) * d]; | ||
return [p0[0] + (p1[0] - p0[0]) * d, p0[1] + (p1[1] - p0[1]) * d]; | ||
} | ||
function getAngle(x0, y0, x1, y1) { | ||
return atan2(y1 - y0, x1 - x0); | ||
function getAngle(p0, p1) { | ||
return atan2(p1[1] - p0[1], p1[0] - p0[0]); | ||
} | ||
function getDistance(x0, y0, x1, y1) { | ||
return hypot(y1 - y0, x1 - x0); | ||
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)); | ||
} | ||
function toPointsArray(points) { | ||
@@ -125,4 +83,8 @@ if (Array.isArray(points[0])) { | ||
} | ||
/* --------------------- Methods -------------------- */ | ||
var abs = Math.abs, | ||
min$1 = Math.min, | ||
PI$1 = Math.PI, | ||
TAU = PI$1 / 2, | ||
SHARP = PI$1 * 0.7; | ||
/** | ||
@@ -132,14 +94,10 @@ * ## getStrokePoints | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param streamline How much to streamline the stroke. | ||
*/ | ||
function getStrokePoints(points, options) { | ||
if (options === void 0) { | ||
options = {}; | ||
function getStrokePoints(points, streamline) { | ||
if (streamline === void 0) { | ||
streamline = 0.5; | ||
} | ||
var _options = options, | ||
_options$streamline = _options.streamline, | ||
streamline = _options$streamline === void 0 ? 0.5 : _options$streamline; | ||
var aPoints = toPointsArray(points); | ||
@@ -149,3 +107,3 @@ var x, | ||
angle, | ||
length = 0, | ||
totalLength = 0, | ||
distance = 0.01, | ||
@@ -172,15 +130,9 @@ len = aPoints.length, | ||
distance = getDistance(x, y, px, py); // Angle | ||
distance = getDistance([x, y], prev); // Angle | ||
angle = getAngle(px, py, x, y); // If distance is very short, blend the angles | ||
angle = getAngle(prev, [x, y]); // Increment total length | ||
if (distance < 1) angle = lerpAngles(prev[2], angle, 0.5); | ||
length += distance; | ||
prev = [x, y, angle, ip, distance, length]; | ||
totalLength += distance; | ||
prev = [x, y, ip, angle, distance, totalLength]; | ||
pts.push(prev); | ||
} // Assign second angle to first point | ||
if (pts.length > 1) { | ||
pts[0][2] = pts[1][2]; | ||
} | ||
@@ -191,9 +143,13 @@ | ||
/** | ||
* ## getShortStrokeOutlinePoints | ||
* @description Draw an outline around a short stroke. | ||
* ## getStrokeOutlinePoints | ||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
function getShortStrokeOutlinePoints(points, options) { | ||
function getStrokeOutlinePoints(points, options) { | ||
if (options === void 0) { | ||
@@ -203,163 +159,147 @@ options = {}; | ||
var _options2 = options, | ||
_options2$minSize = _options2.minSize, | ||
minSize = _options2$minSize === void 0 ? 2.5 : _options2$minSize, | ||
_options2$maxSize = _options2.maxSize, | ||
maxSize = _options2$maxSize === void 0 ? 8 : _options2$maxSize; | ||
var len = points.length; // Can't draw an outline without any points | ||
var _options = options, | ||
_options$size = _options.size, | ||
size = _options$size === void 0 ? 8 : _options$size, | ||
_options$thinning = _options.thinning, | ||
thinning = _options$thinning === void 0 ? 0.5 : _options$thinning, | ||
_options$smoothing = _options.smoothing, | ||
smoothing = _options$smoothing === void 0 ? 0.5 : _options$smoothing, | ||
_options$simulatePres = _options.simulatePressure, | ||
simulatePressure = _options$simulatePres === void 0 ? true : _options$simulatePres; | ||
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, | ||
pp = 0, | ||
// Previous (maybe simulated) pressure | ||
r = size / 2, | ||
// The current point radius | ||
_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. | ||
var _points$ = points[0], | ||
x0 = _points$[0], | ||
y0 = _points$[1], | ||
_points = points[len - 1], | ||
x1 = _points[0], | ||
y1 = _points[1], | ||
p = points[len - 1][3], | ||
leftPts = [], | ||
rightPts = [], | ||
size = clamp(minSize + (maxSize - minSize) * (p ? p : 0.5), minSize, maxSize), | ||
angle = x0 === x1 ? 0 : getAngle(x0, y0, x1, y1); | ||
for (var t = 0, step = 0.1; t <= 1; t += step) { | ||
leftPts.push(projectPoint(x1, y1, angle + TAU - t * PI, size - 1)); | ||
rightPts.push(projectPoint(x0, y0, angle + TAU + t * PI, size - 1)); | ||
} | ||
if (len === 1 || totalLength < size / 2) { | ||
var first = points[0], | ||
last = points[len - 1], | ||
angle = getAngle(first, last); | ||
return leftPts.concat(rightPts.reverse()); | ||
} | ||
/** | ||
* ## getStrokeOutlinePoints | ||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
*/ | ||
if (thinning) { | ||
var pressure = last[3] ? clamp(last[3], 0, 1) : 0.5; | ||
r = (thinning > 0 ? lerp(size - size * thinning, size, clamp(pressure, 0, 1)) : lerp(size, size + size * thinning, clamp(pressure, 0, 1))) / 2; | ||
} | ||
function getStrokeOutlinePoints(points, options) { | ||
if (options === void 0) { | ||
options = {}; | ||
} | ||
for (var t = 0, step = 0.1; t <= 1; t += step) { | ||
tl = projectPoint(first, angle + PI$1 + TAU - t * PI$1, r - 1); | ||
tr = projectPoint(last, angle + TAU - t * PI$1, r - 1); | ||
leftPts.push(tl); | ||
rightPts.push(tr); | ||
} | ||
var _options3 = options, | ||
_options3$simulatePre = _options3.simulatePressure, | ||
simulatePressure = _options3$simulatePre === void 0 ? true : _options3$simulatePre, | ||
_options3$pressure = _options3.pressure, | ||
pressure = _options3$pressure === void 0 ? true : _options3$pressure, | ||
_options3$minSize = _options3.minSize, | ||
minSize = _options3$minSize === void 0 ? 2.5 : _options3$minSize, | ||
_options3$maxSize = _options3.maxSize, | ||
maxSize = _options3$maxSize === void 0 ? 8 : _options3$maxSize, | ||
_options3$smooth = _options3.smooth, | ||
smooth = _options3$smooth === void 0 ? 8 : _options3$smooth; | ||
var len = points.length, | ||
p0 = points[0], | ||
p1 = points[0], | ||
t0 = p0, | ||
t1 = p1, | ||
m0 = p0, | ||
m1 = p0, | ||
size = 0, | ||
pp = 0.5, | ||
started = false, | ||
length = 0, | ||
leftPts = [p0], | ||
rightPts = [p0], | ||
d0, | ||
d1; | ||
return leftPts.concat(rightPts); | ||
} // For a point with more than one point, create an outline shape. | ||
if (len === 0) { | ||
return []; | ||
} // Use the points to create an outline shape, where the width | ||
// of the shape is determined by the pressure at each point. | ||
for (var i = 1; i < len; i++) { | ||
var _points2 = points[i - 1], | ||
px = _points2[0], | ||
py = _points2[1], | ||
pa = _points2[2]; | ||
var prev = points[i - 1], | ||
pa = prev[3]; | ||
var _points$i = points[i], | ||
x = _points$i[0], | ||
y = _points$i[1], | ||
angle = _points$i[2], | ||
ip = _points$i[3], | ||
_pressure = _points$i[2], | ||
_angle = _points$i[3], | ||
distance = _points$i[4], | ||
clen = _points$i[5]; | ||
length += clen; // Size | ||
clen = _points$i[5]; // 1. | ||
// Calculate the size of the current point. | ||
if (pressure) { | ||
if (thinning) { | ||
if (simulatePressure) { | ||
// Simulate pressure by accellerating the reported pressure. | ||
var rp = min(1 - distance / maxSize, 1); | ||
var sp = min(distance / maxSize, 1); | ||
ip = min(1, pp + (rp - pp) * (sp / 2)); | ||
} // Compute the size based on the 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)); | ||
} // Compute the size based on the pressure and thinning. | ||
size = clamp(minSize + ip * (maxSize - minSize), minSize, maxSize); | ||
} else { | ||
size = maxSize; | ||
} // Handle line start | ||
r = (thinning > 0 ? lerp(size - size * thinning, size, clamp(_pressure, 0, 1)) : lerp(size, size + size * thinning, clamp(_pressure, 0, 1))) / 2; | ||
} // 2. | ||
// Draw a cap once we've reached the minimum length. | ||
if (!started && length > size / 2) { | ||
var _points$2 = points[0], | ||
sx = _points$2[0], | ||
sy = _points$2[1]; | ||
if (_short) { | ||
if (clen < size / 2) { | ||
continue; | ||
} // The first point after we've reached the minimum length. | ||
for (var t = 0, step = 0.25; t <= 1; t += step) { | ||
m0 = projectPoint(sx, sy, angle + TAU + t * PI, size - 1); | ||
leftPts.push(m0); | ||
m1 = projectPoint(sx, sy, angle - TAU + t * -PI, size - 1); | ||
rightPts.push(m1); | ||
} | ||
started = true; | ||
continue; | ||
} // 3. Shape | ||
_short = false; // Draw a cap at the first point angled toward the current point. | ||
var _first = points[0]; | ||
p0 = projectPoint(x, y, angle - TAU, size); // left | ||
for (var _t = 0, _step = 0.1; _t <= 1; _t += _step) { | ||
tl = projectPoint(_first, _angle + TAU + _t * PI$1, r - 1); | ||
leftPts.push(tl); | ||
} | ||
p1 = projectPoint(x, y, angle + TAU, size); // right | ||
tr = projectPoint(_first, _angle + TAU, r - 1); | ||
rightPts.push(tr); | ||
} // 3. | ||
// Add points for the current point. | ||
var delta = angleDelta(pa, angle); // Handle sharp corners differently | ||
if (i === points.length - 1 || abs(delta) > PI * 0.75 && length > size) { | ||
var _getPointBetween = getPointBetween(px, py, x, y, 0.5), | ||
mx = _getPointBetween[0], | ||
my = _getPointBetween[1]; | ||
for (var _t = 0, _step = 0.25; _t <= 1; _t += _step) { | ||
m0 = projectPoint(mx, my, pa - TAU + _t * PI, size - 1); | ||
leftPts.push(m0); | ||
m1 = projectPoint(mx, my, pa + TAU + _t * -PI, size - 1); | ||
rightPts.push(m1); | ||
if (i === len - 1) { | ||
// The last point in the line. | ||
// Add points for an end cap. | ||
for (var _t2 = 0, _step2 = 0.1; _t2 <= 1; _t2 += _step2) { | ||
tr = projectPoint([x, y], _angle + TAU - _t2 * PI$1, r - 1); | ||
rightPts.push(tr); | ||
} | ||
t0 = m0; | ||
t1 = m1; | ||
} else { | ||
// Project sideways | ||
d0 = getDistance(p0[0], p0[1], t0[0], t0[1]); | ||
// Find the delta between the current and previous angle. | ||
var delta = getAngleDelta(prev[3], _angle); | ||
if (d0 > smooth) { | ||
leftPts.push(m0); | ||
m0 = getPointBetween(t0[0], t0[1], p0[0], p0[1], 0.5); | ||
t0 = p0; | ||
} | ||
if (abs(delta) > SHARP && clen > r) { | ||
// A sharp corner. | ||
// Project points (left and right) for a cap. | ||
var mid = getPointBetween(prev, [x, y], 0.5); | ||
d1 = getDistance(p1[0], p1[1], t1[0], t1[1]); | ||
for (var _t3 = 0, _step3 = 0.25; _t3 <= 1; _t3 += _step3) { | ||
tl = projectPoint(mid, pa - TAU + _t3 * PI$1, r - 1); | ||
tr = projectPoint(mid, pa + TAU + _t3 * -PI$1, r - 1); | ||
leftPts.push(tl); | ||
rightPts.push(tr); | ||
} | ||
} else { | ||
// A regular point. | ||
// Add projected points left and right. | ||
pl = projectPoint([x, y], _angle - TAU, r); | ||
pr = projectPoint([x, y], _angle + TAU, r); // Add projected point if far enough away from last left point | ||
if (d1 > smooth) { | ||
rightPts.push(m1); | ||
m1 = getPointBetween(t1[0], t1[1], p1[0], p1[1], 0.5); | ||
t1 = p1; | ||
if (getDistance(pl, tl) > minDist) { | ||
leftPts.push(getPointBetween(tl, pl, 0.5)); | ||
tl = pl; | ||
} // Add point if far enough away from last right point | ||
if (getDistance(pr, tr) > minDist) { | ||
rightPts.push(getPointBetween(tr, pr, 0.5)); | ||
tr = pr; | ||
} | ||
} | ||
pp = _pressure; | ||
} | ||
pp = ip; | ||
} | ||
@@ -370,18 +310,14 @@ | ||
/** | ||
* ## clipPath | ||
* @description Returns a clipped polygon of the provided points. | ||
* @param points An array of points (as number[]), the output of getStrokeOutlinePoints. | ||
*/ | ||
function clipPath(points) { | ||
return polygonClipping.union([points]); | ||
} | ||
/** | ||
* ## getPath | ||
* @description Returns a pressure sensitive stroke SVG data | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
function getPath(points, options) { | ||
function getStroke(points, options) { | ||
if (options === void 0) { | ||
@@ -391,69 +327,8 @@ options = {}; | ||
if (points.length === 0) { | ||
return ''; | ||
} | ||
var _options4 = options, | ||
_options4$clip = _options4.clip, | ||
clip = _options4$clip === void 0 ? true : _options4$clip, | ||
_options4$maxSize = _options4.maxSize, | ||
maxSize = _options4$maxSize === void 0 ? 8 : _options4$maxSize; | ||
var ps = getStrokePoints(points, options), | ||
totalLength = ps[ps.length - 1][5], | ||
pts = totalLength < maxSize ? getShortStrokeOutlinePoints(ps, options) : getStrokeOutlinePoints(ps, options), | ||
d = []; // If the length is too short, just draw a dot. | ||
// If we're clipping the path, then find the polygon and add its faces. | ||
if (clip) { | ||
var poly = clipPath(pts); | ||
for (var _iterator = _createForOfIteratorHelperLoose(poly), _step2; !(_step2 = _iterator()).done;) { | ||
var face = _step2.value; | ||
for (var _iterator2 = _createForOfIteratorHelperLoose(face), _step3; !(_step3 = _iterator2()).done;) { | ||
var verts = _step3.value; | ||
var v0 = verts[0]; | ||
var v1 = verts[1]; | ||
verts.push(v0); | ||
d.push("M " + v0[0] + " " + v0[1]); | ||
for (var i = 1; i < verts.length; i++) { | ||
var _getPointBetween2 = getPointBetween(v0[0], v0[1], v1[0], v1[1], 0.5), | ||
mpx = _getPointBetween2[0], | ||
mpy = _getPointBetween2[1]; | ||
d.push(" Q " + v0[0] + "," + v0[1] + " " + mpx + "," + mpy); | ||
v0 = v1; | ||
v1 = verts[i + 1]; | ||
} | ||
} | ||
} | ||
} else { | ||
// If we're not clipping the path, just trace it. | ||
var _v = pts[0]; | ||
var _v2 = pts[1]; | ||
pts.push(_v); | ||
d.push("M " + _v[0] + " " + _v[1]); | ||
for (var _i = 1; _i < pts.length; _i++) { | ||
var _getPointBetween3 = getPointBetween(_v[0], _v[1], _v2[0], _v2[1], 0.5), | ||
_mpx = _getPointBetween3[0], | ||
_mpy = _getPointBetween3[1]; | ||
d.push("Q " + _v[0] + "," + _v[1] + " " + _mpx + "," + _mpy); | ||
_v = _v2; | ||
_v2 = pts[_i + 1]; | ||
} | ||
} | ||
d.push('Z'); | ||
return d.join(' '); | ||
return getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options); | ||
} | ||
exports.clipPath = clipPath; | ||
exports.default = getPath; | ||
exports.getShortStrokeOutlinePoints = getShortStrokeOutlinePoints; | ||
exports.default = getStroke; | ||
exports.getStrokeOutlinePoints = getStrokeOutlinePoints; | ||
exports.getStrokePoints = getStrokePoints; | ||
exports.lerpAngles = lerpAngles; | ||
//# sourceMappingURL=perfect-freehand.cjs.development.js.map |
@@ -1,2 +0,2 @@ | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var r,t=(r=require("polygon-clipping"))&&"object"==typeof r&&"default"in r?r.default:r;function e(r,t){(null==t||t>r.length)&&(t=r.length);for(var e=0,n=new Array(t);e<t;e++)n[e]=r[e];return n}function n(r,t){var n;if("undefined"==typeof Symbol||null==r[Symbol.iterator]){if(Array.isArray(r)||(n=function(r,t){if(r){if("string"==typeof r)return e(r,void 0);var n=Object.prototype.toString.call(r).slice(8,-1);return"Object"===n&&r.constructor&&(n=r.constructor.name),"Map"===n||"Set"===n?Array.from(r):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?e(r,void 0):void 0}}(r))||t&&r&&"number"==typeof r.length){n&&(r=n);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(n=r[Symbol.iterator]()).next.bind(n)}var o=Math.abs,i=Math.hypot,u=Math.cos,a=Math.max,s=Math.min,v=Math.sin,f=Math.atan2,l=Math.PI,p=l/2,h=2*l;function c(r,t,e,n){return[u(e)*n+r,v(e)*n+t]}function d(r,t){var e=(t-r)%h;return 2*e%h-e}function m(r,t,e){return r+d(r,t)*e}function g(r,t,e,n,o){return void 0===o&&(o=.5),[r+(e-r)*o,t+(n-t)*o]}function y(r,t,e,n){return f(n-t,e-r)}function b(r,t,e,n){return i(n-t,e-r)}function S(r,t,e){return a(t,s(e,r))}function x(r,t){void 0===t&&(t={});var e,n,o,i=t.streamline,u=void 0===i?.5:i,a=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),s=0,v=.01,f=a.length,l=[].concat(a[0],[0,0,0]),p=[l];if(0===f)return[];for(var h=1;h<f;h++){var c=a[h],d=c[2],g=l[0],S=l[1];v=b(e=g+(c[0]-g)*(1-u),n=S+(c[1]-S)*(1-u),g,S),o=y(g,S,e,n),v<1&&(o=m(l[2],o,.5)),p.push(l=[e,n,o,d,v,s+=v])}return p.length>1&&(p[0][2]=p[1][2]),p}function M(r,t){void 0===t&&(t={});var e=t.minSize,n=void 0===e?2.5:e,o=t.maxSize,i=void 0===o?8:o,u=r.length;if(0===u)return[];for(var a=r[0],s=a[0],v=a[1],f=r[u-1],h=f[0],d=f[1],m=[],g=[],b=S(n+(i-n)*(r[u-1][3]||.5),n,i),x=s===h?0:y(s,v,h,d),M=0;M<=1;M+=.1)m.push(c(h,d,x+p-M*l,b-1)),g.push(c(s,v,x+p+M*l,b-1));return m.concat(g.reverse())}function A(r,t){void 0===t&&(t={});var e=t.simulatePressure,n=void 0===e||e,i=t.pressure,u=void 0===i||i,a=t.minSize,v=void 0===a?2.5:a,f=t.maxSize,h=void 0===f?8:f,m=t.smooth,y=void 0===m?8:m,x=r.length,M=r[0],A=r[0],P=M,j=A,z=M,O=M,I=0,k=.5,w=!1,Q=0,_=[M],q=[M];if(0===x)return[];for(var C=1;C<x;C++){var E=r[C-1],T=E[0],U=E[1],Z=E[2],$=r[C],B=$[0],D=$[1],F=$[2],G=$[3],H=$[4];if(Q+=$[5],u){if(n){var J=s(1-H/h,1),K=s(H/h,1);G=s(1,k+K/2*(J-k))}I=S(v+G*(h-v),v,h)}else I=h;if(!w&&Q>I/2){for(var L=r[0],N=L[0],R=L[1],V=0;V<=1;V+=.25)z=c(N,R,F+p+V*l,I-1),_.push(z),O=c(N,R,F-p+V*-l,I-1),q.push(O);w=!0}else{M=c(B,D,F-p,I),A=c(B,D,F+p,I);var W=d(Z,F);if(C===r.length-1||o(W)>.75*l&&Q>I){for(var X=g(T,U,B,D,.5),Y=X[0],rr=X[1],tr=0;tr<=1;tr+=.25)z=c(Y,rr,Z-p+tr*l,I-1),_.push(z),O=c(Y,rr,Z+p+tr*-l,I-1),q.push(O);P=z,j=O}else b(M[0],M[1],P[0],P[1])>y&&(_.push(z),z=g(P[0],P[1],M[0],M[1],.5),P=M),b(A[0],A[1],j[0],j[1])>y&&(q.push(O),O=g(j[0],j[1],A[0],A[1],.5),j=A);k=G}}return _.concat(q.reverse())}function P(r){return t.union([r])}exports.clipPath=P,exports.default=function(r,t){if(void 0===t&&(t={}),0===r.length)return"";var e=t.clip,o=void 0===e||e,i=t.maxSize,u=void 0===i?8:i,a=x(r,t),s=a[a.length-1][5]<u?M(a,t):A(a,t),v=[];if(o)for(var f,l=n(P(s));!(f=l()).done;)for(var p,h=n(f.value);!(p=h()).done;){var c=p.value,d=c[0],m=c[1];c.push(d),v.push("M "+d[0]+" "+d[1]);for(var y=1;y<c.length;y++){var b=g(d[0],d[1],m[0],m[1],.5);v.push(" Q "+d[0]+","+d[1]+" "+b[0]+","+b[1]),d=m,m=c[y+1]}}else{var S=s[0],j=s[1];s.push(S),v.push("M "+S[0]+" "+S[1]);for(var z=1;z<s.length;z++){var O=g(S[0],S[1],j[0],j[1],.5);v.push("Q "+S[0]+","+S[1]+" "+O[0]+","+O[1]),S=j,j=s[z+1]}}return v.push("Z"),v.join(" ")},exports.getShortStrokeOutlinePoints=M,exports.getStrokeOutlinePoints=A,exports.getStrokePoints=x,exports.lerpAngles=m; | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var r=Math.hypot,t=Math.cos,n=Math.max,e=Math.min,i=Math.sin,u=Math.atan2,o=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 f(r,t,n){return void 0===n&&(n=.5),[r[0]+(t[0]-r[0])*n,r[1]+(t[1]-r[1])*n]}function v(r,t){return u(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,M=Math.PI,l=M/2,m=.7*M;function g(r,t){void 0===t&&(t=.5);var n,e,i,u=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),o=0,a=.01,s=u.length,f=[].concat(u[0],[0,0,0]),c=[f];if(0===s)return[];for(var p=1;p<s;p++){var d=u[p],M=d[2],l=f[0],m=f[1];a=h([n=l+(d[0]-l)*(1-t),e=m+(d[1]-m)*(1-t)],f),i=v(f,[n,e]),c.push(f=[n,e,M,i,a,o+=a])}return c}function x(r,t){void 0===t&&(t={});var n=t.size,e=void 0===n?8:n,i=t.thinning,u=void 0===i?.5:i,g=t.smoothing,x=t.simulatePressure,P=void 0===x||x,y=r.length,b=e*(void 0===g?.5:g),k=[],A=[],I=r[0],O=r[0],S=I,_=O,j=0,z=e/2,q=!0;if(0===y)return[];if(1===y||r[y-1][5]<e/2){var w=r[0],B=r[y-1],C=v(w,B);if(u){var D=B[3]?c(B[3],0,1):.5;z=(u>0?a(e-e*u,e,c(D,0,1)):a(e,e+e*u,c(D,0,1)))/2}for(var E=0;E<=1;E+=.1)S=s(w,C+M+l-E*M,z-1),_=s(B,C+l-E*M,z-1),k.push(S),A.push(_);return k.concat(A)}for(var F=1;F<y;F++){var G=r[F-1],H=G[3],J=r[F],K=J[0],L=J[1],N=J[2],Q=J[3],R=J[4],T=J[5];if(u){if(P){var U=d(1-R/e,1),V=d(R/e,1);N=d(1,j+V/2*(U-j))}z=(u>0?a(e-e*u,e,c(N,0,1)):a(e,e+e*u,c(N,0,1)))/2}if(q){if(T<e/2)continue;q=!1;for(var W=r[0],X=0;X<=1;X+=.1)S=s(W,Q+l+X*M,z-1),k.push(S);_=s(W,Q+l,z-1),A.push(_)}if(F===y-1)for(var Y=0;Y<=1;Y+=.1)_=s([K,L],Q+l-Y*M,z-1),A.push(_);else{var Z=function(r,t){var n=2*o,e=(t-r)%n;return 2*e%n-e}(G[3],Q);if(p(Z)>m&&T>z)for(var $=f(G,[K,L],.5),rr=0;rr<=1;rr+=.25)S=s($,H-l+rr*M,z-1),_=s($,H+l+rr*-M,z-1),k.push(S),A.push(_);else I=s([K,L],Q-l,z),O=s([K,L],Q+l,z),h(I,S)>b&&(k.push(f(S,I,.5)),S=I),h(O,_)>b&&(A.push(f(_,O,.5)),_=O);j=N}}return k.concat(A.reverse())}exports.default=function(r,t){return void 0===t&&(t={}),x(g(r,t.streamline),t)},exports.getStrokeOutlinePoints=x,exports.getStrokePoints=g; | ||
//# sourceMappingURL=perfect-freehand.cjs.production.min.js.map |
@@ -1,49 +0,2 @@ | ||
import polygonClipping from 'polygon-clipping'; | ||
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); | ||
} | ||
/* --------------------- Helpers -------------------- */ | ||
var abs = Math.abs, | ||
hypot = Math.hypot, | ||
var hypot = Math.hypot, | ||
cos = Math.cos, | ||
@@ -54,12 +7,28 @@ max = Math.max, | ||
atan2 = Math.atan2, | ||
PI = Math.PI, | ||
TAU = PI / 2, | ||
PI2 = PI * 2; | ||
PI = Math.PI; | ||
/** | ||
* Linear interpolation betwen two numbers. | ||
* @param y1 | ||
* @param y2 | ||
* @param mu | ||
*/ | ||
function projectPoint(x0, y0, a, d) { | ||
return [cos(a) * d + x0, sin(a) * d + y0]; | ||
function lerp(y1, y2, mu) { | ||
return y1 * (1 - mu) + y2 * mu; | ||
} | ||
/** | ||
* Project a point in a direction, by an angle. | ||
* @param x0 | ||
* @param y0 | ||
* @param a | ||
* @param d | ||
* @returns | ||
*/ | ||
function projectPoint(p0, a, d) { | ||
return [cos(a) * d + p0[0], sin(a) * d + p0[1]]; | ||
} | ||
function shortAngleDist(a0, a1) { | ||
var max = PI2; | ||
var max = PI * 2; | ||
var da = (a1 - a0) % max; | ||
@@ -69,11 +38,6 @@ return 2 * da % max - da; | ||
function lerpAngles(a0, a1, t) { | ||
return a0 + shortAngleDist(a0, a1) * t; | ||
} | ||
function angleDelta(a0, a1) { | ||
function getAngleDelta(a0, a1) { | ||
return shortAngleDist(a0, a1); | ||
} | ||
function getPointBetween(x0, y0, x1, y1, d) { | ||
function getPointBetween(p0, p1, d) { | ||
if (d === void 0) { | ||
@@ -83,17 +47,13 @@ d = 0.5; | ||
return [x0 + (x1 - x0) * d, y0 + (y1 - y0) * d]; | ||
return [p0[0] + (p1[0] - p0[0]) * d, p0[1] + (p1[1] - p0[1]) * d]; | ||
} | ||
function getAngle(x0, y0, x1, y1) { | ||
return atan2(y1 - y0, x1 - x0); | ||
function getAngle(p0, p1) { | ||
return atan2(p1[1] - p0[1], p1[0] - p0[0]); | ||
} | ||
function getDistance(x0, y0, x1, y1) { | ||
return hypot(y1 - y0, x1 - x0); | ||
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)); | ||
} | ||
function toPointsArray(points) { | ||
@@ -118,4 +78,8 @@ if (Array.isArray(points[0])) { | ||
} | ||
/* --------------------- Methods -------------------- */ | ||
var abs = Math.abs, | ||
min$1 = Math.min, | ||
PI$1 = Math.PI, | ||
TAU = PI$1 / 2, | ||
SHARP = PI$1 * 0.7; | ||
/** | ||
@@ -125,14 +89,10 @@ * ## getStrokePoints | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param streamline How much to streamline the stroke. | ||
*/ | ||
function getStrokePoints(points, options) { | ||
if (options === void 0) { | ||
options = {}; | ||
function getStrokePoints(points, streamline) { | ||
if (streamline === void 0) { | ||
streamline = 0.5; | ||
} | ||
var _options = options, | ||
_options$streamline = _options.streamline, | ||
streamline = _options$streamline === void 0 ? 0.5 : _options$streamline; | ||
var aPoints = toPointsArray(points); | ||
@@ -142,3 +102,3 @@ var x, | ||
angle, | ||
length = 0, | ||
totalLength = 0, | ||
distance = 0.01, | ||
@@ -165,15 +125,9 @@ len = aPoints.length, | ||
distance = getDistance(x, y, px, py); // Angle | ||
distance = getDistance([x, y], prev); // Angle | ||
angle = getAngle(px, py, x, y); // If distance is very short, blend the angles | ||
angle = getAngle(prev, [x, y]); // Increment total length | ||
if (distance < 1) angle = lerpAngles(prev[2], angle, 0.5); | ||
length += distance; | ||
prev = [x, y, angle, ip, distance, length]; | ||
totalLength += distance; | ||
prev = [x, y, ip, angle, distance, totalLength]; | ||
pts.push(prev); | ||
} // Assign second angle to first point | ||
if (pts.length > 1) { | ||
pts[0][2] = pts[1][2]; | ||
} | ||
@@ -184,9 +138,13 @@ | ||
/** | ||
* ## getShortStrokeOutlinePoints | ||
* @description Draw an outline around a short stroke. | ||
* ## getStrokeOutlinePoints | ||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
function getShortStrokeOutlinePoints(points, options) { | ||
function getStrokeOutlinePoints(points, options) { | ||
if (options === void 0) { | ||
@@ -196,163 +154,147 @@ options = {}; | ||
var _options2 = options, | ||
_options2$minSize = _options2.minSize, | ||
minSize = _options2$minSize === void 0 ? 2.5 : _options2$minSize, | ||
_options2$maxSize = _options2.maxSize, | ||
maxSize = _options2$maxSize === void 0 ? 8 : _options2$maxSize; | ||
var len = points.length; // Can't draw an outline without any points | ||
var _options = options, | ||
_options$size = _options.size, | ||
size = _options$size === void 0 ? 8 : _options$size, | ||
_options$thinning = _options.thinning, | ||
thinning = _options$thinning === void 0 ? 0.5 : _options$thinning, | ||
_options$smoothing = _options.smoothing, | ||
smoothing = _options$smoothing === void 0 ? 0.5 : _options$smoothing, | ||
_options$simulatePres = _options.simulatePressure, | ||
simulatePressure = _options$simulatePres === void 0 ? true : _options$simulatePres; | ||
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, | ||
pp = 0, | ||
// Previous (maybe simulated) pressure | ||
r = size / 2, | ||
// The current point radius | ||
_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. | ||
var _points$ = points[0], | ||
x0 = _points$[0], | ||
y0 = _points$[1], | ||
_points = points[len - 1], | ||
x1 = _points[0], | ||
y1 = _points[1], | ||
p = points[len - 1][3], | ||
leftPts = [], | ||
rightPts = [], | ||
size = clamp(minSize + (maxSize - minSize) * (p ? p : 0.5), minSize, maxSize), | ||
angle = x0 === x1 ? 0 : getAngle(x0, y0, x1, y1); | ||
for (var t = 0, step = 0.1; t <= 1; t += step) { | ||
leftPts.push(projectPoint(x1, y1, angle + TAU - t * PI, size - 1)); | ||
rightPts.push(projectPoint(x0, y0, angle + TAU + t * PI, size - 1)); | ||
} | ||
if (len === 1 || totalLength < size / 2) { | ||
var first = points[0], | ||
last = points[len - 1], | ||
angle = getAngle(first, last); | ||
return leftPts.concat(rightPts.reverse()); | ||
} | ||
/** | ||
* ## getStrokeOutlinePoints | ||
* @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
*/ | ||
if (thinning) { | ||
var pressure = last[3] ? clamp(last[3], 0, 1) : 0.5; | ||
r = (thinning > 0 ? lerp(size - size * thinning, size, clamp(pressure, 0, 1)) : lerp(size, size + size * thinning, clamp(pressure, 0, 1))) / 2; | ||
} | ||
function getStrokeOutlinePoints(points, options) { | ||
if (options === void 0) { | ||
options = {}; | ||
} | ||
for (var t = 0, step = 0.1; t <= 1; t += step) { | ||
tl = projectPoint(first, angle + PI$1 + TAU - t * PI$1, r - 1); | ||
tr = projectPoint(last, angle + TAU - t * PI$1, r - 1); | ||
leftPts.push(tl); | ||
rightPts.push(tr); | ||
} | ||
var _options3 = options, | ||
_options3$simulatePre = _options3.simulatePressure, | ||
simulatePressure = _options3$simulatePre === void 0 ? true : _options3$simulatePre, | ||
_options3$pressure = _options3.pressure, | ||
pressure = _options3$pressure === void 0 ? true : _options3$pressure, | ||
_options3$minSize = _options3.minSize, | ||
minSize = _options3$minSize === void 0 ? 2.5 : _options3$minSize, | ||
_options3$maxSize = _options3.maxSize, | ||
maxSize = _options3$maxSize === void 0 ? 8 : _options3$maxSize, | ||
_options3$smooth = _options3.smooth, | ||
smooth = _options3$smooth === void 0 ? 8 : _options3$smooth; | ||
var len = points.length, | ||
p0 = points[0], | ||
p1 = points[0], | ||
t0 = p0, | ||
t1 = p1, | ||
m0 = p0, | ||
m1 = p0, | ||
size = 0, | ||
pp = 0.5, | ||
started = false, | ||
length = 0, | ||
leftPts = [p0], | ||
rightPts = [p0], | ||
d0, | ||
d1; | ||
return leftPts.concat(rightPts); | ||
} // For a point with more than one point, create an outline shape. | ||
if (len === 0) { | ||
return []; | ||
} // Use the points to create an outline shape, where the width | ||
// of the shape is determined by the pressure at each point. | ||
for (var i = 1; i < len; i++) { | ||
var _points2 = points[i - 1], | ||
px = _points2[0], | ||
py = _points2[1], | ||
pa = _points2[2]; | ||
var prev = points[i - 1], | ||
pa = prev[3]; | ||
var _points$i = points[i], | ||
x = _points$i[0], | ||
y = _points$i[1], | ||
angle = _points$i[2], | ||
ip = _points$i[3], | ||
_pressure = _points$i[2], | ||
_angle = _points$i[3], | ||
distance = _points$i[4], | ||
clen = _points$i[5]; | ||
length += clen; // Size | ||
clen = _points$i[5]; // 1. | ||
// Calculate the size of the current point. | ||
if (pressure) { | ||
if (thinning) { | ||
if (simulatePressure) { | ||
// Simulate pressure by accellerating the reported pressure. | ||
var rp = min(1 - distance / maxSize, 1); | ||
var sp = min(distance / maxSize, 1); | ||
ip = min(1, pp + (rp - pp) * (sp / 2)); | ||
} // Compute the size based on the 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)); | ||
} // Compute the size based on the pressure and thinning. | ||
size = clamp(minSize + ip * (maxSize - minSize), minSize, maxSize); | ||
} else { | ||
size = maxSize; | ||
} // Handle line start | ||
r = (thinning > 0 ? lerp(size - size * thinning, size, clamp(_pressure, 0, 1)) : lerp(size, size + size * thinning, clamp(_pressure, 0, 1))) / 2; | ||
} // 2. | ||
// Draw a cap once we've reached the minimum length. | ||
if (!started && length > size / 2) { | ||
var _points$2 = points[0], | ||
sx = _points$2[0], | ||
sy = _points$2[1]; | ||
if (_short) { | ||
if (clen < size / 2) { | ||
continue; | ||
} // The first point after we've reached the minimum length. | ||
for (var t = 0, step = 0.25; t <= 1; t += step) { | ||
m0 = projectPoint(sx, sy, angle + TAU + t * PI, size - 1); | ||
leftPts.push(m0); | ||
m1 = projectPoint(sx, sy, angle - TAU + t * -PI, size - 1); | ||
rightPts.push(m1); | ||
} | ||
started = true; | ||
continue; | ||
} // 3. Shape | ||
_short = false; // Draw a cap at the first point angled toward the current point. | ||
var _first = points[0]; | ||
p0 = projectPoint(x, y, angle - TAU, size); // left | ||
for (var _t = 0, _step = 0.1; _t <= 1; _t += _step) { | ||
tl = projectPoint(_first, _angle + TAU + _t * PI$1, r - 1); | ||
leftPts.push(tl); | ||
} | ||
p1 = projectPoint(x, y, angle + TAU, size); // right | ||
tr = projectPoint(_first, _angle + TAU, r - 1); | ||
rightPts.push(tr); | ||
} // 3. | ||
// Add points for the current point. | ||
var delta = angleDelta(pa, angle); // Handle sharp corners differently | ||
if (i === points.length - 1 || abs(delta) > PI * 0.75 && length > size) { | ||
var _getPointBetween = getPointBetween(px, py, x, y, 0.5), | ||
mx = _getPointBetween[0], | ||
my = _getPointBetween[1]; | ||
for (var _t = 0, _step = 0.25; _t <= 1; _t += _step) { | ||
m0 = projectPoint(mx, my, pa - TAU + _t * PI, size - 1); | ||
leftPts.push(m0); | ||
m1 = projectPoint(mx, my, pa + TAU + _t * -PI, size - 1); | ||
rightPts.push(m1); | ||
if (i === len - 1) { | ||
// The last point in the line. | ||
// Add points for an end cap. | ||
for (var _t2 = 0, _step2 = 0.1; _t2 <= 1; _t2 += _step2) { | ||
tr = projectPoint([x, y], _angle + TAU - _t2 * PI$1, r - 1); | ||
rightPts.push(tr); | ||
} | ||
t0 = m0; | ||
t1 = m1; | ||
} else { | ||
// Project sideways | ||
d0 = getDistance(p0[0], p0[1], t0[0], t0[1]); | ||
// Find the delta between the current and previous angle. | ||
var delta = getAngleDelta(prev[3], _angle); | ||
if (d0 > smooth) { | ||
leftPts.push(m0); | ||
m0 = getPointBetween(t0[0], t0[1], p0[0], p0[1], 0.5); | ||
t0 = p0; | ||
} | ||
if (abs(delta) > SHARP && clen > r) { | ||
// A sharp corner. | ||
// Project points (left and right) for a cap. | ||
var mid = getPointBetween(prev, [x, y], 0.5); | ||
d1 = getDistance(p1[0], p1[1], t1[0], t1[1]); | ||
for (var _t3 = 0, _step3 = 0.25; _t3 <= 1; _t3 += _step3) { | ||
tl = projectPoint(mid, pa - TAU + _t3 * PI$1, r - 1); | ||
tr = projectPoint(mid, pa + TAU + _t3 * -PI$1, r - 1); | ||
leftPts.push(tl); | ||
rightPts.push(tr); | ||
} | ||
} else { | ||
// A regular point. | ||
// Add projected points left and right. | ||
pl = projectPoint([x, y], _angle - TAU, r); | ||
pr = projectPoint([x, y], _angle + TAU, r); // Add projected point if far enough away from last left point | ||
if (d1 > smooth) { | ||
rightPts.push(m1); | ||
m1 = getPointBetween(t1[0], t1[1], p1[0], p1[1], 0.5); | ||
t1 = p1; | ||
if (getDistance(pl, tl) > minDist) { | ||
leftPts.push(getPointBetween(tl, pl, 0.5)); | ||
tl = pl; | ||
} // Add point if far enough away from last right point | ||
if (getDistance(pr, tr) > minDist) { | ||
rightPts.push(getPointBetween(tr, pr, 0.5)); | ||
tr = pr; | ||
} | ||
} | ||
pp = _pressure; | ||
} | ||
pp = ip; | ||
} | ||
@@ -363,18 +305,14 @@ | ||
/** | ||
* ## clipPath | ||
* @description Returns a clipped polygon of the provided points. | ||
* @param points An array of points (as number[]), the output of getStrokeOutlinePoints. | ||
*/ | ||
function clipPath(points) { | ||
return polygonClipping.union([points]); | ||
} | ||
/** | ||
* ## getPath | ||
* @description Returns a pressure sensitive stroke SVG data | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
function getPath(points, options) { | ||
function getStroke(points, options) { | ||
if (options === void 0) { | ||
@@ -384,65 +322,7 @@ options = {}; | ||
if (points.length === 0) { | ||
return ''; | ||
} | ||
var _options4 = options, | ||
_options4$clip = _options4.clip, | ||
clip = _options4$clip === void 0 ? true : _options4$clip, | ||
_options4$maxSize = _options4.maxSize, | ||
maxSize = _options4$maxSize === void 0 ? 8 : _options4$maxSize; | ||
var ps = getStrokePoints(points, options), | ||
totalLength = ps[ps.length - 1][5], | ||
pts = totalLength < maxSize ? getShortStrokeOutlinePoints(ps, options) : getStrokeOutlinePoints(ps, options), | ||
d = []; // If the length is too short, just draw a dot. | ||
// If we're clipping the path, then find the polygon and add its faces. | ||
if (clip) { | ||
var poly = clipPath(pts); | ||
for (var _iterator = _createForOfIteratorHelperLoose(poly), _step2; !(_step2 = _iterator()).done;) { | ||
var face = _step2.value; | ||
for (var _iterator2 = _createForOfIteratorHelperLoose(face), _step3; !(_step3 = _iterator2()).done;) { | ||
var verts = _step3.value; | ||
var v0 = verts[0]; | ||
var v1 = verts[1]; | ||
verts.push(v0); | ||
d.push("M " + v0[0] + " " + v0[1]); | ||
for (var i = 1; i < verts.length; i++) { | ||
var _getPointBetween2 = getPointBetween(v0[0], v0[1], v1[0], v1[1], 0.5), | ||
mpx = _getPointBetween2[0], | ||
mpy = _getPointBetween2[1]; | ||
d.push(" Q " + v0[0] + "," + v0[1] + " " + mpx + "," + mpy); | ||
v0 = v1; | ||
v1 = verts[i + 1]; | ||
} | ||
} | ||
} | ||
} else { | ||
// If we're not clipping the path, just trace it. | ||
var _v = pts[0]; | ||
var _v2 = pts[1]; | ||
pts.push(_v); | ||
d.push("M " + _v[0] + " " + _v[1]); | ||
for (var _i = 1; _i < pts.length; _i++) { | ||
var _getPointBetween3 = getPointBetween(_v[0], _v[1], _v2[0], _v2[1], 0.5), | ||
_mpx = _getPointBetween3[0], | ||
_mpy = _getPointBetween3[1]; | ||
d.push("Q " + _v[0] + "," + _v[1] + " " + _mpx + "," + _mpy); | ||
_v = _v2; | ||
_v2 = pts[_i + 1]; | ||
} | ||
} | ||
d.push('Z'); | ||
return d.join(' '); | ||
return getStrokeOutlinePoints(getStrokePoints(points, options.streamline), options); | ||
} | ||
export default getPath; | ||
export { clipPath, getShortStrokeOutlinePoints, getStrokeOutlinePoints, getStrokePoints, lerpAngles }; | ||
export default getStroke; | ||
export { getStrokeOutlinePoints, getStrokePoints }; | ||
//# sourceMappingURL=perfect-freehand.esm.js.map |
{ | ||
"version": "0.2.5", | ||
"version": "0.3.0", | ||
"license": "MIT", | ||
@@ -55,5 +55,3 @@ "main": "dist/index.js", | ||
}, | ||
"dependencies": { | ||
"polygon-clipping": "^0.15.2" | ||
} | ||
"dependencies": {} | ||
} |
165
README.md
@@ -23,16 +23,15 @@ # Perfect Freehand | ||
The library exports a default function, `getPath`, that accepts an array of points and an (optional) options object and returns SVG path data for a stroke. | ||
The library exports a default function, `getStroke`, that: | ||
The array of points may be _either_ an array of number pairs representing the point's x, y, and (optionally) pressure... | ||
- accepts an array of points and an (optional) options object | ||
- returns a stroke as an array of points formatted as `[x, y]` | ||
```js | ||
import getPath from 'perfect-freehand' | ||
import getStroke from 'perfect-freehand' | ||
``` | ||
const path = getPath([ | ||
[0, 0], | ||
[10, 5], | ||
[20, 8], | ||
]) | ||
You may format your input points _either_ as an array or an object as shown below. In both cases, the pressure value is optional. | ||
const path = getPath([ | ||
```js | ||
getStroke([ | ||
[0, 0, 0], | ||
@@ -42,15 +41,5 @@ [10, 5, 0.5], | ||
]) | ||
``` | ||
...or an array of objects with `x`, `y`, and (optionally) `pressure` properties. | ||
``` | ||
getPath([ | ||
{ x: 0, y: 0 }, | ||
{ x: 10, y: 5 }, | ||
{ x: 20, y: 8 }, | ||
]) | ||
getPath([ | ||
{ x: 0, y: 0, pressure: 0, }, | ||
getStroke([ | ||
{ x: 0, y: 0, pressure: 0 }, | ||
{ x: 10, y: 5, pressure: 0.5 }, | ||
@@ -63,31 +52,61 @@ { x: 20, y: 8, pressure: 0.3 }, | ||
The options object is optional, as are its properties. | ||
The options object is optional, as are each of its properties. | ||
| Property | Type | Default | Description | | ||
| ------------------ | ------- | ------- | ---------------------------------------------------- | | ||
| `minSize` | number | 2.5 | The thinnest size of the stroke. | | ||
| `maxSize` | number | 8 | The thickest size of the stroke. | | ||
| `simulatePressure` | boolean | true | Whether to simulate pressure based on velocity. | | ||
| `pressure` | boolean | true | Whether to apply pressure. | | ||
| `streamline` | number | .5 | How much to streamline the stroke. | | ||
| `smooth` | number | .5 | How much to soften the stroke's edges. | | ||
| `clip` | boolean | true | Whether to flatten the stroke into a single polygon. | | ||
| Property | Type | Default | Description | | ||
| ------------------ | ------- | ------- | ----------------------------------------------- | | ||
| `size` | number | 8 | The base size (diameter) of the stroke. | | ||
| `thinning` | number | .5 | The effect of pressure on the stroke's size. | | ||
| `smoothing` | number | .5 | How much to soften the stroke's edges. | | ||
| `streamline` | number | .5 | How much to streamline the stroke. | | ||
| `simulatePressure` | boolean | true | Whether to simulate pressure based on velocity. | | ||
```js | ||
getPath(myPoints, { | ||
minSize: 2.5, | ||
maxSize: 8, | ||
getStroke(myPoints, { | ||
size: 8, | ||
thinning: 0.5, | ||
smoothing: 0.5, | ||
streamline: 0.5, | ||
simulatePressure: true, | ||
pressure: true, | ||
streamline: 0.5, | ||
smooth: 0.5, | ||
clip: true, | ||
}) | ||
``` | ||
## Example | ||
> **Tip:** To create a stroke that gets thinner with pressure instead of thicker, use a negative number for the `thinning` option. | ||
### Rendering | ||
While `getStroke` returns an array of points representing a stroke, it's up to you to decide how you will render the stroke. The library does not export any rendering solutions. | ||
For example, here is a function that takes in a stroke and returns SVG path data. You can use the string returned by this function in two ways. For SVG, you can pass the data into `path` element's [`d` property](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d). For HTML canvas, you can pass the string into the [`Path2D` constructor](https://developer.mozilla.org/en-US/docs/Web/API/Path2D/Path2D#using_svg_paths) and then stroke or fill the path. | ||
```js | ||
import getStroke from 'perfect-freehand' | ||
// Create SVG path data using the points from perfect-freehand. | ||
function getSvgPathFromStroke(stroke) { | ||
const d = [] | ||
let [p0, p1] = stroke | ||
d.push(`M ${p0[0]} ${p0[1]} Q`) | ||
for (let i = 1; i < stroke.length; i++) { | ||
const mpx = p0[0] + (p1[0] - p0[0]) / 2 | ||
const mpy = p0[1] + (p1[1] - p0[1]) / 2 | ||
d.push(`${p0[0]},${p0[1]} ${mpx},${mpy}`) | ||
p0 = p1 | ||
p1 = stroke[i + 1] | ||
} | ||
d.push('Z') | ||
return d.join(' ') | ||
} | ||
``` | ||
# Example | ||
```jsx | ||
import * as React from 'react' | ||
import getPath from 'perfect-freehand' | ||
import getStroke from 'perfect-freehand' | ||
import getSvgPathFromStroke from './utils' // See "Rendering" section above. | ||
@@ -98,5 +117,7 @@ export default function Example() { | ||
function handlePointerDown(e) { | ||
const point = [e.pageX, e.pageY, e.pressure] | ||
setCurrentMark({ | ||
type: e.pointerType, | ||
points: [[e.pageX, e.pageY, e.pressure]], | ||
points: [point], | ||
}) | ||
@@ -106,6 +127,8 @@ } | ||
function handlePointerMove(e) { | ||
const point = [e.pageX, e.pageY, e.pressure] | ||
if (e.buttons === 1) { | ||
setCurrentMark({ | ||
...currentMark, | ||
points: [...currentMark.points, [e.pageX, e.pageY, e.pressure]], | ||
points: [...currentMark.points, point], | ||
}) | ||
@@ -115,2 +138,10 @@ } | ||
const stroke = getStroke(currentMark.points, { | ||
size: 8, | ||
thinning: 0.5, | ||
smoothing: 0.5, | ||
streamline: 0.5, | ||
simulatePressure: currentMark.type !== 'pen', | ||
}) | ||
return ( | ||
@@ -124,9 +155,3 @@ <svg | ||
> | ||
{currentMark && ( | ||
<path | ||
d={getPath(currentMark.points, { | ||
simulatePressure: currentMark.type !== 'pen', | ||
})} | ||
/> | ||
)} | ||
{currentMark && <path d={getSvgPathFromStroke(stroke)} />} | ||
</svg> | ||
@@ -139,6 +164,8 @@ ) | ||
## Advanced Usage | ||
# Advanced Usage | ||
For advanced usage, the library also exports smaller functions that `getPath` uses to generate its SVG data. While you can use `getPath`'s data to render strokes with an HTML canvas (via the Path2D element) or with SVG paths, these new functions will allow you to create paths in other rendering technologies. | ||
## Functions | ||
For advanced usage, the library also exports smaller functions that `getStroke` uses to generate its SVG data. While you can use `getStroke`'s data to render strokes with an HTML canvas (via the Path2D element) or with SVG paths, these new functions will allow you to create paths in other rendering technologies. | ||
#### `getStrokePoints` | ||
@@ -152,8 +179,36 @@ | ||
#### `getShortStrokeOutlinePoints` | ||
## Rendering a Flattened Stroke | ||
Works like `getStrokeOutlinePoints`, but designed to work with short paths. | ||
To render a stroke as a flat polygon, add the `polygon-clipping` package and use (or refer to) the following function. | ||
#### `clipPath` | ||
```js | ||
import getStroke from 'perfect-freehand' | ||
import polygonClipping from 'polygon-clipping' | ||
Accepts a series of points (formatted as `[x, y]`, i.e. the output of `getStrokeOutlinePoints` or `getShortStrokeOutlinePoints`) and returns a polygon (a series of faces) from the stroke. | ||
function getFlatSvgPathFromStroke(stroke) { | ||
const poly = polygonClipping.union([stroke] as any) | ||
const d = [] | ||
for (let face of poly) { | ||
for (let pts of face) { | ||
let [p0, p1] = pts | ||
d.push(`M ${p0[0]} ${p0[1]} Q`) | ||
for (let i = 1; i < pts.length; i++) { | ||
const mpx = p0[0] + (p1[0] - p0[0]) / 2 | ||
const mpy = p0[1] + (p1[1] - p0[1]) / 2 | ||
d.push(`${p0[0]},${p0[1]} ${mpx},${mpy}`) | ||
p0 = p1 | ||
p1 = pts[i + 1] | ||
} | ||
d.push('Z') | ||
} | ||
} | ||
return d.join(' ') | ||
} | ||
``` |
437
src/index.ts
@@ -1,87 +0,17 @@ | ||
import polygonClipping from 'polygon-clipping' | ||
import { | ||
toPointsArray, | ||
clamp, | ||
getAngle, | ||
getAngleDelta, | ||
getDistance, | ||
getPointBetween, | ||
projectPoint, | ||
lerp, | ||
} from './utils' | ||
import { StrokeOptions } from './types' | ||
/* --------------------- Helpers -------------------- */ | ||
const { abs, hypot, cos, max, min, sin, atan2, PI } = Math, | ||
const { abs, min, PI } = Math, | ||
TAU = PI / 2, | ||
PI2 = PI * 2 | ||
SHARP = PI * 0.7 | ||
function projectPoint(x0: number, y0: number, a: number, d: number) { | ||
return [cos(a) * d + x0, sin(a) * d + y0] | ||
} | ||
function shortAngleDist(a0: number, a1: number) { | ||
var max = PI2 | ||
var da = (a1 - a0) % max | ||
return ((2 * da) % max) - da | ||
} | ||
export function lerpAngles(a0: number, a1: number, t: number) { | ||
return a0 + shortAngleDist(a0, a1) * t | ||
} | ||
function angleDelta(a0: number, a1: number) { | ||
return shortAngleDist(a0, a1) | ||
} | ||
function getPointBetween( | ||
x0: number, | ||
y0: number, | ||
x1: number, | ||
y1: number, | ||
d = 0.5 | ||
) { | ||
return [x0 + (x1 - x0) * d, y0 + (y1 - y0) * d] | ||
} | ||
function getAngle(x0: number, y0: number, x1: number, y1: number) { | ||
return atan2(y1 - y0, x1 - x0) | ||
} | ||
function getDistance(x0: number, y0: number, x1: number, y1: number) { | ||
return hypot(y1 - y0, x1 - x0) | ||
} | ||
function clamp(n: number, a: number, b: number) { | ||
return max(a, min(b, n)) | ||
} | ||
function toPointsArray< | ||
T extends number[], | ||
K extends { x: number; y: number; pressure?: number } | ||
>(points: (T | K)[]): number[][] { | ||
if (Array.isArray(points[0])) { | ||
return (points as number[][]).map(([x, y, pressure = 0.5]) => [ | ||
x, | ||
y, | ||
pressure, | ||
]) | ||
} else { | ||
return (points as { | ||
x: number | ||
y: number | ||
pressure?: number | ||
}[]).map(({ x, y, pressure = 0.5 }) => [x, y, pressure]) | ||
} | ||
} | ||
/* ---------------------- Types --------------------- */ | ||
export interface StrokePointsOptions { | ||
streamline?: number | ||
} | ||
export interface StrokeOutlineOptions extends StrokePointsOptions { | ||
simulatePressure?: boolean | ||
pressure?: boolean | ||
minSize?: number | ||
maxSize?: number | ||
smooth?: number | ||
} | ||
export interface StrokeOptions extends StrokeOutlineOptions { | ||
clip?: boolean | ||
} | ||
/* --------------------- Methods -------------------- */ | ||
/** | ||
@@ -91,3 +21,3 @@ * ## getStrokePoints | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param streamline How much to streamline the stroke. | ||
*/ | ||
@@ -97,8 +27,3 @@ export function getStrokePoints< | ||
K extends { x: number; y: number; pressure?: number } | ||
>( | ||
points: (T | K)[], | ||
options: StrokePointsOptions = {} as StrokePointsOptions | ||
): number[][] { | ||
const { streamline = 0.5 } = options | ||
>(points: (T | K)[], streamline = 0.5): number[][] { | ||
const aPoints = toPointsArray(points) | ||
@@ -109,3 +34,3 @@ | ||
angle: number, | ||
length = 0, | ||
totalLength = 0, | ||
distance = 0.01, | ||
@@ -129,20 +54,15 @@ len = aPoints.length, | ||
// Distance | ||
distance = getDistance(x, y, px, py) | ||
distance = getDistance([x, y], prev) | ||
// Angle | ||
angle = getAngle(px, py, x, y) | ||
angle = getAngle(prev, [x, y]) | ||
// If distance is very short, blend the angles | ||
if (distance < 1) angle = lerpAngles(prev[2], angle, 0.5) | ||
// Increment total length | ||
totalLength += distance | ||
length += distance | ||
prev = [x, y, angle, ip, distance, length] | ||
prev = [x, y, ip, angle, distance, totalLength] | ||
pts.push(prev) | ||
} | ||
// Assign second angle to first point | ||
if (pts.length > 1) { | ||
pts[0][2] = pts[1][2] | ||
} | ||
return pts | ||
@@ -152,40 +72,2 @@ } | ||
/** | ||
* ## getShortStrokeOutlinePoints | ||
* @description Draw an outline around a short stroke. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
*/ | ||
export function getShortStrokeOutlinePoints( | ||
points: number[][], | ||
options: StrokeOutlineOptions = {} as StrokeOutlineOptions | ||
) { | ||
const { minSize = 2.5, maxSize = 8 } = options | ||
const len = points.length | ||
// Can't draw an outline without any points | ||
if (len === 0) { | ||
return [] | ||
} | ||
const [x0, y0] = points[0], | ||
[x1, y1] = points[len - 1], | ||
p = points[len - 1][3], | ||
leftPts: number[][] = [], | ||
rightPts: number[][] = [], | ||
size = clamp( | ||
minSize + (maxSize - minSize) * (p ? p : 0.5), | ||
minSize, | ||
maxSize | ||
), | ||
angle = x0 === x1 ? 0 : getAngle(x0, y0, x1, y1) | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
leftPts.push(projectPoint(x1, y1, angle + TAU - t * PI, size - 1)) | ||
rightPts.push(projectPoint(x0, y0, angle + TAU + t * PI, size - 1)) | ||
} | ||
return leftPts.concat(rightPts.reverse()) | ||
} | ||
/** | ||
* ## getStrokeOutlinePoints | ||
@@ -195,31 +77,33 @@ * @description Get an array of points (as `[x, y]`) representing the outline of a stroke. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
export function getStrokeOutlinePoints( | ||
points: number[][], | ||
options: StrokeOutlineOptions = {} as StrokeOutlineOptions | ||
options: StrokeOptions = {} as StrokeOptions | ||
): number[][] { | ||
const { | ||
size = 8, | ||
thinning = 0.5, | ||
smoothing = 0.5, | ||
simulatePressure = true, | ||
pressure = true, | ||
minSize = 2.5, | ||
maxSize = 8, | ||
smooth = 8, | ||
} = options | ||
let len = points.length, | ||
p0 = points[0], | ||
p1 = points[0], | ||
t0 = p0, | ||
t1 = p1, | ||
m0 = p0, | ||
m1 = p0, | ||
size = 0, | ||
pp = 0.5, | ||
started = false, | ||
length = 0, | ||
leftPts: number[][] = [p0], | ||
rightPts: number[][] = [p0], | ||
d0: number, | ||
d1: number | ||
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[][] = [] | ||
let pl = points[0], // Previous left and right points | ||
pr = points[0], | ||
tl = pl, // Points to test distance from | ||
tr = pr, | ||
pp = 0, // Previous (maybe simulated) pressure | ||
r = size / 2, // The current point radius | ||
short = true // Whether the line is drawn far enough | ||
// We can't do anything with an empty array. | ||
if (len === 0) { | ||
@@ -229,77 +113,122 @@ return [] | ||
// Use the points to create an outline shape, where the width | ||
// of the shape is determined by the pressure at each point. | ||
// If the point is only one point long, draw two caps at either end. | ||
if (len === 1 || totalLength < size / 2) { | ||
let first = points[0], | ||
last = points[len - 1], | ||
angle = getAngle(first, last) | ||
if (thinning) { | ||
const pressure = last[3] ? clamp(last[3], 0, 1) : 0.5 | ||
r = | ||
(thinning > 0 | ||
? lerp(size - size * thinning, size, clamp(pressure, 0, 1)) | ||
: lerp(size, size + size * thinning, clamp(pressure, 0, 1))) / 2 | ||
} | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
tl = projectPoint(first, angle + PI + TAU - t * PI, r - 1) | ||
tr = projectPoint(last, angle + TAU - t * PI, r - 1) | ||
leftPts.push(tl) | ||
rightPts.push(tr) | ||
} | ||
return leftPts.concat(rightPts) | ||
} | ||
// For a point with more than one point, create an outline shape. | ||
for (let i = 1; i < len; i++) { | ||
const [px, py, pa] = points[i - 1] | ||
let [x, y, angle, ip, distance, clen] = points[i] | ||
const prev = points[i - 1], | ||
pa = prev[3] | ||
length += clen | ||
let [x, y, pressure, angle, distance, clen] = points[i] | ||
// Size | ||
if (pressure) { | ||
// 1. | ||
// Calculate the size of the current point. | ||
if (thinning) { | ||
if (simulatePressure) { | ||
// Simulate pressure by accellerating the reported pressure. | ||
const rp = min(1 - distance / maxSize, 1) | ||
const sp = min(distance / maxSize, 1) | ||
ip = min(1, pp + (rp - pp) * (sp / 2)) | ||
const rp = min(1 - distance / size, 1) | ||
const sp = min(distance / size, 1) | ||
pressure = min(1, pp + (rp - pp) * (sp / 2)) | ||
} | ||
// Compute the size based on the pressure. | ||
size = clamp(minSize + ip * (maxSize - minSize), minSize, maxSize) | ||
} else { | ||
size = maxSize | ||
// Compute the size based on the pressure and thinning. | ||
r = | ||
(thinning > 0 | ||
? lerp(size - size * thinning, size, clamp(pressure, 0, 1)) | ||
: lerp(size, size + size * thinning, clamp(pressure, 0, 1))) / 2 | ||
} | ||
// Handle line start | ||
if (!started && length > size / 2) { | ||
const [sx, sy] = points[0] | ||
// 2. | ||
// Draw a cap once we've reached the minimum length. | ||
if (short) { | ||
if (clen < size / 2) { | ||
continue | ||
} | ||
for (let t = 0, step = 0.25; t <= 1; t += step) { | ||
m0 = projectPoint(sx, sy, angle + TAU + t * PI, size - 1) | ||
leftPts.push(m0) | ||
// The first point after we've reached the minimum length. | ||
short = false | ||
m1 = projectPoint(sx, sy, angle - TAU + t * -PI, size - 1) | ||
rightPts.push(m1) | ||
// Draw a cap at the first point angled toward the current point. | ||
const first = points[0] | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
tl = projectPoint(first, angle + TAU + t * PI, r - 1) | ||
leftPts.push(tl) | ||
} | ||
started = true | ||
continue | ||
tr = projectPoint(first, angle + TAU, r - 1) | ||
rightPts.push(tr) | ||
} | ||
// 3. Shape | ||
p0 = projectPoint(x, y, angle - TAU, size) // left | ||
p1 = projectPoint(x, y, angle + TAU, size) // right | ||
// 3. | ||
// Add points for the current point. | ||
if (i === len - 1) { | ||
// The last point in the line. | ||
const delta = angleDelta(pa, angle) | ||
// Add points for an end cap. | ||
for (let t = 0, step = 0.1; t <= 1; t += step) { | ||
tr = projectPoint([x, y], angle + TAU - t * PI, r - 1) | ||
rightPts.push(tr) | ||
} | ||
} else { | ||
// Find the delta between the current and previous angle. | ||
const delta = getAngleDelta(prev[3], angle) | ||
// Handle sharp corners differently | ||
if (i === points.length - 1 || (abs(delta) > PI * 0.75 && length > size)) { | ||
const [mx, my] = getPointBetween(px, py, x, y, 0.5) | ||
if (abs(delta) > SHARP && clen > r) { | ||
// A sharp corner. | ||
for (let t = 0, step = 0.25; t <= 1; t += step) { | ||
m0 = projectPoint(mx, my, pa - TAU + t * PI, size - 1) | ||
leftPts.push(m0) | ||
// Project points (left and right) for a cap. | ||
const mid = getPointBetween(prev, [x, y], 0.5) | ||
m1 = projectPoint(mx, my, pa + TAU + t * -PI, size - 1) | ||
rightPts.push(m1) | ||
for (let t = 0, step = 0.25; t <= 1; t += step) { | ||
tl = projectPoint(mid, pa - TAU + t * PI, r - 1) | ||
tr = projectPoint(mid, pa + TAU + t * -PI, r - 1) | ||
leftPts.push(tl) | ||
rightPts.push(tr) | ||
} | ||
} else { | ||
// A regular point. | ||
// Add projected points left and right. | ||
pl = projectPoint([x, y], angle - TAU, r) | ||
pr = projectPoint([x, y], angle + TAU, r) | ||
// Add projected point if far enough away from last left point | ||
if (getDistance(pl, tl) > minDist) { | ||
leftPts.push(getPointBetween(tl, pl, 0.5)) | ||
tl = pl | ||
} | ||
// Add point if far enough away from last right point | ||
if (getDistance(pr, tr) > minDist) { | ||
rightPts.push(getPointBetween(tr, pr, 0.5)) | ||
tr = pr | ||
} | ||
} | ||
t0 = m0 | ||
t1 = m1 | ||
} else { | ||
// Project sideways | ||
d0 = getDistance(p0[0], p0[1], t0[0], t0[1]) | ||
if (d0 > smooth) { | ||
leftPts.push(m0) | ||
m0 = getPointBetween(t0[0], t0[1], p0[0], p0[1], 0.5) | ||
t0 = p0 | ||
} | ||
d1 = getDistance(p1[0], p1[1], t1[0], t1[1]) | ||
if (d1 > smooth) { | ||
rightPts.push(m1) | ||
m1 = getPointBetween(t1[0], t1[1], p1[0], p1[1], 0.5) | ||
t1 = p1 | ||
} | ||
pp = pressure | ||
} | ||
pp = ip | ||
} | ||
@@ -311,72 +240,22 @@ | ||
/** | ||
* ## clipPath | ||
* @description Returns a clipped polygon of the provided points. | ||
* @param points An array of points (as number[]), the output of getStrokeOutlinePoints. | ||
*/ | ||
export function clipPath(points: number[][]) { | ||
return polygonClipping.union([points] as any) | ||
} | ||
/** | ||
* ## getPath | ||
* @description Returns a pressure sensitive stroke SVG data | ||
* ## getStroke | ||
* @description Returns a stroke as an array of points. | ||
* @param points An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is optional. | ||
* @param options An (optional) object with options. | ||
* @param options.size The base size (diameter) of the stroke. | ||
* @param options.thinning The effect of pressure on the stroke's size. | ||
* @param options.smoothing How much to soften the stroke's edges. | ||
* @param options.streamline How much to streamline the stroke. | ||
* @param options.simulatePressure Whether to simulate pressure based on velocity. | ||
*/ | ||
export default function getPath< | ||
export default function getStroke< | ||
T extends number[], | ||
K extends { x: number; y: number; pressure?: number } | ||
>(points: (T | K)[], options: StrokeOptions = {} as StrokeOptions): string { | ||
if (points.length === 0) { | ||
return '' | ||
} | ||
>(points: (T | K)[], options: StrokeOptions = {} as StrokeOptions): number[][] { | ||
return getStrokeOutlinePoints( | ||
getStrokePoints(points, options.streamline), | ||
options | ||
) | ||
} | ||
const { clip = true, maxSize = 8 } = options | ||
let ps = getStrokePoints(points, options), | ||
totalLength = ps[ps.length - 1][5], | ||
pts = | ||
totalLength < maxSize | ||
? getShortStrokeOutlinePoints(ps, options) | ||
: getStrokeOutlinePoints(ps, options), | ||
d: string[] = [] | ||
// If the length is too short, just draw a dot. | ||
// If we're clipping the path, then find the polygon and add its faces. | ||
if (clip) { | ||
const poly = clipPath(pts) | ||
for (let face of poly) { | ||
for (let verts of face) { | ||
let v0 = verts[0] | ||
let v1 = verts[1] | ||
verts.push(v0) | ||
d.push(`M ${v0[0]} ${v0[1]}`) | ||
for (let i = 1; i < verts.length; i++) { | ||
const [mpx, mpy] = getPointBetween(v0[0], v0[1], v1[0], v1[1], 0.5) | ||
d.push(` Q ${v0[0]},${v0[1]} ${mpx},${mpy}`) | ||
v0 = v1 | ||
v1 = verts[i + 1] | ||
} | ||
} | ||
} | ||
} else { | ||
// If we're not clipping the path, just trace it. | ||
let v0 = pts[0] | ||
let v1 = pts[1] | ||
pts.push(v0) | ||
d.push(`M ${v0[0]} ${v0[1]}`) | ||
for (let i = 1; i < pts.length; i++) { | ||
const [mpx, mpy] = getPointBetween(v0[0], v0[1], v1[0], v1[1], 0.5) | ||
d.push(`Q ${v0[0]},${v0[1]} ${mpx},${mpy}`) | ||
v0 = v1 | ||
v1 = pts[i + 1] | ||
} | ||
} | ||
d.push('Z') | ||
return d.join(' ') | ||
} | ||
export { StrokeOptions } |
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
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
0
17
2
206
95036
908
1
- Removedpolygon-clipping@^0.15.2
- Removedpolygon-clipping@0.15.7(transitive)
- Removedrobust-predicates@3.0.2(transitive)
- Removedsplaytree@3.1.2(transitive)