From c10549012b8b6714754ca53f42b477911487bbda Mon Sep 17 00:00:00 2001 From: agustin-littlehat Date: Sat, 16 May 2026 20:49:28 -0300 Subject: [PATCH] fix: normalize projective quad matrices --- AGENTS.md | 2 +- packages/polycss/src/render/polyDOM.test.ts | 13 ++++++++----- packages/polycss/src/render/textureAtlas.ts | 14 ++++++++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 066c29d7..59fd8712 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ Public API is **mirrored** across React and Vue. Adding a hook on one side witho | Tag | Strategy | When chosen | Paint mechanism | Atlas memory | |---|---|---|---|---| -| `` | **Quads** | Axis-aligned rectangle, or untextured convex quad when the homography passes stability guards | `background: currentColor`; canonical quads use a 1px rectangle mapped by `matrix3d` / projective `matrix3d` with tiny solid bleed to overlap antialias seams | None | +| `` | **Quads** | Axis-aligned rectangle, or untextured convex quad when the homography passes stability guards | `background: currentColor`; affine rects use a 1px rectangle mapped by `matrix3d`; projective quads use a bbox-sized rectangle with a normalized high-precision projective `matrix3d` and tiny solid bleed to overlap antialias seams | None | | `` | **Border-shape clipped solid** | Untextured non-rect on browsers with CSS `border-shape` (Chromium + `pointer:fine` + `hover:hover`) | `border-color: currentColor` on a fixed 16px border-shape primitive, clipped by `border-shape: polygon(...)`; polygon bbox scale and tiny solid bleed are folded into `matrix3d` | None | | `` | **Atlas slice** | Textured polygons, or untextured non-rect on browsers without `border-shape` | `background-image` slice of packed bitmap on a canonical 1px primitive; atlas position/size are normalized to the slice, scale lives in `matrix3d`, and shared textured edges get low-alpha atlas pixels repaired during atlas generation | Bounding-rect area | | `` | **Stable solid triangle** | Opt-in for triangles via `renderPolygonsWithStableTriangles` | CSS border-color triangle trick with a fixed canonical 1px border triangle; tiny solid bleed is folded into `matrix3d` | None | diff --git a/packages/polycss/src/render/polyDOM.test.ts b/packages/polycss/src/render/polyDOM.test.ts index 3af9691e..717a9fd8 100644 --- a/packages/polycss/src/render/polyDOM.test.ts +++ b/packages/polycss/src/render/polyDOM.test.ts @@ -1300,8 +1300,8 @@ describe("renderPolygonsWithTextureAtlas — strategies.disable", () => { expect(element.tagName.toLowerCase()).toBe("b"); expect(result.rendered[0].kind).toBe("solid"); expect(style).toContain("transform:matrix3d("); - expect(style).not.toContain("width"); - expect(style).not.toContain("height"); + expect(style).toContain("width:"); + expect(style).toContain("height:"); expect(style).not.toContain("border-shape"); expect(canvases).toHaveLength(0); result.dispose(); @@ -1323,6 +1323,9 @@ describe("renderPolygonsWithTextureAtlas — strategies.disable", () => { { doc }, ); const matrix = extractMatrix(result.rendered[0].element); + const plan = result.rendered[0].plan!; + const width = plan.canvasW; + const height = plan.canvasH; const expected = NON_RECT_QUAD.vertices.map((vertex): [number, number, number] => [ vertex[1] * 50, vertex[0] * 50, @@ -1330,9 +1333,9 @@ describe("renderPolygonsWithTextureAtlas — strategies.disable", () => { ]); expectPointClose(transformMatrixPoint(matrix, 0, 0), expected[0]); - expectPointClose(transformMatrixPoint(matrix, 1, 0), expected[1]); - expectPointClose(transformMatrixPoint(matrix, 1, 1), expected[2]); - expectPointClose(transformMatrixPoint(matrix, 0, 1), expected[3]); + expectPointClose(transformMatrixPoint(matrix, width, 0), expected[1]); + expectPointClose(transformMatrixPoint(matrix, width, height), expected[2]); + expectPointClose(transformMatrixPoint(matrix, 0, height), expected[3]); result.dispose(); }); diff --git a/packages/polycss/src/render/textureAtlas.ts b/packages/polycss/src/render/textureAtlas.ts index c25555a0..c1405cdf 100644 --- a/packages/polycss/src/render/textureAtlas.ts +++ b/packages/polycss/src/render/textureAtlas.ts @@ -496,6 +496,8 @@ function computeProjectiveQuadMatrix( tx: number, ty: number, tz: number, + sourceWidth: number, + sourceHeight: number, guards: ProjectiveQuadGuardSettings, ): string | null { if (screenPts.length !== 8) return null; @@ -518,6 +520,8 @@ function computeProjectiveQuadMatrix( if (!coeffs) return null; const { g, h, w1, w3 } = coeffs; const [q0, q1, , q3] = q; + const sx = Math.max(1, sourceWidth); + const sy = Math.max(1, sourceHeight); const p0: Vec3 = [ tx + q0[0] * xAxis[0] + q0[1] * yAxis[0], @@ -531,11 +535,11 @@ function computeProjectiveQuadMatrix( ]; return formatMatrix3dValues([ - ...projectiveColumn(q1, w1), g, - ...projectiveColumn(q3, w3), h, + ...projectiveColumn(q1, w1).map((value) => value / sx), g / sx, + ...projectiveColumn(q3, w3).map((value) => value / sy), h / sy, normal[0], normal[1], normal[2], 0, p0[0], p0[1], p0[2], 1, - ]); + ], 6); } function formatPercent(value: number, decimals = DEFAULT_BORDER_SHAPE_DECIMALS): string { @@ -1524,6 +1528,8 @@ function computeTextureAtlasPlan( tx, ty, tz, + canvasW, + canvasH, projectiveQuadGuards, ) : null; @@ -2750,7 +2756,7 @@ function createProjectiveSolidElement( solidPaintDefaults?: SolidPaintDefaults, ): HTMLElement { const el = doc.createElement("b"); - el.setAttribute("style", `transform:matrix3d(${entry.projectiveMatrix})`); + el.setAttribute("style", `width:${formatCssLength(entry.canvasW)};height:${formatCssLength(entry.canvasH)};transform:matrix3d(${entry.projectiveMatrix})`); applyPolygonDataAttrs(el, entry.polygon); applySolidPaint(el, entry, textureLighting, solidPaintDefaults);