Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified packages/skia/src/__tests__/snapshots/demos/product2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/skia/src/__tests__/snapshots/drawings/skew-transform.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions packages/skia/src/skia/__tests__/Skew.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { mapPoint3d, processTransform3d, toMatrix3 } from "../types/Matrix4";

const ANGLE = Math.PI / 6;
const TAN = Math.tan(ANGLE);

describe("Skew transforms", () => {
it("skewX offsets x in proportion to y", () => {
const m = processTransform3d([{ skewX: ANGLE }]);
// A point on the y axis slides horizontally by tan(angle) * y.
expect(mapPoint3d(m, [0, 100, 0])).toEqual([100 * TAN, 100, 0]);
// A point on the x axis is left where it is.
expect(mapPoint3d(m, [100, 0, 0])).toEqual([100, 0, 0]);
});

it("skewY offsets y in proportion to x", () => {
const m = processTransform3d([{ skewY: ANGLE }]);
expect(mapPoint3d(m, [100, 0, 0])).toEqual([100, 100 * TAN, 0]);
expect(mapPoint3d(m, [0, 100, 0])).toEqual([0, 100, 0]);
});

it("maps skewX and skewY onto the matching SkMatrix slots", () => {
// SkMatrix is [scaleX, skewX, transX, skewY, scaleY, transY, ...]
const [, skewX, , skewY] = toMatrix3(
processTransform3d([{ skewX: ANGLE }])
);
expect(skewX).toBeCloseTo(TAN);
expect(skewY).toBe(0);
const [, skewX2, , skewY2] = toMatrix3(
processTransform3d([{ skewY: ANGLE }])
);
expect(skewX2).toBe(0);
expect(skewY2).toBeCloseTo(TAN);
});
});
8 changes: 6 additions & 2 deletions packages/skia/src/skia/types/Matrix4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,16 @@ export const multiply4 = (a: Matrix4, b: Matrix4): Matrix4 => {
return result as unknown as Matrix4;
};

const skewY = (angle: number): Matrix4 => {
// These matrices are row-major, so the shear factor of skewX - which offsets x
// in proportion to y - belongs in row 0, column 1, and skewY's in row 1,
// column 0. React Native's MatrixMath stores the same transforms column-major,
// and porting its indices verbatim is what swapped the two axes here.
const skewX = (angle: number): Matrix4 => {
"worklet";
return [1, Math.tan(angle), 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
};

const skewX = (angle: number): Matrix4 => {
const skewY = (angle: number): Matrix4 => {
"worklet";
return [1, 0, 0, 0, Math.tan(angle), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
};
Expand Down
2 changes: 1 addition & 1 deletion packages/skia/src/sksg/Recorder/commands/Shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ const declareImageShader = (ctx: DrawingContext, props: ImageShaderProps) => {
);
} else {
shader = ctx.track(
image.makeShaderCubic(
image.makeShaderOptions(
TileMode[enumKey(tx)],
TileMode[enumKey(ty)],
sampling?.filter ?? FilterMode.Linear,
Expand Down
75 changes: 75 additions & 0 deletions packages/skia/src/sksg/__tests__/ImageShaderSampling.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from "react";

import { importSkia } from "../../renderer/__tests__/setup";
import { FilterMode, MipmapMode } from "../../skia/types";
import type { SamplingOptions, Skia, SkImage } from "../../skia/types";
import { SkiaSGRoot } from "../Reconciler";

const SIZE = 256;
const RECT = { x: 0, y: 0, width: SIZE, height: SIZE };

// A 2x2 checkerboard blown up to 256x256: every filter mode produces a
// distinctive result at the seam between two texels.
const makeCheckerboard = (Skia: Skia) => {
const surface = Skia.Surface.Make(2, 2)!;
const canvas = surface.getCanvas();
const black = Skia.Paint();
black.setColor(Skia.Color("black"));
const white = Skia.Paint();
white.setColor(Skia.Color("white"));
canvas.drawRect(Skia.XYWHRect(0, 0, 1, 1), black);
canvas.drawRect(Skia.XYWHRect(1, 0, 1, 1), white);
canvas.drawRect(Skia.XYWHRect(0, 1, 1, 1), white);
canvas.drawRect(Skia.XYWHRect(1, 1, 1, 1), black);
surface.flush();
return surface.makeImageSnapshot();
};

const luminanceAt = (image: SkImage, x: number, y: number) => {
const pixels = image.readPixels() as Uint8Array;
return pixels[(y * image.width() + x) * 4];
};

const drawShader = async (image: SkImage, sampling?: SamplingOptions) => {
const { Skia } = importSkia();
const root = new SkiaSGRoot(Skia);
await root.render(
<skRect rect={RECT}>
<skImageShader
image={image}
tx="clamp"
ty="clamp"
fit="fill"
rect={RECT}
sampling={sampling}
/>
</skRect>
);
const surface = Skia.Surface.Make(SIZE, SIZE)!;
root.drawOnCanvas(surface.getCanvas());
surface.flush();
const out = surface.makeImageSnapshot();
root.unmount();
return out;
};

describe("ImageShader sampling", () => {
it("honours nearest neighbour sampling", async () => {
const { Skia } = importSkia();
const image = await drawShader(makeCheckerboard(Skia), {
filter: FilterMode.Nearest,
mipmap: MipmapMode.None,
});
// Nearest keeps the texel boundary a hard step - no blending at the seam.
expect(luminanceAt(image, 126, 60)).toBe(0);
expect(luminanceAt(image, 130, 60)).toBe(255);
});

it("defaults to linear sampling rather than a cubic filter", async () => {
const { Skia } = importSkia();
const image = await drawShader(makeCheckerboard(Skia));
// A B-spline cubic (B=1, C=0) never reaches the source extremes, so the
// far corner of the top-left texel stays washed out under it.
expect(luminanceAt(image, 2, 2)).toBe(0);
});
});
Loading