mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-09 13:44:29 +08:00
impr(screenshot): switch to modern-screenshot for enhancements (@byseif21) (#6884)
Switching the screenshot library from html2canvas to modern-screenshot. for both visual for users and some technical/codebase benefits. ### Visual Improvements : * Background css filters now shows in the screenshot. fix: #6862 , #1613 , https://github.com/monkeytypegame/monkeytype/issues/6249#issuecomment-2651215569 * Sharper, higher-quality screenshots noticeably especially on high-DPI screens. * Backgrounds now render correctly on small screens that were previously missing on mobile or small viewports, now included and properly scaled. * Previously, with extra height e.g input history opened, the background failed to cover everything even when it should have. * The screenshot now more closely matches what users actually see across devices and layouts. ### Non-Visual (Technical/Codebase) Improvements : * Supporting modern css makes us now able to use css for the heatmap instead of the JS. #5892 , #5879 * Reduced bundle size: Dropping html2canvas and its dependencies. * Up-to-date library, easier future improvements. --------- Co-authored-by: Samuel Hautamäki <70753342+SirObby@users.noreply.github.com> Co-authored-by: samuelhautamaki <samuelhautamaki@noreply.codeberg.org> Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
a1293e79aa
commit
e6519b166c
6 changed files with 125 additions and 82 deletions
|
|
@ -6,7 +6,6 @@ module.exports = {
|
|||
globals: {
|
||||
$: "readonly",
|
||||
jQuery: "readonly",
|
||||
html2canvas: "readonly",
|
||||
ClipboardItem: "readonly",
|
||||
grecaptcha: "readonly",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -96,13 +96,13 @@
|
|||
"firebase": "12.0.0",
|
||||
"hangul-js": "0.2.6",
|
||||
"howler": "2.2.3",
|
||||
"html2canvas": "1.4.1",
|
||||
"idb": "8.0.3",
|
||||
"jquery": "3.7.1",
|
||||
"jquery-color": "2.2.0",
|
||||
"jquery.easing": "1.4.1",
|
||||
"konami": "1.7.0",
|
||||
"lz-ts": "1.1.2",
|
||||
"modern-screenshot": "4.6.5",
|
||||
"object-hash": "3.0.0",
|
||||
"slim-select": "2.9.2",
|
||||
"stemmer": "2.0.1",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
@keyframes loader {
|
||||
0% {
|
||||
width: 0;
|
||||
left: 0;
|
||||
transform: translateX(-100%) scaleX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
transform: translateX(0) scaleX(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
width: 0;
|
||||
left: 100%;
|
||||
transform: translateX(100%) scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const element = $("#backgroundLoader");
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
|
||||
let visible = false;
|
||||
|
||||
function clearTimeout(): void {
|
||||
|
|
@ -9,17 +9,23 @@ function clearTimeout(): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function show(): void {
|
||||
export function show(instant = false): void {
|
||||
if (visible) return;
|
||||
timeout = setTimeout(() => {
|
||||
$("#backgroundLoader").stop(true, true).show();
|
||||
}, 125);
|
||||
visible = true;
|
||||
|
||||
if (instant) {
|
||||
element.stop(true, true).show();
|
||||
visible = true;
|
||||
} else {
|
||||
timeout = setTimeout(() => {
|
||||
element.stop(true, true).show();
|
||||
}, 125);
|
||||
visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function hide(): void {
|
||||
if (!visible) return;
|
||||
clearTimeout();
|
||||
$("#backgroundLoader").stop(true, true).fadeOut(125);
|
||||
element.stop(true, true).fadeOut(125);
|
||||
visible = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,6 @@ import { getHtmlByUserFlags } from "../controllers/user-flag-controller";
|
|||
import * as Notifications from "../elements/notifications";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
|
||||
async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
|
||||
return (await import("html2canvas")).default;
|
||||
}
|
||||
|
||||
let revealReplay = false;
|
||||
let revertCookie = false;
|
||||
|
||||
|
|
@ -48,15 +44,16 @@ function revert(): void {
|
|||
}
|
||||
}
|
||||
|
||||
let firefoxClipboardNotificatoinShown = false;
|
||||
let firefoxClipboardNotificationShown = false;
|
||||
|
||||
/**
|
||||
* Prepares UI, generates screenshot canvas using html2canvas, and reverts UI changes.
|
||||
* Prepares UI, generates screenshot canvas using modern-screenshot, and reverts UI changes.
|
||||
* Returns the generated canvas element or null on failure.
|
||||
* Handles its own loader and basic error notifications for canvas generation.
|
||||
*/
|
||||
async function generateCanvas(): Promise<HTMLCanvasElement | null> {
|
||||
Loader.show();
|
||||
const { domToCanvas } = await import("modern-screenshot");
|
||||
Loader.show(true);
|
||||
|
||||
if (!$("#resultReplay").hasClass("hidden")) {
|
||||
revealReplay = true;
|
||||
|
|
@ -110,7 +107,7 @@ async function generateCanvas(): Promise<HTMLCanvasElement | null> {
|
|||
}
|
||||
|
||||
(document.querySelector("html") as HTMLElement).style.scrollBehavior = "auto";
|
||||
window.scrollTo({ top: 0, behavior: "instant" as ScrollBehavior }); // Use instant scroll
|
||||
window.scrollTo({ top: 0, behavior: "auto" });
|
||||
|
||||
// --- Target Element Calculation ---
|
||||
const src = $("#result .wrapper");
|
||||
|
|
@ -120,40 +117,117 @@ async function generateCanvas(): Promise<HTMLCanvasElement | null> {
|
|||
revert();
|
||||
return null;
|
||||
}
|
||||
// Ensure offset calculations happen *after* potential layout shifts from UI prep
|
||||
await new Promise((resolve) => setTimeout(resolve, 50)); // Small delay for render updates
|
||||
await Misc.sleep(50); // Small delay for render updates
|
||||
|
||||
const sourceX = src.offset()?.left ?? 0;
|
||||
const sourceY = src.offset()?.top ?? 0;
|
||||
const sourceWidth = src.outerWidth(true) as number;
|
||||
const sourceHeight = src.outerHeight(true) as number;
|
||||
const paddingX = convertRemToPixels(2);
|
||||
const paddingY = convertRemToPixels(2);
|
||||
|
||||
// --- Canvas Generation ---
|
||||
try {
|
||||
const paddingX = convertRemToPixels(2);
|
||||
const paddingY = convertRemToPixels(2);
|
||||
// Compute full-document render size to keep the target area in frame on small viewports
|
||||
const root = document.documentElement;
|
||||
const { scrollWidth, clientWidth, scrollHeight, clientHeight } = root;
|
||||
const targetWidth = Math.max(scrollWidth, clientWidth);
|
||||
const targetHeight = Math.max(scrollHeight, clientHeight);
|
||||
|
||||
const canvas = await (
|
||||
await gethtml2canvas()
|
||||
)(document.body, {
|
||||
// Target the HTML root to include .customBackground
|
||||
const fullCanvas = await domToCanvas(root, {
|
||||
backgroundColor: await ThemeColors.get("bg"),
|
||||
width: sourceWidth + paddingX * 2,
|
||||
height: sourceHeight + paddingY * 2,
|
||||
x: sourceX - paddingX,
|
||||
y: sourceY - paddingY,
|
||||
logging: false, // Suppress html2canvas logs in console
|
||||
useCORS: true, // May be needed if user flags/icons are external
|
||||
// Sharp output
|
||||
scale: window.devicePixelRatio ?? 1,
|
||||
style: {
|
||||
width: `${targetWidth}px`,
|
||||
height: `${targetHeight}px`,
|
||||
overflow: "hidden", // for scrollbar in small viewports
|
||||
},
|
||||
// Fetch (for custom background URLs)
|
||||
fetch: {
|
||||
requestInit: { mode: "cors", credentials: "omit" },
|
||||
bypassingCache: true,
|
||||
},
|
||||
|
||||
// skipping hidden elements (THAT IS SO IMPORTANT!)
|
||||
filter: (el: Node): boolean => {
|
||||
if (!(el instanceof HTMLElement)) return true;
|
||||
const cs = getComputedStyle(el);
|
||||
return !(el.classList.contains("hidden") || cs.display === "none");
|
||||
},
|
||||
// Normalize the background layer so its negative z-index doesn't get hidden
|
||||
onCloneEachNode: (cloned) => {
|
||||
if (cloned instanceof HTMLElement) {
|
||||
const el = cloned;
|
||||
if (el.classList.contains("customBackground")) {
|
||||
el.style.zIndex = "0";
|
||||
el.style.width = `${targetWidth}px`;
|
||||
el.style.height = `${targetHeight}px`;
|
||||
// for the inner image scales
|
||||
const img = el.querySelector("img");
|
||||
if (img) {
|
||||
// (<= 720px viewport width) wpm & acc text wrapper!!
|
||||
if (window.innerWidth <= 720) {
|
||||
img.style.transform = "translateY(20vh)";
|
||||
img.style.height = "100%";
|
||||
} else {
|
||||
img.style.width = "100%"; // safety nothing more
|
||||
img.style.height = "100%"; // for image fit full screen even when words history is opened with many lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
revert(); // Revert UI *after* canvas is successfully generated
|
||||
// Scale and create output canvas
|
||||
const scale = fullCanvas.width / targetWidth;
|
||||
const paddedWidth = sourceWidth + paddingX * 2;
|
||||
const paddedHeight = sourceHeight + paddingY * 2;
|
||||
|
||||
const scaledPaddedWCanvas = Math.round(paddedWidth * scale);
|
||||
const scaledPaddedHCanvas = Math.round(paddedHeight * scale);
|
||||
const scaledPaddedWForCrop = Math.ceil(paddedWidth * scale);
|
||||
const scaledPaddedHForCrop = Math.ceil(paddedHeight * scale);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = scaledPaddedWCanvas;
|
||||
canvas.height = scaledPaddedHCanvas;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
Notifications.add("Failed to get canvas context for screenshot", -1);
|
||||
return null;
|
||||
}
|
||||
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
|
||||
// Calculate crop coordinates with proper clamping
|
||||
const cropX = Math.max(0, Math.floor((sourceX - paddingX) * scale));
|
||||
const cropY = Math.max(0, Math.floor((sourceY - paddingY) * scale));
|
||||
const cropW = Math.min(scaledPaddedWForCrop, fullCanvas.width - cropX);
|
||||
const cropH = Math.min(scaledPaddedHForCrop, fullCanvas.height - cropY);
|
||||
|
||||
ctx.drawImage(
|
||||
fullCanvas,
|
||||
cropX,
|
||||
cropY,
|
||||
cropW,
|
||||
cropH,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
return canvas;
|
||||
} catch (e) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Error creating screenshot canvas"),
|
||||
-1
|
||||
);
|
||||
revert(); // Ensure UI is reverted on error
|
||||
return null;
|
||||
} finally {
|
||||
revert(); // Ensure UI is reverted on both success and error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,9 +266,9 @@ export async function copyToClipboard(): Promise<void> {
|
|||
// Firefox specific message (only show once)
|
||||
if (
|
||||
navigator.userAgent.toLowerCase().includes("firefox") &&
|
||||
!firefoxClipboardNotificatoinShown
|
||||
!firefoxClipboardNotificationShown
|
||||
) {
|
||||
firefoxClipboardNotificatoinShown = true;
|
||||
firefoxClipboardNotificationShown = true;
|
||||
Notifications.add(
|
||||
"On Firefox you can enable the asyncClipboard.clipboardItem permission in about:config to enable copying straight to the clipboard",
|
||||
0,
|
||||
|
|
|
|||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
|
|
@ -321,9 +321,6 @@ importers:
|
|||
howler:
|
||||
specifier: 2.2.3
|
||||
version: 2.2.3
|
||||
html2canvas:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1
|
||||
idb:
|
||||
specifier: 8.0.3
|
||||
version: 8.0.3
|
||||
|
|
@ -342,6 +339,9 @@ importers:
|
|||
lz-ts:
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2
|
||||
modern-screenshot:
|
||||
specifier: 4.6.5
|
||||
version: 4.6.5
|
||||
object-hash:
|
||||
specifier: 3.0.0
|
||||
version: 3.0.0
|
||||
|
|
@ -3661,10 +3661,6 @@ packages:
|
|||
bare-events:
|
||||
optional: true
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
|
|
@ -4341,9 +4337,6 @@ packages:
|
|||
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
css-line-break@2.1.0:
|
||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
||||
|
||||
css-select@5.1.0:
|
||||
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||
|
||||
|
|
@ -5621,10 +5614,6 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
html2canvas@1.4.1:
|
||||
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
htmlparser2@5.0.1:
|
||||
resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
|
||||
|
||||
|
|
@ -6935,6 +6924,9 @@ packages:
|
|||
mobx@6.13.1:
|
||||
resolution: {integrity: sha512-ekLRxgjWJr8hVxj9ZKuClPwM/iHckx3euIJ3Np7zLVNtqJvfbbq7l370W/98C8EabdQ1pB5Jd3BbDWxJPNnaOg==}
|
||||
|
||||
modern-screenshot@4.6.5:
|
||||
resolution: {integrity: sha512-0sDePJ9ssXWDO7V+yW9lwAxAu8jmVp4CXlBbjskSqrDxkIrcZO2EGqwD2mLtfTTinqZjmP4X/V6INOvNM1K7CQ==}
|
||||
|
||||
module-definition@6.0.0:
|
||||
resolution: {integrity: sha512-sEGP5nKEXU7fGSZUML/coJbrO+yQtxcppDAYWRE9ovWsTbFoUHB2qDUx564WUzDaBHXsD46JBbIK5WVTwCyu3w==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -8727,9 +8719,6 @@ packages:
|
|||
text-hex@1.0.0:
|
||||
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
|
|
@ -9193,9 +9182,6 @@ packages:
|
|||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
utrie@1.0.2:
|
||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
||||
|
||||
uuid@10.0.0:
|
||||
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
|
||||
hasBin: true
|
||||
|
|
@ -13134,8 +13120,6 @@ snapshots:
|
|||
bare-events: 2.6.0
|
||||
optional: true
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
basic-auth-connect@1.0.0: {}
|
||||
|
|
@ -13893,10 +13877,6 @@ snapshots:
|
|||
|
||||
css-color-keywords@1.0.0: {}
|
||||
|
||||
css-line-break@2.1.0:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
|
||||
css-select@5.1.0:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
|
@ -15712,11 +15692,6 @@ snapshots:
|
|||
relateurl: 0.2.7
|
||||
uglify-js: 3.19.1
|
||||
|
||||
html2canvas@1.4.1:
|
||||
dependencies:
|
||||
css-line-break: 2.1.0
|
||||
text-segmentation: 1.0.3
|
||||
|
||||
htmlparser2@5.0.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
|
@ -17226,6 +17201,8 @@ snapshots:
|
|||
|
||||
mobx@6.13.1: {}
|
||||
|
||||
modern-screenshot@4.6.5: {}
|
||||
|
||||
module-definition@6.0.0:
|
||||
dependencies:
|
||||
ast-module-types: 6.0.0
|
||||
|
|
@ -19370,10 +19347,6 @@ snapshots:
|
|||
|
||||
text-hex@1.0.0: {}
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
|
|
@ -19846,10 +19819,6 @@ snapshots:
|
|||
|
||||
utils-merge@1.0.1: {}
|
||||
|
||||
utrie@1.0.2:
|
||||
dependencies:
|
||||
base64-arraybuffer: 1.0.2
|
||||
|
||||
uuid@10.0.0: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue