import { describe, expect, it } from "vitest"; import { contentRect, cropToSourcePx, fitRatio, fracRatio, MIN_CROP, moveCrop, resizeCrop, type Frac, } from "./cropGeometry"; describe("contentRect (display-px letterbox mapping)", () => { it("returns the full box when box aspect matches the video aspect", () => { // 16:9 picture in a 16:9 box → no bars, content == box expect(contentRect(1600, 900, 16 / 9)).toEqual({ left: 0, top: 0, width: 1600, height: 900, }); }); it("pillarboxes (bars left/right) when the box is wider than the picture", () => { // 16:9 picture in a 2:1 box → full height, narrower centered width const r = contentRect(2000, 1000, 16 / 9)!; expect(r.height).toBe(1000); expect(r.width).toBeCloseTo(1000 * (16 / 9), 3); expect(r.top).toBe(0); expect(r.left).toBeCloseTo((2000 - 1000 * (16 / 9)) / 2, 3); }); it("letterboxes (bars top/bottom) when the box is taller than the picture", () => { // 16:9 picture in a 1:1 box → full width, shorter centered height const r = contentRect(1000, 1000, 16 / 9)!; expect(r.width).toBe(1000); expect(r.height).toBeCloseTo(1000 / (16 / 9), 3); expect(r.left).toBe(0); expect(r.top).toBeCloseTo((1000 - 1000 / (16 / 9)) / 2, 3); }); it("returns null for unmeasurable inputs", () => { expect(contentRect(0, 900, 16 / 9)).toBeNull(); expect(contentRect(1600, 0, 16 / 9)).toBeNull(); expect(contentRect(1600, 900, 0)).toBeNull(); expect(contentRect(1600, 900, NaN)).toBeNull(); }); }); describe("cropToSourcePx (fraction-of-content → even source px)", () => { it("scales content fractions straight to source pixels", () => { // A marquee anchored to the content rect: a fraction is a fraction of the real picture. expect(cropToSourcePx({ x: 0.25, y: 0.25, w: 0.5, h: 0.5 }, 1920, 1080)).toEqual({ x: 480, y: 270, w: 960, h: 540, }); }); it("rounds to even dimensions", () => { // 0.1 * 1920 = 192 (even); 0.333 * 1080 = 359.64 → 360 const c = cropToSourcePx({ x: 0.1, y: 0.333, w: 0.2, h: 0.111 }, 1920, 1080)!; expect(c.w % 2).toBe(0); expect(c.h % 2).toBe(0); expect(c.x % 2).toBe(0); expect(c.y % 2).toBe(0); }); it("allows a top-left origin (x/y may be 0)", () => { const c = cropToSourcePx({ x: 0, y: 0, w: 0.5, h: 0.5 }, 1920, 1080)!; expect(c.x).toBe(0); expect(c.y).toBe(0); }); it("clamps the window inside the frame so even-rounding can't overflow", () => { // A full-frame crop must stay exactly at the source size, not round past it. const c = cropToSourcePx({ x: 0, y: 0, w: 1, h: 1 }, 1921, 1081)!; expect(c.w).toBeLessThanOrEqual(1921); expect(c.h).toBeLessThanOrEqual(1081); expect(c.x + c.w).toBeLessThanOrEqual(1921); expect(c.y + c.h).toBeLessThanOrEqual(1081); }); it("keeps x+w within the frame for an off-square region near the right edge", () => { const srcW = 1280; const srcH = 720; const c = cropToSourcePx({ x: 0.7, y: 0.1, w: 0.3, h: 0.8 }, srcW, srcH)!; expect(c.x + c.w).toBeLessThanOrEqual(srcW); expect(c.y + c.h).toBeLessThanOrEqual(srcH); }); it("returns null for an unusable source size", () => { expect(cropToSourcePx({ x: 0, y: 0, w: 1, h: 1 }, 0, 1080)).toBeNull(); expect(cropToSourcePx({ x: 0, y: 0, w: 1, h: 1 }, 1920, 0)).toBeNull(); }); }); const near = (r: Frac, want: Frac) => { for (const k of ["x", "y", "w", "h"] as const) expect(r[k], k).toBeCloseTo(want[k], 6); }; // The pixel shape of a frame on a 16:9 picture. const shape = (r: Frac) => (r.w * 16) / 9 / r.h; const inside = (r: Frac) => { expect(r.x).toBeGreaterThanOrEqual(-1e-9); expect(r.y).toBeGreaterThanOrEqual(-1e-9); expect(r.x + r.w).toBeLessThanOrEqual(1 + 1e-9); expect(r.y + r.h).toBeLessThanOrEqual(1 + 1e-9); }; describe("the crop frame a user draws", () => { const r: Frac = { x: 0.2, y: 0.2, w: 0.4, h: 0.4 }; it("moves whole, stopping at the picture's edge", () => { near(moveCrop(r, 0.1, -0.05), { x: 0.3, y: 0.15, w: 0.4, h: 0.4 }); near(moveCrop(r, 0.9, -0.9), { x: 0.6, y: 0, w: 0.4, h: 0.4 }); }); it("resizes free from an edge or a corner, the opposite side staying put", () => { near(resizeCrop(r, "e", 0.1, 0.3, null), { x: 0.2, y: 0.2, w: 0.5, h: 0.4 }); near(resizeCrop(r, "nw", -0.1, -0.05, null), { x: 0.1, y: 0.15, w: 0.5, h: 0.45 }); // past the picture, and past the opposite edge: stopped at the edge and at the floor near(resizeCrop(r, "se", 0.9, 0.9, null), { x: 0.2, y: 0.2, w: 0.8, h: 0.8 }); near(resizeCrop(r, "w", 0.9, 0, null), { x: 0.6 - MIN_CROP, y: 0.2, w: MIN_CROP, h: 0.4 }); }); it("keeps a locked shape from a corner, following the side the pointer changed more", () => { const ratio = fracRatio(9 / 16, 16 / 9); const start = fitRatio(r, ratio); const grown = resizeCrop(start, "se", 0, -0.2, ratio); expect(shape(grown)).toBeCloseTo(9 / 16, 6); inside(grown); // a mostly sideways drag inward shrinks it (a "larger of the two" rule never would) const narrowed = resizeCrop({ x: 0.3, y: 0.1, w: 0.2, h: 0.6 }, "ne", -0.08, 0.01, 0.75); expect(narrowed.w).toBeCloseTo(0.12, 6); expect(narrowed.h).toBeCloseTo(0.16, 6); expect(narrowed.y + narrowed.h).toBeCloseTo(0.7, 6); // the anchored corner (sw) stays }); it("keeps a locked shape from an edge, growing the other side about the middle and in bounds", () => { const ratio = fracRatio(1, 16 / 9); const sq = { x: 0.4, y: 0.4, w: 0.2 * ratio, h: 0.2 }; const out = resizeCrop(sq, "e", 0.5, 0, ratio); expect(shape(out)).toBeCloseTo(1, 6); expect(out.y + out.h / 2).toBeCloseTo(0.5, 6); inside(out); const tall = resizeCrop(sq, "s", 0, 0.9, ratio); expect(shape(tall)).toBeCloseTo(1, 6); inside(tall); }); it("fits a chosen shape as large as the picture allows, centred on the frame", () => { const tall = fitRatio({ x: 0.7, y: 0.1, w: 0.2, h: 0.2 }, fracRatio(9 / 16, 16 / 9)); expect(tall.h).toBeCloseTo(1, 6); expect(shape(tall)).toBeCloseTo(9 / 16, 6); expect(tall.x + tall.w / 2).toBeCloseTo(0.8, 6); const wide = fitRatio({ x: 0, y: 0, w: 1, h: 1 }, fracRatio(21 / 9, 16 / 9)); near(wide, { x: 0, y: (1 - (16 / 9) * (9 / 21)) / 2, w: 1, h: (16 / 9) * (9 / 21) }); }); });