Tldraw/packages/tldraw/src/lib/shapes/shared/freehand/getStrokePoints.ts

188 wiersze
5.0 KiB
TypeScript

import { Vec, VecLike } from '@tldraw/editor'
import type { StrokeOptions, StrokePoint } from './types'
const MIN_START_PRESSURE = 0.025
const MIN_END_PRESSURE = 0.01
/**
* ## getStrokePoints
*
* Get an array of points as objects with an adjusted point, pressure, vector, distance, and
* runningLength.
*
* @param points - An array of points (as `[x, y, pressure]` or `{x, y, pressure}`). Pressure is
* optional in both cases.
* @param options - An object with options.
* @public
*/
export function getStrokePoints(
rawInputPoints: VecLike[],
options: StrokeOptions = {}
): StrokePoint[] {
const { streamline = 0.5, size = 16, simulatePressure = false } = options
// If we don't have any points, return an empty array.
if (rawInputPoints.length === 0) return []
// Find the interpolation level between points.
const t = 0.15 + (1 - streamline) * 0.85
// Whatever the input is, make sure that the points are in number[][].
let pts = rawInputPoints.map(Vec.From)
let pointsRemovedFromNearEnd = 0
if (!simulatePressure) {
// Strip low pressure points from the start of the array.
let pt = pts[0]
while (pt) {
if (pt.z >= MIN_START_PRESSURE) break
pts.shift()
pt = pts[0]
}
}
if (!simulatePressure) {
// Strip low pressure points from the end of the array.
let pt = pts[pts.length - 1]
while (pt) {
if (pt.z >= MIN_END_PRESSURE) break
pts.pop()
pt = pts[pts.length - 1]
}
}
if (pts.length === 0)
return [
{
point: Vec.From(rawInputPoints[0]),
input: Vec.From(rawInputPoints[0]),
pressure: simulatePressure ? 0.5 : 0.15,
vector: new Vec(1, 1),
distance: 0,
runningLength: 0,
radius: 1,
},
]
// Strip points that are too close to the first point.
let pt = pts[1]
while (pt) {
if (Vec.Dist2(pt, pts[0]) > (size / 3) ** 2) break
pts[0].z = Math.max(pts[0].z, pt.z) // Use maximum pressure
pts.splice(1, 1)
pt = pts[1]
}
// Strip points that are too close to the last point.
const last = pts.pop()!
pt = pts[pts.length - 1]
while (pt) {
if (Vec.Dist2(pt, last) > (size / 3) ** 2) break
pts.pop()
pt = pts[pts.length - 1]
pointsRemovedFromNearEnd++
}
pts.push(last)
const isComplete =
options.last ||
!options.simulatePressure ||
(pts.length > 1 && Vec.Dist2(pts[pts.length - 1], pts[pts.length - 2]) < size ** 2) ||
pointsRemovedFromNearEnd > 0
// Add extra points between the two, to help avoid "dash" lines
// for strokes with tapered start and ends. Don't mutate the
// input array!
if (pts.length === 2 && options.simulatePressure) {
const last = pts[1]
pts = pts.slice(0, -1)
for (let i = 1; i < 5; i++) {
const next = Vec.Lrp(pts[0], last, i / 4)
next.z = ((pts[0].z + (last.z - pts[0].z)) * i) / 4
pts.push(next)
}
}
// The strokePoints array will hold the points for the stroke.
// Start it out with the first point, which needs no adjustment.
const strokePoints: StrokePoint[] = [
{
point: pts[0],
input: pts[0],
pressure: simulatePressure ? 0.5 : pts[0].z,
vector: new Vec(1, 1),
distance: 0,
runningLength: 0,
radius: 1,
},
]
// We use the totalLength to keep track of the total distance
let totalLength = 0
// We're set this to the latest point, so we can use it to calculate
// the distance and vector of the next point.
let prev = strokePoints[0]
// Iterate through all of the points, creating StrokePoints.
let point: Vec, distance: number
if (isComplete && streamline > 0) {
pts.push(pts[pts.length - 1].clone())
}
for (let i = 1, n = pts.length; i < n; i++) {
point =
!t || (options.last && i === n - 1) ? pts[i].clone() : pts[i].clone().lrp(prev.point, 1 - t)
// If the new point is the same as the previous point, skip ahead.
if (prev.point.equals(point)) continue
// How far is the new point from the previous point?
distance = Vec.Dist(point, prev.point)
// Add this distance to the total "running length" of the line.
totalLength += distance
// At the start of the line, we wait until the new point is a
// certain distance away from the original point, to avoid noise
if (i < 4 && totalLength < size) {
continue
}
// Create a new strokepoint (it will be the new "previous" one).
prev = {
input: pts[i],
// The adjusted point
point,
// The input pressure (or .5 if not specified)
pressure: simulatePressure ? 0.5 : pts[i].z,
// The vector from the current point to the previous point
vector: Vec.Sub(prev.point, point).uni(),
// The distance between the current point and the previous point
distance,
// The total distance so far
runningLength: totalLength,
// The stroke point's radius
radius: 1,
}
// Push it to the strokePoints array.
strokePoints.push(prev)
}
// Set the vector of the first point to be the same as the second point.
if (strokePoints[1]?.vector) {
strokePoints[0].vector = strokePoints[1].vector.clone()
}
if (totalLength < 1) {
const maxPressureAmongPoints = Math.max(0.5, ...strokePoints.map((s) => s.pressure))
strokePoints.forEach((s) => (s.pressure = maxPressureAmongPoints))
}
return strokePoints
}