UNPKG

tldraw

Version:

A tiny little drawing editor.

188 lines (158 loc) • 5.15 kB
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 }