diff --git a/public/index.html b/public/index.html
index e47983f..8e3a1ec 100644
--- a/public/index.html
+++ b/public/index.html
@@ -6,7 +6,7 @@
PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.
-
+
diff --git a/public/scripts/heic2any.min.js b/public/scripts/libs/heic2any.min.js
similarity index 100%
rename from public/scripts/heic2any.min.js
rename to public/scripts/libs/heic2any.min.js
diff --git a/public/scripts/no-sleep.min.js b/public/scripts/libs/no-sleep.min.js
similarity index 100%
rename from public/scripts/no-sleep.min.js
rename to public/scripts/libs/no-sleep.min.js
diff --git a/public/scripts/qr-code.min.js b/public/scripts/libs/qr-code.min.js
similarity index 100%
rename from public/scripts/qr-code.min.js
rename to public/scripts/libs/qr-code.min.js
diff --git a/public/scripts/zip.min.js b/public/scripts/libs/zip.min.js
similarity index 100%
rename from public/scripts/zip.min.js
rename to public/scripts/libs/zip.min.js
diff --git a/public/scripts/main.js b/public/scripts/main.js
index 9929627..4883642 100644
--- a/public/scripts/main.js
+++ b/public/scripts/main.js
@@ -14,10 +14,10 @@ class PairDrop {
"scripts/util.js",
"scripts/network.js",
"scripts/ui.js",
- "scripts/qr-code.min.js",
- "scripts/zip.min.js",
- "scripts/no-sleep.min.js",
- "scripts/heic2any.min.js"
+ "scripts/libs/heic2any.min.js",
+ "scripts/libs/no-sleep.min.js",
+ "scripts/libs/qr-code.min.js",
+ "scripts/libs/zip.min.js"
];
this.registerServiceWorker();
diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js
index 3289ee7..01ad2ab 100644
--- a/public/scripts/ui-main.js
+++ b/public/scripts/ui-main.js
@@ -333,64 +333,234 @@ class FooterUI {
class BackgroundCanvas {
constructor() {
- this.c = $$('canvas');
- this.cCtx = this.c.getContext('2d');
+ this.$canvas = $$('canvas');
this.$footer = $$('footer');
- // redraw canvas
- Events.on('resize', _ => this.init());
- Events.on('redraw-canvas', _ => this.init());
- Events.on('translation-loaded', _ => this.init());
-
- // ShareMode
- Events.on('share-mode-changed', e => this.onShareModeChanged(e.detail.active));
+ this.initAnimation();
}
async fadeIn() {
- this.c.classList.remove('opacity-0');
+ this.$canvas.classList.remove('opacity-0');
}
- init() {
- let oldW = this.w;
- let oldH = this.h;
- let oldOffset = this.offset
- this.w = document.documentElement.clientWidth;
- this.h = document.documentElement.clientHeight;
- this.offset = this.$footer.offsetHeight - 27;
+ initAnimation() {
+ this.baseColorNormal = '168 168 168';
+ this.baseColorShareMode = '168 168 255';
+ this.baseOpacityNormal = 0.4;
+ this.baseOpacityShareMode = 0.8;
+ this.speed = 0.5;
+ this.fps = 40;
- if (oldW === this.w && oldH === this.h && oldOffset === this.offset) return; // nothing has changed
+ // if browser supports OffscreenCanvas
+ // -> put canvas drawing into serviceworker to unblock main thread
+ // otherwise
+ // -> use main thread
+ let {init, startAnimation, switchAnimation, onShareModeChange} =
+ this.$canvas.transferControlToOffscreen
+ ? this.initAnimationOffscreen()
+ : this.initAnimationOnscreen();
- this.c.width = this.w;
- this.c.height = this.h;
- this.x0 = this.w / 2;
- this.y0 = this.h - this.offset;
- this.dw = Math.round(Math.max(this.w, this.h, 1000) / 13);
- this.baseColor = '165, 165, 165';
- this.baseOpacity = 0.3;
+ init();
+ startAnimation();
- this.drawCircles(this.cCtx);
+ // redraw canvas
+ Events.on('resize', _ => init());
+ Events.on('redraw-canvas', _ => init());
+ Events.on('translation-loaded', _ => init());
+
+ // ShareMode
+ Events.on('share-mode-changed', e => onShareModeChange(e.detail.active));
+
+ // Start and stop animation
+ Events.on('background-animation', e => switchAnimation(e.detail.animate))
+ Events.on('offline', _ => switchAnimation(false));
+ Events.on('online', _ => switchAnimation(true));
}
- onShareModeChanged(active) {
- this.baseColor = active ? '165, 165, 255' : '165, 165, 165';
- this.baseOpacity = active ? 0.5 : 0.3;
- this.drawCircles(this.cCtx);
- }
+ initAnimationOnscreen() {
+ let $canvas = this.$canvas;
+ let $footer = this.$footer;
+ let baseColorNormal = this.baseColorNormal;
+ let baseColorShareMode = this.baseColorShareMode;
+ let baseOpacityNormal = this.baseOpacityNormal;
+ let baseOpacityShareMode = this.baseOpacityShareMode;
+ let speed = this.speed;
+ let fps = this.fps;
- drawCircle(ctx, radius) {
- ctx.beginPath();
- ctx.lineWidth = 2;
- let opacity = Math.max(0, this.baseOpacity * (1 - 1.2 * radius / Math.max(this.w, this.h)));
- ctx.strokeStyle = `rgba(${this.baseColor}, ${opacity})`;
- ctx.arc(this.x0, this.y0, radius, 0, 2 * Math.PI);
- ctx.stroke();
- }
+ let c;
+ let cCtx;
- drawCircles(ctx) {
- ctx.clearRect(0, 0, this.w, this.h);
- for (let i = 0; i < 13; i++) {
- this.drawCircle(ctx, this.dw * i + 33 + 66);
+ let x0, y0, w, h, dw, offset;
+
+ let startTime;
+ let animate = true;
+ let currentFrame = 0;
+ let lastFrame;
+ let baseColor;
+ let baseOpacity;
+
+ function createCanvas() {
+ c = $canvas;
+ cCtx = c.getContext('2d');
+
+ lastFrame = fps / speed - 1;
+ baseColor = baseColorNormal;
+ baseOpacity = baseOpacityNormal;
}
+
+ function init() {
+ initCanvas($footer.offsetHeight, document.documentElement.clientWidth, document.documentElement.clientHeight);
+ }
+
+ function initCanvas(footerOffsetHeight, clientWidth, clientHeight) {
+ let oldW = w;
+ let oldH = h;
+ let oldOffset = offset;
+ w = clientWidth;
+ h = clientHeight;
+ offset = footerOffsetHeight - 28;
+
+ if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
+
+ c.width = w;
+ c.height = h;
+ x0 = w / 2;
+ y0 = h - offset;
+ dw = Math.round(Math.min(Math.max(w, h), 800) / 10);
+
+ drawFrame(currentFrame);
+ }
+
+ function startAnimation() {
+ startTime = Date.now();
+ animateBg();
+ }
+
+ function switchAnimation(state) {
+ if (!animate && state) {
+ // animation starts again. Set startTime to specific value to prevent frame jump
+ startTime = Date.now() - 1000 * currentFrame / fps;
+ }
+ animate = state;
+ requestAnimationFrame(animateBg);
+ }
+
+ function onShareModeChange(active) {
+ baseColor = active ? baseColorShareMode : baseColorNormal;
+ baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal;
+ drawFrame(currentFrame);
+ }
+
+ function drawCircle(ctx, radius) {
+ ctx.lineWidth = 2;
+
+ let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h)));
+ if (radius > dw * 7) {
+ opacity *= (8 * dw - radius) / dw
+ }
+
+ if (ctx.setStrokeColor) {
+ // older blink/webkit browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor
+ let baseColorRgb = baseColor.split(" ");
+ ctx.setStrokeColor(baseColorRgb[0], baseColorRgb[1], baseColorRgb[2], opacity);
+ }
+ else {
+ ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`;
+ }
+ ctx.beginPath();
+ ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
+ ctx.stroke();
+ }
+
+ function drawCircles(ctx, frame) {
+ ctx.clearRect(0, 0, w, h);
+ for (let i = 7; i >= 0; i--) {
+ drawCircle(ctx, dw * i + speed * dw * frame / fps + 33);
+ }
+ }
+
+ function drawFrame(frame) {
+ cCtx.clearRect(0, 0, w, h);
+ drawCircles(cCtx, frame);
+ }
+
+ function animateBg() {
+ let now = Date.now();
+
+ if (!animate && currentFrame === lastFrame) {
+ // Animation stopped and cycle finished -> stop drawing frames
+ return;
+ }
+
+ let timeSinceLastFullCycle = (now - startTime) % (1000 / speed);
+ let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000);
+
+ // Only draw frame if it differs from current frame
+ if (nextFrame !== currentFrame) {
+ drawFrame(nextFrame);
+ currentFrame = nextFrame;
+ }
+
+ requestAnimationFrame(animateBg);
+ }
+
+ createCanvas();
+
+ return {init, startAnimation, switchAnimation, onShareModeChange};
+ }
+
+ initAnimationOffscreen() {
+ console.log("Use OffscreenCanvas to draw background animation.")
+
+ let baseColorNormal = this.baseColorNormal;
+ let baseColorShareMode = this.baseColorShareMode;
+ let baseOpacityNormal = this.baseOpacityNormal;
+ let baseOpacityShareMode = this.baseOpacityShareMode;
+ let speed = this.speed;
+ let fps = this.fps;
+ let $canvas = this.$canvas;
+ let $footer = this.$footer;
+
+ const offscreen = $canvas.transferControlToOffscreen();
+ const worker = new Worker("scripts/worker/canvas-worker.js");
+
+ function createCanvas() {
+ worker.postMessage({
+ type: "createCanvas",
+ canvas: offscreen,
+ baseColorNormal: baseColorNormal,
+ baseColorShareMode: baseColorShareMode,
+ baseOpacityNormal: baseOpacityNormal,
+ baseOpacityShareMode: baseOpacityShareMode,
+ speed: speed,
+ fps: fps
+ }, [offscreen]);
+ }
+
+ function init() {
+ worker.postMessage({
+ type: "initCanvas",
+ footerOffsetHeight: $footer.offsetHeight,
+ clientWidth: document.documentElement.clientWidth,
+ clientHeight: document.documentElement.clientHeight
+ });
+ }
+
+ function startAnimation() {
+ worker.postMessage({ type: "startAnimation" });
+ }
+
+ function onShareModeChange(active) {
+ worker.postMessage({ type: "onShareModeChange", active: active });
+ }
+
+ function switchAnimation(animate) {
+ worker.postMessage({ type: "switchAnimation", animate: animate });
+ }
+
+ createCanvas();
+
+ return {init, startAnimation, switchAnimation, onShareModeChange};
}
}
\ No newline at end of file
diff --git a/public/scripts/ui.js b/public/scripts/ui.js
index a4f17af..cf4d014 100644
--- a/public/scripts/ui.js
+++ b/public/scripts/ui.js
@@ -150,10 +150,17 @@ class PeersUI {
}
_onPeerDisconnected(peerId) {
+ // Remove peer from UI
const $peer = $(peerId);
if (!$peer) return;
$peer.remove();
this._evaluateOverflowingPeers();
+
+ // If no peer is shown -> start background animation again
+ if ($$('x-peers:empty')) {
+ Events.fire('background-animation', {animate: true});
+ }
+
}
_onRoomTypeRemoved(peerId, roomType) {
@@ -417,6 +424,9 @@ class PeerUI {
// ShareMode
Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));
+
+ // Stop background animation
+ Events.fire('background-animation', {animate: false});
}
html() {
diff --git a/public/scripts/worker/canvas-worker.js b/public/scripts/worker/canvas-worker.js
new file mode 100644
index 0000000..26ec1f0
--- /dev/null
+++ b/public/scripts/worker/canvas-worker.js
@@ -0,0 +1,142 @@
+self.onmessage = (e) => {
+ switch (e.data.type) {
+ case "createCanvas": createCanvas(e.data);
+ break;
+ case "initCanvas": initCanvas(e.data.footerOffsetHeight, e.data.clientWidth, e.data.clientHeight);
+ break;
+ case "startAnimation": startAnimation();
+ break;
+ case "onShareModeChange": onShareModeChange(e.data.active);
+ break;
+ case "switchAnimation": switchAnimation(e.data.animate);
+ break;
+ }
+};
+
+let baseColorNormal;
+let baseColorShareMode;
+let baseOpacityNormal;
+let baseOpacityShareMode;
+let speed;
+let fps;
+
+let c;
+let cCtx;
+
+let x0, y0, w, h, dw, offset;
+
+let startTime;
+let animate = true;
+let currentFrame = 0;
+let lastFrame;
+let baseColor;
+let baseOpacity;
+
+function createCanvas(data) {
+ baseColorNormal = data.baseColorNormal;
+ baseColorShareMode = data.baseColorShareMode;
+ baseOpacityNormal = data.baseOpacityNormal;
+ baseOpacityShareMode = data.baseOpacityShareMode;
+ speed = data.speed;
+ fps = data.fps;
+
+ c = data.canvas;
+ cCtx = c.getContext("2d");
+
+ lastFrame = fps / speed - 1;
+ baseColor = baseColorNormal;
+ baseOpacity = baseOpacityNormal;
+}
+
+function initCanvas(footerOffsetHeight, clientWidth, clientHeight) {
+ let oldW = w;
+ let oldH = h;
+ let oldOffset = offset;
+ w = clientWidth;
+ h = clientHeight;
+ offset = footerOffsetHeight - 28;
+
+ if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
+
+ c.width = w;
+ c.height = h;
+ x0 = w / 2;
+ y0 = h - offset;
+ dw = Math.round(Math.min(Math.max(w, h), 800) / 10);
+
+ drawFrame(currentFrame);
+}
+
+function startAnimation() {
+ startTime = Date.now();
+ animateBg();
+}
+
+function switchAnimation(state) {
+ if (!animate && state) {
+ // animation starts again. Set startTime to specific value to prevent frame jump
+ startTime = Date.now() - 1000 * currentFrame / fps;
+ }
+ animate = state;
+ requestAnimationFrame(animateBg);
+}
+
+function onShareModeChange(active) {
+ baseColor = active ? baseColorShareMode : baseColorNormal;
+ baseOpacity = active ? baseOpacityShareMode : baseOpacityNormal;
+ drawFrame(currentFrame);
+}
+
+function drawCircle(ctx, radius) {
+ ctx.lineWidth = 2;
+
+ let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h)));
+ if (radius > dw * 7) {
+ opacity *= (8 * dw - radius) / dw
+ }
+
+ if (ctx.setStrokeColor) {
+ // older blink/webkit based browsers do not understand opacity in strokeStyle. Use deprecated setStrokeColor instead
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle#webkitblink-specific_note
+ let baseColorRgb = baseColor.split(" ");
+ ctx.setStrokeColor(baseColorRgb[0], baseColorRgb[1], baseColorRgb[2], opacity);
+ }
+ else {
+ ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`;
+ }
+ ctx.beginPath();
+ ctx.arc(x0, y0, radius, 0, 2 * Math.PI);
+ ctx.stroke();
+}
+
+function drawCircles(ctx, frame) {
+ ctx.clearRect(0, 0, w, h);
+ for (let i = 7; i >= 0; i--) {
+ drawCircle(ctx, dw * i + speed * dw * frame / fps + 33);
+ }
+}
+
+function drawFrame(frame) {
+ cCtx.clearRect(0, 0, w, h);
+ drawCircles(cCtx, frame);
+}
+
+function animateBg() {
+ let now = Date.now();
+
+ if (!animate && currentFrame === lastFrame) {
+ // Animation stopped and cycle finished -> stop drawing frames
+ return;
+ }
+
+ let timeSinceLastFullCycle = (now - startTime) % (1000 / speed);
+ let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000);
+
+ // Only draw frame if it differs from current frame
+ if (nextFrame !== currentFrame) {
+ drawFrame(nextFrame);
+ currentFrame = nextFrame;
+ }
+
+ requestAnimationFrame(animateBg);
+}
\ No newline at end of file
diff --git a/public/service-worker.js b/public/service-worker.js
index c6420fe..07e2c50 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -10,13 +10,15 @@ const relativePathsToCache = [
'scripts/localization.js',
'scripts/main.js',
'scripts/network.js',
- 'scripts/no-sleep.min.js',
'scripts/persistent-storage.js',
- 'scripts/qr-code.min.js',
'scripts/ui.js',
'scripts/ui-main.js',
'scripts/util.js',
- 'scripts/zip.min.js',
+ 'scripts/worker/canvas-worker.js',
+ 'scripts/libs/heic2any.min.js',
+ 'scripts/libs/no-sleep.min.js',
+ 'scripts/libs/qr-code.min.js',
+ 'scripts/libs/zip.min.js',
'sounds/blop.mp3',
'sounds/blop.ogg',
'images/favicon-96x96.png',