Skip to content

fix: responsive canvas dimensions for static and infinite scroll pages#27

Open
anth0nycodes wants to merge 13 commits intomainfrom
anthony/canvas-dimensions-fix
Open

fix: responsive canvas dimensions for static and infinite scroll pages#27
anth0nycodes wants to merge 13 commits intomainfrom
anthony/canvas-dimensions-fix

Conversation

@anth0nycodes
Copy link
Copy Markdown
Owner

@anth0nycodes anth0nycodes commented Mar 8, 2026

Change Summary + Media (if applicable)

Context

The previous implementation of setting the dimensions for the canvas is ineffective for sites that render in content gradually (i.e infinite scroll) as clientHeight does not change when content is added that overflows beyond the element’s visible area. We also couldn't directly use scrollHeight because that causes too much overhead, causing each operation we try to do on the canvas to be expensive -> laggy.

Summary

Replaced static clientHeight sizing with a dynamic scroll-based expansion approach that works on both static pages and infinite scroll sites. Fixed infinite expansion bug at extreme zoom levels by using a collapse-and-measure technique to get clean scrollHeight readings without the canvas inflating its own measurements. Also, added a 8,000px height cap to prevent performance degradation.

How the collapse-and-measure approach works

The core issue was that the canvas (despite being absolutely positioned) inflated the document's scrollHeight, creating a feedback loop: the canvas expands → page becomes taller → user can scroll further → canvas expands again.

The collapse-and-measure approach breaks this cycle:

  1. Collapse the canvas height to 0, removing it from affecting scrollHeight.
  2. Measure scrollHeight to get the real content height of the page.
  3. Restore the canvas to its previous height.
  4. Guard the expansion — the canvas only grows if visibleBottomY <= contentScrollHeight, meaning the user is still within actual page content, not scrolling into empty space the canvas itself created.

Test plan

  • Open an infinite scroll site (e.g., Twitter) — canvas should expand as new content loads
  • Zoom to 25% on a static page and scroll to the bottom — canvas should stop at the content boundary
  • Verify no lag on initial page load
  • Confirm the alert fires when canvas exceeds 8,000px
  • Confirm canvas width respects the vertical scrollbar

@anth0nycodes anth0nycodes changed the title fix: canvas dimensions fix: responsive canvas dimensions for static and infinite scroll pages Mar 11, 2026
@anth0nycodes anth0nycodes requested a review from Copilot March 11, 2026 02:35
@anth0nycodes anth0nycodes self-assigned this Mar 11, 2026
@anth0nycodes anth0nycodes added the enhancement New feature or request label Mar 11, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates Tracemark’s Fabric canvas sizing logic to better support responsive layouts and infinite-scroll pages by dynamically expanding height based on scroll position while introducing a maximum height cap.

Changes:

  • Split canvas sizing into width, base height, and scroll-driven height expansion helpers.
  • Add scroll handler + ResizeObserver-based resizing to react to page changes.
  • Introduce an 8,000px canvas height cap (with an alert) to limit performance impact.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +102 to +103
updateCanvasWidth(fc);
updateCanvasHeight(fc);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleResize calls updateCanvasHeight, which sets the canvas height to clientHeight (viewport). Because ResizeObserver is observing documentElement/body, it can fire on content growth (e.g., infinite scroll) and shrink an already-expanded canvas back down to the viewport, potentially hiding existing drawings. Consider making resize only adjust width, or set height to Math.max(fc.getHeight(), viewportHeight/current desired height), or invoke the same dynamic-height logic instead of forcing clientHeight.

Suggested change
updateCanvasWidth(fc);
updateCanvasHeight(fc);
// On resize, only adjust the canvas width. Height is managed dynamically
// via updateDynamicCanvasHeight to avoid shrinking an already-expanded canvas.
updateCanvasWidth(fc);

Copilot uses AI. Check for mistakes.
Comment on lines 129 to 133
return () => {
fc.dispose();
window.removeEventListener("resize", initCanvasDimensions);
resizeObserver.disconnect();
window.removeEventListener("scroll", handleScroll);
};
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resizeTimeout can still fire after unmount/fc.dispose() (e.g., a ResizeObserver callback scheduled right before cleanup), which would call handleResize on a disposed Fabric canvas. Clear the pending timeout in the cleanup function to avoid post-dispose dimension updates/errors.

Copilot uses AI. Check for mistakes.
window.addEventListener("resize", initCanvasDimensions);
resizeObserver.observe(document.documentElement);
resizeObserver.observe(document.body);
window.addEventListener("scroll", handleScroll);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateDynamicCanvasHeight is invoked directly on every scroll event and performs multiple setDimensions calls (including a collapse to height 0). This can be expensive during scrolling. Consider throttling via requestAnimationFrame/debounce and registering the listener as { passive: true } since it doesn't call preventDefault().

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 118 to 140
@@ -69,7 +135,8 @@ export function Canvas({ currentTool, setCurrentTool }: CanvasProps) {

return () => {
fc.dispose();
window.removeEventListener("resize", initCanvasDimensions);
resizeObserver.disconnect();
window.removeEventListener("scroll", handleScroll);
};
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resizeTimeout is not cleared in the effect cleanup. If a resize event schedules handleResize and the component unmounts (disposing the Fabric canvas) before the timeout fires, the callback can run against a disposed fc. Clear the pending timeout during cleanup (and consider typing it as ReturnType<typeof setTimeout> | undefined to reflect the runtime behavior).

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +66
if (
visibleBottomY >= currentCanvasHeight &&
currentCanvasHeight > CANVAS_MAX_HEIGHT_IN_PIXELS
) {
alert(
"This page is too tall for Tracemark to support. You can still draw on the visible canvas area, but it won't expand further."
);
fc.setDimensions({
height: CANVAS_MAX_HEIGHT_IN_PIXELS,
});
// TODO: remove alert and replace with better user-facing error handling
return;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using alert(...) here is blocking and can be triggered repeatedly as the user scrolls near the bottom (especially if the height briefly exceeds the cap). Consider switching to non-blocking in-app UI and/or guarding so the message is only shown once per page/session.

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +80
while (
newCanvasHeight < visibleBottomY &&
visibleBottomY <= contentScrollHeight &&
newCanvasHeight < CANVAS_MAX_HEIGHT_IN_PIXELS
) {
newCanvasHeight += EXPANSION_INCREMENT_IN_PIXELS;
}

if (newCanvasHeight > currentCanvasHeight) {
fc.setDimensions({
height: Math.max(newCanvasHeight, viewportHeight),
});
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newCanvasHeight can exceed CANVAS_MAX_HEIGHT_IN_PIXELS because the loop condition checks the value before adding EXPANSION_INCREMENT_IN_PIXELS. This allows setting the Fabric canvas height above the intended cap (e.g. 7800 -> 8300). Clamp the final height to the max (or change the loop to prevent overshoot) and consider adjusting the alert/guard to trigger when an expansion would cross the cap (not only when currentCanvasHeight > CANVAS_MAX_HEIGHT_IN_PIXELS).

Copilot uses AI. Check for mistakes.
initCanvasDimensions();
const handleResize = () => {
updateCanvasWidth(fc);
updateCanvasHeight(fc);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleResize currently calls updateCanvasHeight, which sets the canvas height to clientHeight (viewport height). If the canvas has already been expanded via scrolling, any resize/observer trigger can shrink it back down and effectively cut off previously drawable areas. Consider keeping the existing expanded height (e.g., max(currentHeight, viewportHeight)) and only growing on resize, not shrinking.

Suggested change
updateCanvasHeight(fc);
const viewportHeight = document.documentElement.clientHeight;
const currentHeight = fc.getHeight();
const desiredHeight = Math.max(currentHeight, viewportHeight);
const newHeight = Math.min(CANVAS_MAX_HEIGHT_IN_PIXELS, desiredHeight);
if (newHeight !== currentHeight) {
fc.setHeight(newHeight);
fc.renderAll();
}

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +127
const handleScroll = () => {
updateDynamicCanvasHeight(fc);
};

let resizeTimeout: number;

const resizeObserver = new ResizeObserver(() => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(handleResize, 50); // debounce to prevent rapid calls
});

window.addEventListener("resize", initCanvasDimensions);
resizeObserver.observe(document.documentElement);
resizeObserver.observe(document.body);
window.addEventListener("scroll", handleScroll);
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scroll handler runs on every scroll event and updateDynamicCanvasHeight does multiple setDimensions calls plus a scrollHeight read, which can force layout and be expensive during scrolling. Consider adding a requestAnimationFrame-based throttle and registering the listener as { passive: true } to reduce main-thread jank.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants