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.
This commit is contained in:
2026-09-23 07:33:44 +07:00
parent 500068e63e
commit 3a09674831
+14 -1
View File
@@ -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