mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Blurhash implementation (from scratch)
This commit is contained in:
parent
2efdc94146
commit
8ef7a7a054
4 changed files with 139 additions and 18 deletions
|
@ -16,6 +16,12 @@ _define('fetch', function() {
|
|||
return fetch;
|
||||
});
|
||||
|
||||
// Blurhash
|
||||
var blurhash = require('blurhash');
|
||||
_define('blurhash', function() {
|
||||
return blurhash;
|
||||
});
|
||||
|
||||
// query-string
|
||||
var query = require('query-string');
|
||||
_define('queryString', function() {
|
||||
|
|
|
@ -505,6 +505,9 @@ import 'programStyles';
|
|||
let imgUrl = null;
|
||||
let coverImage = false;
|
||||
let uiAspect = null;
|
||||
let blurhash;
|
||||
let blurhashimg = item.ImageBlurHashes;
|
||||
let imgtag;
|
||||
|
||||
if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) {
|
||||
|
||||
|
@ -513,6 +516,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Thumb
|
||||
});
|
||||
imgtag = item.ImageTags.Thumb;
|
||||
|
||||
} else if ((options.preferBanner || shape === 'banner') && item.ImageTags && item.ImageTags.Banner) {
|
||||
|
||||
|
@ -521,6 +525,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Banner
|
||||
});
|
||||
imgtag = item.ImageTags.Banner;
|
||||
|
||||
} else if (options.preferDisc && item.ImageTags && item.ImageTags.Disc) {
|
||||
|
||||
|
@ -529,6 +534,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Disc
|
||||
});
|
||||
imgtag = item.ImageTags.Disc;
|
||||
|
||||
} else if (options.preferLogo && item.ImageTags && item.ImageTags.Logo) {
|
||||
|
||||
|
@ -537,6 +543,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Logo
|
||||
});
|
||||
imgtag = item.ImageTags.Logo;
|
||||
|
||||
} else if (options.preferLogo && item.ParentLogoImageTag && item.ParentLogoItemId) {
|
||||
|
||||
|
@ -545,6 +552,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ParentLogoImageTag
|
||||
});
|
||||
imgtag = item.ParentLogoImageTag;
|
||||
|
||||
} else if (options.preferThumb && item.SeriesThumbImageTag && options.inheritThumb !== false) {
|
||||
|
||||
|
@ -553,6 +561,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.SeriesThumbImageTag
|
||||
});
|
||||
imgtag = item.SeriesThumbImageTag;
|
||||
|
||||
} else if (options.preferThumb && item.ParentThumbItemId && options.inheritThumb !== false && item.MediaType !== 'Photo') {
|
||||
|
||||
|
@ -561,6 +570,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ParentThumbImageTag
|
||||
});
|
||||
imgtag = item.ParentThumbImageTag;
|
||||
|
||||
} else if (options.preferThumb && item.BackdropImageTags && item.BackdropImageTags.length) {
|
||||
|
||||
|
@ -569,6 +579,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.BackdropImageTags[0]
|
||||
});
|
||||
imgtag = item.BackdropImageTags[0];
|
||||
|
||||
forceName = true;
|
||||
|
||||
|
@ -579,6 +590,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ParentBackdropImageTags[0]
|
||||
});
|
||||
imgtag = item.ParentBackdropImageTags[0];
|
||||
|
||||
} else if (item.ImageTags && item.ImageTags.Primary) {
|
||||
|
||||
|
@ -590,6 +602,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Primary
|
||||
});
|
||||
imgtag = item.ImageTags.Primary;
|
||||
|
||||
if (options.preferThumb && options.showTitle !== false) {
|
||||
forceName = true;
|
||||
|
@ -612,6 +625,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.PrimaryImageTag
|
||||
});
|
||||
imgtag = item.PrimaryImageTag;
|
||||
|
||||
if (options.preferThumb && options.showTitle !== false) {
|
||||
forceName = true;
|
||||
|
@ -630,6 +644,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ParentPrimaryImageTag
|
||||
});
|
||||
imgtag = item.ParentPrimaryImageTag;
|
||||
} else if (item.SeriesPrimaryImageTag) {
|
||||
|
||||
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
|
||||
|
@ -637,6 +652,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.SeriesPrimaryImageTag
|
||||
});
|
||||
imgtag = item.SeriesPrimaryImageTag;
|
||||
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
|
||||
|
||||
height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null;
|
||||
|
@ -647,6 +663,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.AlbumPrimaryImageTag
|
||||
});
|
||||
imgtag = item.AlbumPrimaryImageTag;
|
||||
|
||||
if (primaryImageAspectRatio) {
|
||||
uiAspect = getDesiredAspect(shape);
|
||||
|
@ -661,6 +678,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Thumb
|
||||
});
|
||||
imgtag = item.ImageTags.Thumb;
|
||||
|
||||
} else if (item.BackdropImageTags && item.BackdropImageTags.length) {
|
||||
|
||||
|
@ -669,6 +687,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.BackdropImageTags[0]
|
||||
});
|
||||
imgtag = item.BackdropImageTags[0];
|
||||
|
||||
} else if (item.ImageTags && item.ImageTags.Thumb) {
|
||||
|
||||
|
@ -677,6 +696,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ImageTags.Thumb
|
||||
});
|
||||
imgtag = item.ImageTags.Thumb;
|
||||
|
||||
} else if (item.SeriesThumbImageTag && options.inheritThumb !== false) {
|
||||
|
||||
|
@ -685,6 +705,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.SeriesThumbImageTag
|
||||
});
|
||||
imgtag = item.SeriesThumbImageTag;
|
||||
|
||||
} else if (item.ParentThumbItemId && options.inheritThumb !== false) {
|
||||
|
||||
|
@ -693,6 +714,7 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ParentThumbImageTag
|
||||
});
|
||||
imgtag = item.ParentThumbImageTag;
|
||||
|
||||
} else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length && options.inheritThumb !== false) {
|
||||
|
||||
|
@ -701,11 +723,15 @@ import 'programStyles';
|
|||
maxWidth: width,
|
||||
tag: item.ParentBackdropImageTags[0]
|
||||
});
|
||||
imgtag = item.ParentBackdropImageTags[0];
|
||||
|
||||
}
|
||||
|
||||
blurhash = imageLoader.getImageBlurhashStr(blurhashimg, imgtag);
|
||||
|
||||
return {
|
||||
imgUrl: imgUrl,
|
||||
blurhash: blurhash,
|
||||
forceName: forceName,
|
||||
coverImage: coverImage
|
||||
};
|
||||
|
@ -1321,6 +1347,7 @@ import 'programStyles';
|
|||
|
||||
const imgInfo = getCardImageUrl(item, apiClient, options, shape);
|
||||
const imgUrl = imgInfo.imgUrl;
|
||||
const blurhash = imgInfo.blurhash;
|
||||
|
||||
const forceName = imgInfo.forceName;
|
||||
|
||||
|
@ -1445,15 +1472,20 @@ import 'programStyles';
|
|||
cardContentClass += ' cardContent-shadow';
|
||||
}
|
||||
|
||||
let blurhashAttrib = '';
|
||||
if (blurhash && blurhash.length > 0) {
|
||||
blurhashAttrib = 'data-blurhash="' + blurhash + '"';
|
||||
}
|
||||
|
||||
if (layoutManager.tv) {
|
||||
|
||||
// Don't use the IMG tag with safari because it puts a white border around it
|
||||
cardImageContainerOpen = imgUrl ? ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + ' lazy" data-src="' + imgUrl + '">') : ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + '">');
|
||||
cardImageContainerOpen = imgUrl ? ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + ' lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + '>') : ('<div class="' + cardImageContainerClass + ' ' + cardContentClass + '">');
|
||||
|
||||
cardImageContainerClose = '</div>';
|
||||
} else {
|
||||
// Don't use the IMG tag with safari because it puts a white border around it
|
||||
cardImageContainerOpen = imgUrl ? ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '">') : ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction">');
|
||||
cardImageContainerOpen = imgUrl ? ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction lazy" data-src="' + imgUrl + '" ' + blurhashAttrib + '>') : ('<button data-action="' + action + '" class="cardContent-button ' + cardImageContainerClass + ' ' + cardContentClass + ' itemAction">');
|
||||
|
||||
cardImageContainerClose = '</button>';
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as lazyLoader from 'lazyLoader';
|
||||
import * as userSettings from 'userSettings';
|
||||
import * as blurhash from 'blurhash';
|
||||
import 'css!./style';
|
||||
/* eslint-disable indent */
|
||||
|
||||
|
@ -11,6 +12,82 @@ import 'css!./style';
|
|||
fillImageElement(elem, source);
|
||||
}
|
||||
|
||||
export function getImageBlurhashStr(hashes, tags) {
|
||||
if (hashes && tags) {
|
||||
return hashes[tags];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// function destroyBlurhash(target) {
|
||||
// let canvas = target.getElementsByClassName('blurhash-canvas')[0];
|
||||
// target.removeChild(canvas);
|
||||
// target.classList.remove('blurhashed');
|
||||
// }
|
||||
|
||||
function itemBlurhashing(entry) {
|
||||
// This intersection ratio ensures that items that are near the borders are also blurhashed, alongside items that are outside the viewport
|
||||
// if (entry.intersectionRation <= 0.025)
|
||||
if (entry.target) {
|
||||
let target = entry.target;
|
||||
// We only keep max 80 items blurhashed in screen to save memory
|
||||
// if (document.getElementsByClassName('blurhashed').length <= 80) {
|
||||
|
||||
//} else {
|
||||
// destroyBlurhash(target);
|
||||
//}
|
||||
let blurhashstr = target.getAttribute('data-blurhash');
|
||||
if (blurhash.isBlurhashValid(blurhashstr) && target.getElementsByClassName('blurhash-canvas').length === 0) {
|
||||
console.log('Blurhashing item ' + target.parentElement.parentElement.parentElement.getAttribute('data-index') + ' with intersection ratio ' + entry.intersectionRatio);
|
||||
let width = target.offsetWidth;
|
||||
let height = target.offsetHeight;
|
||||
if (width && height) {
|
||||
let pixels;
|
||||
try {
|
||||
pixels = blurhash.decode(blurhashstr, width, height);
|
||||
} catch (err) {
|
||||
console.log('Blurhash decode error: ' + err.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
let ctx = canvas.getContext('2d');
|
||||
let imgData = ctx.createImageData(width, height);
|
||||
|
||||
imgData.data.set(pixels);
|
||||
// Values taken from https://www.npmjs.com/package/blurhash
|
||||
ctx.putImageData(imgData, 1, 1);
|
||||
|
||||
let child = target.appendChild(canvas);
|
||||
child.classList.add('blurhash-canvas');
|
||||
child.style.opacity = 1;
|
||||
if (userSettings.enableFastFadein()) {
|
||||
child.classList.add('lazy-blurhash-fadein-fast');
|
||||
} else {
|
||||
child.classList.add('lazy-blurhash-fadein');
|
||||
}
|
||||
|
||||
target.classList.add('blurhashed');
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function switchCanvas(elem) {
|
||||
let child = elem.getElementsByClassName('blurhash-canvas')[0];
|
||||
if (child) {
|
||||
if (elem.getAttribute('data-src')) {
|
||||
child.style.opacity = 1;
|
||||
} else {
|
||||
child.style.opacity = 0;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
export function fillImage(entry) {
|
||||
if (!entry) {
|
||||
throw new Error('entry cannot be null');
|
||||
|
@ -23,6 +100,10 @@ import 'css!./style';
|
|||
source = entry;
|
||||
}
|
||||
|
||||
if (!entry.target.classList.contains('blurhashed')) {
|
||||
itemBlurhashing(entry);
|
||||
}
|
||||
|
||||
if (entry.intersectionRatio > 0) {
|
||||
if (source) fillImageElement(entry.target, source);
|
||||
} else if (!source) {
|
||||
|
@ -45,14 +126,12 @@ import 'css!./style';
|
|||
elem.setAttribute('src', url);
|
||||
}
|
||||
|
||||
if (userSettings.enableFastFadein()) {
|
||||
elem.classList.add('lazy-image-fadein-fast');
|
||||
} else {
|
||||
elem.classList.add('lazy-image-fadein');
|
||||
}
|
||||
|
||||
elem.removeAttribute('data-src');
|
||||
switchCanvas(elem);
|
||||
});
|
||||
// preloaderImg.onload = function () {
|
||||
|
||||
// };
|
||||
}
|
||||
|
||||
function emptyImageElement(elem) {
|
||||
|
@ -67,9 +146,7 @@ import 'css!./style';
|
|||
}
|
||||
|
||||
elem.setAttribute('data-src', url);
|
||||
|
||||
elem.classList.remove('lazy-image-fadein-fast');
|
||||
elem.classList.remove('lazy-image-fadein');
|
||||
switchCanvas(elem);
|
||||
}
|
||||
|
||||
export function lazyChildren(elem) {
|
||||
|
@ -148,6 +225,7 @@ import 'css!./style';
|
|||
export default {
|
||||
fillImages: fillImages,
|
||||
fillImage: fillImage,
|
||||
getImageBlurhashStr: getImageBlurhashStr,
|
||||
lazyImage: lazyImage,
|
||||
lazyChildren: lazyChildren,
|
||||
getPrimaryImageAspectRatio: getPrimaryImageAspectRatio
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
.cardImageContainer.lazy {
|
||||
opacity: 0;
|
||||
.lazy-blurhash-fadein-fast {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.cardImageContainer.lazy.lazy-image-fadein {
|
||||
opacity: 1;
|
||||
.lazy-blurhash-fadein {
|
||||
transition: opacity 0.7s;
|
||||
}
|
||||
|
||||
.cardImageContainer.lazy.lazy-image-fadein-fast {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
/* We let the canvas overflow a little, so it gives a cooler zoom effect when transitioning */
|
||||
.blurhash-canvas {
|
||||
align-items: stretch;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
width: 105%;
|
||||
height: 105%;
|
||||
z-index: -1000;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue