Created
April 6, 2024 11:24
-
-
Save PhilippeVay/9ed5dc541f7c51f2b30924ee7b292b99 to your computer and use it in GitHub Desktop.
Highlight images and alike while making the rest of content barely visible (SVG masks, perf isn't good while scrolling)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Highlight images and alike while making the rest of content barely visible. | |
* | |
* Useful for accessibility audits (RGAA / WCAG) to help distinguish visually HTML images from background images, icon fonts, etc | |
* Performance isn't good (not that bad either). Scrolling is kind of sluggish in taaaall pages with dozens of images (bursts of | |
* 100% CPU time with a Ryzen 5 laptop). Initial execution time is OK. | |
* Pure CSS solution with `rgba(0,0,0, 0.2)` is way better. | |
* | |
* This script: | |
* - gets the position and dimensions of each images, | |
* - injects an SVG with masks surimposed to images | |
* - uses CSS (mask-image with multiple values) to nearly hide content except images | |
*/ | |
// Commenting const/let so it can be launched multiple times in the browser console | |
/* const */ imageSel = 'img, [role="img"], area, input[type="image"], img[ismap], object[type^="image/"], embed[type^="image/"]'; | |
/* const */ delay = 1000; | |
/* const */ nsSvg = "http://www.w3.org/2000/svg"; // namespace of SVG | |
document.addEventListener("DOMContentLoaded", (event) => { | |
/* let */ $imageList = Array.from(document.querySelectorAll(imageSel)); | |
/* const */ imgRects = []; | |
/** | |
* Position and dimension of a collection of image elements | |
* | |
* @param {[HTMLElement]} elts - Image elements to get position and dimension from | |
* @return {[{DOMRect}]} - {x, y, width, height} of each image | |
*/ | |
function getImageBoundaries(elts) { | |
const geos = []; | |
elts.forEach(el => { | |
// Images have null dimensions before they're _really_ loaded. | |
// Input[type=image] and other non-IMG don't have `.complete` | |
if (el.tagName !== "IMG" || el.complete) { | |
const geo = el.getBoundingClientRect(); | |
geos.push(geo); | |
} else { | |
console.log("Image hasn't loaded (yet)", el); | |
} | |
}) | |
return geos; | |
} | |
/** | |
* Create masking SVG <svg><defs></defs></svg> | |
* | |
* @return {[HTMLElement, HTMLElement]} Parent SVG and its <defs> child | |
*/ | |
function createRootSvg(/* id */) { | |
const svgRoot = document.createElementNS(nsSvg, "svg"); | |
// svgRoot.id = id; | |
svgRoot.style = "position: absolute; top: 0; left: 0; pointer-events: none;"; | |
const svgDefs = document.createElementNS(nsSvg, "defs"); | |
svgRoot.appendChild(svgDefs); | |
return [svgRoot, svgDefs]; | |
} | |
/** | |
* Inject a <mask> into <defs>. It's surimposed to an image | |
* | |
* @param {DOMRect} geo - Position and dimension of an image | |
* @param {Number} ref - Rank of image to create an unique id (will allow to reference mask in CSS) | |
* @param {Number} scrX - Horizontal scroll position in the page | |
* @param {Number} scrY - Vertical scroll position in the page | |
* @param {HTMLElement} eltParent - Element <defs> where mask is injected | |
*/ | |
function createMask(geo, ref, scrX, scrY, eltParent) { | |
const mask = document.createElementNS(nsSvg, "mask"); | |
mask.id = `m-${ref.toString()}`; | |
eltParent.appendChild(mask); | |
const rect = document.createElementNS(nsSvg, "rect"); | |
rect.setAttribute("x", geo.x + scrX); | |
rect.setAttribute("y", geo.y + scrY); | |
rect.setAttribute("width", geo.width); | |
rect.setAttribute("height", geo.height); | |
rect.setAttribute("fill", "var(--rgaac-c)"); | |
mask.appendChild(rect); | |
} | |
/** | |
* Final mask (inverted color). Same size as the page (i.e. huge), nearly black so rest of content nearly disappears | |
* | |
* @param {String} id - id to be added on the <mask> | |
* @param {Number} w - Width of page | |
* @param {Number} h - Height of page | |
*/ | |
function createBgMask(id, w, h) { | |
const bgMask = document.createElementNS(nsSvg, "mask"); | |
bgMask.id = id; | |
const bg = document.createElementNS(nsSvg, "rect"); | |
bg.setAttribute("x", "0"); | |
bg.setAttribute("y", "0"); | |
bg.setAttribute("width", w); | |
bg.setAttribute("height", h); | |
bg.setAttribute("fill", "var(--rgaac-bg)"); | |
bgMask.prepend(bg); | |
return bgMask; | |
} | |
/** | |
* Generate needed CSS declarations, especially multiple `url(#m-id)` and adds to body as inline style | |
* TODO: if body already has inline styles, they're overridden and lost. OK for a demo | |
* | |
* @param {Number} len - Number of masks / images | |
*/ | |
function cssInlineMask(len) { | |
let cssMask = 'mask-image: '; | |
for (let i = 0; i < len; i++) { | |
cssMask += `url(#m-${i}), `; | |
} | |
cssMask += 'url(#bgMask);'; | |
return `${cssMask} mask-size: 100% 100%; mask-repeat: no-repeat; --rgaac-c: white; --rgaac-bg: #222; ` | |
} | |
setTimeout(function () { | |
const body = document.body; | |
// Array of {x, y, width, height} of each image | |
imgRects = getImageBoundaries($imageList); | |
const [svgRoot, svgDefs] = createRootSvg(); // Parameter not needed anymore : id "imgMask" | |
// Scroll position in the page | |
const scrX = window.scrollX; | |
const scrY = window.scrollY; | |
// Main feature: each image has a surimposed white rect so it isn't masked at all | |
imgRects.forEach((geo, j) => { | |
createMask(geo, j, scrX, scrY, svgDefs); | |
}); | |
// Masking everything else with near-black rect the size of the page | |
const pageW = body.scrollWidth; // page dimensions | |
const pageH = body.scrollHeight; | |
svgDefs.appendChild( createBgMask("bgMask", pageW, pageH) ); | |
// Parent SVG is injected before `</body>` | |
body.appendChild(svgRoot); | |
// CSS: building mask-image declaration referencing each mask id over each image (and the huge mask over page) | |
body.style = cssInlineMask(imgRects.length); | |
console.log('Done: SVG is added and masking images vs content'); | |
}, delay); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment