-
Notifications
You must be signed in to change notification settings - Fork 0
New functions and big refactor #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
960788a
test: better test coverage, add visual tests, and more
samedii 1caebb5
feature: filter and draw image
samedii 2f97991
refactor!: reimplementation
samedii 7b2f6b0
improve: maybe working implementation
samedii 3e0798b
improve: cleanup and update version
samedii 4a36313
test: disable node 14 and 16 testing
samedii fc42101
test: pixelmatch import fix
samedii 82ef9bd
test: try two ways of importing
samedii 5ab5dd9
test: disable browser test on github actions
samedii File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,83 +1,125 @@ | ||
| # far-canvas | ||
| # Far Canvas | ||
|
|
||
| ## install | ||
| Render 2D canvas content at large coordinates with ease. | ||
|
|
||
| ```bash | ||
| npm install @nextml/far-canvas | ||
| ## The problem | ||
|
|
||
| When rendering 2D canvas content at large coordinates, you may experience issues with precision. For example, drawing a horizontal line from `(100_000_000, 0.5)` to `(100_000_001, 0.5)` may render a diagonal line, or no line at all. | ||
|
|
||
| ## The solution | ||
|
|
||
| Far Canvas is a wrapper around the HTML5 2D canvas API that avoids precision issues at large coordinates. | ||
|
|
||
| ## NEW: Transform Support | ||
|
|
||
| Far Canvas now supports all Canvas 2D transform operations! When running in a modern browser or environment with full Canvas 2D support, far-canvas will automatically use a Transform-Aware implementation that: | ||
|
|
||
| - ✅ Supports `translate()`, `rotate()`, `scale()`, `transform()`, `setTransform()`, and `resetTransform()` | ||
| - ✅ Supports `getTransform()` to retrieve the current transformation matrix | ||
| - ✅ Leverages hardware-accelerated native Canvas transforms for better performance | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. mention of hardware-accelerated transforms might be a bit misleading here |
||
| - ✅ Maintains the same precision guarantees for large coordinates | ||
| - ✅ Falls back gracefully to coordinate transformation when transforms aren't available | ||
|
|
||
| ```javascript | ||
| import { far } from "@nextml/far-canvas"; | ||
|
|
||
| const canvas = document.getElementById("myCanvas"); | ||
| const ctx = far(canvas, { | ||
| x: 100_000_000, // Render with huge coordinate offset | ||
| y: 100_000_000, | ||
| scale: 2, | ||
| }).getContext("2d"); | ||
|
|
||
| // All transform operations now work! | ||
| ctx.save(); | ||
| ctx.translate(50, 50); | ||
| ctx.rotate(Math.PI / 4); | ||
| ctx.scale(1.5, 1.5); | ||
| ctx.fillRect(-25, -25, 50, 50); | ||
| ctx.restore(); | ||
|
|
||
| // Draw at world coordinates - far-canvas handles the offset | ||
| ctx.fillStyle = "red"; | ||
| ctx.fillRect(100_000_000, 100_000_000, 100, 100); | ||
| ``` | ||
|
|
||
| ## motivation | ||
| ## Quick Start | ||
|
|
||
| For example: translated `100'000'000px` away from the center (and a scaling of 1.5) and rendering the objects that far away: | ||
| ```javascript | ||
| import { far } from "@nextml/far-canvas"; | ||
|
|
||
| ### vanilla canvas exapmle at 0px translation | ||
| const canvas = document.getElementById("myCanvas"); | ||
|
|
||
| <img | ||
| src="static/reference-canvas.png" | ||
| alt="vanilla canvas example" | ||
| title="Vanilla Canvas Example" | ||
| style="display: inline-block; margin: 0 auto;"> | ||
| const myFarCanvas = far(canvas, { | ||
| x: 100_000_000, | ||
| y: 0, | ||
| scale: 2, | ||
| }); | ||
|
|
||
| ### vanilla canvas example at 100Mpx translation | ||
| const context = myFarCanvas.getContext("2d"); | ||
|
|
||
| <img | ||
| src="static/vanilla-canvas.png" | ||
| alt="vanilla canvas example" | ||
| title="Vanilla Canvas Example" | ||
| style="display: inline-block; margin: 0 auto;"> | ||
| // This will be a horizontal line! | ||
| context.strokeStyle = "red"; | ||
| context.beginPath(); | ||
| context.moveTo(100_000_000, 0.5); | ||
| context.lineTo(100_000_001, 0.5); | ||
| context.stroke(); | ||
| ``` | ||
|
|
||
| ### far canvas example at 100Mpx translation | ||
| ## Install | ||
|
|
||
| <img | ||
| src="static/far-canvas.png" | ||
| alt="far canvas example" | ||
| title="Far Canvas Example" | ||
| style="display: inline-block; margin: 0 auto;"> | ||
| `npm install @nextml/far-canvas` | ||
|
|
||
| 1. Images, rectangles and lines are all missaligned. | ||
| 2. `lineWidth=8px` is not rendered correctly. | ||
| ## Usage | ||
|
|
||
| ## usage | ||
| ### `far( canvas: HTMLCanvasElement, options?: FarCanvasOptions ): FarCanvas` | ||
|
|
||
| ### Node | ||
| Creates a far canvas instance. Options are: | ||
|
|
||
| ```javascript | ||
| const { far } = require("../lib.cjs/index.js"); | ||
| - `x`: The x offset to apply to all drawing operations (default: 0) | ||
| - `y`: The y offset to apply to all drawing operations (default: 0) | ||
| - `scale`: The scale to apply to all drawing operations (default: 1) | ||
|
|
||
| const farAway = 100000000; | ||
| const context = far(canvas, {y: -farAway, scale: 2}).getContext("2d"); | ||
| ### `FarCanvas` | ||
|
|
||
| context.clearCanvas(); | ||
| context.fillRect(32, farAway + 16, 128, 128); | ||
| The far canvas instance has a single method: | ||
|
|
||
| context.canvas; // underlying canvas for which the default unit is pixels | ||
| context.s; // coordinate system | ||
| context.s.inv; // inverse coordinate system | ||
| - `getContext( '2d' )`: Returns a `FarCanvasRenderingContext2D` | ||
|
|
||
| ... | ||
| ``` | ||
| ### `FarCanvasRenderingContext2D` | ||
|
|
||
| ### Web | ||
| The far canvas rendering context implements the full [`CanvasRenderingContext2D`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) interface, with the following additions: | ||
|
|
||
| ```javascript | ||
| const canvas = document.getElementById('far'); | ||
| - `clearCanvas()`: Clears the entire canvas (ignoring any transforms) | ||
| - `canvasDimensions`: Returns the dimensions of the canvas in the far coordinate system | ||
|
|
||
| const farAway = 100000000; | ||
| const context = far.far(canvas, {y: -farAway, scale: 2}).getContext("2d"); | ||
| When transform support is available (modern browsers), all transform methods work as expected. In fallback mode, the following methods will throw an error: | ||
|
|
||
| ... | ||
| ``` | ||
| - `translate()`, `rotate()`, `scale()`, `transform()`, `setTransform()`, `resetTransform()`, `getTransform()` | ||
|
|
||
| ## development | ||
| ## How it works | ||
|
|
||
| ### run example | ||
| Far Canvas uses two approaches depending on the environment: | ||
|
|
||
| ```bash | ||
| npm run example | ||
| ``` | ||
| ### Transform-Aware Mode (when `setTransform` is available) | ||
|
|
||
| ### update version | ||
| Uses Canvas 2D's native transform matrix to efficiently handle large coordinate offsets: | ||
|
|
||
| ```bash | ||
| npm version patch | minor | major | ||
| ``` | ||
| - Applies a hybrid approach: coordinate transformation for far-canvas offset, native transforms for user operations | ||
| - All drawing operations transform coordinates in JavaScript to avoid precision issues | ||
| - Leverages hardware acceleration for user transforms when available | ||
| - Supports all transform operations seamlessly | ||
|
|
||
| ### Fallback Mode (when transforms aren't supported) | ||
|
|
||
| Falls back to coordinate transformation: | ||
|
|
||
| - Intercepts all drawing calls and transforms coordinates before passing to the underlying context | ||
| - Ensures compatibility with older browsers or limited Canvas implementations | ||
| - Transform operations are not supported in this mode | ||
|
|
||
| The appropriate mode is automatically selected based on feature detection. | ||
|
|
||
| ## License | ||
|
|
||
| Apache-2.0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # Browser Testing Methodology for Far-Canvas | ||
|
|
||
| ## Overview | ||
|
|
||
| This document describes the reliable automated testing approach developed for testing far-canvas rendering consistency between near and far focus in browser environments. | ||
|
|
||
| ## Problem Statement | ||
|
|
||
| Initial attempts to test far-canvas using Puppeteer screenshots revealed significant challenges: | ||
|
|
||
| - Screenshot-based comparisons produced hundreds of mismatched pixels even for identical vanilla canvas renders | ||
| - Visual differences were inconsistent and unreliable for automated testing | ||
| - Manual inspection was required, defeating the purpose of automation | ||
|
|
||
| ## Solution: Canvas.toDataURL() Comparison | ||
|
|
||
| We developed a robust testing methodology using `canvas.toDataURL()` for pixel-perfect comparison of canvas rendering output. | ||
|
|
||
| ### Key Advantages | ||
|
|
||
| 1. **Zero False Positives**: Identical canvas draws produce identical DataURLs | ||
| 2. **Reliable Detection**: Clearly identifies actual rendering differences | ||
| 3. **Automated Execution**: No manual intervention required | ||
| 4. **Precise Measurement**: Exact byte-level comparison of canvas output | ||
| 5. **Debugging Artifacts**: Automatically saves images when differences are detected | ||
|
|
||
| ## Implementation | ||
|
|
||
| ### Test Structure | ||
|
|
||
| The testing framework consists of: | ||
|
|
||
| 1. **Browser Test Page** (`example/browser-test.html`): Contains test cases that run in the browser | ||
| 2. **Puppeteer Test Script** (`test/browser-visual-consistency.test.js`): Orchestrates test execution and validation | ||
| 3. **HTTP Server**: Serves the test page and far-canvas library to the browser | ||
|
|
||
| ### Test Cases | ||
|
|
||
| #### Vanilla Canvas Sanity Check | ||
|
|
||
| ```javascript | ||
| { | ||
| name: "Vanilla Canvas Data URL Sanity Check", | ||
| description: "Draws identical simple scenes on vanilla canvas and compares using canvas.toDataURL().", | ||
| run: (canvas, params) => { | ||
| const ctx = canvas.getContext('2d'); | ||
|
|
||
| const drawScene = () => { | ||
| ctx.fillStyle = 'white'; | ||
| ctx.fillRect(0, 0, canvas.width, canvas.height); | ||
| ctx.fillStyle = 'red'; | ||
| ctx.fillRect(10, 10, 50, 30); | ||
| }; | ||
|
|
||
| // Draw scene 1 | ||
| drawScene(); | ||
| const dataURL1 = canvas.toDataURL(); | ||
|
|
||
| // Clear and draw scene 2 (identical) | ||
| ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
| drawScene(); | ||
| const dataURL2 = canvas.toDataURL(); | ||
|
|
||
| // Compare data URLs | ||
| const identical = dataURL1 === dataURL2; | ||
|
|
||
| return { | ||
| pass: identical, | ||
| details: `DataURL comparison: ${identical ? 'IDENTICAL' : 'DIFFERENT'}` | ||
| }; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| #### Far-Canvas Consistency Test | ||
|
|
||
| ```javascript | ||
| { | ||
| name: "Far-Canvas Data URL Comparison", | ||
| description: "Tests far-canvas rendering consistency between near and far focus using canvas.toDataURL().", | ||
| run: (canvas, params) => { | ||
| const FOCUS_NEAR = 5000; | ||
| const FOCUS_FAR = 500000000; | ||
|
|
||
| const drawTestScene = (ctx, focus) => { | ||
| ctx.clearCanvas(); | ||
| // Draw test elements (rectangle, line, text) | ||
| // ... | ||
| }; | ||
|
|
||
| // Test near focus | ||
| const ctxNear = far(canvas, { x: params.x, y: FOCUS_NEAR, scale: params.scale }).getContext('2d'); | ||
| drawTestScene(ctxNear, FOCUS_NEAR); | ||
| const dataURLNear = canvas.toDataURL(); | ||
|
|
||
| // Test far focus | ||
| const ctxFar = far(canvas, { x: params.x, y: FOCUS_FAR, scale: params.scale }).getContext('2d'); | ||
| drawTestScene(ctxFar, FOCUS_FAR); | ||
| const dataURLFar = canvas.toDataURL(); | ||
|
|
||
| // Compare | ||
| const identical = dataURLNear === dataURLFar; | ||
|
|
||
| return { | ||
| pass: identical, | ||
| details: `Near vs Far focus rendering: ${identical ? 'IDENTICAL' : 'DIFFERENT'}` | ||
| }; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Puppeteer Integration | ||
|
|
||
| The Puppeteer script: | ||
|
|
||
| 1. Starts an HTTP server to serve test files | ||
| 2. Launches a browser and navigates to the test page | ||
| 3. Executes specific test cases | ||
| 4. Retrieves DataURL comparison results from the browser | ||
| 5. Saves debugging artifacts (images, DataURLs) when tests fail | ||
| 6. Performs Jest assertions on the results | ||
|
|
||
| ```javascript | ||
| // Get the test results | ||
| const testResults = await page.evaluate(() => window.farCanvasDataURLTest); | ||
|
|
||
| console.log(`Near focus DataURL length: ${testResults.lengthNear}`); | ||
| console.log(`Far focus DataURL length: ${testResults.lengthFar}`); | ||
| console.log(`DataURLs identical: ${testResults.identical}`); | ||
|
|
||
| // Save debugging artifacts if different | ||
| if (!testResults.identical) { | ||
| // Save DataURLs as text files | ||
| fs.writeFileSync("far_canvas_near_focus.txt", testResults.dataURLNear); | ||
| fs.writeFileSync("far_canvas_far_focus.txt", testResults.dataURLFar); | ||
|
|
||
| // Save as PNG images for visual inspection | ||
| const base64DataNear = testResults.dataURLNear.replace( | ||
| /^data:image\/png;base64,/, | ||
| "" | ||
| ); | ||
| const base64DataFar = testResults.dataURLFar.replace( | ||
| /^data:image\/png;base64,/, | ||
| "" | ||
| ); | ||
|
|
||
| fs.writeFileSync("far_canvas_near_focus.png", base64DataNear, "base64"); | ||
| fs.writeFileSync("far_canvas_far_focus.png", base64DataFar, "base64"); | ||
| } | ||
|
|
||
| // Assert identical rendering | ||
| expect(testResults.identical).toBe(true); | ||
| ``` | ||
|
|
||
| ## Test Results | ||
|
|
||
| ### Validation Results | ||
|
|
||
| - **Vanilla Canvas Sanity Check**: ✅ PASSED (DataURLs identical: true) | ||
| - **Far-Canvas Consistency Test**: ❌ FAILED (DataURLs identical: false) | ||
| - Near focus (5000) DataURL length: 3890 | ||
| - Far focus (500000000) DataURL length: 1694 | ||
|
|
||
| ### Debugging Artifacts | ||
|
|
||
| When tests fail, the following files are automatically generated: | ||
|
|
||
| - `far_canvas_near_focus.png` - Visual output at near focus | ||
| - `far_canvas_far_focus.png` - Visual output at far focus | ||
| - `far_canvas_near_focus.txt` - Complete DataURL for near focus | ||
| - `far_canvas_far_focus.txt` - Complete DataURL for far focus | ||
|
|
||
| ## Running Tests | ||
|
|
||
| ```bash | ||
| # Run the browser consistency test | ||
| npm test test/browser-visual-consistency.test.js | ||
| ``` | ||
|
|
||
| ## Benefits | ||
|
|
||
| 1. **Deterministic**: Same input always produces same output | ||
| 2. **Sensitive**: Detects even minor rendering differences | ||
| 3. **Fast**: No image processing or pixel comparison needed | ||
| 4. **Debuggable**: Provides clear artifacts for investigation | ||
| 5. **Maintainable**: Simple string comparison logic | ||
|
|
||
| ## Future Enhancements | ||
|
|
||
| - Add tests for individual rendering primitives (rectangles, lines, text separately) | ||
| - Test different scale factors and focus distances | ||
| - Add performance benchmarking | ||
| - Integrate with CI/CD pipeline for regression detection | ||
|
|
||
| ## Conclusion | ||
|
|
||
| The `canvas.toDataURL()` methodology provides a robust, reliable foundation for automated testing of canvas rendering consistency. It eliminates the noise and unreliability of screenshot-based approaches while providing precise, actionable results for debugging rendering issues. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that only
{x, y, scale}supports big values and not transform it has to be super clear and kinda early that it's the case. Not sure where to put it