@@ -333,64 +333,234 @@ class FooterUI {
333333
334334class BackgroundCanvas {
335335 constructor ( ) {
336- this . c = $$ ( 'canvas' ) ;
337- this . cCtx = this . c . getContext ( '2d' ) ;
336+ this . $canvas = $$ ( 'canvas' ) ;
338337 this . $footer = $$ ( 'footer' ) ;
339338
340- // redraw canvas
341- Events . on ( 'resize' , _ => this . init ( ) ) ;
342- Events . on ( 'redraw-canvas' , _ => this . init ( ) ) ;
343- Events . on ( 'translation-loaded' , _ => this . init ( ) ) ;
344-
345- // ShareMode
346- Events . on ( 'share-mode-changed' , e => this . onShareModeChanged ( e . detail . active ) ) ;
339+ this . initAnimation ( ) ;
347340 }
348341
349342 async fadeIn ( ) {
350- this . c . classList . remove ( 'opacity-0' ) ;
343+ this . $canvas . classList . remove ( 'opacity-0' ) ;
351344 }
352345
353- init ( ) {
354- let oldW = this . w ;
355- let oldH = this . h ;
356- let oldOffset = this . offset
357- this . w = document . documentElement . clientWidth ;
358- this . h = document . documentElement . clientHeight ;
359- this . offset = this . $footer . offsetHeight - 27 ;
346+ initAnimation ( ) {
347+ this . baseColorNormal = '168 168 168' ;
348+ this . baseColorShareMode = '168 168 255' ;
349+ this . baseOpacityNormal = 0.4 ;
350+ this . baseOpacityShareMode = 0.8 ;
351+ this . speed = 0.5 ;
352+ this . fps = 40 ;
360353
361- if ( oldW === this . w && oldH === this . h && oldOffset === this . offset ) return ; // nothing has changed
354+ // if browser supports OffscreenCanvas
355+ // -> put canvas drawing into serviceworker to unblock main thread
356+ // otherwise
357+ // -> use main thread
358+ let { init, startAnimation, switchAnimation, onShareModeChange} =
359+ this . $canvas . transferControlToOffscreen
360+ ? this . initAnimationOffscreen ( )
361+ : this . initAnimationOnscreen ( ) ;
362362
363- this . c . width = this . w ;
364- this . c . height = this . h ;
365- this . x0 = this . w / 2 ;
366- this . y0 = this . h - this . offset ;
367- this . dw = Math . round ( Math . max ( this . w , this . h , 1000 ) / 13 ) ;
368- this . baseColor = '165, 165, 165' ;
369- this . baseOpacity = 0.3 ;
363+ init ( ) ;
364+ startAnimation ( ) ;
370365
371- this . drawCircles ( this . cCtx ) ;
372- }
366+ // redraw canvas
367+ Events . on ( 'resize' , _ => init ( ) ) ;
368+ Events . on ( 'redraw-canvas' , _ => init ( ) ) ;
369+ Events . on ( 'translation-loaded' , _ => init ( ) ) ;
370+
371+ // ShareMode
372+ Events . on ( 'share-mode-changed' , e => onShareModeChange ( e . detail . active ) ) ;
373373
374- onShareModeChanged ( active ) {
375- this . baseColor = active ? '165, 165, 255' : '165, 165, 165' ;
376- this . baseOpacity = active ? 0.5 : 0.3 ;
377- this . drawCircles ( this . cCtx ) ;
374+ // Start and stop animation
375+ Events . on ( 'background-animation' , e => switchAnimation ( e . detail . animate ) )
376+ Events . on ( 'offline' , _ => switchAnimation ( false ) ) ;
377+ Events . on ( 'online' , _ => switchAnimation ( true ) ) ;
378378 }
379379
380+ initAnimationOnscreen ( ) {
381+ let $canvas = this . $canvas ;
382+ let $footer = this . $footer ;
380383
381- drawCircle ( ctx , radius ) {
382- ctx . beginPath ( ) ;
383- ctx . lineWidth = 2 ;
384- let opacity = Math . max ( 0 , this . baseOpacity * ( 1 - 1.2 * radius / Math . max ( this . w , this . h ) ) ) ;
385- ctx . strokeStyle = `rgba(${ this . baseColor } , ${ opacity } )` ;
386- ctx . arc ( this . x0 , this . y0 , radius , 0 , 2 * Math . PI ) ;
387- ctx . stroke ( ) ;
388- }
384+ let baseColorNormal = this . baseColorNormal ;
385+ let baseColorShareMode = this . baseColorShareMode ;
386+ let baseOpacityNormal = this . baseOpacityNormal ;
387+ let baseOpacityShareMode = this . baseOpacityShareMode ;
388+ let speed = this . speed ;
389+ let fps = this . fps ;
390+
391+ let c ;
392+ let cCtx ;
393+
394+ let x0 , y0 , w , h , dw , offset ;
395+
396+ let startTime ;
397+ let animate = true ;
398+ let currentFrame = 0 ;
399+ let lastFrame ;
400+ let baseColor ;
401+ let baseOpacity ;
402+
403+ function createCanvas ( ) {
404+ c = $canvas ;
405+ cCtx = c . getContext ( '2d' ) ;
406+
407+ lastFrame = fps / speed - 1 ;
408+ baseColor = baseColorNormal ;
409+ baseOpacity = baseOpacityNormal ;
410+ }
411+
412+ function init ( ) {
413+ initCanvas ( $footer . offsetHeight , document . documentElement . clientWidth , document . documentElement . clientHeight ) ;
414+ }
415+
416+ function initCanvas ( footerOffsetHeight , clientWidth , clientHeight ) {
417+ let oldW = w ;
418+ let oldH = h ;
419+ let oldOffset = offset ;
420+ w = clientWidth ;
421+ h = clientHeight ;
422+ offset = footerOffsetHeight - 28 ;
423+
424+ if ( oldW === w && oldH === h && oldOffset === offset ) return ; // nothing has changed
425+
426+ c . width = w ;
427+ c . height = h ;
428+ x0 = w / 2 ;
429+ y0 = h - offset ;
430+ dw = Math . round ( Math . min ( Math . max ( w , h ) , 800 ) / 10 ) ;
431+
432+ drawFrame ( currentFrame ) ;
433+ }
434+
435+ function startAnimation ( ) {
436+ startTime = Date . now ( ) ;
437+ animateBg ( ) ;
438+ }
439+
440+ function switchAnimation ( state ) {
441+ if ( ! animate && state ) {
442+ // animation starts again. Set startTime to specific value to prevent frame jump
443+ startTime = Date . now ( ) - 1000 * currentFrame / fps ;
444+ }
445+ animate = state ;
446+ requestAnimationFrame ( animateBg ) ;
447+ }
448+
449+ function onShareModeChange ( active ) {
450+ baseColor = active ? baseColorShareMode : baseColorNormal ;
451+ baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal ;
452+ drawFrame ( currentFrame ) ;
453+ }
454+
455+ function drawCircle ( ctx , radius ) {
456+ ctx . lineWidth = 2 ;
389457
390- drawCircles ( ctx ) {
391- ctx . clearRect ( 0 , 0 , this . w , this . h ) ;
392- for ( let i = 0 ; i < 13 ; i ++ ) {
393- this . drawCircle ( ctx , this . dw * i + 33 + 66 ) ;
458+ let opacity = Math . max ( 0 , baseOpacity * ( 1 - 1.2 * radius / Math . max ( w , h ) ) ) ;
459+ if ( radius > dw * 7 ) {
460+ opacity *= ( 8 * dw - radius ) / dw
461+ }
462+
463+ if ( ctx . setStrokeColor ) {
464+ // older blink/webkit browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor
465+ let baseColorRgb = baseColor . split ( " " ) ;
466+ ctx . setStrokeColor ( baseColorRgb [ 0 ] , baseColorRgb [ 1 ] , baseColorRgb [ 2 ] , opacity ) ;
467+ }
468+ else {
469+ ctx . strokeStyle = `rgb(${ baseColor } / ${ opacity } )` ;
470+ }
471+ ctx . beginPath ( ) ;
472+ ctx . arc ( x0 , y0 , radius , 0 , 2 * Math . PI ) ;
473+ ctx . stroke ( ) ;
394474 }
475+
476+ function drawCircles ( ctx , frame ) {
477+ ctx . clearRect ( 0 , 0 , w , h ) ;
478+ for ( let i = 7 ; i >= 0 ; i -- ) {
479+ drawCircle ( ctx , dw * i + speed * dw * frame / fps + 33 ) ;
480+ }
481+ }
482+
483+ function drawFrame ( frame ) {
484+ cCtx . clearRect ( 0 , 0 , w , h ) ;
485+ drawCircles ( cCtx , frame ) ;
486+ }
487+
488+ function animateBg ( ) {
489+ let now = Date . now ( ) ;
490+
491+ if ( ! animate && currentFrame === lastFrame ) {
492+ // Animation stopped and cycle finished -> stop drawing frames
493+ return ;
494+ }
495+
496+ let timeSinceLastFullCycle = ( now - startTime ) % ( 1000 / speed ) ;
497+ let nextFrame = Math . trunc ( fps * timeSinceLastFullCycle / 1000 ) ;
498+
499+ // Only draw frame if it differs from current frame
500+ if ( nextFrame !== currentFrame ) {
501+ drawFrame ( nextFrame ) ;
502+ currentFrame = nextFrame ;
503+ }
504+
505+ requestAnimationFrame ( animateBg ) ;
506+ }
507+
508+ createCanvas ( ) ;
509+
510+ return { init, startAnimation, switchAnimation, onShareModeChange} ;
511+ }
512+
513+ initAnimationOffscreen ( ) {
514+ console . log ( "Use OffscreenCanvas to draw background animation." )
515+
516+ let baseColorNormal = this . baseColorNormal ;
517+ let baseColorShareMode = this . baseColorShareMode ;
518+ let baseOpacityNormal = this . baseOpacityNormal ;
519+ let baseOpacityShareMode = this . baseOpacityShareMode ;
520+ let speed = this . speed ;
521+ let fps = this . fps ;
522+ let $canvas = this . $canvas ;
523+ let $footer = this . $footer ;
524+
525+ const offscreen = $canvas . transferControlToOffscreen ( ) ;
526+ const worker = new Worker ( "scripts/worker/canvas-worker.js" ) ;
527+
528+ function createCanvas ( ) {
529+ worker . postMessage ( {
530+ type : "createCanvas" ,
531+ canvas : offscreen ,
532+ baseColorNormal : baseColorNormal ,
533+ baseColorShareMode : baseColorShareMode ,
534+ baseOpacityNormal : baseOpacityNormal ,
535+ baseOpacityShareMode : baseOpacityShareMode ,
536+ speed : speed ,
537+ fps : fps
538+ } , [ offscreen ] ) ;
539+ }
540+
541+ function init ( ) {
542+ worker . postMessage ( {
543+ type : "initCanvas" ,
544+ footerOffsetHeight : $footer . offsetHeight ,
545+ clientWidth : document . documentElement . clientWidth ,
546+ clientHeight : document . documentElement . clientHeight
547+ } ) ;
548+ }
549+
550+ function startAnimation ( ) {
551+ worker . postMessage ( { type : "startAnimation" } ) ;
552+ }
553+
554+ function onShareModeChange ( active ) {
555+ worker . postMessage ( { type : "onShareModeChange" , active : active } ) ;
556+ }
557+
558+ function switchAnimation ( animate ) {
559+ worker . postMessage ( { type : "switchAnimation" , animate : animate } ) ;
560+ }
561+
562+ createCanvas ( ) ;
563+
564+ return { init, startAnimation, switchAnimation, onShareModeChange} ;
395565 }
396566}
0 commit comments