@@ -3,18 +3,19 @@ import { keyToCss } from '../../utils/css_map.js';
33import { dom } from '../../utils/dom.js' ;
44import { buildStyle , postSelector } from '../../utils/interface.js' ;
55import { getPreferences } from '../../utils/preferences.js' ;
6+ import { memoize } from '../../utils/memoize.js' ;
67
7- const canvasClass = 'xkit-paused-gif-placeholder' ;
88const pausedPosterAttribute = 'data-paused-gif-use-poster' ;
9+ const pausedContentVar = '--xkit-paused-gif-content' ;
10+ const pausedBackgroundImageVar = '--xkit-paused-gif-background-image' ;
911const hoverContainerAttribute = 'data-paused-gif-hover-container' ;
1012const labelAttribute = 'data-paused-gif-label' ;
13+ const hoverFixAttribute = 'data-paused-gif-hover-fix' ;
1114const containerClass = 'xkit-paused-gif-container' ;
12- const backgroundGifClass = 'xkit-paused-background-gif' ;
1315
1416let loadingMode ;
1517
1618const hovered = `:is(:hover, [${ hoverContainerAttribute } ]:hover *)` ;
17- const parentHovered = `:is(:hover > *, [${ hoverContainerAttribute } ]:hover *)` ;
1819
1920export const styleElement = buildStyle ( `
2021[${ labelAttribute } ]::after {
@@ -44,16 +45,17 @@ export const styleElement = buildStyle(`
4445 transform: translateY(-50%);
4546}
4647
47- .${ canvasClass } {
48- position: absolute;
49- visibility: visible;
48+ [${ labelAttribute } ]${ hovered } ::after,
49+ [${ pausedPosterAttribute } ]:not(${ hovered } ) > div > ${ keyToCss ( 'knightRiderLoader' ) } {
50+ display: none;
51+ }
5052
51- 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);
5256}
5357
54- .${ canvasClass } ${ parentHovered } ,
55- [${ labelAttribute } ]${ hovered } ::after,
56- [${ pausedPosterAttribute } ]:not(${ hovered } ) > div > ${ keyToCss ( 'knightRiderLoader' ) } {
58+ img:is([${ pausedPosterAttribute } ], [style*="${ pausedContentVar } "]):not(${ hovered } ) ~ div > ${ keyToCss ( 'knightRiderLoader' ) } {
5759 display: none;
5860}
5961${ keyToCss ( 'background' ) } [${ labelAttribute } ]::after {
@@ -71,13 +73,16 @@ ${keyToCss('background')}[${labelAttribute}]::after {
7173 display: none;
7274}
7375
74- .${ backgroundGifClass } :not(:hover) {
75- background-image: none !important;
76- background-color: rgb(var(--secondary-accent));
76+ img[style*="${ pausedContentVar } "]:not(${ hovered } ) {
77+ content: var(${ pausedContentVar } );
78+ }
79+ [style*="${ pausedBackgroundImageVar } "]:not(${ hovered } ) {
80+ background-image: var(${ pausedBackgroundImageVar } ) !important;
7781}
7882
79- .${ backgroundGifClass } :not(:hover) > :is(div, span) {
80- color: rgb(var(--black));
83+ [${ hoverFixAttribute } ] {
84+ position: relative;
85+ pointer-events: auto !important;
8186}
8287` ) ;
8388
@@ -92,54 +97,86 @@ const addLabel = (element, inside = false) => {
9297 }
9398} ;
9499
95- const pauseGif = function ( gifElement ) {
96- const image = new Image ( ) ;
97- image . src = gifElement . currentSrc ;
98- image . onload = ( ) => {
99- if ( gifElement . parentNode && gifElement . parentNode . querySelector ( `.${ canvasClass } ` ) === null ) {
100- const canvas = document . createElement ( 'canvas' ) ;
101- canvas . width = image . naturalWidth ;
102- canvas . height = image . naturalHeight ;
103- canvas . className = gifElement . className ;
104- canvas . classList . add ( canvasClass ) ;
105- canvas . setAttribute ( 'style' , gifElement . getAttribute ( 'style' ) ) ;
106- canvas . getContext ( '2d' ) . drawImage ( image , 0 , 0 ) ;
107- gifElement . after ( canvas ) ;
108- addLabel ( gifElement ) ;
100+ const createPausedUrl = memoize ( async sourceUrl => {
101+ const response = await fetch ( sourceUrl , { headers : { Accept : 'image/webp,*/*' } } ) ;
102+ const contentType = response . headers . get ( 'Content-Type' ) ;
103+ const canvas = document . createElement ( 'canvas' ) ;
104+
105+ /* globals ImageDecoder */
106+ if ( typeof ImageDecoder === 'function' && await ImageDecoder . isTypeSupported ( contentType ) ) {
107+ const decoder = new ImageDecoder ( {
108+ type : contentType ,
109+ data : response . body ,
110+ preferAnimation : true
111+ } ) ;
112+ const { image : videoFrame } = await decoder . decode ( ) ;
113+ if ( decoder . tracks . selectedTrack . animated === false ) {
114+ // source image is not animated; decline to pause it
115+ return undefined ;
109116 }
110- } ;
111- } ;
117+ canvas . width = videoFrame . displayWidth ;
118+ canvas . height = videoFrame . displayHeight ;
119+ canvas . getContext ( '2d' ) . drawImage ( videoFrame , 0 , 0 ) ;
120+ } else {
121+ if ( sourceUrl . endsWith ( '.webp' ) ) {
122+ // source image may not be animated; decline to pause it
123+ return undefined ;
124+ }
125+ const imageBitmap = await response . blob ( ) . then ( blob => window . createImageBitmap ( blob ) ) ;
126+ canvas . width = imageBitmap . width ;
127+ canvas . height = imageBitmap . height ;
128+ canvas . getContext ( '2d' ) . drawImage ( imageBitmap , 0 , 0 ) ;
129+ }
130+ const blob = await new Promise ( resolve => canvas . toBlob ( resolve , 'image/webp' , 1 ) ) ;
131+ return URL . createObjectURL ( blob ) ;
132+ } ) ;
112133
113134const processGifs = function ( gifElements ) {
114- gifElements . forEach ( gifElement => {
135+ gifElements . forEach ( async gifElement => {
115136 if ( gifElement . closest ( `${ keyToCss ( 'avatarImage' , 'subAvatarImage' ) } , .block-editor-writing-flow` ) ) return ;
116- const pausedGifElements = [ ...gifElement . parentNode . querySelectorAll ( `.${ canvasClass } ` ) ] ;
117- if ( pausedGifElements . length ) {
118- gifElement . after ( ...pausedGifElements ) ;
119- return ;
120- }
121-
122137 gifElement . decoding = 'sync' ;
123138
124139 const posterElement = gifElement . parentElement . querySelector ( keyToCss ( 'poster' ) ) ;
125140 if ( posterElement ) {
126141 gifElement . parentElement . setAttribute ( pausedPosterAttribute , loadingMode ) ;
127- addLabel ( posterElement ) ;
128- return ;
129- }
130-
131- if ( gifElement . complete && gifElement . currentSrc ) {
132- pauseGif ( gifElement ) ;
133142 } else {
134- gifElement . onload = ( ) => pauseGif ( gifElement ) ;
143+ const sourceUrl = gifElement . currentSrc ||
144+ await new Promise ( resolve => gifElement . addEventListener ( 'load' , ( ) => resolve ( gifElement . currentSrc ) , { once : true } ) ) ;
145+
146+ const pausedUrl = await createPausedUrl ( sourceUrl ) ;
147+ if ( ! pausedUrl ) return ;
148+
149+ gifElement . style . setProperty ( pausedContentVar , `url(${ pausedUrl } )` ) ;
135150 }
151+ addLabel ( gifElement ) ;
152+
153+ gifElement . closest ( keyToCss (
154+ 'albumImage' , // post audio element
155+ 'imgLink' // trending tag: https://www.tumblr.com/explore/trending
156+ ) ) ?. setAttribute ( hoverFixAttribute , '' ) ;
136157 } ) ;
137158} ;
138159
160+ const sourceUrlRegex = / (?< = u r l \( [ " ' ] ) [ ^ ) ] * ?\. (?: g i f | g i f v | w e b p ) (? = [ " ' ] \) ) / g;
139161const processBackgroundGifs = function ( gifBackgroundElements ) {
140- gifBackgroundElements . forEach ( gifBackgroundElement => {
141- gifBackgroundElement . classList . add ( backgroundGifClass ) ;
162+ gifBackgroundElements . forEach ( async gifBackgroundElement => {
163+ const sourceValue = gifBackgroundElement . style . backgroundImage ;
164+ const sourceUrl = sourceValue . match ( sourceUrlRegex ) ?. [ 0 ] ;
165+ if ( ! sourceUrl ) return ;
166+
167+ const pausedUrl = await createPausedUrl ( sourceUrl ) ;
168+ if ( ! pausedUrl ) return ;
169+
170+ gifBackgroundElement . style . setProperty (
171+ pausedBackgroundImageVar ,
172+ sourceValue . replaceAll ( sourceUrlRegex , pausedUrl )
173+ ) ;
142174 addLabel ( gifBackgroundElement , true ) ;
175+
176+ gifBackgroundElement . closest ( keyToCss (
177+ 'media' , // old activity item: "liked your post", "reblogged your post", "mentioned you in a post"
178+ 'activityMedia' // new activity item: "replied to your post", "replied to you in a post"
179+ ) ) ?. setAttribute ( hoverFixAttribute , '' ) ;
143180 } ) ;
144181} ;
145182
@@ -179,30 +216,53 @@ export const main = async function () {
179216 ${
180217 'figure' // post image/imageset; recommended blog carousel entry; blog view sidebar "more like this"; post in grid view; blog card modal post entry
181218 } ,
219+ ${
220+ 'main.labs' // labs settings header: https://www.tumblr.com/settings/labs
221+ } ,
182222 ${ keyToCss (
183223 'linkCard' , // post link element
224+ 'albumImage' , // post audio element
225+ 'messageImage' , // direct message attached image
226+ 'messagePost' , // direct message linked post
184227 'typeaheadRow' , // modal search dropdown entry
185228 'tagImage' , // search page sidebar related tags, recommended tag carousel entry: https://www.tumblr.com/search/gif, https://www.tumblr.com/explore/recommended-for-you
229+ 'headerBanner' , // blog view header
230+ 'headerImage' , // modal blog card header, activity page "biggest fans" header
186231 'topPost' , // activity page top post
232+ 'colorfulListItemWrapper' , // trending tag: https://www.tumblr.com/explore/trending
187233 'takeoverBanner' // advertisement
188234 ) }
189- ) img[srcset*=".gif"]:not(${ keyToCss ( 'poster' ) } )
235+ ) img:is( [srcset*=".gif"], [src*=".gif"], [srcset*=".webp"], [src*=".webp"]) :not(${ keyToCss ( 'poster' ) } )
190236 ` ;
191237 pageModifications . register ( gifImage , processGifs ) ;
192238
193239 const gifBackgroundImage = `
194240 ${ keyToCss (
241+ 'media' , // old activity item: "liked your post", "reblogged your post", "mentioned you in a post"
242+ 'activityMedia' , // new activity item: "replied to your post", "replied to you in a post"
195243 'communityHeaderImage' , // search page tags section header: https://www.tumblr.com/search/gif?v=tag
196244 'bannerImage' , // tagged page sidebar header: https://www.tumblr.com/tagged/gif
197- 'tagChicletWrapper' // "trending" / "your tags" timeline carousel entry: https://www.tumblr.com/dashboard/trending, https://www.tumblr.com/dashboard/hubs
198- ) } [style*=".gif"]
245+ 'tagChicletWrapper' , // "trending" / "your tags" timeline carousel entry: https://www.tumblr.com/dashboard/trending, https://www.tumblr.com/dashboard/hubs
246+ 'communityCategoryImage' // tumblr communities browse page entry: https://www.tumblr.com/communities/browse, https://www.tumblr.com/communities/browse/movies
247+ ) } :is([style*=".gif"], [style*=".webp"])
199248 ` ;
200249 pageModifications . register ( gifBackgroundImage , processBackgroundGifs ) ;
201250
202- pageModifications . register (
203- `${ keyToCss ( 'listTimelineObject' ) } ${ keyToCss ( 'carouselWrapper' ) } ${ keyToCss ( 'postCard' ) } ` , // recommended blog carousel entry: https://www.tumblr.com/tagged/gif
204- processHoverableElements
205- ) ;
251+ const hoverableElement = `
252+ ${
253+ `${ keyToCss ( 'listTimelineObject' ) } ${ keyToCss ( 'carouselWrapper' ) } ${ keyToCss ( 'postCard' ) } ` // recommended blog carousel entry
254+ } ,
255+ ${
256+ `div:has(> a${ keyToCss ( 'cover' ) } ):has(${ keyToCss ( 'communityCategoryImage' ) } )` // tumblr communities browse page entry: https://www.tumblr.com/communities/browse
257+ } ,
258+ ${
259+ `${ keyToCss ( 'linkCard' ) } ${ keyToCss ( 'withImage' ) } ` // post link element
260+ } ,
261+ ${
262+ `${ keyToCss ( 'gridTimelineObject' ) } ` // likes page or patio grid view post: https://www.tumblr.com/likes
263+ }
264+ ` ;
265+ pageModifications . register ( hoverableElement , processHoverableElements ) ;
206266
207267 pageModifications . register (
208268 `:is(${ postSelector } , ${ keyToCss ( 'blockEditorContainer' ) } ) ${ keyToCss ( 'rows' ) } ` ,
@@ -224,9 +284,12 @@ export const clean = async function () {
224284 wrapper . replaceWith ( ...wrapper . children )
225285 ) ;
226286
227- $ ( `.${ canvasClass } ` ) . remove ( ) ;
228- $ ( `.${ backgroundGifClass } ` ) . removeClass ( backgroundGifClass ) ;
229287 $ ( `[${ labelAttribute } ]` ) . removeAttr ( labelAttribute ) ;
230288 $ ( `[${ pausedPosterAttribute } ]` ) . removeAttr ( pausedPosterAttribute ) ;
231289 $ ( `[${ hoverContainerAttribute } ]` ) . removeAttr ( hoverContainerAttribute ) ;
290+ $ ( `[${ hoverFixAttribute } ]` ) . removeAttr ( hoverFixAttribute ) ;
291+ [ ...document . querySelectorAll ( `img[style*="${ pausedContentVar } "]` ) ]
292+ . forEach ( element => element . style . removeProperty ( pausedContentVar ) ) ;
293+ [ ...document . querySelectorAll ( `[style*="${ pausedBackgroundImageVar } "]` ) ]
294+ . forEach ( element => element . style . removeProperty ( pausedBackgroundImageVar ) ) ;
232295} ;
0 commit comments