77 * emote sobem pela tela como balões de hélio, balançando em zigue-zague,
88 * com variação de tamanho, velocidade e opacidade.
99 *
10+ * IMPLEMENTAÇÃO: em vez de desenhar em <canvas> (o que congela GIF/WebP
11+ * animado no Chromium/CEF, usado pelo OBS Browser Source), cada emote é
12+ * um elemento <img> real, posicionado via CSS transform e animado a cada
13+ * requestAnimationFrame. Como é uma <img> de verdade, o navegador anima o
14+ * GIF/WebP nativamente — sem congelar.
15+ *
1016 * Uso básico:
1117 * onScreenCelebration("https://static-cdn.jtvnw.net/emoticons/.../3.0");
1218 *
@@ -85,9 +91,13 @@ function onScreenCelebration(emoteUrl, options = {}) {
8591 // zona de fade-out perto da borda de saída (px)
8692 edgeFadeZone = 140 ,
8793
88- // elemento canvas já existente para reaproveitar (opcional).
89- // Se não for passado, um canvas full-screen é criado e removido
90- // automaticamente ao final da animação.
94+ // elemento container já existente para reaproveitar (opcional).
95+ // Se não for passado, um <div> full-screen é criado e removido
96+ // automaticamente ao final da animação. Aceita também "canvas" por
97+ // compatibilidade com a versão anterior (nesse caso o valor passado
98+ // é ignorado e um novo container próprio é criado, já que não dá
99+ // pra usar um <canvas> como pai de <img>s).
100+ container = null ,
91101 canvas = null ,
92102
93103 // callback disparado quando a animação termina naturalmente
@@ -105,37 +115,28 @@ function onScreenCelebration(emoteUrl, options = {}) {
105115
106116 const rand = ( min , max ) => Math . random ( ) * ( max - min ) + min ;
107117
108- // ---- canvas: usa o fornecido ou cria um overlay full-screen próprio ----
109- let ownCanvas = false ;
110- let cvs = canvas ;
111- if ( ! cvs ) {
112- ownCanvas = true ;
113- cvs = document . createElement ( "canvas" ) ;
114- cvs . style . position = "fixed" ;
115- cvs . style . inset = "0" ;
116- cvs . style . width = "100%" ;
117- cvs . style . height = "100%" ;
118- cvs . style . pointerEvents = "none" ;
119- cvs . style . zIndex = "2147483647" ;
120- //document.body.appendChild(cvs);
121- document . getElementById ( "chat" ) . appendChild ( cvs ) ;
122- }
123- const ctx = cvs . getContext ( "2d" ) ;
124-
125- function resize ( ) {
126- const w = cvs . clientWidth || window . innerWidth ;
127- const h = cvs . clientHeight || window . innerHeight ;
128- cvs . width = w * devicePixelRatio ;
129- cvs . height = h * devicePixelRatio ;
130- ctx . setTransform ( devicePixelRatio , 0 , 0 , devicePixelRatio , 0 , 0 ) ;
118+ // ---- container: usa o fornecido ou cria um overlay full-screen próprio ----
119+ let ownContainer = false ;
120+ let root = container && container . nodeType === 1 ? container : null ;
121+ if ( ! root ) {
122+ ownContainer = true ;
123+ root = document . createElement ( "div" ) ;
124+ root . style . position = "fixed" ;
125+ root . style . inset = "0" ;
126+ root . style . width = "100%" ;
127+ root . style . height = "100%" ;
128+ root . style . overflow = "hidden" ;
129+ root . style . pointerEvents = "none" ;
130+ root . style . zIndex = "2147483647" ;
131+ ( document . getElementById ( "chat" ) || document . body ) . appendChild ( root ) ;
131132 }
132- resize ( ) ;
133- window . addEventListener ( "resize" , resize ) ;
134133
135- // ---- carrega o emote ----
136- const img = new Image ( ) ;
137- img . crossOrigin = "anonymous" ;
138- img . src = emoteUrl ;
134+ function containerSize ( ) {
135+ return {
136+ w : root . clientWidth || window . innerWidth ,
137+ h : root . clientHeight || window . innerHeight
138+ } ;
139+ }
139140
140141 let emotes = [ ] ;
141142 let rafId = null ;
@@ -144,11 +145,23 @@ function onScreenCelebration(emoteUrl, options = {}) {
144145
145146 function spawnEmote ( ) {
146147 const size = rand ( minSize , maxSize ) ;
147- const w = cvs . clientWidth || window . innerWidth ;
148- const h = cvs . clientHeight || window . innerHeight ;
148+ const { w, h } = containerSize ( ) ;
149149 const goingUp = direction === "up" ;
150150
151+ const el = document . createElement ( "img" ) ;
152+ el . src = emoteUrl ;
153+ el . crossOrigin = "anonymous" ;
154+ el . style . position = "absolute" ;
155+ el . style . left = "0" ;
156+ el . style . top = "0" ;
157+ el . style . width = size + "px" ;
158+ el . style . height = size + "px" ;
159+ el . style . willChange = "transform, opacity" ;
160+ el . style . opacity = "0" ;
161+ root . appendChild ( el ) ;
162+
151163 emotes . push ( {
164+ el,
152165 baseX : rand ( 0 , w ) ,
153166 y : goingUp ? h + size : - size ,
154167 size,
@@ -166,10 +179,7 @@ function onScreenCelebration(emoteUrl, options = {}) {
166179 }
167180
168181 function tick ( now ) {
169- const w = cvs . clientWidth || window . innerWidth ;
170- const h = cvs . clientHeight || window . innerHeight ;
171- ctx . clearRect ( 0 , 0 , w , h ) ;
172-
182+ const { h } = containerSize ( ) ;
173183 const goingUp = direction === "up" ;
174184
175185 emotes . forEach ( ( e ) => {
@@ -189,20 +199,22 @@ function onScreenCelebration(emoteUrl, options = {}) {
189199 alpha = e . opacityMax ;
190200 }
191201
192- if ( alpha <= 0 ) return ;
202+ alpha = Math . min ( 1 , Math . max ( 0 , alpha ) ) ;
193203
194- ctx . save ( ) ;
195- ctx . globalAlpha = Math . min ( 1 , Math . max ( 0 , alpha ) ) ;
196- ctx . translate ( x , e . y ) ;
197- ctx . rotate ( e . rot ) ;
198- ctx . drawImage ( img , - e . size / 2 , - e . size / 2 , e . size , e . size ) ;
199- ctx . restore ( ) ;
204+ // translate posiciona o centro do emote em (x, e.y); o segundo
205+ // translate(-50%, -50%) centraliza a <img> nesse ponto, e rotate
206+ // gira em torno do próprio centro.
207+ e . el . style . transform =
208+ `translate( ${ x } px, ${ e . y } px) rotate( ${ e . rot } rad) translate(-50%, -50%)` ;
209+ e . el . style . opacity = String ( alpha ) ;
200210 } ) ;
201211
202212 emotes = emotes . filter ( ( e ) => {
203213 const alive = ( now - e . born ) < e . life ;
204214 const onScreen = goingUp ? ( e . y + e . size > - 20 ) : ( e . y - e . size < h + 20 ) ;
205- return alive && onScreen ;
215+ const keep = alive && onScreen ;
216+ if ( ! keep && e . el . parentNode ) e . el . parentNode . removeChild ( e . el ) ;
217+ return keep ;
206218 } ) ;
207219
208220 if ( spawning || emotes . length ) {
@@ -218,9 +230,8 @@ function onScreenCelebration(emoteUrl, options = {}) {
218230 }
219231
220232 function cleanup ( ) {
221- window . removeEventListener ( "resize" , resize ) ;
222- if ( ownCanvas && cvs . parentNode ) {
223- cvs . parentNode . removeChild ( cvs ) ;
233+ if ( ownContainer && root . parentNode ) {
234+ root . parentNode . removeChild ( root ) ;
224235 }
225236 if ( typeof onComplete === "function" ) onComplete ( ) ;
226237 }
@@ -240,14 +251,7 @@ function onScreenCelebration(emoteUrl, options = {}) {
240251 } , effectiveSpawnInterval ) ;
241252 }
242253
243- if ( img . complete ) {
244- start ( ) ;
245- } else {
246- img . onload = start ;
247- img . onerror = ( ) => {
248- throw new Error ( `onScreenCelebration: falha ao carregar o emote em "${ emoteUrl } ".` ) ;
249- } ;
250- }
254+ start ( ) ;
251255
252256 // controlador: permite interromper novos spawns manualmente
253257 return {
0 commit comments