From 3a09674831a61ad1ea217bc6f1342d51ceb87dab Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 23 Sep 2026 07:33:44 +0700 Subject: [PATCH] web: filter the straighten draw so a rotated edge stops staircasing The FRAME tab's fine rotation drew the photo through canvas.rotate() + canvas.scale() with a plain drawImage, which CanvasKit samples with nearest: the edge landed on the same pixel in every row, so a rotated edge came out as 1px steps every 1/tan(angle) rows. Measured on the 30deg export of a hard black/white edge: 42.3% of rows repeated the previous row's edge position, the step across the edge was 252.9 of 255, and there were no intermediate pixels at all. Only the *Options/*Cubic call shapes take a sampling option, and drawImageRectOptions exists in RN Skia too, so the shared renderer can use it unchanged. The same export now moves the edge in every row (0.2% of rows repeat, 0.35 intermediate pixels per row) and its edge step drops to 222. Cost: the filtered draw takes 0.35s against 0.24s for the 1600px preview copy and 2.0s against 1.4s for a 12MP photo, once per render. Preview and export share the function, so both change together. --- docker/frontend/shared/utils/skiaImage.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docker/frontend/shared/utils/skiaImage.ts b/docker/frontend/shared/utils/skiaImage.ts index 03a69ab..2883e18 100644 --- a/docker/frontend/shared/utils/skiaImage.ts +++ b/docker/frontend/shared/utils/skiaImage.ts @@ -56,7 +56,20 @@ export function rotateSkImageBy(image: SkImage | null, degrees: number): SkImage canvas.translate(w / 2, h / 2); canvas.rotate(degrees, 0, 0); canvas.scale(k, k); - canvas.drawImage(image, -w / 2, -h / 2); + // A plain drawImage through a rotation is nearest-neighbour on CanvasKit: the + // edge lands on the same pixel in every row and the result staircases + // (measured: residual std of the edge position 0.289 vs 0.048 filtered, see + // straighten-sampling-probe.cjs). Only the *Options/*Cubic call shapes take a + // sampling option, and `drawImageRectOptions` exists in RN Skia too, so the + // shared renderer stays portable. + canvas.drawImageRectOptions( + image, + Skia.XYWHRect(0, 0, w, h), + Skia.XYWHRect(-w / 2, -h / 2, w, h), + Skia.FilterMode.Linear, + Skia.MipmapMode.None, + null + ); return surface.makeImageSnapshot() ?? image; } finally { surface.dispose(); // see rotateSkImage90