feat(canvas): add mixer drag-resize and mixer->render bake
This commit is contained in:
@@ -17,6 +17,13 @@ const sourceLoaderMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("@/lib/image-pipeline/source-loader", () => ({
|
||||
loadSourceBitmap: sourceLoaderMocks.loadSourceBitmap,
|
||||
loadRenderSourceBitmap: ({ sourceUrl }: { sourceUrl?: string }) => {
|
||||
if (!sourceUrl) {
|
||||
throw new Error("Render source is required.");
|
||||
}
|
||||
|
||||
return sourceLoaderMocks.loadSourceBitmap(sourceUrl);
|
||||
},
|
||||
}));
|
||||
|
||||
function createPreviewPixels(): Uint8ClampedArray {
|
||||
|
||||
113
tests/image-pipeline/image-pipeline.worker.test.ts
Normal file
113
tests/image-pipeline/image-pipeline.worker.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { RenderFullResult, RenderSourceComposition } from "@/lib/image-pipeline/render-types";
|
||||
|
||||
const bridgeMocks = vi.hoisted(() => ({
|
||||
renderFull: vi.fn(),
|
||||
}));
|
||||
|
||||
const previewRendererMocks = vi.hoisted(() => ({
|
||||
renderPreview: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/image-pipeline/bridge", () => ({
|
||||
renderFull: bridgeMocks.renderFull,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/image-pipeline/preview-renderer", () => ({
|
||||
renderPreview: previewRendererMocks.renderPreview,
|
||||
}));
|
||||
|
||||
type WorkerMessage = {
|
||||
kind: "full";
|
||||
requestId: number;
|
||||
payload: {
|
||||
sourceUrl?: string;
|
||||
sourceComposition?: RenderSourceComposition;
|
||||
steps: [];
|
||||
render: {
|
||||
resolution: "original";
|
||||
format: "png";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type WorkerScopeMock = {
|
||||
postMessage: ReturnType<typeof vi.fn>;
|
||||
onmessage: ((event: MessageEvent<WorkerMessage>) => void) | null;
|
||||
};
|
||||
|
||||
function createFullResult(): RenderFullResult {
|
||||
return {
|
||||
blob: new Blob(["rendered"]),
|
||||
width: 64,
|
||||
height: 64,
|
||||
mimeType: "image/png",
|
||||
format: "png",
|
||||
quality: null,
|
||||
sizeBytes: 8,
|
||||
sourceWidth: 64,
|
||||
sourceHeight: 64,
|
||||
wasSizeClamped: false,
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkerScope(): WorkerScopeMock {
|
||||
return {
|
||||
postMessage: vi.fn(),
|
||||
onmessage: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("image-pipeline.worker full render", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.unstubAllGlobals();
|
||||
bridgeMocks.renderFull.mockReset();
|
||||
bridgeMocks.renderFull.mockResolvedValue(createFullResult());
|
||||
previewRendererMocks.renderPreview.mockReset();
|
||||
});
|
||||
|
||||
it("forwards sourceComposition to renderFull for full requests", async () => {
|
||||
const workerScope = createWorkerScope();
|
||||
vi.stubGlobal("self", workerScope);
|
||||
await import("@/lib/image-pipeline/image-pipeline.worker");
|
||||
|
||||
const sourceComposition: RenderSourceComposition = {
|
||||
kind: "mixer",
|
||||
baseUrl: "https://cdn.example.com/base.png",
|
||||
overlayUrl: "https://cdn.example.com/overlay.png",
|
||||
blendMode: "overlay",
|
||||
opacity: 0.5,
|
||||
overlayX: 32,
|
||||
overlayY: 16,
|
||||
overlayWidth: 128,
|
||||
overlayHeight: 64,
|
||||
};
|
||||
|
||||
workerScope.onmessage?.({
|
||||
data: {
|
||||
kind: "full",
|
||||
requestId: 41,
|
||||
payload: {
|
||||
sourceComposition,
|
||||
steps: [],
|
||||
render: {
|
||||
resolution: "original",
|
||||
format: "png",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as MessageEvent<WorkerMessage>);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(bridgeMocks.renderFull).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(bridgeMocks.renderFull).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceComposition,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -355,4 +355,105 @@ describe("loadSourceBitmap", () => {
|
||||
expect(createImageBitmap).toHaveBeenCalledWith(fakeVideo);
|
||||
expect(revokeObjectUrl).toHaveBeenCalledWith("blob:video-source");
|
||||
});
|
||||
|
||||
it("renders mixer overlays with object-cover semantics instead of stretching", async () => {
|
||||
const baseBlob = new Blob(["base"]);
|
||||
const overlayBlob = new Blob(["overlay"]);
|
||||
const baseBitmap = { width: 100, height: 100 } as ImageBitmap;
|
||||
const overlayBitmap = { width: 200, height: 100 } as ImageBitmap;
|
||||
const composedBitmap = { width: 100, height: 100 } as ImageBitmap;
|
||||
|
||||
const drawImage = vi.fn();
|
||||
const context = {
|
||||
clearRect: vi.fn(),
|
||||
drawImage,
|
||||
globalCompositeOperation: "source-over" as GlobalCompositeOperation,
|
||||
globalAlpha: 1,
|
||||
};
|
||||
const canvas = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
getContext: vi.fn().mockReturnValue(context),
|
||||
} as unknown as HTMLCanvasElement;
|
||||
|
||||
const nativeCreateElement = document.createElement.bind(document);
|
||||
vi.spyOn(document, "createElement").mockImplementation((tagName: string) => {
|
||||
if (tagName.toLowerCase() === "canvas") {
|
||||
return canvas;
|
||||
}
|
||||
|
||||
return nativeCreateElement(tagName);
|
||||
});
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn().mockImplementation(async (input: string | URL | Request) => {
|
||||
const url = String(input);
|
||||
if (url.includes("base.png")) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: vi.fn().mockReturnValue("image/png") },
|
||||
blob: vi.fn().mockResolvedValue(baseBlob),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: vi.fn().mockReturnValue("image/png") },
|
||||
blob: vi.fn().mockResolvedValue(overlayBlob),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal(
|
||||
"createImageBitmap",
|
||||
vi.fn().mockImplementation(async (input: unknown) => {
|
||||
if (input === baseBlob) {
|
||||
return baseBitmap;
|
||||
}
|
||||
if (input === overlayBlob) {
|
||||
return overlayBitmap;
|
||||
}
|
||||
if (input === canvas) {
|
||||
return composedBitmap;
|
||||
}
|
||||
|
||||
throw new Error("Unexpected createImageBitmap input in mixer cover-fit test.");
|
||||
}),
|
||||
);
|
||||
|
||||
const { loadRenderSourceBitmap } = await importSubject();
|
||||
|
||||
await expect(
|
||||
loadRenderSourceBitmap({
|
||||
sourceComposition: {
|
||||
kind: "mixer",
|
||||
baseUrl: "https://cdn.example.com/base.png",
|
||||
overlayUrl: "https://cdn.example.com/overlay.png",
|
||||
blendMode: "overlay",
|
||||
opacity: 80,
|
||||
overlayX: 0.1,
|
||||
overlayY: 0.2,
|
||||
overlayWidth: 0.25,
|
||||
overlayHeight: 0.5,
|
||||
},
|
||||
}),
|
||||
).resolves.toBe(composedBitmap);
|
||||
|
||||
expect(drawImage).toHaveBeenNthCalledWith(1, baseBitmap, 0, 0, 100, 100);
|
||||
expect(drawImage).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
overlayBitmap,
|
||||
75,
|
||||
0,
|
||||
50,
|
||||
100,
|
||||
10,
|
||||
20,
|
||||
25,
|
||||
50,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -341,6 +341,7 @@ describe("webgl backend poc", () => {
|
||||
|
||||
vi.doMock("@/lib/image-pipeline/source-loader", () => ({
|
||||
loadSourceBitmap: vi.fn().mockResolvedValue({ width: 2, height: 2 }),
|
||||
loadRenderSourceBitmap: vi.fn().mockResolvedValue({ width: 2, height: 2 }),
|
||||
}));
|
||||
|
||||
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({
|
||||
|
||||
Reference in New Issue
Block a user