Files
AFFiNE-Mirror/blocksuite/framework/global/src/gfx/perfect-freehand/get-stroke-points.ts
T

133 lines
4.9 KiB
TypeScript

import type { IVec, IVec3 } from '../model/index.js';
import type { StrokeOptions, StrokePoint } from './types.js';
import { add, dist, isEqual, lrp, sub, uni } from './vec.js';
/**
* ## getStrokePoints
* @description 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 (optional) An 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.easing An easing function to apply to each point's pressure.
* @param options.simulatePressure Whether to simulate pressure based on velocity.
* @param options.start Cap, taper and easing for the start of the line.
* @param options.end Cap, taper and easing for the end of the line.
* @param options.last Whether to handle the points as a completed stroke.
*/
export function getStrokePoints<
T extends IVec | IVec3,
K extends { x: number; y: number; pressure?: number },
>(points: (T | K)[], options = {} as StrokeOptions): StrokePoint[] {
const { streamline = 0.5, size = 16, last: isComplete = false } = options;
// If we don't have any points, return an empty array.
if (points.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: (IVec3 | IVec)[] = Array.isArray(points[0])
? (points as T[])
: (points as K[]).map(
({ x, y, pressure = 0.5 }) => [x, y, pressure] as IVec3
);
// 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) {
const last = pts[1];
pts = pts.slice(0, -1);
for (let i = 1; i < 5; i++) {
pts.push(lrp(pts[0] as IVec, last as IVec, i / 4));
}
}
// If there's only one point, add another point at a 1pt offset.
// Don't mutate the input array!
if (pts.length === 1) {
pts = [
...pts,
[...add(pts[0] as IVec, [1, 1]), ...pts[0].slice(2)] as IVec,
];
}
// 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][0], pts[0][1]],
pressure: (pts[0][2] ?? -1) >= 0 ? pts[0][2]! : 0.25,
vector: [1, 1],
distance: 0,
runningLength: 0,
},
];
// A flag to see whether we've already reached out minimum length
let hasReachedMinimumLength = false;
// We use the runningLength to keep track of the total distance
let runningLength = 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];
const max = pts.length - 1;
// Iterate through all of the points, creating StrokePoints.
for (let i = 1; i < pts.length; i++) {
const point =
isComplete && i === max
? // If we're at the last point, and `options.last` is true,
// then add the actual input point.
(pts[i].slice(0, 2) as IVec)
: // Otherwise, using the t calculated from the streamline
// option, interpolate a new point between the previous
// point the current point.
lrp(prev.point, pts[i] as IVec, t);
// If the new point is the same as the previous point, skip ahead.
if (isEqual(prev.point, point)) continue;
// How far is the new point from the previous point?
const distance = dist(point, prev.point);
// Add this distance to the total "running length" of the line.
runningLength += 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 < max && !hasReachedMinimumLength) {
if (runningLength < size) continue;
hasReachedMinimumLength = true;
// TODO: Backfill the missing points so that tapering works correctly.
}
// Create a new strokepoint (it will be the new "previous" one).
prev = {
// The adjusted point
point,
// The input pressure (or .5 if not specified)
pressure: (pts[i][2] ?? -1) >= 0 ? pts[i][2]! : 0.5,
// The vector from the current point to the previous point
vector: uni(sub(prev.point, point)),
// The distance between the current point and the previous point
distance,
// The total distance so far
runningLength,
};
// Push it to the strokePoints array.
strokePoints.push(prev);
}
// Set the vector of the first point to be the same as the second point.
strokePoints[0].vector = strokePoints[1]?.vector || [0, 0];
return strokePoints;
}