From 58a654ba44e9e60e9111a0a4eafa6cb1692b687d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:03:37 +0000 Subject: [PATCH 01/17] Initial plan From 2c86e05d028152a09fed3de63e37f73250426a22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:06:11 +0000 Subject: [PATCH 02/17] Add comprehensive tests and GitHub Actions workflow Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- .github/workflows/test.yml | 29 ++++ deno.json | 3 +- src/lib_test.ts | 314 +++++++++++++++++++++++++++++++++++++ 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100644 src/lib_test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5804c49 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests + +on: + pull_request: + branches: [ main, master ] + push: + branches: [ main, master ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Run tests + run: deno test --allow-read --allow-write + + - name: Check formatting + run: deno fmt --check + + - name: Run linter + run: deno lint diff --git a/deno.json b/deno.json index b61140f..11658fd 100644 --- a/deno.json +++ b/deno.json @@ -6,7 +6,8 @@ "sharp": "npm:sharp@^0.34.5" }, "tasks": { - "compile": "deno compile --allow-all main.ts" + "compile": "deno compile --allow-all main.ts", + "test": "deno test --allow-read --allow-write" }, "version": "0.0.1" } diff --git a/src/lib_test.ts b/src/lib_test.ts new file mode 100644 index 0000000..4fd8f24 --- /dev/null +++ b/src/lib_test.ts @@ -0,0 +1,314 @@ +import { assertEquals, assertThrows } from "jsr:@std/assert"; +import { + findClosestRationalAngle, + gcd, + lcm, + calculateTileDimensions, + validateDimensions, + getOutputPath, + RATIONAL_ANGLES, +} from "./lib.ts"; + +// Test findClosestRationalAngle +Deno.test("findClosestRationalAngle - finds exact match for 45 degrees", () => { + const result = findClosestRationalAngle(45); + assertEquals(result.degrees, 45); + assertEquals(result.m, 1); + assertEquals(result.n, 1); + assertEquals(result.label, "45°"); +}); + +Deno.test("findClosestRationalAngle - finds closest angle for 44 degrees", () => { + const result = findClosestRationalAngle(44); + assertEquals(result.degrees, 45); + assertEquals(result.m, 1); + assertEquals(result.n, 1); +}); + +Deno.test("findClosestRationalAngle - finds closest angle for 46 degrees", () => { + const result = findClosestRationalAngle(46); + assertEquals(result.degrees, 45); + assertEquals(result.m, 1); + assertEquals(result.n, 1); +}); + +Deno.test("findClosestRationalAngle - finds exact match for 0 degrees", () => { + const result = findClosestRationalAngle(0); + assertEquals(result.degrees, 0); + assertEquals(result.m, 0); + assertEquals(result.n, 1); +}); + +Deno.test("findClosestRationalAngle - finds exact match for 90 degrees", () => { + const result = findClosestRationalAngle(90); + assertEquals(result.degrees, 90); + assertEquals(result.m, 1); + assertEquals(result.n, 0); +}); + +Deno.test("findClosestRationalAngle - normalizes negative angles", () => { + const result = findClosestRationalAngle(-45); + assertEquals(result.degrees, -45); + assertEquals(result.m, -1); + assertEquals(result.n, 1); +}); + +Deno.test("findClosestRationalAngle - normalizes angles > 360", () => { + const result = findClosestRationalAngle(405); // 405 - 360 = 45 + assertEquals(result.degrees, 45); + assertEquals(result.m, 1); + assertEquals(result.n, 1); +}); + +// Test gcd function +Deno.test("gcd - calculates GCD of 12 and 8", () => { + const result = gcd(12, 8); + assertEquals(result, 4); +}); + +Deno.test("gcd - calculates GCD of 100 and 50", () => { + const result = gcd(100, 50); + assertEquals(result, 50); +}); + +Deno.test("gcd - calculates GCD of coprime numbers", () => { + const result = gcd(17, 19); + assertEquals(result, 1); +}); + +Deno.test("gcd - handles zero", () => { + const result = gcd(0, 5); + assertEquals(result, 5); +}); + +Deno.test("gcd - handles both zeros", () => { + const result = gcd(0, 0); + assertEquals(result, 0); +}); + +Deno.test("gcd - handles negative numbers", () => { + const result = gcd(-12, 8); + assertEquals(result, 4); +}); + +Deno.test("gcd - handles both negative numbers", () => { + const result = gcd(-12, -8); + assertEquals(result, 4); +}); + +// Test lcm function +Deno.test("lcm - calculates LCM of 12 and 8", () => { + const result = lcm(12, 8); + assertEquals(result, 24); +}); + +Deno.test("lcm - calculates LCM of 4 and 6", () => { + const result = lcm(4, 6); + assertEquals(result, 12); +}); + +Deno.test("lcm - handles zero", () => { + const result = lcm(0, 5); + assertEquals(result, 0); +}); + +Deno.test("lcm - handles coprime numbers", () => { + const result = lcm(7, 13); + assertEquals(result, 91); +}); + +Deno.test("lcm - handles negative numbers", () => { + const result = lcm(-12, 8); + assertEquals(result, 24); +}); + +// Test calculateTileDimensions +Deno.test("calculateTileDimensions - 0 degrees returns original dimensions", () => { + const result = calculateTileDimensions(100, 100, { m: 0, n: 1 }); + assertEquals(result.width, 100); + assertEquals(result.height, 100); +}); + +Deno.test("calculateTileDimensions - 90 degrees swaps dimensions", () => { + const result = calculateTileDimensions(100, 200, { m: 1, n: 0 }); + assertEquals(result.width, 200); + assertEquals(result.height, 100); +}); + +Deno.test("calculateTileDimensions - 45 degrees on square image", () => { + const result = calculateTileDimensions(100, 100, { m: 1, n: 1 }); + // For 45 degrees with square input, output should be larger + assertEquals(result.width > 0, true); + assertEquals(result.height > 0, true); +}); + +Deno.test("calculateTileDimensions - handles 26.565 degrees (1:2 ratio)", () => { + const result = calculateTileDimensions(100, 200, { m: 1, n: 2 }); + assertEquals(result.width > 0, true); + assertEquals(result.height > 0, true); +}); + +Deno.test("calculateTileDimensions - handles negative m value", () => { + const result = calculateTileDimensions(100, 100, { m: -1, n: 1 }); + // Negative m should give same dimensions as positive (absolute value used) + const positive = calculateTileDimensions(100, 100, { m: 1, n: 1 }); + assertEquals(result.width, positive.width); + assertEquals(result.height, positive.height); +}); + +// Test validateDimensions +Deno.test("validateDimensions - accepts valid dimensions", () => { + const errors = validateDimensions({ + width: 500, + height: 500, + validWidth: 2000, + }); + assertEquals(errors.length, 0); +}); + +Deno.test("validateDimensions - rejects width exceeding limit", () => { + const errors = validateDimensions({ + width: 3000, + height: 500, + validWidth: 2000, + }); + assertEquals(errors.length, 1); + assertEquals(errors[0].includes("width"), true); + assertEquals(errors[0].includes("3000"), true); +}); + +Deno.test("validateDimensions - rejects height exceeding limit", () => { + const errors = validateDimensions({ + width: 500, + height: 3000, + validWidth: 2000, + validHeight: 2000, + }); + assertEquals(errors.length, 1); + assertEquals(errors[0].includes("height"), true); + assertEquals(errors[0].includes("3000"), true); +}); + +Deno.test("validateDimensions - rejects zero width", () => { + const errors = validateDimensions({ + width: 0, + height: 500, + validWidth: 2000, + }); + assertEquals(errors.length, 1); + assertEquals(errors[0].includes("zero"), true); +}); + +Deno.test("validateDimensions - rejects zero height", () => { + const errors = validateDimensions({ + width: 500, + height: 0, + validWidth: 2000, + }); + assertEquals(errors.length, 1); + assertEquals(errors[0].includes("zero"), true); +}); + +Deno.test("validateDimensions - rejects infinite width", () => { + const errors = validateDimensions({ + width: Infinity, + height: 500, + validWidth: 2000, + }); + assertEquals(errors.length, 1); + assertEquals(errors[0].includes("infinity"), true); +}); + +Deno.test("validateDimensions - rejects infinite height", () => { + const errors = validateDimensions({ + width: 500, + height: Infinity, + validWidth: 2000, + }); + assertEquals(errors.length, 1); + assertEquals(errors[0].includes("infinity"), true); +}); + +Deno.test("validateDimensions - reports multiple errors", () => { + const errors = validateDimensions({ + width: 3000, + height: 4000, + validWidth: 2000, + validHeight: 2000, + }); + assertEquals(errors.length, 2); +}); + +// Test getOutputPath +Deno.test("getOutputPath - generates default output path", () => { + const result = getOutputPath({ + input: "/path/to/image.png", + rationalAngle: { degrees: 45 }, + }); + assertEquals(result, "/path/to/image-tile-45.png"); +}); + +Deno.test("getOutputPath - handles custom output path", () => { + const result = getOutputPath({ + input: "/path/to/image.png", + rationalAngle: { degrees: 45 }, + output: "/custom/output.png", + }); + assertEquals(result, "/custom/output.png"); +}); + +Deno.test("getOutputPath - handles different extensions", () => { + const result = getOutputPath({ + input: "/path/to/image.jpg", + rationalAngle: { degrees: 26.565 }, + }); + assertEquals(result, "/path/to/image-tile-26.565.jpg"); +}); + +Deno.test("getOutputPath - handles files in current directory", () => { + const result = getOutputPath({ + input: "image.png", + rationalAngle: { degrees: 90 }, + }); + assertEquals(result, "./image-tile-90.png"); +}); + +Deno.test("getOutputPath - handles negative angles", () => { + const result = getOutputPath({ + input: "/path/to/image.png", + rationalAngle: { degrees: -45 }, + }); + assertEquals(result, "/path/to/image-tile--45.png"); +}); + +// Test RATIONAL_ANGLES constant +Deno.test("RATIONAL_ANGLES - contains expected number of angles", () => { + assertEquals(RATIONAL_ANGLES.length, 29); +}); + +Deno.test("RATIONAL_ANGLES - first angle is 0 degrees", () => { + assertEquals(RATIONAL_ANGLES[0].degrees, 0); + assertEquals(RATIONAL_ANGLES[0].m, 0); + assertEquals(RATIONAL_ANGLES[0].n, 1); +}); + +Deno.test("RATIONAL_ANGLES - contains 45 degree angle", () => { + const angle45 = RATIONAL_ANGLES.find((a) => a.degrees === 45); + assertEquals(angle45?.m, 1); + assertEquals(angle45?.n, 1); +}); + +Deno.test("RATIONAL_ANGLES - contains 90 degree angle", () => { + const angle90 = RATIONAL_ANGLES.find((a) => a.degrees === 90); + assertEquals(angle90?.m, 1); + assertEquals(angle90?.n, 0); +}); + +Deno.test("RATIONAL_ANGLES - all angles have required properties", () => { + RATIONAL_ANGLES.forEach((angle) => { + assertEquals(typeof angle.degrees, "number"); + assertEquals(typeof angle.m, "number"); + assertEquals(typeof angle.n, "number"); + assertEquals(typeof angle.label, "string"); + }); +}); From 455524fcfb019b608f0a5200eddc16f583e4b1ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:07:01 +0000 Subject: [PATCH 03/17] Add .gitignore and update README with testing instructions Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- .gitignore | 22 ++++++++++++++++++++++ README.md | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22aa5fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Deno +.deno/ +deno.lock + +# Build outputs +*.exe +tile-maker + +# Logs +*.log + +# OS files +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ + +# Test artifacts +coverage/ +*.lcov diff --git a/README.md b/README.md index 3471b24..5079401 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,25 @@ The output file will be saved in the same directory as the input with the angle deno run -A main.ts ``` +### Running Tests + +Run the test suite: +```sh +deno task test +# or +deno test --allow-read --allow-write +``` + +Check code formatting: +```sh +deno fmt +``` + +Run the linter: +```sh +deno lint +``` + --- ## CLI Usage From e2ebc8b371bef11b5320ee981be32f419ccbc3bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:07:48 +0000 Subject: [PATCH 04/17] Address code review feedback - improve test descriptions Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- src/lib_test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib_test.ts b/src/lib_test.ts index 4fd8f24..abd0e55 100644 --- a/src/lib_test.ts +++ b/src/lib_test.ts @@ -148,7 +148,7 @@ Deno.test("calculateTileDimensions - handles 26.565 degrees (1:2 ratio)", () => assertEquals(result.height > 0, true); }); -Deno.test("calculateTileDimensions - handles negative m value", () => { +Deno.test("calculateTileDimensions - negative m produces same dimensions as positive m", () => { const result = calculateTileDimensions(100, 100, { m: -1, n: 1 }); // Negative m should give same dimensions as positive (absolute value used) const positive = calculateTileDimensions(100, 100, { m: 1, n: 1 }); @@ -273,11 +273,12 @@ Deno.test("getOutputPath - handles files in current directory", () => { assertEquals(result, "./image-tile-90.png"); }); -Deno.test("getOutputPath - handles negative angles", () => { +Deno.test("getOutputPath - handles negative angles with double dash", () => { const result = getOutputPath({ input: "/path/to/image.png", rationalAngle: { degrees: -45 }, }); + // Note: negative angles result in double dash in filename (e.g., -tile--45) assertEquals(result, "/path/to/image-tile--45.png"); }); From 65ead1e1388685df54db15628ec2a463d6e5be17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:08:53 +0000 Subject: [PATCH 05/17] Add explicit permissions to GitHub Actions workflow Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5804c49..ce87eed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,9 @@ jobs: test: runs-on: ubuntu-latest + permissions: + contents: read + steps: - name: Checkout code uses: actions/checkout@v4 From ce42c01ccd6c7a311689d98b0f4dafe931ed5e3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:51:24 +0000 Subject: [PATCH 06/17] Remove master branch and add --allow-env flag for Sharp Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- .github/workflows/test.yml | 6 +++--- deno.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ce87eed..2b7a2b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Tests on: pull_request: - branches: [ main, master ] + branches: [ main ] push: - branches: [ main, master ] + branches: [ main ] jobs: test: @@ -23,7 +23,7 @@ jobs: deno-version: v2.x - name: Run tests - run: deno test --allow-read --allow-write + run: deno test --allow-read --allow-write --allow-env - name: Check formatting run: deno fmt --check diff --git a/deno.json b/deno.json index 11658fd..c98182b 100644 --- a/deno.json +++ b/deno.json @@ -7,7 +7,7 @@ }, "tasks": { "compile": "deno compile --allow-all main.ts", - "test": "deno test --allow-read --allow-write" + "test": "deno test --allow-read --allow-write --allow-env" }, "version": "0.0.1" } From 44f2b9075aaf17dc3f536c8365d9aa0c0d81c302 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:57:28 +0000 Subject: [PATCH 07/17] Add --allow-ffi flag for Sharp native bindings Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- deno.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b7a2b9..86d4a2b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: deno-version: v2.x - name: Run tests - run: deno test --allow-read --allow-write --allow-env + run: deno test --allow-read --allow-write --allow-env --allow-ffi - name: Check formatting run: deno fmt --check diff --git a/deno.json b/deno.json index c98182b..707cbd3 100644 --- a/deno.json +++ b/deno.json @@ -7,7 +7,7 @@ }, "tasks": { "compile": "deno compile --allow-all main.ts", - "test": "deno test --allow-read --allow-write --allow-env" + "test": "deno test --allow-read --allow-write --allow-env --allow-ffi" }, "version": "0.0.1" } From 0ba863ee44201faa596314b21c3342d045f908b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:03:47 +0000 Subject: [PATCH 08/17] Fix failing tests: normalize angles to -180/180 and prevent duplicate infinity errors Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- src/lib.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index dead686..3ccfbd3 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -48,9 +48,10 @@ export const RATIONAL_ANGLES: angleEntry[] = [ * Find the closest rational angle to the given angle */ export function findClosestRationalAngle(angle: number): angleEntry { - // Normalize angle to 0-360 range for matching + // Normalize angle to -180 to 180 range for matching let normalizedAngle = angle % 360; - if (normalizedAngle < 0) normalizedAngle += 360; + if (normalizedAngle > 180) normalizedAngle -= 360; + if (normalizedAngle < -180) normalizedAngle += 360; return RATIONAL_ANGLES.reduce((closest, ra) => Math.abs(normalizedAngle - ra.degrees) < @@ -162,6 +163,7 @@ export function validateDimensions({ if (!isFinite(width) || !isFinite(height)) { errors.push("Tile dimensions resulted in infinity"); + return errors; // Return early to avoid redundant size checks } if (width > validWidth) { From 520bcad06362d1f86e3cd96c6c23cfd0f828ff17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:17:39 +0000 Subject: [PATCH 09/17] Fix formatting and linting issues Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- README.md | 143 +++++++++++++-------- deno.lock | 326 ------------------------------------------------ main.ts | 21 ++-- src/lib.ts | 10 +- src/lib_test.ts | 8 +- 5 files changed, 109 insertions(+), 399 deletions(-) delete mode 100644 deno.lock diff --git a/README.md b/README.md index 5079401..1a6df1a 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,15 @@ ## Problem Statement -When generating tiles for websites, designers and developers often encounter issues when rotating tiles. Common solutions involve less-than-ideal hacks, such as adding pseudoelements, scaling, and rotating them to achieve the desired effect. These workarounds can complicate code, reduce performance, and make maintenance harder. +When generating tiles for websites, designers and developers often encounter +issues when rotating tiles. Common solutions involve less-than-ideal hacks, such +as adding pseudoelements, scaling, and rotating them to achieve the desired +effect. These workarounds can complicate code, reduce performance, and make +maintenance harder. -WTC Tile Maker aims to solve this by providing a robust, programmatic solution for generating and manipulating tiles, including rotation, without relying on CSS hacks. +WTC Tile Maker aims to solve this by providing a robust, programmatic solution +for generating and manipulating tiles, including rotation, without relying on +CSS hacks. --- @@ -24,10 +30,10 @@ deno run -A main.ts list # Help deno run -A main.ts help - ``` -The output file will be saved in the same directory as the input with the angle appended to the filename (e.g., `input-tile-45.png`). +The output file will be saved in the same directory as the input with the angle +appended to the filename (e.g., `input-tile-45.png`). --- @@ -53,6 +59,7 @@ The output file will be saved in the same directory as the input with the angle ### Running Tests Run the test suite: + ```sh deno task test # or @@ -60,11 +67,13 @@ deno test --allow-read --allow-write ``` Check code formatting: + ```sh deno fmt ``` Run the linter: + ```sh deno lint ``` @@ -99,39 +108,40 @@ deno lint ## Available Rational Angles -These are the angles where `tan(θ) = m/n` for small integers m, n. These angles produce periodic tilings when used for rotation. - -| Index | Label | m | n | -| ----- | ---------------------- | --- | --- | -| 0 | 0° | 0 | 1 | -| 1 | 90° | 1 | 0 | -| 2 | -90° | -1 | 0 | -| 3 | 45° | 1 | 1 | -| 4 | -45° | -1 | 1 | -| 5 | 26.565° (arctan 1/2) | 1 | 2 | -| 6 | -26.565° (arctan -1/2) | -1 | 2 | -| 7 | 63.435° (arctan 2) | 2 | 1 | -| 8 | -63.435° (arctan -2) | -2 | 1 | -| 9 | 18.435° (arctan 1/3) | 1 | 3 | -| 10 | -18.435° (arctan -1/3) | -1 | 3 | -| 11 | 71.565° (arctan 3) | 3 | 1 | -| 12 | -71.565° (arctan -3) | -3 | 1 | -| 13 | 14.036° (arctan 1/4) | 1 | 4 | -| 14 | -14.036° (arctan -1/4) | -1 | 4 | -| 15 | 75.964° (arctan 4) | 4 | 1 | -| 16 | -75.964° (arctan -4) | -4 | 1 | -| 17 | 33.690° (arctan 2/3) | 2 | 3 | -| 18 | -33.690° (arctan -2/3) | -2 | 3 | -| 19 | 56.310° (arctan 3/2) | 3 | 2 | -| 20 | -56.310° (arctan -3/2) | -3 | 2 | -| 21 | 36.870° (arctan 3/4) | 3 | 4 | -| 22 | -36.870° (arctan -3/4) | -3 | 4 | -| 23 | 53.130° (arctan 4/3) | 4 | 3 | -| 24 | -53.130° (arctan -4/3) | -4 | 3 | -| 25 | 11.310° (arctan 1/5) | 1 | 5 | -| 26 | -11.310° (arctan -1/5) | -1 | 5 | -| 27 | 78.690° (arctan 5) | 5 | 1 | -| 28 | -78.690° (arctan -5) | -5 | 1 | +These are the angles where `tan(θ) = m/n` for small integers m, n. These angles +produce periodic tilings when used for rotation. + +| Index | Label | m | n | +| ----- | ---------------------- | -- | - | +| 0 | 0° | 0 | 1 | +| 1 | 90° | 1 | 0 | +| 2 | -90° | -1 | 0 | +| 3 | 45° | 1 | 1 | +| 4 | -45° | -1 | 1 | +| 5 | 26.565° (arctan 1/2) | 1 | 2 | +| 6 | -26.565° (arctan -1/2) | -1 | 2 | +| 7 | 63.435° (arctan 2) | 2 | 1 | +| 8 | -63.435° (arctan -2) | -2 | 1 | +| 9 | 18.435° (arctan 1/3) | 1 | 3 | +| 10 | -18.435° (arctan -1/3) | -1 | 3 | +| 11 | 71.565° (arctan 3) | 3 | 1 | +| 12 | -71.565° (arctan -3) | -3 | 1 | +| 13 | 14.036° (arctan 1/4) | 1 | 4 | +| 14 | -14.036° (arctan -1/4) | -1 | 4 | +| 15 | 75.964° (arctan 4) | 4 | 1 | +| 16 | -75.964° (arctan -4) | -4 | 1 | +| 17 | 33.690° (arctan 2/3) | 2 | 3 | +| 18 | -33.690° (arctan -2/3) | -2 | 3 | +| 19 | 56.310° (arctan 3/2) | 3 | 2 | +| 20 | -56.310° (arctan -3/2) | -3 | 2 | +| 21 | 36.870° (arctan 3/4) | 3 | 4 | +| 22 | -36.870° (arctan -3/4) | -3 | 4 | +| 23 | 53.130° (arctan 4/3) | 4 | 3 | +| 24 | -53.130° (arctan -4/3) | -4 | 3 | +| 25 | 11.310° (arctan 1/5) | 1 | 5 | +| 26 | -11.310° (arctan -1/5) | -1 | 5 | +| 27 | 78.690° (arctan 5) | 5 | 1 | +| 28 | -78.690° (arctan -5) | -5 | 1 | You can also run `deno -A main.ts list` to see these angles. @@ -139,7 +149,8 @@ You can also run `deno -A main.ts list` to see these angles. ## Recommended Input Aspect Ratios -The output tile size depends on how well the input dimensions align with the m:n ratios of the angles. For best results, use these aspect ratios: +The output tile size depends on how well the input dimensions align with the m:n +ratios of the angles. For best results, use these aspect ratios: ### Best Aspect Ratios @@ -155,7 +166,9 @@ The output tile size depends on how well the input dimensions align with the m:n ### Ratios to Avoid -Aspect ratios with **prime numbers > 5** (like 7:3, 11:4, 13:8) or **irrational proportions** will produce larger outputs because the GCD calculations yield smaller divisors. +Aspect ratios with **prime numbers > 5** (like 7:3, 11:4, 13:8) or **irrational +proportions** will produce larger outputs because the GCD calculations yield +smaller divisors. --- @@ -175,7 +188,8 @@ deno -A main.ts list ### Generate a Tile -Generate a rotated tile from `Checker.png` using angle index 27, and max size 6000: +Generate a rotated tile from `Checker.png` using angle index 27, and max size +6000: | Step | Description | Example | | ------- | ------------------------------------------------------------------------------------- | ------------------------------------------------- | @@ -183,7 +197,8 @@ Generate a rotated tile from `Checker.png` using angle index 27, and max size 60 | Command | Command to generate a rotated tile using angle index 27, margin 3, and max size 6000. | `generate -lv -a 27 -s 6000 Checker.png` | | Output | Resulting seamlessly tileable image after processing. | | -This will also work with images of different aspect ratios (like 3×2). For example. this enerates a rotated tile from `3x2-checker.png` using 45°: +This will also work with images of different aspect ratios (like 3×2). For +example. this enerates a rotated tile from `3x2-checker.png` using 45°: | Step | Description | Example | | ------- | ------------------------------------------------ | ------------------------------------------------- | @@ -191,11 +206,17 @@ This will also work with images of different aspect ratios (like 3×2). For exam | Command | Command to generate a rotated tile at 45°. | `generate -d 45 3x2-checker.png` | | Output | Seamlessly tileable image after rotating at 45°. | | -_N.B._ Notice, here, how there seems to be a piece missing! This is because the default tile margin is too small to make these rotated tiles cover the output dimensions. This command should be updated to `generate -m 2 -d 45 3x2-checker.png`. +_N.B._ Notice, here, how there seems to be a piece missing! This is because the +default tile margin is too small to make these rotated tiles cover the output +dimensions. This command should be updated to +`generate -m 2 -d 45 3x2-checker.png`. #### A warning about sizes and appropriate aspect ratios -Sometimes, a combination of input size and rotation will produce an output that is either too large or creating an appropriate output tile is just beyong the capabilities of this math. In this case you should likely fall back to hacky methods or get a tile produces in a more predictable aspect ratio. +Sometimes, a combination of input size and rotation will produce an output that +is either too large or creating an appropriate output tile is just beyong the +capabilities of this math. In this case you should likely fall back to hacky +methods or get a tile produces in a more predictable aspect ratio. ### Generate with a Specific Degree @@ -211,8 +232,11 @@ The tool will find the closest rational angle and generate the tile accordingly. ## Common Issues -- **allowLargeBuffers**: If you receive an error about `allowLargeBuffers`, use the `-l` flag. Warning: this may affect performance, but enables processing of narrow rotation / large image combinations. -- **Missing pieces**: If the output appears to be missing pieces around the edges, try increasing the `-m` (tileMargin) value, e.g., `-m 3`. +- **allowLargeBuffers**: If you receive an error about `allowLargeBuffers`, use + the `-l` flag. Warning: this may affect performance, but enables processing of + narrow rotation / large image combinations. +- **Missing pieces**: If the output appears to be missing pieces around the + edges, try increasing the `-m` (tileMargin) value, e.g., `-m 3`. --- @@ -220,11 +244,15 @@ The tool will find the closest rational angle and generate the tile accordingly. ### The Problem with Rotating Tiles -When you rotate a regular tiling pattern (like a checkerboard) by an arbitrary angle, the result doesn't tile seamlessly in the x and y direction anymore. +When you rotate a regular tiling pattern (like a checkerboard) by an arbitrary +angle, the result doesn't tile seamlessly in the x and y direction anymore. ### Rational Angles -The key insight is that **rational angles**—angles where `tan(θ) = m/n` for small integers m and n—produce periodic tilings when used for rotation. At these specific angles, the rotated grid aligns back to integer positions, allowing seamless repetition. +The key insight is that **rational angles**—angles where `tan(θ) = m/n` for +small integers m and n—produce periodic tilings when used for rotation. At these +specific angles, the rotated grid aligns back to integer positions, allowing +seamless repetition. For example: @@ -234,21 +262,28 @@ For example: ### Tile Dimension Calculation -For a rational angle with `tan(θ) = m/n`, the rotated pattern repeats at intervals of `√(m² + n²)` times the original period. +For a rational angle with `tan(θ) = m/n`, the rotated pattern repeats at +intervals of `√(m² + n²)` times the original period. To create a seamlessly tileable output: -- The output dimensions are calculated as: `input dimensions × √(m² + n²) / gcd(m, n)` +- The output dimensions are calculated as: + `input dimensions × √(m² + n²) / gcd(m, n)` - This ensures the rotated grid aligns back to integer positions ### The Process 1. **Input**: A tileable source image (e.g., a checkerboard pattern) -2. **Tile**: The source image is tiled into a larger canvas to provide enough material for the rotation +2. **Tile**: The source image is tiled into a larger canvas to provide enough + material for the rotation 3. **Rotate**: The tiled canvas is rotated by the selected rational angle -4. **Crop**: A precisely calculated region is extracted that will tile seamlessly -5. **Output**: The resulting image can be used as a CSS background and will tile perfectly at the rotated angle - -This approach eliminates the need for CSS hacks like pseudoelements with `transform: rotate()` and `scale()`, resulting in cleaner code, better performance, and more predictable rendering across browsers. +4. **Crop**: A precisely calculated region is extracted that will tile + seamlessly +5. **Output**: The resulting image can be used as a CSS background and will tile + perfectly at the rotated angle + +This approach eliminates the need for CSS hacks like pseudoelements with +`transform: rotate()` and `scale()`, resulting in cleaner code, better +performance, and more predictable rendering across browsers. --- diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 52e04a2..0000000 --- a/deno.lock +++ /dev/null @@ -1,326 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@std/cli@*": "1.0.27", - "jsr:@std/cli@^1.0.27": "1.0.27", - "jsr:@std/fmt@^1.0.9": "1.0.9", - "jsr:@std/fs@*": "1.0.22", - "jsr:@std/fs@^1.0.22": "1.0.22", - "jsr:@std/internal@^1.0.12": "1.0.12", - "jsr:@std/path@^1.1.4": "1.1.4", - "npm:@imagemagick/magick-wasm@0.0.31": "0.0.31", - "npm:@types/node@*": "24.2.0", - "npm:sharp@~0.34.5": "0.34.5" - }, - "jsr": { - "@std/cli@1.0.27": { - "integrity": "eba97edd0891871a7410e835dd94b3c260c709cca5983df2689c25a71fbe04de", - "dependencies": [ - "jsr:@std/fmt", - "jsr:@std/internal" - ] - }, - "@std/fmt@1.0.9": { - "integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0" - }, - "@std/fs@1.0.22": { - "integrity": "de0f277a58a867147a8a01bc1b181d0dfa80bfddba8c9cf2bacd6747bcec9308", - "dependencies": [ - "jsr:@std/internal", - "jsr:@std/path" - ] - }, - "@std/internal@1.0.12": { - "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" - }, - "@std/path@1.1.4": { - "integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5", - "dependencies": [ - "jsr:@std/internal" - ] - } - }, - "npm": { - "@emnapi/runtime@1.8.1": { - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dependencies": [ - "tslib" - ] - }, - "@imagemagick/magick-wasm@0.0.31": { - "integrity": "sha512-QNivAUxSaItuiY8ziI/vRy6TtoecD7TOsD1LGZCG3wv8lfbdGbIj2QiJk0FlGkGwAVR966NlD3mkxPNvQrvq0w==" - }, - "@img/colour@1.0.0": { - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==" - }, - "@img/sharp-darwin-arm64@0.34.5": { - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "optionalDependencies": [ - "@img/sharp-libvips-darwin-arm64" - ], - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@img/sharp-darwin-x64@0.34.5": { - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "optionalDependencies": [ - "@img/sharp-libvips-darwin-x64" - ], - "os": ["darwin"], - "cpu": ["x64"] - }, - "@img/sharp-libvips-darwin-arm64@1.2.4": { - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@img/sharp-libvips-darwin-x64@1.2.4": { - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@img/sharp-libvips-linux-arm64@1.2.4": { - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-libvips-linux-arm@1.2.4": { - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@img/sharp-libvips-linux-ppc64@1.2.4": { - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@img/sharp-libvips-linux-riscv64@1.2.4": { - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@img/sharp-libvips-linux-s390x@1.2.4": { - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "os": ["linux"], - "cpu": ["s390x"] - }, - "@img/sharp-libvips-linux-x64@1.2.4": { - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-libvips-linuxmusl-arm64@1.2.4": { - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-libvips-linuxmusl-x64@1.2.4": { - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-linux-arm64@0.34.5": { - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-arm64" - ], - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-linux-arm@0.34.5": { - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-arm" - ], - "os": ["linux"], - "cpu": ["arm"] - }, - "@img/sharp-linux-ppc64@0.34.5": { - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-ppc64" - ], - "os": ["linux"], - "cpu": ["ppc64"] - }, - "@img/sharp-linux-riscv64@0.34.5": { - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-riscv64" - ], - "os": ["linux"], - "cpu": ["riscv64"] - }, - "@img/sharp-linux-s390x@0.34.5": { - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-s390x" - ], - "os": ["linux"], - "cpu": ["s390x"] - }, - "@img/sharp-linux-x64@0.34.5": { - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "optionalDependencies": [ - "@img/sharp-libvips-linux-x64" - ], - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-linuxmusl-arm64@0.34.5": { - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "optionalDependencies": [ - "@img/sharp-libvips-linuxmusl-arm64" - ], - "os": ["linux"], - "cpu": ["arm64"] - }, - "@img/sharp-linuxmusl-x64@0.34.5": { - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "optionalDependencies": [ - "@img/sharp-libvips-linuxmusl-x64" - ], - "os": ["linux"], - "cpu": ["x64"] - }, - "@img/sharp-wasm32@0.34.5": { - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "dependencies": [ - "@emnapi/runtime" - ], - "cpu": ["wasm32"] - }, - "@img/sharp-win32-arm64@0.34.5": { - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@img/sharp-win32-ia32@0.34.5": { - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@img/sharp-win32-x64@0.34.5": { - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "os": ["win32"], - "cpu": ["x64"] - }, - "@types/node@24.2.0": { - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", - "dependencies": [ - "undici-types" - ] - }, - "detect-libc@2.1.2": { - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" - }, - "semver@7.7.4": { - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "bin": true - }, - "sharp@0.34.5": { - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dependencies": [ - "@img/colour", - "detect-libc", - "semver" - ], - "optionalDependencies": [ - "@img/sharp-darwin-arm64", - "@img/sharp-darwin-x64", - "@img/sharp-libvips-darwin-arm64", - "@img/sharp-libvips-darwin-x64", - "@img/sharp-libvips-linux-arm", - "@img/sharp-libvips-linux-arm64", - "@img/sharp-libvips-linux-ppc64", - "@img/sharp-libvips-linux-riscv64", - "@img/sharp-libvips-linux-s390x", - "@img/sharp-libvips-linux-x64", - "@img/sharp-libvips-linuxmusl-arm64", - "@img/sharp-libvips-linuxmusl-x64", - "@img/sharp-linux-arm", - "@img/sharp-linux-arm64", - "@img/sharp-linux-ppc64", - "@img/sharp-linux-riscv64", - "@img/sharp-linux-s390x", - "@img/sharp-linux-x64", - "@img/sharp-linuxmusl-arm64", - "@img/sharp-linuxmusl-x64", - "@img/sharp-wasm32", - "@img/sharp-win32-arm64", - "@img/sharp-win32-ia32", - "@img/sharp-win32-x64" - ], - "scripts": true - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "undici-types@7.10.0": { - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" - } - }, - "remote": { - "https://deno.land/x/imagemagick_deno@0.0.14/mod.ts": "6b58c767d2308488597c3660e7ff399ede244198d7903900fa43a49cf93c7796", - "https://deno.land/x/imagemagick_deno@0.0.14/src/alpha-option.ts": "749a9f3309e491ec09a1d6bc50ce95d9733887d9f57c6863c4ff1c7e9610227b", - "https://deno.land/x/imagemagick_deno@0.0.14/src/auto-threshold-method.ts": "bb08a00046137e441930e56190b6db10c5fe657cb0a6142cd565a40b1c4250a2", - "https://deno.land/x/imagemagick_deno@0.0.14/src/channels.ts": "a15c5f2d278ee7961b4b425c97cfc1fc62c1955c87706c74743fa6215fa482c9", - "https://deno.land/x/imagemagick_deno@0.0.14/src/color-space.ts": "3d9a60f3a8bfefea8d9525572d7bd6214530c69688e8799dceb492b7797d1d0a", - "https://deno.land/x/imagemagick_deno@0.0.14/src/composite-operator.ts": "f4b5046415c5965d53b17a9e441a42d87e8477b7c158704abd417d6ac10f3ea0", - "https://deno.land/x/imagemagick_deno@0.0.14/src/defines/define.ts": "645fb3a06424ed750250212ac8762ba2ea97c4e4fdbda8aedf21734cbaf4833c", - "https://deno.land/x/imagemagick_deno@0.0.14/src/defines/defines.ts": "fc8e12475e11a30f9f6f9c2b5e2fba94b01d65135654b97694da915d40fae2fe", - "https://deno.land/x/imagemagick_deno@0.0.14/src/distort-method.ts": "13819e00ccb6a636af9ece5d11dfce9451e578d46c94e1f528b0ae5da7721985", - "https://deno.land/x/imagemagick_deno@0.0.14/src/drawables/drawable.ts": "61b40233ea3c28664c2f8dfd8d794772d8a7a779f4228060efd41b0d44762521", - "https://deno.land/x/imagemagick_deno@0.0.14/src/drawables/drawing-wand.ts": "3c495d8cf37eac2c3cc0e840a13aed24457d37414528077b06d3f8aa76cd4cde", - "https://deno.land/x/imagemagick_deno@0.0.14/src/error-metric.ts": "fafe44d95312b0e9dd6e5d6d3efd536764468a4b80e3dc3d7d7efc33a40fb871", - "https://deno.land/x/imagemagick_deno@0.0.14/src/evaluate-operator.ts": "c05d51cb193d95ce0432dee914465cbafc35026ea1102cc48f431571bfb67260", - "https://deno.land/x/imagemagick_deno@0.0.14/src/filter-type.ts": "face0109ae9e56125b778a8842384031d6e0bd688dfcf96c0861f2fd8bb27225", - "https://deno.land/x/imagemagick_deno@0.0.14/src/gravity.ts": "ed99d33e3775c510c0a29fb330ca5ac9445e41dd3644507186c25cc32eb8634a", - "https://deno.land/x/imagemagick_deno@0.0.14/src/image-magick.ts": "1ae1c396bb9539b7918ac40c3710d13475f8e9d3d0a5a9216c087c1b95fab4ba", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/exception/exception.ts": "2c1e1d5f6df4fcaef50403ed18f5ebdf560a5e764944d569db406e97f76f2aae", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/geometry-flags.ts": "56bbc3f668db2e67f607cd1c08e07f51ded80a8c402efb0b6cd4ad98d0f69d19", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/magick-rectangle.ts": "ffffcd9ebffe20f871396af22c9f5acb332b5d503a5b21200a94e1e61e4e68b3", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/native-instance.ts": "25b42d5db19439ba7016821bf363d85cabe6fa0784e9ec0e84a975f9ca4a850c", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/native/array.ts": "bcfa4f33246feaf3e1cfd219f188819caf2ed84562f986f508b1ba8beecd28fb", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/native/string.ts": "a3985bf82a8c0e0507001ab1af72c817f6a9f3fffcba532c5504b75102107ce3", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/pointer/pointer.ts": "d866febf67a2d72678e6bd0fd70f751622348c3c2c4ad0aba42dbd750c4f8526", - "https://deno.land/x/imagemagick_deno@0.0.14/src/internal/string-info.ts": "6121081f0382fdfe259bb6c95655b1626cc68af778ad91af437daa8c55965575", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-color.ts": "6e849e94f3183d86f44d55f4646af394d0d3573fbce8b26b6d6bfbda03dcaf5c", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-colors.ts": "c3a4cdbbca0ebce9386ae71f835118847d8770573efcb63a35c54242aa156f90", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-error-severity.ts": "160e5f07bad67542c9c95a8ec61e70f294333bf7f3c463419dc4fadfacdbdbf6", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-error.ts": "5a515e203373ef48903bda51635e04f232bf3144eaee48c66d65df1e705346d4", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-format-info.ts": "3c20c60a0eab8883cf7268c6993855718de5c3b53cab36097af611ac1a5219f9", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-format.ts": "b5fa87a4dcc9ccdc1465fbee8cc3a6999767c94d67ad0d86b7998af26bf8c309", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-geometry.ts": "c41ec925e2cba2f4a07ab278de87d533aac282f68b038d6ca7075fc09570f759", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-image-collection.ts": "7a1249264e27e9ae7d0e416d6dffe057750f212819c7973f5666f3a80e274e4a", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick-image.ts": "e712e9f6d6f87a426e8a0cf6b467d100d10ee5ca70d81b145c370a1007ca8b27", - "https://deno.land/x/imagemagick_deno@0.0.14/src/magick.ts": "990bbb125a908afd71bba8b9601704f64abfe68b861e906ec7495f87b2f4c776", - "https://deno.land/x/imagemagick_deno@0.0.14/src/orientation-type.ts": "a5c48feec25d432e5c3ad3ed76c929a7960836d3ab1012525c0f7883e4f46c30", - "https://deno.land/x/imagemagick_deno@0.0.14/src/paint-method.ts": "0178827b90549bf587e8ae9e2757cb96607b1fffa5c05d0534a8de136a346d29", - "https://deno.land/x/imagemagick_deno@0.0.14/src/percentage.ts": "00240337512949c97e407b006cdd025af5fc6db600adce9ca6193ab61e326291", - "https://deno.land/x/imagemagick_deno@0.0.14/src/pixel-channel.ts": "8039ee75caf150f4f817c49a12f025dd7ca01e263dfd3bd882a55ad0eb17086c", - "https://deno.land/x/imagemagick_deno@0.0.14/src/pixel-interpolate-method.ts": "d2c62675acb5d8fffca3e2c91c9a35bfebec62f2424268e5e240f9f17f57d356", - "https://deno.land/x/imagemagick_deno@0.0.14/src/pixels/pixel-collection.ts": "e21b9e3ecd31cd94f7a57939f565f9df3d3009e68fe8a03d8760780096d2e457", - "https://deno.land/x/imagemagick_deno@0.0.14/src/point.ts": "f664938d0f39eadd41fe5eb8ca81c52b59a7f7138539afea3ddc863d25a4a935", - "https://deno.land/x/imagemagick_deno@0.0.14/src/profiles/image-profile.ts": "ea1bb6406430a03cf9263a40260fcd8f99bcc14fa3629206fcbcd2679c94b4a2", - "https://deno.land/x/imagemagick_deno@0.0.14/src/quantum.ts": "7e92f9cf73fc6ec89df48ab4c462339fad580f0b6a1e1009d3c5a5cb599dc3ed", - "https://deno.land/x/imagemagick_deno@0.0.14/src/settings/distort-settings.ts": "cdb352260b90a140191c222bafde0740114062822400bdf89709bef1c2f40563", - "https://deno.land/x/imagemagick_deno@0.0.14/src/settings/drawing-settings.ts": "40eb95416367982afd13de2138dd06527a937e1459d6e374f8d5f8e7fa0deb7e", - "https://deno.land/x/imagemagick_deno@0.0.14/src/settings/magick-read-settings.ts": "95417d00701245c7c5bd202cd0d4f02546ae01a77ecc9d0523c97a84ddf862d5", - "https://deno.land/x/imagemagick_deno@0.0.14/src/settings/magick-settings.ts": "8fb86c3bd354023d8026624bb4bca78501ae92adea6287f97e687b8b762095d1", - "https://deno.land/x/imagemagick_deno@0.0.14/src/settings/native-drawing-settings.ts": "b6a04740bd9261a478ff44b631cb039c9f909ed2243cae4e6ae6b3ebcce6dc21", - "https://deno.land/x/imagemagick_deno@0.0.14/src/settings/native-magick-settings.ts": "859787363161a2c6a693ab5b475859f9e9b02dfb7128215b0c03a68839892d1c", - "https://deno.land/x/imagemagick_deno@0.0.14/src/virtual-pixel-method.ts": "ae2f0520e05b382299e4d41f4d7e2c67baf727ef7c816037e601c978948b1451", - "https://deno.land/x/imagemagick_deno@0.0.14/src/wasm/magick.ts": "b5ec7d6c3c7379f8f9ba0c23238f7024aa35f3a15edb2d1cbca4ccc44a186ac9", - "https://deno.land/x/imagemagick_deno@0.0.14/src/wasm/magick_native.js": "e7f2cfe41531d94286bf839783c08cc0b4b9c89e6ad7bbd0e2a4dcf70086ca75", - "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts": "124d7f045429f6e6c486b86e72d025410d09576bc0d8075e69f97118a1a33413" - }, - "workspace": { - "dependencies": [ - "jsr:@std/cli@^1.0.27", - "jsr:@std/fs@^1.0.22", - "jsr:@std/path@^1.1.4", - "npm:sharp@~0.34.5" - ] - } -} diff --git a/main.ts b/main.ts index ee01295..b10f918 100644 --- a/main.ts +++ b/main.ts @@ -3,14 +3,13 @@ import { relative } from "@std/path"; import { existsSync } from "@std/fs/exists"; import { + calculateTileDimensions, findClosestRationalAngle, + generateTile, getImageProperties, - calculateTileDimensions, getOutputPath, - generateTile, RATIONAL_ANGLES, validateDimensions, - GenerateTileOptions, } from "./src/lib.ts"; // Define a proper interface for parsed arguments @@ -241,19 +240,19 @@ type Command = typeof VALID_COMMANDS[number]; // Type guard for valid commands function isValidCommand(cmd: unknown): cmd is Command { - return typeof cmd === "string" && + return typeof cmd === "string" && (VALID_COMMANDS as readonly string[]).includes(cmd); } async function main() { const commandInput = args._[0] ?? "help"; - + if (!isValidCommand(commandInput)) { - throw new Error(`Unknown command${args._[0] ? `: ${args._[0]}` : '.'}`); + throw new Error(`Unknown command${args._[0] ? `: ${args._[0]}` : "."}`); } - + const command = commandInput; - + switch (command) { case "help": help(); @@ -266,7 +265,7 @@ async function main() { if (typeof input !== "string") { throw new Error("Input file must be a string"); } - + await generate({ angleOption: args.angleOption, degrees: args.degrees, @@ -290,9 +289,9 @@ try { await main(); } catch (error) { if (error instanceof Error) { - if (error.message == "Input image exceeds pixel limit") + if (error.message == "Input image exceeds pixel limit") { console.log(`Error: ${error.message}. Try setting the -l flag.`); - else console.error(`Error: ${error.message}`); + } else console.error(`Error: ${error.message}`); } else { console.error(error); } diff --git a/src/lib.ts b/src/lib.ts index 7ccdc97..d92d800 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -55,9 +55,9 @@ export function findClosestRationalAngle(angle: number): AngleEntry { return RATIONAL_ANGLES.reduce((closest, ra) => Math.abs(normalizedAngle - ra.degrees) < - Math.abs(normalizedAngle - closest.degrees) + Math.abs(normalizedAngle - closest.degrees) ? ra - : closest, + : closest ); } @@ -228,7 +228,8 @@ export function getOutputPath({ const inputExt = extname(input as string); const inputBase = basename(input as string, inputExt); const inputDir = dirname(input as string); - const outputFileName = `${inputBase}-tile-${rationalAngle.degrees}${inputExt}`; + const outputFileName = + `${inputBase}-tile-${rationalAngle.degrees}${inputExt}`; return output ? (output as string) : `${inputDir}/${outputFileName}`; } @@ -270,12 +271,13 @@ export async function generateTile({ height: metadata.height * tileRepeat.y, }; - if (verbose) + if (verbose) { console.log( "Tiled dimensions and repeat counts:", tiledDimensions, tileRepeat, ); + } console.log("Starting tile generation, this may take a moment."); diff --git a/src/lib_test.ts b/src/lib_test.ts index abd0e55..e0a7e9f 100644 --- a/src/lib_test.ts +++ b/src/lib_test.ts @@ -1,12 +1,12 @@ -import { assertEquals, assertThrows } from "jsr:@std/assert"; +import { assertEquals } from "jsr:@std/assert"; import { + calculateTileDimensions, findClosestRationalAngle, gcd, - lcm, - calculateTileDimensions, - validateDimensions, getOutputPath, + lcm, RATIONAL_ANGLES, + validateDimensions, } from "./lib.ts"; // Test findClosestRationalAngle From ee117841d5db91c83c4f8c642d1db53404e96a7c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:17:57 +0000 Subject: [PATCH 10/17] Fix incorrect comment in findClosestRationalAngle Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- src/lib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.ts b/src/lib.ts index d92d800..59a18f4 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -48,7 +48,7 @@ export const RATIONAL_ANGLES: AngleEntry[] = [ * Find the closest rational angle to the given angle */ export function findClosestRationalAngle(angle: number): AngleEntry { - // Normalize angle to 0-360 range for matching + // Normalize angle to -180 to 180 range for matching let normalizedAngle = angle % 360; if (normalizedAngle > 180) normalizedAngle -= 360; if (normalizedAngle < -180) normalizedAngle += 360; From f6fd1ea19ce0eb56b9bb41a2c0a1d869449980f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:18:34 +0000 Subject: [PATCH 11/17] Fix typos in README Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a6df1a..1aacb90 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Generate a rotated tile from `Checker.png` using angle index 27, and max size | Output | Resulting seamlessly tileable image after processing. | | This will also work with images of different aspect ratios (like 3×2). For -example. this enerates a rotated tile from `3x2-checker.png` using 45°: +example. this generates a rotated tile from `3x2-checker.png` using 45°: | Step | Description | Example | | ------- | ------------------------------------------------ | ------------------------------------------------- | @@ -214,7 +214,7 @@ dimensions. This command should be updated to #### A warning about sizes and appropriate aspect ratios Sometimes, a combination of input size and rotation will produce an output that -is either too large or creating an appropriate output tile is just beyong the +is either too large or creating an appropriate output tile is just beyond the capabilities of this math. In this case you should likely fall back to hacky methods or get a tile produces in a more predictable aspect ratio. From f6ba586fe1bab1a6633698012d321fbb1916bf80 Mon Sep 17 00:00:00 2001 From: "Marlon U. Marcello" Date: Tue, 3 Mar 2026 09:19:31 -0800 Subject: [PATCH 12/17] chore: small docs tweaks Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- README.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86d4a2b..7cc8f19 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: deno-version: v2.x - name: Run tests - run: deno test --allow-read --allow-write --allow-env --allow-ffi + run: deno task test - name: Check formatting run: deno fmt --check diff --git a/README.md b/README.md index 1aacb90..0f8bc4d 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,7 @@ Run the test suite: ```sh deno task test -# or -deno test --allow-read --allow-write + ``` Check code formatting: From 9ec1ff43c722fe9180b0dd7b83110f36e67f2d90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:52:32 +0000 Subject: [PATCH 13/17] Apply code review feedback: fix permissions, typos, and path handling Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- .github/workflows/test.yml | 8 ++++---- README.md | 9 ++++----- deno.json | 3 ++- src/lib.ts | 4 ++-- src/lib_test.ts | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7cc8f19..ab86d4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,17 +2,17 @@ name: Tests on: pull_request: - branches: [ main ] + branches: [main] push: - branches: [ main ] + branches: [main] jobs: test: runs-on: ubuntu-latest - + permissions: contents: read - + steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/README.md b/README.md index 0f8bc4d..3f1ab80 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ When generating tiles for websites, designers and developers often encounter issues when rotating tiles. Common solutions involve less-than-ideal hacks, such -as adding pseudoelements, scaling, and rotating them to achieve the desired +as adding pseudo-elements, scaling, and rotating them to achieve the desired effect. These workarounds can complicate code, reduce performance, and make maintenance harder. @@ -62,7 +62,6 @@ Run the test suite: ```sh deno task test - ``` Check code formatting: @@ -197,7 +196,7 @@ Generate a rotated tile from `Checker.png` using angle index 27, and max size | Output | Resulting seamlessly tileable image after processing. | | This will also work with images of different aspect ratios (like 3×2). For -example. this generates a rotated tile from `3x2-checker.png` using 45°: +example, this generates a rotated tile from `3x2-checker.png` using 45°: | Step | Description | Example | | ------- | ------------------------------------------------ | ------------------------------------------------- | @@ -215,7 +214,7 @@ dimensions. This command should be updated to Sometimes, a combination of input size and rotation will produce an output that is either too large or creating an appropriate output tile is just beyond the capabilities of this math. In this case you should likely fall back to hacky -methods or get a tile produces in a more predictable aspect ratio. +methods or get a tile produced in a more predictable aspect ratio. ### Generate with a Specific Degree @@ -281,7 +280,7 @@ To create a seamlessly tileable output: 5. **Output**: The resulting image can be used as a CSS background and will tile perfectly at the rotated angle -This approach eliminates the need for CSS hacks like pseudoelements with +This approach eliminates the need for CSS hacks like pseudo-elements with `transform: rotate()` and `scale()`, resulting in cleaner code, better performance, and more predictable rendering across browsers. diff --git a/deno.json b/deno.json index 707cbd3..568896e 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,6 @@ { "imports": { + "@std/assert": "jsr:@std/assert@^1.0.19", "@std/cli": "jsr:@std/cli@^1.0.27", "@std/fs": "jsr:@std/fs@^1.0.22", "@std/path": "jsr:@std/path@^1.1.4", @@ -7,7 +8,7 @@ }, "tasks": { "compile": "deno compile --allow-all main.ts", - "test": "deno test --allow-read --allow-write --allow-env --allow-ffi" + "test": "deno test --allow-read --allow-env --allow-ffi" }, "version": "0.0.1" } diff --git a/src/lib.ts b/src/lib.ts index 59a18f4..17986ed 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,4 +1,4 @@ -import { basename, dirname, extname } from "@std/path"; +import { basename, dirname, extname, join } from "@std/path"; import sharp from "sharp"; export interface AngleEntry { @@ -230,7 +230,7 @@ export function getOutputPath({ const inputDir = dirname(input as string); const outputFileName = `${inputBase}-tile-${rationalAngle.degrees}${inputExt}`; - return output ? (output as string) : `${inputDir}/${outputFileName}`; + return output ? (output as string) : join(inputDir, outputFileName); } export interface GenerateTileOptions { diff --git a/src/lib_test.ts b/src/lib_test.ts index e0a7e9f..50f037d 100644 --- a/src/lib_test.ts +++ b/src/lib_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "jsr:@std/assert"; +import { assertEquals } from "@std/assert"; import { calculateTileDimensions, findClosestRationalAngle, From 3d1197d7f76ff9d57bb3deac2c14c7d025cb9ea5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:11:35 +0000 Subject: [PATCH 14/17] Fix path handling to preserve ./ prefix for current directory files Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- src/lib.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.ts b/src/lib.ts index 17986ed..fa1373d 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -230,7 +230,10 @@ export function getOutputPath({ const inputDir = dirname(input as string); const outputFileName = `${inputBase}-tile-${rationalAngle.degrees}${inputExt}`; - return output ? (output as string) : join(inputDir, outputFileName); + if (output) return output as string; + const outputPath = join(inputDir, outputFileName); + // Preserve "./" prefix for files in current directory + return inputDir === "." ? `./${outputFileName}` : outputPath; } export interface GenerateTileOptions { From 97bb6da534a56da0c078a5592ee0609d66d27d7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:21:18 +0000 Subject: [PATCH 15/17] Use SEPARATOR constant for cross-platform path prefix handling Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- src/lib.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index fa1373d..8a7849b 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,4 +1,4 @@ -import { basename, dirname, extname, join } from "@std/path"; +import { basename, dirname, extname, join, SEPARATOR } from "@std/path"; import sharp from "sharp"; export interface AngleEntry { @@ -232,8 +232,8 @@ export function getOutputPath({ `${inputBase}-tile-${rationalAngle.degrees}${inputExt}`; if (output) return output as string; const outputPath = join(inputDir, outputFileName); - // Preserve "./" prefix for files in current directory - return inputDir === "." ? `./${outputFileName}` : outputPath; + // Preserve "./" or ".\" prefix for files in current directory for cross-platform compatibility + return inputDir === "." ? `.${SEPARATOR}${outputFileName}` : outputPath; } export interface GenerateTileOptions { From 83130e2397c16037fc25458539ab7f8c13f3b73a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:26:34 +0000 Subject: [PATCH 16/17] Make tests platform-agnostic and add specific assertions for calculateTileDimensions Co-authored-by: marlonmarcello <1956448+marlonmarcello@users.noreply.github.com> --- src/lib_test.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/lib_test.ts b/src/lib_test.ts index 50f037d..8ad3324 100644 --- a/src/lib_test.ts +++ b/src/lib_test.ts @@ -1,4 +1,5 @@ import { assertEquals } from "@std/assert"; +import { join, SEPARATOR } from "@std/path"; import { calculateTileDimensions, findClosestRationalAngle, @@ -137,15 +138,16 @@ Deno.test("calculateTileDimensions - 90 degrees swaps dimensions", () => { Deno.test("calculateTileDimensions - 45 degrees on square image", () => { const result = calculateTileDimensions(100, 100, { m: 1, n: 1 }); - // For 45 degrees with square input, output should be larger - assertEquals(result.width > 0, true); - assertEquals(result.height > 0, true); + // For 45 degrees (m=1, n=1) on 100x100, expect 141x141 + assertEquals(result.width, 141); + assertEquals(result.height, 141); }); Deno.test("calculateTileDimensions - handles 26.565 degrees (1:2 ratio)", () => { const result = calculateTileDimensions(100, 200, { m: 1, n: 2 }); - assertEquals(result.width > 0, true); - assertEquals(result.height > 0, true); + // For 26.565 degrees (m=1, n=2) on 100x200, expect 447x224 + assertEquals(result.width, 447); + assertEquals(result.height, 224); }); Deno.test("calculateTileDimensions - negative m produces same dimensions as positive m", () => { @@ -242,27 +244,27 @@ Deno.test("validateDimensions - reports multiple errors", () => { // Test getOutputPath Deno.test("getOutputPath - generates default output path", () => { const result = getOutputPath({ - input: "/path/to/image.png", + input: join("path", "to", "image.png"), rationalAngle: { degrees: 45 }, }); - assertEquals(result, "/path/to/image-tile-45.png"); + assertEquals(result, join("path", "to", "image-tile-45.png")); }); Deno.test("getOutputPath - handles custom output path", () => { const result = getOutputPath({ - input: "/path/to/image.png", + input: join("path", "to", "image.png"), rationalAngle: { degrees: 45 }, - output: "/custom/output.png", + output: join("custom", "output.png"), }); - assertEquals(result, "/custom/output.png"); + assertEquals(result, join("custom", "output.png")); }); Deno.test("getOutputPath - handles different extensions", () => { const result = getOutputPath({ - input: "/path/to/image.jpg", + input: join("path", "to", "image.jpg"), rationalAngle: { degrees: 26.565 }, }); - assertEquals(result, "/path/to/image-tile-26.565.jpg"); + assertEquals(result, join("path", "to", "image-tile-26.565.jpg")); }); Deno.test("getOutputPath - handles files in current directory", () => { @@ -270,16 +272,16 @@ Deno.test("getOutputPath - handles files in current directory", () => { input: "image.png", rationalAngle: { degrees: 90 }, }); - assertEquals(result, "./image-tile-90.png"); + assertEquals(result, `.${SEPARATOR}image-tile-90.png`); }); Deno.test("getOutputPath - handles negative angles with double dash", () => { const result = getOutputPath({ - input: "/path/to/image.png", + input: join("path", "to", "image.png"), rationalAngle: { degrees: -45 }, }); // Note: negative angles result in double dash in filename (e.g., -tile--45) - assertEquals(result, "/path/to/image-tile--45.png"); + assertEquals(result, join("path", "to", "image-tile--45.png")); }); // Test RATIONAL_ANGLES constant From 4b7d2c038814643c4c1d6c43aa84abf0ca979d20 Mon Sep 17 00:00:00 2001 From: "Marlon U. Marcello" Date: Tue, 3 Mar 2026 17:36:17 -0800 Subject: [PATCH 17/17] chore: pin deno version Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab86d4a..40c2d06 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: Setup Deno uses: denoland/setup-deno@v2 with: - deno-version: v2.x + deno-version: v2.1.x - name: Run tests run: deno task test