@@ -5,8 +5,8 @@ import { buildStyle, postSelector } from '../../utils/interface.js';
55import { getPreferences } from '../../utils/preferences.js' ;
66import { memoize } from '../../utils/memoize.js' ;
77
8- const canvasClass = 'xkit-paused-gif-placeholder' ;
98const pausedPosterAttribute = 'data-paused-gif-use-poster' ;
9+ const pausedContentVar = '--xkit-paused-gif-content' ;
1010const pausedBackgroundImageVar = '--xkit-paused-gif-background-image' ;
1111const hoverContainerAttribute = 'data-paused-gif-hover-container' ;
1212const labelAttribute = 'data-paused-gif-label' ;
@@ -16,7 +16,6 @@ const containerClass = 'xkit-paused-gif-container';
1616let loadingMode ;
1717
1818const hovered = `:is(:hover, [${ hoverContainerAttribute } ]:hover *)` ;
19- const parentHovered = `:is(:hover > *, [${ hoverContainerAttribute } ]:hover *)` ;
2019
2120export const styleElement = buildStyle ( `
2221[${ labelAttribute } ]::after {
@@ -46,16 +45,17 @@ export const styleElement = buildStyle(`
4645 transform: translateY(-50%);
4746}
4847
49- .${ canvasClass } {
50- position: absolute;
51- visibility: visible;
48+ [${ labelAttribute } ]${ hovered } ::after,
49+ [${ pausedPosterAttribute } ]:not(${ hovered } ) > div > ${ keyToCss ( 'knightRiderLoader' ) } {
50+ display: none;
51+ }
5252
53- background-color: rgb(var(--white));
53+ ${ keyToCss ( 'blogCard' ) } ${ keyToCss ( 'headerImage' ) } ${ keyToCss ( 'small' ) } [${ labelAttribute } ]::after {
54+ font-size: 0.8rem;
55+ top: calc(140px - 1em - 2.2ch);
5456}
5557
56- .${ canvasClass } ${ parentHovered } ,
57- [${ labelAttribute } ]${ hovered } ::after,
58- [${ pausedPosterAttribute } ]:not(${ hovered } ) > div > ${ keyToCss ( 'knightRiderLoader' ) } {
58+ img:is([${ pausedPosterAttribute } ], [style*="${ pausedContentVar } "]):not(${ hovered } ) ~ div > ${ keyToCss ( 'knightRiderLoader' ) } {
5959 display: none;
6060}
6161${ keyToCss ( 'background' ) } [${ labelAttribute } ]::after {
@@ -73,6 +73,9 @@ ${keyToCss('background')}[${labelAttribute}]::after {
7373 display: none;
7474}
7575
76+ img[style*="${ pausedContentVar } "]:not(${ hovered } ) {
77+ content: var(${ pausedContentVar } );
78+ }
7679[style*="${ pausedBackgroundImageVar } "]:not(${ hovered } ) {
7780 background-image: var(${ pausedBackgroundImageVar } ) !important;
7881}
@@ -95,12 +98,12 @@ const addLabel = (element, inside = false) => {
9598} ;
9699
97100/**
98- * Fetches the selected image, tests if it is animated, and returns a canvas element with the paused
99- * image if it is. This is expensive; avoid using this where practical. On older browsers without
100- * ImageDecoder support, GIF images are assumed to be animated and WebP images are assumed to not be
101- * animated.
101+ * Fetches the selected image, tests if it is animated, and returns a blob URL with the paused image
102+ * if it is. This is expensive and is a small memory leak, as the blob URL will never be revoked;
103+ * avoid using this where practical. On older browsers without ImageDecoder support, GIF images are
104+ * assumed to be animated and WebP images are assumed to not be animated.
102105 */
103- const createPausedCanvasIfAnimated = memoize ( async sourceUrl => {
106+ const createPausedUrlIfAnimated = memoize ( async sourceUrl => {
104107 const response = await fetch ( sourceUrl , { headers : { Accept : 'image/webp,*/*' } } ) ;
105108 const contentType = response . headers . get ( 'Content-Type' ) ;
106109 const canvas = document . createElement ( 'canvas' ) ;
@@ -129,75 +132,31 @@ const createPausedCanvasIfAnimated = memoize(async sourceUrl => {
129132 canvas . height = imageBitmap . height ;
130133 canvas . getContext ( '2d' ) . drawImage ( imageBitmap , 0 , 0 ) ;
131134 }
132- return canvas ;
135+ const blob = await new Promise ( resolve => canvas . toBlob ( resolve , 'image/webp' , 1 ) ) ;
136+ return URL . createObjectURL ( blob ) ;
133137} ) ;
134138
135- /**
136- * Fetches the selected image, tests if it is animated, and returns a blob URL with the paused image
137- * if it is. This is expensive and is a small memory leak, as the blob URL will never be revoked;
138- * avoid using this where practical.
139- */
140- const createPausedUrlIfAnimated = memoize ( async sourceUrl => {
141- const canvas = await createPausedCanvasIfAnimated ( sourceUrl ) ;
142- if ( canvas ) {
143- const blob = await new Promise ( resolve => canvas . toBlob ( resolve , 'image/webp' , 1 ) ) ;
144- return URL . createObjectURL ( blob ) ;
145- }
146- } ) ;
147-
148- const pauseGif = function ( gifElement ) {
149- const image = new Image ( ) ;
150- image . src = gifElement . currentSrc ;
151- image . onload = ( ) => {
152- if ( gifElement . parentNode && gifElement . parentNode . querySelector ( `.${ canvasClass } ` ) === null ) {
153- const canvas = document . createElement ( 'canvas' ) ;
154- canvas . width = image . naturalWidth ;
155- canvas . height = image . naturalHeight ;
156- canvas . className = gifElement . className ;
157- canvas . classList . add ( canvasClass ) ;
158- canvas . setAttribute ( 'style' , gifElement . getAttribute ( 'style' ) ) ;
159- canvas . getContext ( '2d' ) . drawImage ( image , 0 , 0 ) ;
160- gifElement . after ( canvas ) ;
161- addLabel ( gifElement ) ;
162- }
163- } ;
164- } ;
165-
166- const pauseWebP = async function ( gifElement ) {
167- const canvas = await createPausedCanvasIfAnimated ( gifElement . currentSrc ) ;
168- if ( canvas ) {
169- canvas . className = gifElement . className ;
170- canvas . classList . add ( canvasClass ) ;
171- canvas . setAttribute ( 'style' , gifElement . getAttribute ( 'style' ) ) ;
172- gifElement . after ( canvas ) ;
173- addLabel ( gifElement ) ;
174- }
175- } ;
176-
177139const processGifs = function ( gifElements ) {
178140 gifElements . forEach ( async gifElement => {
179141 if ( gifElement . closest ( `${ keyToCss ( 'avatarImage' , 'subAvatarImage' ) } , .block-editor-writing-flow` ) ) return ;
180- const pausedGifElements = [ ...gifElement . parentNode . querySelectorAll ( `.${ canvasClass } ` ) ] ;
181- if ( pausedGifElements . length ) {
182- gifElement . after ( ...pausedGifElements ) ;
183- return ;
184- }
185-
186142 gifElement . decoding = 'sync' ;
187143
188144 const posterElement = gifElement . parentElement . querySelector ( keyToCss ( 'poster' ) ) ;
189145 if ( posterElement ) {
190146 gifElement . parentElement . setAttribute ( pausedPosterAttribute , loadingMode ) ;
191- addLabel ( posterElement ) ;
192- return ;
193- }
147+ } else {
148+ const sourceUrl = gifElement . currentSrc ||
149+ await new Promise ( resolve => gifElement . addEventListener ( 'load' , ( ) => resolve ( gifElement . currentSrc ) , { once : true } ) ) ;
194150
195- const sourceUrl = gifElement . currentSrc ||
196- await new Promise ( resolve => gifElement . addEventListener ( 'load' , ( ) => resolve ( gifElement . currentSrc ) , { once : true } ) ) ;
151+ const pausedUrl = await createPausedUrlIfAnimated ( sourceUrl ) ;
152+ if ( ! pausedUrl ) return ;
197153
198- sourceUrl . endsWith ( '.webp' ) ? pauseWebP ( gifElement ) : pauseGif ( gifElement ) ;
154+ gifElement . style . setProperty ( pausedContentVar , `url(${ pausedUrl } )` ) ;
155+ }
156+ addLabel ( gifElement ) ;
199157
200158 gifElement . closest ( keyToCss (
159+ 'albumImage' , // post audio element
201160 'imgLink' // trending tag: https://www.tumblr.com/explore/trending
202161 ) ) ?. setAttribute ( hoverFixAttribute , '' ) ;
203162 } ) ;
@@ -262,10 +221,18 @@ export const main = async function () {
262221 ${
263222 'figure' // post image/imageset; recommended blog carousel entry; blog view sidebar "more like this"; post in grid view; blog card modal post entry
264223 } ,
224+ ${
225+ 'main.labs' // labs settings header: https://www.tumblr.com/settings/labs
226+ } ,
265227 ${ keyToCss (
266228 'linkCard' , // post link element
229+ 'albumImage' , // post audio element
230+ 'messageImage' , // direct message attached image
231+ 'messagePost' , // direct message linked post
267232 'typeaheadRow' , // modal search dropdown entry
268233 'tagImage' , // search page sidebar related tags, recommended tag carousel entry: https://www.tumblr.com/search/gif, https://www.tumblr.com/explore/recommended-for-you
234+ 'headerBanner' , // blog view header
235+ 'headerImage' , // modal blog card header, activity page "biggest fans" header
269236 'topPost' , // activity page top post
270237 'colorfulListItemWrapper' , // trending tag: https://www.tumblr.com/explore/trending
271238 'takeoverBanner' // advertisement
@@ -313,11 +280,12 @@ export const clean = async function () {
313280 wrapper . replaceWith ( ...wrapper . children )
314281 ) ;
315282
316- $ ( `.${ canvasClass } ` ) . remove ( ) ;
317283 $ ( `[${ labelAttribute } ]` ) . removeAttr ( labelAttribute ) ;
318284 $ ( `[${ pausedPosterAttribute } ]` ) . removeAttr ( pausedPosterAttribute ) ;
319285 $ ( `[${ hoverContainerAttribute } ]` ) . removeAttr ( hoverContainerAttribute ) ;
320286 $ ( `[${ hoverFixAttribute } ]` ) . removeAttr ( hoverFixAttribute ) ;
287+ [ ...document . querySelectorAll ( `img[style*="${ pausedContentVar } "]` ) ]
288+ . forEach ( element => element . style . removeProperty ( pausedContentVar ) ) ;
321289 [ ...document . querySelectorAll ( `[style*="${ pausedBackgroundImageVar } "]` ) ]
322290 . forEach ( element => element . style . removeProperty ( pausedBackgroundImageVar ) ) ;
323291} ;
0 commit comments