const Mode = { Light: 1, Dark: 2, }; /** * Convert to grayscale * @param {{r: number, g: number, b: number}} color * @returns Number between 0 and 255 */ const colorToGrayscale = (color) => { return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b; }; /** * Extract colors from an image or SVG element using canvas * @param {HTMLImageElement | SVGSVGElement} element Image or SVG source element * @returns {Promise} Colors representing the element */ const extractColors = (element) => { return new Promise((resolve, reject) => { // Create a canvas with the same dimensions as the element const canvas = document.createElement("canvas"); let imageSource; if (element instanceof SVGSVGElement) { // SVG element handling canvas.width = element.viewBox.baseVal.width; canvas.height = element.viewBox.baseVal.height; const svgString = new XMLSerializer().serializeToString(element); const svgBlob = new Blob([svgString], { type: "image/svg+xml" }); const svgUrl = URL.createObjectURL(svgBlob); imageSource = new Image(); imageSource.src = svgUrl; imageSource.onload = () => { processImage(imageSource); URL.revokeObjectURL(svgUrl); }; imageSource.onerror = (error) => { URL.revokeObjectURL(svgUrl); reject(error); }; } else { // Regular image handling canvas.width = element.naturalWidth || element.width; canvas.height = element.naturalHeight || element.height; processImage(element); } const processImage = (img) => { const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData( 0, 0, Math.max(1, canvas.width), Math.max(1, canvas.height) ); const pixelData = imageData.data; const colors = []; for (let i = 0; i < pixelData.length; i += 4) { if (pixelData[i + 3] > 0) { colors.push({ r: pixelData[i], g: pixelData[i + 1], b: pixelData[i + 2], }); } } resolve(colors); }; }); }; /** * Change the color theme based on the mode * @param {Mode} mode */ const changeTheme = (mode) => { // Process SVG elements for (const svgElement of document.getElementsByTagName("svg")) { extractColors(svgElement) .then((colors) => { applyFilter(mode, svgElement, colors); }) .catch((error) => { console.error("Error extracting colors:", error); }); } // Process SVG images embedded as base64 for (const item of document.getElementsByTagName("img")) { if (!item.src.startsWith("data:image/svg+xml;base64,")) { // Exclude images that aren't SVG and base64 encoded continue; } extractColors(item) .then((colors) => { applyFilter(mode, item, colors); }) .catch((error) => { console.error("Error extracting colors:", error); }); } }; /** * Apply the filter based on the selected mode * @param {Mode} mode Current theme * @param {HTMLImageElement | SVGSVGElement} item Element * @param {{r: number, g: number, b: number}} colors Array of the colors of the element * @returns */ const applyFilter = (mode, item, colors) => { // Calculate the average grayscale value const grayscaleValues = colors.map(colorToGrayscale); const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0); const averageGrayscale = totalGrayscale / grayscaleValues.length; const treshold = 128; const style = "filter: "; const dim = "brightness(0.8) contrast(1.2)"; if (averageGrayscale < treshold && mode === Mode.Dark) { item.style = style + dim + " invert(1);"; return; } if (averageGrayscale > treshold && mode === Mode.Light) { item.style = style + "invert(1);"; return; } if (mode === Mode.Dark) { item.style = style + `${dim};`; return; } item.style = ""; }; window.addEventListener("load", () => { // Fix SVG images changeTheme( window.matchMedia("(prefers-color-scheme: dark)").matches ? Mode.Dark : Mode.Light ); }); window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", (event) => changeTheme(event.matches ? Mode.Dark : Mode.Light) );