From 302094523e5ff9d282df9d11b647f341c13f5c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 4 Nov 2021 23:12:16 +0100 Subject: [PATCH 1/9] Remove unused worker plugin --- package-lock.json | 89 ++++++++++++++++++++++++++--------------------- package.json | 3 +- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e9bca4b6..76928e9aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3082,7 +3082,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", - "dev": true + "dev": true, + "requires": {} }, "@webpack-cli/info": { "version": "1.4.0", @@ -3097,7 +3098,8 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", - "dev": true + "dev": true, + "requires": {} }, "@xmldom/xmldom": { "version": "0.7.5", @@ -3142,13 +3144,15 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "aggregate-error": { "version": "3.1.0", @@ -3205,7 +3209,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -4648,7 +4653,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", - "dev": true + "dev": true, + "requires": {} }, "csso": { "version": "4.2.0", @@ -5801,7 +5807,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true + "dev": true, + "requires": {} }, "eslint-rule-composer": { "version": "0.3.0", @@ -6025,7 +6032,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", "integrity": "sha512-2RExSo0yJiqP+xiUue13jQa2IHE8kLDzTI7b6kn+vUlBVvlzNSiLDzo4e5Pp5J039usvTUnxZ8sUOhv0Kg15NA==", - "dev": true + "dev": true, + "requires": {} }, "express": { "version": "4.17.2", @@ -7030,7 +7038,8 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true + "dev": true, + "requires": {} }, "idb": { "version": "6.1.2", @@ -7762,8 +7771,8 @@ "integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg==" }, "libass-wasm": { - "version": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6", - "from": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4" + "version": "git+ssh://git@github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6", + "from": "libass-wasm@git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4" }, "lie": { "version": "3.1.1", @@ -9058,25 +9067,29 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", - "dev": true + "dev": true, + "requires": {} }, "postcss-double-position-gradients": { "version": "3.0.4", @@ -9692,7 +9705,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true + "dev": true, + "requires": {} }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -9748,7 +9762,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", - "dev": true + "dev": true, + "requires": {} }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -10150,7 +10165,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.2.tgz", "integrity": "sha512-xfdkU128CkKKKVAwkyt0M8OdnelJ3MRcIRAPPQkRpoPeuzWY3RIeg7piRCpZ79MK7Q16diLXMMAD9dN5mauPlQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-selector-not": { "version": "5.0.0", @@ -10225,7 +10241,8 @@ "version": "0.36.2", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true + "dev": true, + "requires": {} }, "postcss-unique-selectors": { "version": "5.0.2", @@ -11495,6 +11512,14 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -11542,14 +11567,6 @@ "define-properties": "^1.1.3" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "stringify-entities": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-1.3.2.tgz", @@ -11633,7 +11650,8 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true + "dev": true, + "requires": {} }, "style-search": { "version": "0.1.0", @@ -11845,7 +11863,8 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true + "dev": true, + "requires": {} }, "postcss-selector-parser": { "version": "6.0.8", @@ -13663,7 +13682,8 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-7.0.1.tgz", "integrity": "sha512-iLBFYz6VRYyLJEJsBJ8M3TCqNcckVzz4wFounSc5Oez35ogE/X+aoC5fFu103Ot7NyvjU3/xqIXn93Gp3kJk4g==", - "dev": true + "dev": true, + "requires": {} } } }, @@ -15340,15 +15360,6 @@ } } }, - "worker-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-5.0.1.tgz", - "integrity": "sha512-Pn7+19jIiANcGuTSGdy+vrzyF+SGH03A5wV8iu4jRTMAOfAC9bNeiHo4+l5tPS7F0uvICMBv+h8UCvL7lunxcA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 8e9132689..173aefe54 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,7 @@ "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.7.2", "webpack-merge": "^5.8.0", - "workbox-webpack-plugin": "^6.2.4", - "worker-plugin": "^5.0.1" + "workbox-webpack-plugin": "^6.2.4" }, "dependencies": { "@fontsource/noto-sans": "^4.5.1", From a47dd1b35ce5e6dcd4b6a45ea4c3c2f4384c9518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 4 Nov 2021 20:07:07 +0100 Subject: [PATCH 2/9] Decode Blurhash pixels in a WebWorker --- src/components/images/blurhash.worker.ts | 25 +++++++++++++++++ src/components/images/imageLoader.js | 35 ++++++++++++++---------- 2 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 src/components/images/blurhash.worker.ts diff --git a/src/components/images/blurhash.worker.ts b/src/components/images/blurhash.worker.ts new file mode 100644 index 000000000..d72e5b3ee --- /dev/null +++ b/src/components/images/blurhash.worker.ts @@ -0,0 +1,25 @@ +import { decode } from 'blurhash'; +import { expose } from 'comlink'; + +/** + * Decodes blurhash outside the main thread, in a web worker + * + * @param {string} hash - Hash to decode. + * @param {number} width - Width of the decoded pixel array + * @param {number} height - Height of the decoded pixel array. + * @param {number} punch - Contrast of the decoded pixels + * @returns {Uint8ClampedArray} - Returns the decoded pixels in the proxied response by Comlink + */ +function getPixels({ + hash, + width, + height +}): Uint8ClampedArray { + try { + return decode(hash, width, height); + } catch { + throw new TypeError(`Blurhash ${hash} is not valid`); + } +} + +expose(getPixels); diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index ef89cd1d0..dbd38de9e 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -1,6 +1,12 @@ import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; import * as userSettings from '../../scripts/settings/userSettings'; -import { decode, isBlurhashValid } from 'blurhash'; +import { wrap } from 'comlink'; +const getPixels = wrap( + new Worker( + // eslint-disable-next-line compat/compat + new URL('./blurhash.worker.ts', import.meta.url) + ) +); import './style.scss'; /* eslint-disable indent */ @@ -12,21 +18,18 @@ import './style.scss'; fillImageElement(elem, source); } - function itemBlurhashing(target, blurhashstr) { - if (isBlurhashValid(blurhashstr)) { + async function itemBlurhashing(target, hash) { + try { // Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us, // improving the performance and reducing the memory usage, while retaining almost full blur quality. // Lower values had more visible pixelation - const width = 18; - const height = 18; - let pixels; - try { - pixels = decode(blurhashstr, width, height); - } catch (err) { - console.error('Blurhash decode error: ', err); - target.classList.add('non-blurhashable'); - return; - } + const width = 32; + const height = 32; + const pixels = await getPixels({ + hash, + width, + height + }); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; @@ -48,6 +51,10 @@ import './style.scss'; target.classList.add('blurhashed'); target.removeAttribute('data-blurhash'); }); + } catch (err) { + console.log(err); + target.classList.add('non-blurhashable'); + return; } } @@ -142,7 +149,7 @@ import './style.scss'; for (const lazyElem of elem.querySelectorAll('.lazy')) { const blurhashstr = lazyElem.getAttribute('data-blurhash'); if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) { - itemBlurhashing(lazyElem, blurhashstr); + Promise.resolve(itemBlurhashing(lazyElem, blurhashstr)); } else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) { lazyElem.classList.add('non-blurhashable'); } From 18fe70b978b725df2eb5361548dc698691a1af10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Fri, 5 Nov 2021 13:50:17 +0100 Subject: [PATCH 3/9] Increase pixels resolution --- src/components/images/imageLoader.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index dbd38de9e..ae237f73c 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -23,8 +23,8 @@ import './style.scss'; // Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us, // improving the performance and reducing the memory usage, while retaining almost full blur quality. // Lower values had more visible pixelation - const width = 32; - const height = 32; + const width = 20; + const height = 20; const pixels = await getPixels({ hash, width, From eb486ef4b755bd844c243089b031c75d7a929c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Fri, 5 Nov 2021 01:13:50 +0100 Subject: [PATCH 4/9] Reduce blink when switching between image and blurhash --- src/components/images/imageLoader.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index ae237f73c..63c3dd2ef 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -106,11 +106,23 @@ import './style.scss'; elem.classList.add('lazy-image-fadein'); } - const canvas = elem.previousSibling; - if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') { - canvas.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein'); - canvas.classList.add('lazy-hidden'); - } + elem.addEventListener('transitionend', () => { + requestIdleCallback(() => { + const canvas = elem.previousSibling; + if ( + elem.classList.contains('blurhashed') && + canvas && + canvas.tagName === 'CANVAS' + ) { + canvas.classList.remove( + 'lazy-image-fadein-fast', + 'lazy-image-fadein' + ); + canvas.classList.add('lazy-hidden'); + } + }); + // Run the event listener only once after being added + }, false); }); }); } From e678d59a411c12a9ef31732529c328def6f3d12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Fri, 5 Nov 2021 01:30:24 +0100 Subject: [PATCH 5/9] Fix npm changing lockfile to ssh --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 76928e9aa..fde7e301e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7771,7 +7771,7 @@ "integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg==" }, "libass-wasm": { - "version": "git+ssh://git@github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6", + "version": "git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#f4625ac313b318bd5d2e0ae18679ff516370bae6", "from": "libass-wasm@git+https://github.com/jellyfin/JavascriptSubtitlesOctopus.git#4.0.0-jf-4" }, "lie": { From 5716e4cf05ed8e833364db65b2dc4f3b5b4fb462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Sun, 14 Nov 2021 00:13:19 +0100 Subject: [PATCH 6/9] implement worker without proxies for better compatibility with old devices --- src/components/images/blurhash.worker.ts | 29 ++-- src/components/images/imageLoader.js | 131 +++++++++--------- .../lazyLoaderIntersectionObserver.js | 5 +- 3 files changed, 78 insertions(+), 87 deletions(-) diff --git a/src/components/images/blurhash.worker.ts b/src/components/images/blurhash.worker.ts index d72e5b3ee..a37d8aaf0 100644 --- a/src/components/images/blurhash.worker.ts +++ b/src/components/images/blurhash.worker.ts @@ -1,25 +1,16 @@ +/* eslint-disable no-restricted-globals */ import { decode } from 'blurhash'; -import { expose } from 'comlink'; -/** - * Decodes blurhash outside the main thread, in a web worker - * - * @param {string} hash - Hash to decode. - * @param {number} width - Width of the decoded pixel array - * @param {number} height - Height of the decoded pixel array. - * @param {number} punch - Contrast of the decoded pixels - * @returns {Uint8ClampedArray} - Returns the decoded pixels in the proxied response by Comlink - */ -function getPixels({ - hash, - width, - height -}): Uint8ClampedArray { +self.onmessage = ({ data: { hash, width, height } }): void => { try { - return decode(hash, width, height); + self.postMessage({ + pixels: decode(hash, width, height), + hsh: hash, + width: width, + height: height + }); } catch { throw new TypeError(`Blurhash ${hash} is not valid`); } -} - -expose(getPixels); +}; +/* eslint-enable no-restricted-globals */ diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index 63c3dd2ef..9d1c21d13 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -1,13 +1,18 @@ import * as lazyLoader from '../lazyLoader/lazyLoaderIntersectionObserver'; import * as userSettings from '../../scripts/settings/userSettings'; -import { wrap } from 'comlink'; -const getPixels = wrap( - new Worker( - // eslint-disable-next-line compat/compat - new URL('./blurhash.worker.ts', import.meta.url) - ) -); import './style.scss'; +// eslint-disable-next-line compat/compat +const worker = new Worker(new URL('./blurhash.worker.ts', import.meta.url)); +const targetDic = {}; +worker.addEventListener( + 'message', + ({ data: { pixels, hsh, width, height } }) => { + if (targetDic[hsh]) { + drawBlurhash(targetDic[hsh], pixels, width, height); + delete targetDic[hsh]; + } + } +); /* eslint-disable indent */ export function lazyImage(elem, source = elem.getAttribute('data-src')) { @@ -18,41 +23,46 @@ import './style.scss'; fillImageElement(elem, source); } - async function itemBlurhashing(target, hash) { + function drawBlurhash(target, pixels, width, height) { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + const imgData = ctx.createImageData(width, height); + + imgData.data.set(pixels); + ctx.putImageData(imgData, 0, 0); + + requestAnimationFrame(() => { + canvas.classList.add('blurhash-canvas'); + if (userSettings.enableFastFadein()) { + canvas.classList.add('lazy-blurhash-fadein-fast'); + } else { + canvas.classList.add('lazy-blurhash-fadein'); + } + + target.parentNode.insertBefore(canvas, target); + target.classList.add('blurhashed'); + target.removeAttribute('data-blurhash'); + }); + } + + function itemBlurhashing(target, hash) { try { // Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us, // improving the performance and reducing the memory usage, while retaining almost full blur quality. // Lower values had more visible pixelation const width = 20; const height = 20; - const pixels = await getPixels({ + targetDic[hash] = target; + + worker.postMessage({ hash, width, height }); - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - const imgData = ctx.createImageData(width, height); - - imgData.data.set(pixels); - ctx.putImageData(imgData, 0, 0); - - requestAnimationFrame(() => { - canvas.classList.add('blurhash-canvas'); - if (userSettings.enableFastFadein()) { - canvas.classList.add('lazy-blurhash-fadein-fast'); - } else { - canvas.classList.add('lazy-blurhash-fadein'); - } - - target.parentNode.insertBefore(canvas, target); - target.classList.add('blurhashed'); - target.removeAttribute('data-blurhash'); - }); } catch (err) { - console.log(err); + console.error(err); target.classList.add('non-blurhashable'); return; } @@ -71,15 +81,26 @@ import './style.scss'; source = entry; } - if (entry.intersectionRatio > 0) { - if (source) fillImageElement(target, source); + if (entry.isIntersecting) { + if (source) { + fillImageElement(target, source); + } } else if (!source) { - requestAnimationFrame(() => { - emptyImageElement(target); - }); + emptyImageElement(target); } } + function onTransitionEnd(event) { + const elem = event.target; + requestAnimationFrame(() => { + const canvas = elem.previousSibling; + if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') { + canvas.classList.add('lazy-hidden'); + } + }); + elem.removeEventListener('transitionend', onTransitionEnd); + } + function fillImageElement(elem, url) { if (url === undefined) { throw new TypeError('url cannot be undefined'); @@ -89,6 +110,7 @@ import './style.scss'; preloaderImg.src = url; elem.classList.add('lazy-hidden'); + elem.addEventListener('transitionend', onTransitionEnd); preloaderImg.addEventListener('load', () => { requestAnimationFrame(() => { @@ -105,31 +127,16 @@ import './style.scss'; } else { elem.classList.add('lazy-image-fadein'); } - - elem.addEventListener('transitionend', () => { - requestIdleCallback(() => { - const canvas = elem.previousSibling; - if ( - elem.classList.contains('blurhashed') && - canvas && - canvas.tagName === 'CANVAS' - ) { - canvas.classList.remove( - 'lazy-image-fadein-fast', - 'lazy-image-fadein' - ); - canvas.classList.add('lazy-hidden'); - } - }); - // Run the event listener only once after being added - }, false); }); }); } function emptyImageElement(elem) { - // block repeated call - requestAnimationFrame twice for one image - if (elem.getAttribute('data-src')) return; + elem.removeEventListener('transitionend', onTransitionEnd); + const canvas = elem.previousSibling; + if (canvas && canvas.tagName === 'CANVAS') { + canvas.classList.remove('lazy-hidden'); + } let url; @@ -144,16 +151,6 @@ import './style.scss'; elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein'); elem.classList.add('lazy-hidden'); - - const canvas = elem.previousSibling; - if (canvas && canvas.tagName === 'CANVAS') { - canvas.classList.remove('lazy-hidden'); - if (userSettings.enableFastFadein()) { - canvas.classList.add('lazy-image-fadein-fast'); - } else { - canvas.classList.add('lazy-image-fadein'); - } - } } export function lazyChildren(elem) { @@ -161,7 +158,7 @@ import './style.scss'; for (const lazyElem of elem.querySelectorAll('.lazy')) { const blurhashstr = lazyElem.getAttribute('data-blurhash'); if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) { - Promise.resolve(itemBlurhashing(lazyElem, blurhashstr)); + itemBlurhashing(lazyElem, blurhashstr); } else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) { lazyElem.classList.add('non-blurhashable'); } diff --git a/src/components/lazyLoader/lazyLoaderIntersectionObserver.js b/src/components/lazyLoader/lazyLoaderIntersectionObserver.js index 7751fe8bc..3b78083cc 100644 --- a/src/components/lazyLoader/lazyLoaderIntersectionObserver.js +++ b/src/components/lazyLoader/lazyLoaderIntersectionObserver.js @@ -13,7 +13,10 @@ callback(entry); }); }, - {rootMargin: '25%'}); + { + rootMargin: '50%', + threshold: 0 + }); this.observer = observer; } From d1e0a7b1d7efe69ca8f2dadb972b72a7d4587c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 30 Dec 2021 21:46:52 +0100 Subject: [PATCH 7/9] handle multiple DOM nodes with same blurhash string --- src/components/images/imageLoader.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index 9d1c21d13..28dbf98ce 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -54,7 +54,8 @@ worker.addEventListener( // Lower values had more visible pixelation const width = 20; const height = 20; - targetDic[hash] = target; + targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target); + targetDic[hash].push(target); worker.postMessage({ hash, From 486b0db0cdbfb65bf99846644f6e51944700982b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Thu, 30 Dec 2021 22:55:27 +0100 Subject: [PATCH 8/9] remove unnecessary canvas animations and switch to CSS3 animations --- src/components/images/imageLoader.js | 25 ++++++++++++------------- src/components/images/style.scss | 26 +++++++++----------------- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index 28dbf98ce..b360e86fc 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -7,8 +7,11 @@ const targetDic = {}; worker.addEventListener( 'message', ({ data: { pixels, hsh, width, height } }) => { - if (targetDic[hsh]) { - drawBlurhash(targetDic[hsh], pixels, width, height); + const elems = targetDic[hsh]; + if (elems && elems.length) { + for (const elem of elems) { + drawBlurhash(elem, pixels, width, height); + } delete targetDic[hsh]; } } @@ -34,12 +37,8 @@ worker.addEventListener( ctx.putImageData(imgData, 0, 0); requestAnimationFrame(() => { + // This class is just an utility class, so users can customize the canvas using their own CSS. canvas.classList.add('blurhash-canvas'); - if (userSettings.enableFastFadein()) { - canvas.classList.add('lazy-blurhash-fadein-fast'); - } else { - canvas.classList.add('lazy-blurhash-fadein'); - } target.parentNode.insertBefore(canvas, target); target.classList.add('blurhashed'); @@ -82,7 +81,7 @@ worker.addEventListener( source = entry; } - if (entry.isIntersecting) { + if (entry.intersectionRatio > 0) { if (source) { fillImageElement(target, source); } @@ -91,7 +90,7 @@ worker.addEventListener( } } - function onTransitionEnd(event) { + function onAnimationEnd(event) { const elem = event.target; requestAnimationFrame(() => { const canvas = elem.previousSibling; @@ -99,7 +98,7 @@ worker.addEventListener( canvas.classList.add('lazy-hidden'); } }); - elem.removeEventListener('transitionend', onTransitionEnd); + elem.removeEventListener('animationend', onAnimationEnd); } function fillImageElement(elem, url) { @@ -111,7 +110,7 @@ worker.addEventListener( preloaderImg.src = url; elem.classList.add('lazy-hidden'); - elem.addEventListener('transitionend', onTransitionEnd); + elem.addEventListener('animationend', onAnimationEnd); preloaderImg.addEventListener('load', () => { requestAnimationFrame(() => { @@ -122,18 +121,18 @@ worker.addEventListener( } elem.removeAttribute('data-src'); - elem.classList.remove('lazy-hidden'); if (userSettings.enableFastFadein()) { elem.classList.add('lazy-image-fadein-fast'); } else { elem.classList.add('lazy-image-fadein'); } + elem.classList.remove('lazy-hidden'); }); }); } function emptyImageElement(elem) { - elem.removeEventListener('transitionend', onTransitionEnd); + elem.removeEventListener('animationend', onAnimationEnd); const canvas = elem.previousSibling; if (canvas && canvas.tagName === 'CANVAS') { canvas.classList.remove('lazy-hidden'); diff --git a/src/components/images/style.scss b/src/components/images/style.scss index 7e8b01aff..9fae14fd0 100644 --- a/src/components/images/style.scss +++ b/src/components/images/style.scss @@ -1,17 +1,3 @@ -.lazy-image-fadein { - opacity: 1; - transition: opacity 0.5s; -} - -.lazy-image-fadein-fast { - opacity: 1; - transition: opacity 0.1s; -} - -.lazy-hidden { - opacity: 0; -} - @keyframes fadein { from { opacity: 0; @@ -22,12 +8,18 @@ } } -.lazy-blurhash-fadein-fast { +.lazy-image-fadein { + opacity: 1; + animation: fadein 0.5s; +} + +.lazy-image-fadein-fast { + opacity: 1; animation: fadein 0.1s; } -.lazy-blurhash-fadein { - animation: fadein 0.4s; +.lazy-hidden { + opacity: 0; } .blurhash-canvas { From 41b8fc8d2a942f675a1b6cc97d5b52785ee8e70f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Fri, 14 Jan 2022 10:33:33 +0100 Subject: [PATCH 9/9] Correct outdated comment --- src/components/images/imageLoader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index b360e86fc..81b47da25 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -48,7 +48,7 @@ worker.addEventListener( function itemBlurhashing(target, hash) { try { - // Although the default values recommended by Blurhash developers is 32x32, a size of 18x18 seems to be the sweet spot for us, + // Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us, // improving the performance and reducing the memory usage, while retaining almost full blur quality. // Lower values had more visible pixelation const width = 20;