rendertransform.ts follows RenderTransform from accelerator.pyx: the same matrices in the same order, with transform_anchor measuring the anchor in the child. Scene images are laid out from it (left/top plus a matrix3d with origin 0 0), so rotation turns about the picture's centre and the box grows to its diagonal as in Ren'Py; xpan/ypan are a wrapped shift of a repeating background, xtile/ytile repeat it, blend and a fully additive picture become mix-blend-mode. Checked in a real browser page and by unit tests of the math. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
60 lines
3.3 KiB
JavaScript
60 lines
3.3 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { fileURLToPath } from "node:url";
|
|
import { test } from "node:test";
|
|
import { build } from "esbuild";
|
|
|
|
const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/rendertransform.ts", import.meta.url))],
|
|
bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
|
|
const { Matrix4, layoutTransform } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`);
|
|
const close = (a, b, eps = 1e-6) => assert.ok(Math.abs(a - b) < eps, `${a} != ${b}`);
|
|
|
|
test("zoom grows the box and scales the child about its centre; the anchor is a fraction of the grown box", () => {
|
|
const out = layoutTransform({ zoom: 2, xpos: 0.5, ypos: 1, xanchor: 0.5, yanchor: 1 }, 100, 50, 1920, 1080);
|
|
close(out.width, 200); close(out.height, 100);
|
|
close(out.left, 960 - 100); close(out.top, 1080 - 100);
|
|
assert.equal(out.transform, "matrix3d(2,0,0,0,0,2,0,0,0,0,1,0,0,0,0,1)");
|
|
});
|
|
|
|
test("rotate pads the box to its diagonal (rotate_pad) and rotates about the centre", () => {
|
|
const out = layoutTransform({ rotate: 90, xpos: 0, ypos: 0 }, 60, 80, 1000, 1000);
|
|
close(out.width, 100); close(out.height, 100);
|
|
// the child's centre (30, 40) must land on the box centre (50, 50); rotating 90 degrees clockwise
|
|
const css = out.transform.match(/matrix3d\((.*)\)/)[1].split(",").map(Number);
|
|
const x = css[0] * 30 + css[4] * 40 + css[12], y = css[1] * 30 + css[5] * 40 + css[13];
|
|
close(x, 50, 1e-4); close(y, 50, 1e-4);
|
|
// the child's top-left corner (0,0) goes to (50+40, 50-30)
|
|
close(css[12], 90, 1e-4); close(css[13], 20, 1e-4);
|
|
});
|
|
|
|
test("without rotate_pad the box is the bounding box of the rotated child", () => {
|
|
const out = layoutTransform({ rotate: 90, rotate_pad: false }, 60, 80, 1000, 1000);
|
|
close(out.width, 80); close(out.height, 60);
|
|
});
|
|
|
|
test("transform_anchor measures the anchor in the child and carries it through the transform", () => {
|
|
const out = layoutTransform({ zoom: 0.5, transform_anchor: true, xanchor: 985, yanchor: 1535, xpos: 0, ypos: 0 }, 2000, 3000, 1920, 1080);
|
|
close(out.left, -985 * 0.5); close(out.top, -1535 * 0.5);
|
|
});
|
|
|
|
test("a negative xzoom flips the child inside the same box", () => {
|
|
const out = layoutTransform({ xzoom: -1 }, 100, 50, 1000, 1000);
|
|
close(out.width, 100);
|
|
assert.equal(out.transform, "matrix3d(-1,0,0,0,0,1,0,0,0,0,1,0,100,0,0,1)");
|
|
});
|
|
|
|
test("crop shows the rectangle and the box takes its size; size and fit scale first", () => {
|
|
const out = layoutTransform({ crop: [10, 20, 30, 40] }, 100, 100, 1000, 1000);
|
|
close(out.width, 30); close(out.height, 40);
|
|
assert.equal(out.clip, "inset(20px 60px 40px 10px)");
|
|
const fit = layoutTransform({ xysize: [200, 200], fit: "contain" }, 100, 50, 1000, 1000);
|
|
close(fit.width, 200); close(fit.height, 100);
|
|
});
|
|
|
|
test("matrix products follow Ren'Py's order: a.mul(b) applies b first, and 3D rotation about the centre", () => {
|
|
const m = Matrix4.offset(10, 0).mul(Matrix4.scale(2, 2, 1));
|
|
assert.deepEqual(m.transform(1, 1).slice(0, 2), [12, 2]);
|
|
const out = layoutTransform({ yrotate: 180 }, 100, 50, 1000, 1000);
|
|
const css = out.transform.match(/matrix3d\((.*)\)/)[1].split(",").map(Number);
|
|
close(css[0], -1, 1e-5); // the picture is seen from behind: mirrored
|
|
});
|