philomena/assets/js/image_expansion.js
Eliot Partridge 65da36369d
Pause spoilered videos on image pages, unpause on unspoiler (#259)
* Pause spoilered videos on image pages, unpause on unspoiler

* Add test

* Fix comment
2024-05-14 21:32:45 -04:00

179 lines
5.6 KiB
JavaScript

import { $$, clearEl } from './utils/dom';
import store from './utils/store';
const imageVersions = {
// [width, height]
small: [320, 240],
medium: [800, 600],
large: [1280, 1024]
};
/**
* Picks the appropriate image version for a given width and height
* of the viewport and the image dimensions.
*/
function selectVersion(imageWidth, imageHeight, imageSize, imageMime) {
let viewWidth = document.documentElement.clientWidth,
viewHeight = document.documentElement.clientHeight;
// load hires if that's what you asked for
if (store.get('serve_hidpi')) {
viewWidth *= window.devicePixelRatio || 1;
viewHeight *= window.devicePixelRatio || 1;
}
if (viewWidth > 1024 && imageHeight > 1024 && imageHeight > 2.5 * imageWidth) {
// Treat as comic-sized dimensions..
return 'tall';
}
// Find a version that is larger than the view in one/both axes
// .find() is not supported in older browsers, using a loop
for (let i = 0, versions = Object.keys(imageVersions); i < versions.length; ++i) {
const version = versions[i],
dimensions = imageVersions[version],
versionWidth = Math.min(imageWidth, dimensions[0]),
versionHeight = Math.min(imageHeight, dimensions[1]);
if (versionWidth > viewWidth || versionHeight > viewHeight) {
return version;
}
}
// If the view is larger than any available version, display the original image.
//
// Sanity check to make sure we're not serving unintentionally huge assets
// all at once (where "huge" > 25 MiB). Videos are loaded in chunks so it
// doesn't matter too much there.
if (imageMime === 'video/webm' || imageSize <= 26214400) {
return 'full';
}
return 'large';
}
/**
* Given a target container element, chooses and scales an image
* to an appropriate dimension.
*/
function pickAndResize(elem) {
const imageWidth = parseInt(elem.dataset.width, 10),
imageHeight = parseInt(elem.dataset.height, 10),
imageSize = parseInt(elem.dataset.imageSize, 10),
imageMime = elem.dataset.mimeType,
scaled = elem.dataset.scaled,
uris = JSON.parse(elem.dataset.uris);
let version = 'full';
if (scaled === 'true') {
version = selectVersion(imageWidth, imageHeight, imageSize, imageMime);
}
const uri = uris[version];
let imageFormat = /\.(\w+?)$/.exec(uri)[1];
if (version === 'full' && store.get('serve_webm') && Boolean(uris.mp4)) {
imageFormat = 'mp4';
}
// Check if we need to change to avoid flickering
if (imageFormat === 'mp4' || imageFormat === 'webm') {
for (const sourceEl of elem.querySelectorAll('video source')) {
if (sourceEl.src.endsWith(uri) || (imageFormat === 'mp4' && sourceEl.src.endsWith(uris.mp4))) return;
}
// Scrub out the target element.
clearEl(elem);
}
const muted = store.get('unmute_videos') ? '' : 'muted';
const autoplay = elem.classList.contains('hidden') ? '' : 'autoplay'; // Fix for spoilered image pages
if (imageFormat === 'mp4') {
elem.classList.add('full-height');
elem.insertAdjacentHTML('afterbegin',
`<video controls ${autoplay} loop ${muted} playsinline preload="auto" id="image-display"
width="${imageWidth}" height="${imageHeight}">
<source src="${uris.webm}" type="video/webm">
<source src="${uris.mp4}" type="video/mp4">
<p class="block block--fixed block--warning">
Your browser supports neither MP4/H264 nor
WebM/VP8! Please update it to the latest version.
</p>
</video>`
);
}
else if (imageFormat === 'webm') {
elem.insertAdjacentHTML('afterbegin',
`<video controls ${autoplay} loop ${muted} playsinline id="image-display">
<source src="${uri}" type="video/webm">
<source src="${uri.replace(/webm$/, 'mp4')}" type="video/mp4">
<p class="block block--fixed block--warning">
Your browser supports neither MP4/H264 nor
WebM/VP8! Please update it to the latest version.
</p>
</video>`
);
const video = elem.querySelector('video');
if (scaled === 'true') {
video.className = 'image-scaled';
}
else if (scaled === 'partscaled') {
video.className = 'image-partscaled';
}
}
else {
let image;
if (scaled === 'true') {
image = `<picture><img id="image-display" src="${uri}" class="image-scaled"></picture>`;
}
else if (scaled === 'partscaled') {
image = `<picture><img id="image-display" src="${uri}" class="image-partscaled"></picture>`;
}
else {
image = `<picture><img id="image-display" src="${uri}" width="${imageWidth}" height="${imageHeight}"></picture>`;
}
if (elem.innerHTML === image) return;
clearEl(elem);
elem.insertAdjacentHTML('afterbegin', image);
}
}
/**
* Bind an event to an image container for updating an image on
* click/tap.
*/
function bindImageForClick(target) {
target.addEventListener('click', () => {
if (target.getAttribute('data-scaled') === 'true') {
target.setAttribute('data-scaled', 'partscaled');
}
else if (target.getAttribute('data-scaled') === 'partscaled') {
target.setAttribute('data-scaled', 'false');
}
else {
target.setAttribute('data-scaled', 'true');
}
pickAndResize(target);
});
}
function bindImageTarget(node = document) {
$$('.image-target', node).forEach(target => {
pickAndResize(target);
if (target.dataset.mimeType === 'video/webm') {
// Don't interfere with media controls on video
return;
}
bindImageForClick(target);
window.addEventListener('resize', () => {
pickAndResize(target);
});
});
}
export { bindImageTarget };