Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|---|---|
| `<b>` | **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 |
| `<b>` | **Quads** | Axis-aligned rectangle, or untextured convex quad when the homography passes stability guards | `background: currentColor` on a fixed 64px rectangle; affine and projective quads normalize their `matrix3d` to that primitive, with tiny solid bleed on projective quads to overlap antialias seams | None |
| `<i>` | **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 |
| `<s>` | **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 |
| `<u>` | **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 |
Expand Down
32 changes: 19 additions & 13 deletions packages/polycss/src/render/polyDOM.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const UNSTABLE_PROJECTIVE_QUAD: Polygon = {
color: "#ff00ff",
};

const QUAD_CANONICAL_SIZE = 64;

const OFFAXIS_TRIANGLE: Polygon = {
vertices: [
[0, 0, 0],
Expand Down Expand Up @@ -160,15 +162,21 @@ function computeExpectedMatrix(
return computeExpectedPlan(vertices, tileSize, elev).matrix;
}

function computeExpectedCanonicalMatrix(
function computeExpectedQuadMatrix(
vertices: [number, number, number][],
tileSize = 50,
elev = tileSize,
): number[] {
const { matrix, canvasW, canvasH } = computeExpectedPlan(vertices, tileSize, elev);
return [
matrix[0] * canvasW, matrix[1] * canvasW, matrix[2] * canvasW, 0,
matrix[4] * canvasH, matrix[5] * canvasH, matrix[6] * canvasH, 0,
matrix[0] * canvasW / QUAD_CANONICAL_SIZE,
matrix[1] * canvasW / QUAD_CANONICAL_SIZE,
matrix[2] * canvasW / QUAD_CANONICAL_SIZE,
0,
matrix[4] * canvasH / QUAD_CANONICAL_SIZE,
matrix[5] * canvasH / QUAD_CANONICAL_SIZE,
matrix[6] * canvasH / QUAD_CANONICAL_SIZE,
0,
matrix[8], matrix[9], matrix[10], 0,
matrix[12], matrix[13], matrix[14], 1,
];
Expand Down Expand Up @@ -283,7 +291,7 @@ describe("renderPoly — matrix math parity", () => {
it("vertical quad matrix3d values match expected", () => {
const result = renderPoly(VERTICAL_QUAD)!;
const actual = extractMatrix(result.element);
const expected = roundedMatrix(computeExpectedCanonicalMatrix(VERTICAL_QUAD.vertices as [number, number, number][]));
const expected = roundedMatrix(computeExpectedQuadMatrix(VERTICAL_QUAD.vertices as [number, number, number][]));
expect(actual.length).toBe(16);
for (let i = 0; i < 16; i++) expect(actual[i]).toBeCloseTo(expected[i], 6);
result.dispose();
Expand All @@ -309,7 +317,7 @@ describe("renderPoly — matrix math parity", () => {
};
const result = renderPoly(poly, { tileSize: 50, layerElevation: 25 })!;
const actual = extractMatrix(result.element);
const expected = roundedMatrix(computeExpectedCanonicalMatrix(poly.vertices as [number, number, number][], 50, 25));
const expected = roundedMatrix(computeExpectedQuadMatrix(poly.vertices as [number, number, number][], 50, 25));
for (let i = 0; i < 16; i++) expect(actual[i]).toBeCloseTo(expected[i], 6);
result.dispose();
});
Expand Down Expand Up @@ -1298,10 +1306,11 @@ describe("renderPolygonsWithTextureAtlas — strategies.disable", () => {
const element = result.rendered[0].element;
const style = element.getAttribute("style") ?? "";
expect(element.tagName.toLowerCase()).toBe("b");
expect(element.className).toBe("");
expect(result.rendered[0].kind).toBe("solid");
expect(style).toContain("transform:matrix3d(");
expect(style).toContain("width:");
expect(style).toContain("height:");
expect(style).not.toContain("width");
expect(style).not.toContain("height");
expect(style).not.toContain("border-shape");
expect(canvases).toHaveLength(0);
result.dispose();
Expand All @@ -1323,19 +1332,16 @@ 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,
vertex[2] * 50,
]);

expectPointClose(transformMatrixPoint(matrix, 0, 0), expected[0]);
expectPointClose(transformMatrixPoint(matrix, width, 0), expected[1]);
expectPointClose(transformMatrixPoint(matrix, width, height), expected[2]);
expectPointClose(transformMatrixPoint(matrix, 0, height), expected[3]);
expectPointClose(transformMatrixPoint(matrix, 64, 0), expected[1]);
expectPointClose(transformMatrixPoint(matrix, 64, 64), expected[2]);
expectPointClose(transformMatrixPoint(matrix, 0, 64), expected[3]);
result.dispose();
});

Expand Down
25 changes: 15 additions & 10 deletions packages/polycss/src/render/textureAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ const BORDER_SHAPE_CENTER_PERCENT = 50;
const BORDER_SHAPE_POINT_EPS = 1e-7;
const BORDER_SHAPE_CANONICAL_SIZE = 16;
const BORDER_SHAPE_BLEED = 0.9;
const QUAD_CANONICAL_SIZE = 64;
const PROJECTIVE_QUAD_DENOM_EPS = 0.05;
const PROJECTIVE_QUAD_MAX_WEIGHT_RATIO = Number.POSITIVE_INFINITY;
const PROJECTIVE_QUAD_BLEED = 0.6;
Expand Down Expand Up @@ -496,8 +497,6 @@ function computeProjectiveQuadMatrix(
tx: number,
ty: number,
tz: number,
sourceWidth: number,
sourceHeight: number,
guards: ProjectiveQuadGuardSettings,
): string | null {
if (screenPts.length !== 8) return null;
Expand All @@ -520,8 +519,7 @@ 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 sourceSize = QUAD_CANONICAL_SIZE;

const p0: Vec3 = [
tx + q0[0] * xAxis[0] + q0[1] * yAxis[0],
Expand All @@ -535,8 +533,8 @@ function computeProjectiveQuadMatrix(
];

return formatMatrix3dValues([
...projectiveColumn(q1, w1).map((value) => value / sx), g / sx,
...projectiveColumn(q3, w3).map((value) => value / sy), h / sy,
...projectiveColumn(q1, w1).map((value) => value / sourceSize), g / sourceSize,
...projectiveColumn(q3, w3).map((value) => value / sourceSize), h / sourceSize,
normal[0], normal[1], normal[2], 0,
p0[0], p0[1], p0[2], 1,
], 6);
Expand Down Expand Up @@ -1528,8 +1526,6 @@ function computeTextureAtlasPlan(
tx,
ty,
tz,
canvasW,
canvasH,
projectiveQuadGuards,
)
: null;
Expand Down Expand Up @@ -2293,6 +2289,14 @@ function formatPlanElementStyle(
return `transform:matrix3d(${entry.canonicalMatrix})${shape}`;
}

function formatQuadMatrix(entry: TextureAtlasPlan): string {
return formatScaledMatrixFromPlan(
entry,
entry.canvasW / QUAD_CANONICAL_SIZE,
entry.canvasH / QUAD_CANONICAL_SIZE,
);
}

function formatScaledMatrixFromPlan(
entry: TextureAtlasPlan,
scaleX: number,
Expand Down Expand Up @@ -2729,7 +2733,8 @@ function createSolidElement(
solidPaintDefaults?: SolidPaintDefaults,
): HTMLElement {
const el = doc.createElement("b");
applyPlanElementBase(el, entry);
el.setAttribute("style", `transform:matrix3d(${formatQuadMatrix(entry)})`);
applyPolygonDataAttrs(el, entry.polygon);
applySolidPaint(el, entry, textureLighting, solidPaintDefaults);

return el;
Expand All @@ -2756,7 +2761,7 @@ function createProjectiveSolidElement(
solidPaintDefaults?: SolidPaintDefaults,
): HTMLElement {
const el = doc.createElement("b");
el.setAttribute("style", `width:${formatCssLength(entry.canvasW)};height:${formatCssLength(entry.canvasH)};transform:matrix3d(${entry.projectiveMatrix})`);
el.setAttribute("style", `transform:matrix3d(${entry.projectiveMatrix})`);
applyPolygonDataAttrs(el, entry.polygon);
applySolidPaint(el, entry, textureLighting, solidPaintDefaults);

Expand Down
4 changes: 2 additions & 2 deletions packages/polycss/src/styles/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ const CORE_BASE_STYLES = `

.polycss-scene b {
background: currentColor;
width: 1px;
height: 1px;
width: 64px;
height: 64px;
}

.polycss-scene i {
Expand Down
22 changes: 16 additions & 6 deletions packages/react/src/scene/textureAtlas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const DEFAULT_ATLAS_CSS_DECIMALS = 4;
const BORDER_SHAPE_CENTER_PERCENT = 50;
const BORDER_SHAPE_POINT_EPS = 1e-7;
const BORDER_SHAPE_CANONICAL_SIZE = 16;
const QUAD_CANONICAL_SIZE = 64;
const PROJECTIVE_QUAD_DENOM_EPS = 0.05;
const PROJECTIVE_QUAD_MAX_WEIGHT_RATIO = 4;
const PROJECTIVE_QUAD_BLEED = 0.6;
Expand Down Expand Up @@ -577,6 +578,14 @@ function formatScaledMatrixFromPlan(
return formatMatrix3dValues(values);
}

function formatQuadMatrix(entry: TextureAtlasPlan): string {
return formatScaledMatrixFromPlan(
entry,
entry.canvasW / QUAD_CANONICAL_SIZE,
entry.canvasH / QUAD_CANONICAL_SIZE,
);
}

function formatBorderShapeMatrix(entry: TextureAtlasPlan): string {
return formatScaledMatrixFromPlan(
entry,
Expand Down Expand Up @@ -764,13 +773,14 @@ function computeProjectiveQuadMatrix(
p3[1] * w3 - p0[1],
p3[2] * w3 - p0[2],
];
const sourceSize = QUAD_CANONICAL_SIZE;

return [
xCol[0], xCol[1], xCol[2], g,
yCol[0], yCol[1], yCol[2], h,
return formatMatrix3dValues([
xCol[0] / sourceSize, xCol[1] / sourceSize, xCol[2] / sourceSize, g / sourceSize,
yCol[0] / sourceSize, yCol[1] / sourceSize, yCol[2] / sourceSize, h / sourceSize,
normal[0], normal[1], normal[2], 0,
p0[0], p0[1], p0[2], 1,
].join(",");
], 6);
}

function cssPoints(vertices: Vec3[], tile: number, elev: number): Vec3[] {
Expand Down Expand Up @@ -1958,7 +1968,7 @@ export function TextureBorderShapePoly({
else el.style.removeProperty("border-shape");
orderBrushInlineStyle(el);
}, [borderShape]);
const transform = formatMatrix3d(borderShape ? formatBorderShapeMatrix(entry) : entry.canonicalMatrix);
const transform = formatMatrix3d(borderShape ? formatBorderShapeMatrix(entry) : formatQuadMatrix(entry));
const style: CSSProperties = fullRect
? {
transform,
Expand Down Expand Up @@ -2027,7 +2037,7 @@ export function TextureProjectiveSolidPoly({
const base = parseHex(entry.polygon.color ?? "#cccccc");
const useDefaultDynamicColor = dynamic && rgbKey(base) === solidPaintDefaults?.dynamicColorKey;
const style: CSSProperties = {
transform: formatMatrix3d(entry.projectiveMatrix),
transform: formatMatrix3d(entry.projectiveMatrix, 6),
color: dynamic || entry.shadedColor === solidPaintDefaults?.paintColor
? undefined
: entry.shadedColor,
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/styles/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const CORE_BASE_STYLES = `

.polycss-scene b {
background: currentColor;
width: 1px;
height: 1px;
width: 64px;
height: 64px;
}

.polycss-scene i {
Expand Down
22 changes: 16 additions & 6 deletions packages/vue/src/scene/textureAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const DEFAULT_ATLAS_CSS_DECIMALS = 4;
const BORDER_SHAPE_CENTER_PERCENT = 50;
const BORDER_SHAPE_POINT_EPS = 1e-7;
const BORDER_SHAPE_CANONICAL_SIZE = 16;
const QUAD_CANONICAL_SIZE = 64;
const PROJECTIVE_QUAD_DENOM_EPS = 0.05;
const PROJECTIVE_QUAD_MAX_WEIGHT_RATIO = 4;
const PROJECTIVE_QUAD_BLEED = 0.6;
Expand Down Expand Up @@ -585,6 +586,14 @@ function formatScaledMatrixFromPlan(
return formatMatrix3dValues(values);
}

function formatQuadMatrix(entry: TextureAtlasPlan): string {
return formatScaledMatrixFromPlan(
entry,
entry.canvasW / QUAD_CANONICAL_SIZE,
entry.canvasH / QUAD_CANONICAL_SIZE,
);
}

function formatBorderShapeMatrix(entry: TextureAtlasPlan): string {
return formatScaledMatrixFromPlan(
entry,
Expand Down Expand Up @@ -772,13 +781,14 @@ function computeProjectiveQuadMatrix(
p3[1] * w3 - p0[1],
p3[2] * w3 - p0[2],
];
const sourceSize = QUAD_CANONICAL_SIZE;

return [
xCol[0], xCol[1], xCol[2], g,
yCol[0], yCol[1], yCol[2], h,
return formatMatrix3dValues([
xCol[0] / sourceSize, xCol[1] / sourceSize, xCol[2] / sourceSize, g / sourceSize,
yCol[0] / sourceSize, yCol[1] / sourceSize, yCol[2] / sourceSize, h / sourceSize,
normal[0], normal[1], normal[2], 0,
p0[0], p0[1], p0[2], 1,
].join(",");
], 6);
}

function cssPoints(vertices: Vec3[], tile: number, elev: number): Vec3[] {
Expand Down Expand Up @@ -2055,7 +2065,7 @@ export function renderTextureBorderShapePoly({
const useIForFullRect = fullRect && forceBorderShape && borderShapeSupported();
const borderShape = (!fullRect || useIForFullRect) ? cssBorderShapeForPlan(entry) : null;
const useDefaultPaint = entry.shadedColor === solidPaintDefaults?.paintColor;
const transform = formatMatrix3d(borderShape ? formatBorderShapeMatrix(entry) : entry.canonicalMatrix);
const transform = formatMatrix3d(borderShape ? formatBorderShapeMatrix(entry) : formatQuadMatrix(entry));
const style: CSSProperties = fullRect
? {
transform,
Expand Down Expand Up @@ -2116,7 +2126,7 @@ export function renderTextureProjectiveSolidPoly({
const base = parseHex(entry.polygon.color ?? "#cccccc");
const useDefaultDynamicColor = dynamic && rgbKey(base) === solidPaintDefaults?.dynamicColorKey;
const style: CSSProperties = {
transform: formatMatrix3d(entry.projectiveMatrix),
transform: formatMatrix3d(entry.projectiveMatrix, 6),
color: dynamic || entry.shadedColor === solidPaintDefaults?.paintColor
? undefined
: entry.shadedColor,
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/src/styles/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const CORE_BASE_STYLES = `

.polycss-scene b {
background: currentColor;
width: 1px;
height: 1px;
width: 64px;
height: 64px;
}

.polycss-scene i {
Expand Down
Loading