Skip to content

replace html2canvas with snapdom - #7920

Open
fikrydev wants to merge 11 commits into
mainfrom
chore/replace-html2canvas-with-snapdom
Open

replace html2canvas with snapdom#7920
fikrydev wants to merge 11 commits into
mainfrom
chore/replace-html2canvas-with-snapdom

Conversation

@fikrydev

@fikrydev fikrydev commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace html2canvas with [snapdom](https://github.com/zumerlab/snapdom) for the group table / crosstable / bracket / match list / prize pool / participants / standings image export feature, and rework how the exported image is composed:

  • Capture is now WYSIWYG. The old implementation rendered the content inside a hidden iframe pinned to a fixed 1440px width, to get consistent @media resolution regardless of the exporting device. In practice this caused several regressions: toggle-dependent views (FFA/Swiss standings detail columns, participant card compact mode) always rendered in their "wide desktop" state regardless of the toggle, Match List captures could come out visually broken, and mobile exports were blurrier and much larger in file size than before. The rewrite drops the iframe/fixed-width sandbox entirely — the element is still cloned (rebuilding a shallow ancestor spine so theme--dark and other ancestor-scoped CSS, plus :has()-driven toggle state, still apply) but the clone stays in the live document at its natural width, so the export matches whatever the exporting device is actually showing.
  • Header/footer are now real DOM, not a hand-drawn canvas overlay. Previously, content was captured first, then a title/subtitle header and a "Powered by Liquipedia" footer were manually drawn onto a second canvas with JS-computed text measurements and line-wrapping. That measurement pass ran against a font that wasn't guaranteed to be loaded yet, causing inconsistent header layout between exports. The header and footer are now built as ordinary HTML/CSS, wrapped around the captured content, and rasterized together with it in a single snapdom.toCanvas() call — wrapping, centering and sizing are handled by the browser's layout engine instead of custom math.
  • Images are explicitly waited on before capture. snapdom doesn't wait for <img> elements to finish decoding on its own; without this, flags and the footer logo could be rasterized blank, or (worse) an undecoded image could collapse a row's height and push later rows out of place in the capture. All images inside the captured tree are now awaited via decode() (with a timeout fallback) before the snapshot is taken.
  • Fixed oversized exports. snapdom's scale and dpr options multiply together into the final canvas size. The initial migration derived scale from devicePixelRatio but never set dpr, so snapdom multiplied by the real device pixel ratio a second time on top of that — producing exports 4-9x larger than necessary on HiDPI screens. dpr is now pinned to 1, with all oversampling driven through scale alone.
  • Fixed blurry exports on WebKit/iOS. Once the oversampling bug above was fixed, a separate softness became visible: snapdom's WebKit-only "shadow-safe" fallback renders shadowed content at native size first, then bitmap-stretches it up, trading sharpness for correct box/text-shadow rendering. This codebase relies on inset box-shadow as a structural border/divider technique in several components (tables, standings, crosstables), so shadows can't just be stripped from captures to avoid the fallback. Instead, WebKit/iOS user agents now get a higher oversampling ceiling (3-4x vs. the default 2-3x) to buy back sharpness lost to the resample.
  • Fixed broken Match List exports. The captured .brkts-matchlist-collapse-area element uses CSS subgrid to inherit its column tracks from its live .brkts-matchlist parent. Reparenting the clone under the header/footer flex wrapper (with the real grid ancestor only reattached further up the spine) left it with no grid to inherit from, collapsing every column into one and squashing the whole match list into a narrow vertical stack. Fixed by baking the parent's already-resolved pixel track widths onto the clone before it's detached, turning it into a self-contained grid.
  • Net simplification. The ImageCache and CanvasComposer classes (image preloading + hand-rolled canvas compositing, font measuring, text wrapping, gradient/rounded-rect drawing) are gone — that responsibility moved to plain CSS on the composed DOM. Despite the fixes above adding some logic back, the file is still ~150 lines smaller than the html2canvas-based version.

How did you test this change?

Test it here: https://fikry.wiki.tldev.eu/rocketleague/FIFAe_World_Cup/2025

@fikrydev fikrydev self-assigned this Aug 10, 2026
Copilot AI lite review requested due to automatic review settings August 10, 2026 12:07
@fikrydev
fikrydev requested review from a team as code owners August 10, 2026 12:07
@fikrydev fikrydev added the qol Developer quality-of-life changes that are not deployed to wiki label Aug 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates the image-export feature in ExportImage.js to use snapdom for DOM-to-canvas capture (instead of html2canvas), including adapting the clone-mutation logic to snapdom’s API constraints.

Changes:

  • Replaced html2canvas capture flow with snapdom.toCanvas(...) and updated the loader to mw.loader.using('snapdom').
  • Added an offscreen “capture clone” path and adjusted clone-fixup utilities to operate on an element root (including self-matching selectors).
  • Updated dropdown preloading to call ensureSnapdomLoaded().

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread javascript/commons/ExportImage.js Outdated
Comment thread javascript/commons/ExportImage.js
Comment thread javascript/commons/ExportImage.js
@ElectricalBoy ElectricalBoy removed the qol Developer quality-of-life changes that are not deployed to wiki label Aug 11, 2026
@fikrydev
fikrydev force-pushed the chore/replace-html2canvas-with-snapdom branch from 154bcb9 to a3e66ef Compare August 13, 2026 08:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 no new comments.

Suppressed comments (2)

javascript/commons/ExportImage.js:306

  • wrapper (the element passed to snapdom.toCanvas) is never inserted into the offscreen root that gets appended to the document. Since wrapWithHeaderFooter() moves target into wrapper, the appended root ends up containing empty ancestor shells, while the captured wrapper is detached from the DOM (so computed layout/CSS can be missing and capture can fail/produce 0×0). Insert wrapper back into the ancestor spine before appending root so the clone is connected and ancestor-scoped CSS applies.
		document.body.appendChild( root );

javascript/commons/ExportImage.js:140

  • buildAncestorSpine() currently clones all ancestors up to <html> and then generateImageBlob() appends that clone tree to document.body, which nests <html>/<body> elements inside the live <body>. This is invalid DOM and can trigger browser normalization/reparenting in ways that break the offscreen mount and the capture. Stop cloning at document.body/documentElement and wrap the spine in a normal <div> that carries the page-level classes needed for ancestor-scoped CSS.

This issue also appears on line 306 of the same file.

		while ( ancestor ) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants