@@ -12,10 +12,6 @@ import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
1212import * as Notifications from "../elements/notifications" ;
1313import { convertRemToPixels } from "../utils/numbers" ;
1414
15- async function gethtml2canvas ( ) : Promise < typeof import ( "html2canvas" ) . default > {
16- return ( await import ( "html2canvas" ) ) . default ;
17- }
18-
1915let revealReplay = false ;
2016let revertCookie = false ;
2117
@@ -48,15 +44,16 @@ function revert(): void {
4844 }
4945}
5046
51- let firefoxClipboardNotificatoinShown = false ;
47+ let firefoxClipboardNotificationShown = false ;
5248
5349/**
54- * Prepares UI, generates screenshot canvas using html2canvas , and reverts UI changes.
50+ * Prepares UI, generates screenshot canvas using modern-screenshot , and reverts UI changes.
5551 * Returns the generated canvas element or null on failure.
5652 * Handles its own loader and basic error notifications for canvas generation.
5753 */
5854async function generateCanvas ( ) : Promise < HTMLCanvasElement | null > {
59- Loader . show ( ) ;
55+ const { domToCanvas } = await import ( "modern-screenshot" ) ;
56+ Loader . show ( true ) ;
6057
6158 if ( ! $ ( "#resultReplay" ) . hasClass ( "hidden" ) ) {
6259 revealReplay = true ;
@@ -110,7 +107,7 @@ async function generateCanvas(): Promise<HTMLCanvasElement | null> {
110107 }
111108
112109 ( document . querySelector ( "html" ) as HTMLElement ) . style . scrollBehavior = "auto" ;
113- window . scrollTo ( { top : 0 , behavior : "instant" as ScrollBehavior } ) ; // Use instant scroll
110+ window . scrollTo ( { top : 0 , behavior : "auto" } ) ;
114111
115112 // --- Target Element Calculation ---
116113 const src = $ ( "#result .wrapper" ) ;
@@ -120,40 +117,117 @@ async function generateCanvas(): Promise<HTMLCanvasElement | null> {
120117 revert ( ) ;
121118 return null ;
122119 }
123- // Ensure offset calculations happen *after* potential layout shifts from UI prep
124- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ; // Small delay for render updates
120+ await Misc . sleep ( 50 ) ; // Small delay for render updates
125121
126122 const sourceX = src . offset ( ) ?. left ?? 0 ;
127123 const sourceY = src . offset ( ) ?. top ?? 0 ;
128124 const sourceWidth = src . outerWidth ( true ) as number ;
129125 const sourceHeight = src . outerHeight ( true ) as number ;
126+ const paddingX = convertRemToPixels ( 2 ) ;
127+ const paddingY = convertRemToPixels ( 2 ) ;
130128
131- // --- Canvas Generation ---
132129 try {
133- const paddingX = convertRemToPixels ( 2 ) ;
134- const paddingY = convertRemToPixels ( 2 ) ;
135-
136- const canvas = await (
137- await gethtml2canvas ( )
138- ) ( document . body , {
130+ // Compute full-document render size to keep the target area in frame on small viewports
131+ const root = document . documentElement ;
132+ const { scrollWidth, clientWidth, scrollHeight, clientHeight } = root ;
133+ const targetWidth = Math . max ( scrollWidth , clientWidth ) ;
134+ const targetHeight = Math . max ( scrollHeight , clientHeight ) ;
135+
136+ // Target the HTML root to include .customBackground
137+ const fullCanvas = await domToCanvas ( root , {
139138 backgroundColor : await ThemeColors . get ( "bg" ) ,
140- width : sourceWidth + paddingX * 2 ,
141- height : sourceHeight + paddingY * 2 ,
142- x : sourceX - paddingX ,
143- y : sourceY - paddingY ,
144- logging : false , // Suppress html2canvas logs in console
145- useCORS : true , // May be needed if user flags/icons are external
139+ // Sharp output
140+ scale : window . devicePixelRatio ?? 1 ,
141+ style : {
142+ width : `${ targetWidth } px` ,
143+ height : `${ targetHeight } px` ,
144+ overflow : "hidden" , // for scrollbar in small viewports
145+ } ,
146+ // Fetch (for custom background URLs)
147+ fetch : {
148+ requestInit : { mode : "cors" , credentials : "omit" } ,
149+ bypassingCache : true ,
150+ } ,
151+
152+ // skipping hidden elements (THAT IS SO IMPORTANT!)
153+ filter : ( el : Node ) : boolean => {
154+ if ( ! ( el instanceof HTMLElement ) ) return true ;
155+ const cs = getComputedStyle ( el ) ;
156+ return ! ( el . classList . contains ( "hidden" ) || cs . display === "none" ) ;
157+ } ,
158+ // Normalize the background layer so its negative z-index doesn't get hidden
159+ onCloneEachNode : ( cloned ) => {
160+ if ( cloned instanceof HTMLElement ) {
161+ const el = cloned ;
162+ if ( el . classList . contains ( "customBackground" ) ) {
163+ el . style . zIndex = "0" ;
164+ el . style . width = `${ targetWidth } px` ;
165+ el . style . height = `${ targetHeight } px` ;
166+ // for the inner image scales
167+ const img = el . querySelector ( "img" ) ;
168+ if ( img ) {
169+ // (<= 720px viewport width) wpm & acc text wrapper!!
170+ if ( window . innerWidth <= 720 ) {
171+ img . style . transform = "translateY(20vh)" ;
172+ img . style . height = "100%" ;
173+ } else {
174+ img . style . width = "100%" ; // safety nothing more
175+ img . style . height = "100%" ; // for image fit full screen even when words history is opened with many lines
176+ }
177+ }
178+ }
179+ }
180+ } ,
146181 } ) ;
147182
148- revert ( ) ; // Revert UI *after* canvas is successfully generated
183+ // Scale and create output canvas
184+ const scale = fullCanvas . width / targetWidth ;
185+ const paddedWidth = sourceWidth + paddingX * 2 ;
186+ const paddedHeight = sourceHeight + paddingY * 2 ;
187+
188+ const scaledPaddedWCanvas = Math . round ( paddedWidth * scale ) ;
189+ const scaledPaddedHCanvas = Math . round ( paddedHeight * scale ) ;
190+ const scaledPaddedWForCrop = Math . ceil ( paddedWidth * scale ) ;
191+ const scaledPaddedHForCrop = Math . ceil ( paddedHeight * scale ) ;
192+
193+ const canvas = document . createElement ( "canvas" ) ;
194+ canvas . width = scaledPaddedWCanvas ;
195+ canvas . height = scaledPaddedHCanvas ;
196+ const ctx = canvas . getContext ( "2d" ) ;
197+ if ( ! ctx ) {
198+ Notifications . add ( "Failed to get canvas context for screenshot" , - 1 ) ;
199+ return null ;
200+ }
201+
202+ ctx . imageSmoothingEnabled = true ;
203+ ctx . imageSmoothingQuality = "high" ;
204+
205+ // Calculate crop coordinates with proper clamping
206+ const cropX = Math . max ( 0 , Math . floor ( ( sourceX - paddingX ) * scale ) ) ;
207+ const cropY = Math . max ( 0 , Math . floor ( ( sourceY - paddingY ) * scale ) ) ;
208+ const cropW = Math . min ( scaledPaddedWForCrop , fullCanvas . width - cropX ) ;
209+ const cropH = Math . min ( scaledPaddedHForCrop , fullCanvas . height - cropY ) ;
210+
211+ ctx . drawImage (
212+ fullCanvas ,
213+ cropX ,
214+ cropY ,
215+ cropW ,
216+ cropH ,
217+ 0 ,
218+ 0 ,
219+ canvas . width ,
220+ canvas . height
221+ ) ;
149222 return canvas ;
150223 } catch ( e ) {
151224 Notifications . add (
152225 Misc . createErrorMessage ( e , "Error creating screenshot canvas" ) ,
153226 - 1
154227 ) ;
155- revert ( ) ; // Ensure UI is reverted on error
156228 return null ;
229+ } finally {
230+ revert ( ) ; // Ensure UI is reverted on both success and error
157231 }
158232}
159233
@@ -192,9 +266,9 @@ export async function copyToClipboard(): Promise<void> {
192266 // Firefox specific message (only show once)
193267 if (
194268 navigator . userAgent . toLowerCase ( ) . includes ( "firefox" ) &&
195- ! firefoxClipboardNotificatoinShown
269+ ! firefoxClipboardNotificationShown
196270 ) {
197- firefoxClipboardNotificatoinShown = true ;
271+ firefoxClipboardNotificationShown = true ;
198272 Notifications . add (
199273 "On Firefox you can enable the asyncClipboard.clipboardItem permission in about:config to enable copying straight to the clipboard" ,
200274 0 ,
0 commit comments