Skip to content

Commit 0e574ae

Browse files
Merge pull request #378 from schlagmichdoch/bring_back_animation
Bring back optimized background animation
2 parents 3c7042d + 3240921 commit 0e574ae

File tree

10 files changed

+375
-51
lines changed

10 files changed

+375
-51
lines changed

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
77
<!-- Web App Config -->
88
<title>PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.</title>
9-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
9+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
1010
<meta name="theme-color" content="#3367d6">
1111
<meta name="color-scheme" content="dark light">
1212
<meta name="apple-mobile-web-app-capable" content="yes">

public/scripts/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ class PairDrop {
1414
"scripts/util.js",
1515
"scripts/network.js",
1616
"scripts/ui.js",
17-
"scripts/qr-code.min.js",
18-
"scripts/zip.min.js",
19-
"scripts/no-sleep.min.js",
20-
"scripts/heic2any.min.js"
17+
"scripts/libs/heic2any.min.js",
18+
"scripts/libs/no-sleep.min.js",
19+
"scripts/libs/qr-code.min.js",
20+
"scripts/libs/zip.min.js"
2121
];
2222

2323
this.registerServiceWorker();

public/scripts/ui-main.js

Lines changed: 213 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -333,64 +333,234 @@ class FooterUI {
333333

334334
class 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
}

public/scripts/ui.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,17 @@ class PeersUI {
150150
}
151151

152152
_onPeerDisconnected(peerId) {
153+
// Remove peer from UI
153154
const $peer = $(peerId);
154155
if (!$peer) return;
155156
$peer.remove();
156157
this._evaluateOverflowingPeers();
158+
159+
// If no peer is shown -> start background animation again
160+
if ($$('x-peers:empty')) {
161+
Events.fire('background-animation', {animate: true});
162+
}
163+
157164
}
158165

159166
_onRoomTypeRemoved(peerId, roomType) {
@@ -417,6 +424,9 @@ class PeerUI {
417424

418425
// ShareMode
419426
Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));
427+
428+
// Stop background animation
429+
Events.fire('background-animation', {animate: false});
420430
}
421431

422432
html() {

0 commit comments

Comments
 (0)