diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index 43a42bf083..f84bba09c7 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -266,7 +266,7 @@ const generateElementCanvas = ( const rc = rough.canvas(canvas); - drawElementOnCanvas(element, rc, context, renderConfig); + drawElementOnCanvas(element, rc, context, renderConfig, scale); context.restore(); @@ -402,6 +402,7 @@ const drawElementOnCanvas = ( rc: RoughCanvas, context: CanvasRenderingContext2D, renderConfig: StaticCanvasRenderConfig, + scale = 1, ) => { switch (element.type) { case "rectangle": @@ -429,7 +430,7 @@ const drawElementOnCanvas = ( } case "freedraw": { context.save(); - drawFreeDrawSegments(element, context, renderConfig, 0); + drawFreeDrawSegments(element, context, renderConfig, 0, undefined, scale); context.restore(); break; } diff --git a/packages/element/src/renderFreedraw.ts b/packages/element/src/renderFreedraw.ts index c6a1f0950a..caebdabd8c 100644 --- a/packages/element/src/renderFreedraw.ts +++ b/packages/element/src/renderFreedraw.ts @@ -161,11 +161,13 @@ const drawSubdividedSegment = ( t0y: number, t1x: number, t1y: number, + scale: number, ) => { const segLen = Math.sqrt((p1x - p0x) ** 2 + (p1y - p0y) ** 2); + // Target spacing is in screen pixels; divide by scale to get scene units. const nSubdiv = Math.max( 1, - Math.ceil(segLen / BEZIER_SUBDIVIDE_TARGET_SPACING), + Math.ceil((segLen * scale) / BEZIER_SUBDIVIDE_TARGET_SPACING), ); // Cubic Bezier control points derived from Catmull-Rom tangents. @@ -220,6 +222,7 @@ export const drawFreeDrawSegments = ( renderConfig: StaticCanvasRenderConfig, fromIndex: number, upToIndex?: number, + scale = 1, ) => { const { points, pressures } = element; const N = points.length; @@ -296,6 +299,7 @@ export const drawFreeDrawSegments = ( t0[1], t1[0], t1[1], + scale, ); } }; @@ -546,6 +550,7 @@ export const generateOrUpdateFreeDrawIncrementalCanvas = ( renderConfig, committedFromIndex, newCommittedCount, // upToIndex - stop before the last provisional segment + canvasScale, ); }); inc.committedPointCount = newCommittedCount; @@ -567,6 +572,7 @@ export const generateOrUpdateFreeDrawIncrementalCanvas = ( renderConfig, inc.committedPointCount - 1, undefined, // draw to natural end (the tip segment) + canvasScale, ); }); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index a4de75a4f0..ca17459074 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -9495,6 +9495,15 @@ class App extends React.Component { let prevRawY: number | null = null; let prevTs: number | null = null; // previous event timestamp (ms) + // Pen pressure warmup: the first few samples from a pen digitizer are + // unreliable (often spike to ~0.5). We apply an EMA whose alpha ramps + // from PEN_PRESSURE_INITIAL_ALPHA up to 1 over PEN_PRESSURE_WARMUP_SAMPLES + // so the stroke gradually eases from heavy smoothing into raw pressure. + let penPressureEma: number | null = null; + let penPressureSampleCount = 0; + const PEN_PRESSURE_WARMUP_SAMPLES = 20; + const PEN_PRESSURE_INITIAL_ALPHA = 0.1; + // Tuning constants: // MIN_CUTOFF – cutoff frequency (Hz) when speed ≈ 0; smaller = more smoothing // BETA – rate at which cutoff grows with speed; larger = quicker adaptation @@ -10236,7 +10245,11 @@ class App extends React.Component { emaDy = alphaD * rawDyDt + (1 - alphaD) * emaDy; // 2. Adaptive cutoff: higher speed → higher cutoff → less lag. - const speed = Math.sqrt(emaDx * emaDx + emaDy * emaDy); + // Normalize to screen-space pixels/sec so BETA behaves the same + // regardless of zoom level (scene units are 1/zoom px at high zoom). + const speed = + Math.sqrt(emaDx * emaDx + emaDy * emaDy) * + this.state.zoom.value; const cutoff = MIN_CUTOFF + BETA * speed; const alphaP = emaAlpha(cutoff, dt); @@ -10257,7 +10270,27 @@ class App extends React.Component { if (!lastPoint || lastPoint[0] !== dx || lastPoint[1] !== dy) { const pt = pointFrom(dx, dy); newPoints.push(pt); - newPressures.push(ev.pressure); + + let pressure = ev.pressure; + if (event.pointerType === "pen") { + // Gradually ease from aggressive smoothing into raw pressure to + // suppress the unreliable spike at the start of a pen stroke. + const progress = Math.min( + 1, + penPressureSampleCount / PEN_PRESSURE_WARMUP_SAMPLES, + ); + const alpha = + PEN_PRESSURE_INITIAL_ALPHA + + progress * (1 - PEN_PRESSURE_INITIAL_ALPHA); + penPressureEma = + penPressureEma === null + ? pressure + : alpha * pressure + (1 - alpha) * penPressureEma; + pressure = penPressureEma; + penPressureSampleCount++; + } + + newPressures.push(pressure); lastPoint = pt; } }