feat: Pressure smoothing causal
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
This commit is contained in:
@@ -468,6 +468,15 @@ const drawTaperedCapsule = (
|
|||||||
*/
|
*/
|
||||||
const BEZIER_SUBDIVIDE_TARGET_SPACING = 3;
|
const BEZIER_SUBDIVIDE_TARGET_SPACING = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Half-width (in samples) of the triangular smoothing kernel applied to raw
|
||||||
|
* pressure values before computing stroke radii. A radius of R means each
|
||||||
|
* pressure sample is averaged with R neighbours on each side, weighted
|
||||||
|
* linearly so the centre sample has weight R+1 and the outermost weight 1.
|
||||||
|
* Larger values produce a smoother, more uniform stroke width.
|
||||||
|
*/
|
||||||
|
const PRESSURE_SMOOTHING_RADIUS = 6;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Catmull-Rom tangent vector at points[i], using the neighbouring
|
* Returns the Catmull-Rom tangent vector at points[i], using the neighbouring
|
||||||
* points for a uniform parameterisation. At the first point a one-sided
|
* points for a uniform parameterisation. At the first point a one-sided
|
||||||
@@ -621,12 +630,33 @@ const drawFreeDrawSegments = (
|
|||||||
|
|
||||||
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
const baseRadius = (element.strokeWidth * 1.25) / 2;
|
||||||
|
|
||||||
const getPressure = (i: number): number =>
|
// Causal (one-sided) triangular-kernel weighted average of past pressure
|
||||||
pressures.length > i ? pressures[i] : DEFAULT_FREEDRAW_PRESSURE;
|
// samples. Only looks backward [i-R .. i], so a newly-arrived point never
|
||||||
|
// retroactively changes the smoothed pressure of any previously rendered
|
||||||
|
// segment. This ensures live and final renders are identical at all points.
|
||||||
|
const getSmoothedPressure = (i: number): number => {
|
||||||
|
if (pressures.length === 0) {
|
||||||
|
return DEFAULT_FREEDRAW_PRESSURE;
|
||||||
|
}
|
||||||
|
let sum = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (let k = -PRESSURE_SMOOTHING_RADIUS; k <= 0; k++) {
|
||||||
|
const idx = i + k;
|
||||||
|
if (idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const p =
|
||||||
|
idx < pressures.length ? pressures[idx] : DEFAULT_FREEDRAW_PRESSURE;
|
||||||
|
const w = PRESSURE_SMOOTHING_RADIUS + 1 + k; // 1 at i-R, R+1 at i
|
||||||
|
sum += p * w;
|
||||||
|
totalWeight += w;
|
||||||
|
}
|
||||||
|
return totalWeight > 0 ? sum / totalWeight : DEFAULT_FREEDRAW_PRESSURE;
|
||||||
|
};
|
||||||
|
|
||||||
if (fromIndex === 0 && N === 1) {
|
if (fromIndex === 0 && N === 1) {
|
||||||
// Single-point stroke → filled circle (dot)
|
// Single-point stroke → filled circle (dot)
|
||||||
const r = baseRadius * getPressure(0) * 2;
|
const r = baseRadius * getSmoothedPressure(0) * 2;
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2);
|
context.arc(points[0][0], points[0][1], r, 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
@@ -645,8 +675,8 @@ const drawFreeDrawSegments = (
|
|||||||
const p1 = points[i];
|
const p1 = points[i];
|
||||||
// Very first pressure values are often unreliable,
|
// Very first pressure values are often unreliable,
|
||||||
// so for the first couple of segments use a radius
|
// so for the first couple of segments use a radius
|
||||||
const r0 = i <= 2 ? 0 : baseRadius * getPressure(i - 1) * 2;
|
const r0 = baseRadius * getSmoothedPressure(i - 1) * 2;
|
||||||
const r1 = i < 2 ? 0 : baseRadius * getPressure(i) * 2;
|
const r1 = baseRadius * getSmoothedPressure(i) * 2;
|
||||||
|
|
||||||
// Catmull-Rom tangents. At the last real point, use predictedPoint as
|
// Catmull-Rom tangents. At the last real point, use predictedPoint as
|
||||||
// the look-ahead so the tip curves smoothly toward the expected position.
|
// the look-ahead so the tip curves smoothly toward the expected position.
|
||||||
@@ -678,7 +708,7 @@ const drawFreeDrawSegments = (
|
|||||||
// the next real pointer event arrives.
|
// the next real pointer event arrives.
|
||||||
if (predictedPoint !== undefined && N >= 1) {
|
if (predictedPoint !== undefined && N >= 1) {
|
||||||
const lastPt = points[N - 1];
|
const lastPt = points[N - 1];
|
||||||
const r0 = N <= 2 ? 0 : baseRadius * getPressure(N - 1) * 2;
|
const r0 = baseRadius * getSmoothedPressure(N - 1) * 2;
|
||||||
|
|
||||||
// Tangent at the last real point (with predicted look-ahead)
|
// Tangent at the last real point (with predicted look-ahead)
|
||||||
const t0 = getCatmullRomTangent(points, N - 1, predictedPoint);
|
const t0 = getCatmullRomTangent(points, N - 1, predictedPoint);
|
||||||
@@ -1072,11 +1102,10 @@ const generateOrUpdateFreeDrawIncrementalCanvas = (
|
|||||||
// Fetch predicted point for this element (set by the pointer-move handler).
|
// Fetch predicted point for this element (set by the pointer-move handler).
|
||||||
const predictedPoint = freedrawPredictedPoints.get(element.id);
|
const predictedPoint = freedrawPredictedPoints.get(element.id);
|
||||||
|
|
||||||
// When Catmull-Rom tangents are in use the tangent at the last committed
|
// Roll back 2 points so the Catmull-Rom look-ahead tangent at the last
|
||||||
// point changes each time a new real point arrives (the new point becomes
|
// committed segment is redrawn correctly when a new point arrives.
|
||||||
// the look-ahead for the previous segment). Roll back two points so that
|
// Pressure smoothing is causal (one-sided) so it requires no extra rollback.
|
||||||
// the revised last-committed segment is always redrawn with the correct
|
// Overpainting with an opaque fill is harmless.
|
||||||
// tangent. Overpainting with an opaque fill is harmless.
|
|
||||||
const drawFrom = Math.max(0, fromIndex - 2);
|
const drawFrom = Math.max(0, fromIndex - 2);
|
||||||
|
|
||||||
// Draw new (and possibly revised) segments plus the ghost toward the
|
// Draw new (and possibly revised) segments plus the ghost toward the
|
||||||
|
|||||||
Reference in New Issue
Block a user