Skip to content

Commit

Permalink
implement poster and css pausing
Browse files Browse the repository at this point in the history
  • Loading branch information
marcustyphoon committed Feb 21, 2025
1 parent 9a7ffbf commit 322de41
Showing 1 changed file with 90 additions and 56 deletions.
146 changes: 90 additions & 56 deletions src/features/accesskit/disable_gifs.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { pageModifications } from '../../utils/mutations.js';
import { keyToCss } from '../../utils/css_map.js';
import { dom } from '../../utils/dom.js';
import { buildStyle, postSelector } from '../../utils/interface.js';
import { memoize } from '../../utils/memoize.js';

const canvasClass = 'xkit-paused-gif-placeholder';
const posterAttribute = 'data-paused-gif-placeholder';
const pausedContentVar = '--xkit-paused-gif-content';
const pausedBackgroundImageVar = '--xkit-paused-gif-background-image';
const hoverContainerAttribute = 'data-paused-gif-hover-container';
const labelClass = 'xkit-paused-gif-label';
const containerClass = 'xkit-paused-gif-container';
const backgroundGifClass = 'xkit-paused-background-gif';

const hovered = `:is(:hover > *, [${hoverContainerAttribute}]:hover *)`;

export const styleElement = buildStyle(`
.${labelClass} {
Expand All @@ -25,90 +29,116 @@ export const styleElement = buildStyle(`
font-weight: bold;
line-height: 1em;
}
.${labelClass}::before {
content: "GIF";
}
.${labelClass}.mini {
font-size: 0.6rem;
}
.${canvasClass} {
position: absolute;
visibility: visible;
background-color: rgb(var(--white));
}
*:hover > .${canvasClass},
*:hover > .${labelClass},
[${hoverContainerAttribute}]:hover .${canvasClass},
[${hoverContainerAttribute}]:hover .${labelClass} {
.${labelClass}${hovered},
${keyToCss('loader')}:has(~ .${labelClass}):not(${hovered}) {
display: none;
}
.${backgroundGifClass}:not(:hover) {
background-image: none !important;
background-color: rgb(var(--secondary-accent));
[${posterAttribute}]:not(${hovered}) {
visibility: visible !important;
}
img:has(~ [${posterAttribute}]):not(${hovered}) {
visibility: hidden !important;
}
.${backgroundGifClass}:not(:hover) > div {
color: rgb(var(--black));
img[style*="${pausedContentVar}"]:not(${hovered}) {
content: var(${pausedContentVar});
}
[style*="${pausedBackgroundImageVar}"]:not(${hovered}) {
background-image: var(${pausedBackgroundImageVar}) !important;
}
`);

const addLabel = (element, inside = false) => {
if (element.parentNode.querySelector(`.${labelClass}`) === null) {
const target = inside ? element : element.parentElement;
if (target) {
[...target.querySelectorAll(`.${labelClass}`)].forEach(existingLabel => existingLabel.remove());

const gifLabel = document.createElement('p');
gifLabel.className = element.clientWidth && element.clientWidth < 150
gifLabel.className = target.clientWidth && target.clientWidth < 150
? `${labelClass} mini`
: labelClass;

inside ? element.append(gifLabel) : element.parentNode.append(gifLabel);
target.append(gifLabel);
}
};

const pauseGif = function (gifElement) {
const image = new Image();
image.src = gifElement.currentSrc;
image.onload = () => {
if (gifElement.parentNode && gifElement.parentNode.querySelector(`.${canvasClass}`) === null) {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
canvas.className = gifElement.className;
canvas.classList.add(canvasClass);
canvas.getContext('2d').drawImage(image, 0, 0);
gifElement.parentNode.append(canvas);
addLabel(gifElement);
const createPausedUrl = memoize(async sourceUrl => {
const response = await fetch(sourceUrl, { headers: { Accept: 'image/webp,*/*' } });
const contentType = response.headers.get('Content-Type');
const canvas = document.createElement('canvas');

/* globals ImageDecoder */
if (typeof ImageDecoder === 'function' && await ImageDecoder.isTypeSupported(contentType)) {
const decoder = new ImageDecoder({
type: contentType,
data: response.body,
preferAnimation: true
});
const { image: videoFrame } = await decoder.decode();
if (decoder.tracks.selectedTrack.animated === false) {
// source image is not animated; decline to pause it
return undefined;
}
};
};
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
canvas.getContext('2d').drawImage(videoFrame, 0, 0);
} else {
if (sourceUrl.endsWith('.webp')) {
// source image may not be animated; decline to pause it
return undefined;
}
const imageBitmap = await response.blob().then(blob => window.createImageBitmap(blob));
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
canvas.getContext('2d').drawImage(imageBitmap, 0, 0);
}
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 1));
return URL.createObjectURL(blob);
});

const processGifs = function (gifElements) {
gifElements.forEach(gifElement => {
gifElements.forEach(async gifElement => {
if (gifElement.closest('.block-editor-writing-flow')) return;
const pausedGifElements = [
...gifElement.parentNode.querySelectorAll(`.${canvasClass}`),
...gifElement.parentNode.querySelectorAll(`.${labelClass}`)
];
if (pausedGifElements.length) {
gifElement.after(...pausedGifElements);
return;
}

if (gifElement.complete && gifElement.currentSrc) {
pauseGif(gifElement);
const posterElement = gifElement.parentElement.querySelector(keyToCss('poster'));
if (posterElement) {
posterElement.setAttribute(posterAttribute, '');
} else {
gifElement.onload = () => pauseGif(gifElement);
const sourceUrl = gifElement.currentSrc ||
await new Promise(resolve => gifElement.addEventListener('load', () => resolve(gifElement.currentSrc), { once: true }));

const pausedUrl = await createPausedUrl(sourceUrl);
if (!pausedUrl) return;

gifElement.style.setProperty(pausedContentVar, `url(${pausedUrl})`);
}
addLabel(gifElement);
gifElement.decoding = 'sync';
});
};

const sourceUrlRegex = /(?<=url\(["'])[^)]*?\.(?:gif|gifv|webp)(?=["']\))/g;
const processBackgroundGifs = function (gifBackgroundElements) {
gifBackgroundElements.forEach(gifBackgroundElement => {
gifBackgroundElement.classList.add(backgroundGifClass);
gifBackgroundElements.forEach(async gifBackgroundElement => {
const sourceValue = gifBackgroundElement.style.backgroundImage;
const sourceUrl = sourceValue.match(sourceUrlRegex)?.[0];
if (!sourceUrl) return;

const pausedUrl = await createPausedUrl(sourceUrl);
if (!pausedUrl) return;

gifBackgroundElement.style.setProperty(
pausedBackgroundImageVar,
sourceValue.replaceAll(sourceUrlRegex, pausedUrl)
);
addLabel(gifBackgroundElement, true);
});
};
Expand All @@ -134,12 +164,12 @@ const processHoverableElements = elements =>

export const main = async function () {
const gifImage = `
:is(figure, ${keyToCss('tagImage', 'takeoverBanner')}) img[srcset*=".gif"]:not(${keyToCss('poster')})
:is(figure, ${keyToCss('tagImage', 'takeoverBanner')}) img:is([srcset*=".gif"], [src*=".gif"], [srcset*=".webp"], [src*=".webp"]):not(${keyToCss('poster')})
`;
pageModifications.register(gifImage, processGifs);

const gifBackgroundImage = `
${keyToCss('communityHeaderImage', 'bannerImage')}[style*=".gif"]
${keyToCss('communityHeaderImage', 'bannerImage')}:is([style*=".gif"], [style*=".webp"])
`;
pageModifications.register(gifBackgroundImage, processBackgroundGifs);

Expand All @@ -164,7 +194,11 @@ export const clean = async function () {
wrapper.replaceWith(...wrapper.children)
);

$(`.${canvasClass}, .${labelClass}`).remove();
$(`.${backgroundGifClass}`).removeClass(backgroundGifClass);
$(`.${labelClass}`).remove();
$(`[${posterAttribute}]`).removeAttr(posterAttribute);
$(`[${hoverContainerAttribute}]`).removeAttr(hoverContainerAttribute);
[...document.querySelectorAll(`img[style*="${pausedContentVar}"]`)]
.forEach(element => element.style.removeProperty(pausedContentVar));
[...document.querySelectorAll(`[style*="${pausedBackgroundImageVar}"]`)]
.forEach(element => element.style.removeProperty(pausedBackgroundImageVar));
};

0 comments on commit 322de41

Please sign in to comment.