1
0
Fork 0
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:
ferferga 2020-05-23 18:35:34 +02:00
parent 2efdc94146
commit 8ef7a7a054
4 changed files with 139 additions and 18 deletions

View file

@ -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() {

View file

@ -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>';
}

View file

@ -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

View file

@ -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;
}