Skip to content

Instantly share code, notes, and snippets.

@PhilippeVay
Created April 6, 2024 11:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PhilippeVay/9ed5dc541f7c51f2b30924ee7b292b99 to your computer and use it in GitHub Desktop.
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)
/**
* 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