From 8a3c60d3a677d5dcd9b28d0b91f4de61be29db11 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 14 Feb 2025 20:54:15 +0100 Subject: [PATCH 1/7] Bring back optimized background animation --- public/scripts/ui-main.js | 183 +++++++++++++++++++++++++++----------- public/scripts/ui.js | 10 +++ 2 files changed, 140 insertions(+), 53 deletions(-) diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js index bbdece4..682dbe3 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -333,65 +333,142 @@ class FooterUI { class BackgroundCanvas { constructor() { - this.c = $$('canvas'); - this.cCtx = this.c.getContext('2d'); - this.$footer = $$('footer'); + this.canvas = $$('canvas'); + this.initAnimation(); + } + + initAnimation() { + let c = this.canvas; + let cCtx = c.getContext('2d'); + let $footer = $$('footer'); + + let x0, y0, w, h, dw, offset, baseColor, baseOpacity; + + let offscreenCanvases; + let shareMode = false; + + let animate = true; + let currentFrame = 0; + + let fpsInterval, now, then, elapsed; + + let speed = 1.5; + + function init() { + let oldW = w; + let oldH = h; + let oldOffset = offset + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; + offset = $footer.offsetHeight - 33; + if (h > 800) offset += 16; + + 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.max(w, h, 1000) / 12); + + drawCircles(cCtx, 0); + + // enforce redrawing of frames + offscreenCanvases = {true: [], false: []}; + } + + function drawCircle(ctx, radius) { + ctx.lineWidth = 2; + + baseColor = shareMode ? '168 168 255' : '168 168 168'; + baseOpacity = shareMode ? 0.8 : 0.4; + + let opacity = baseOpacity * radius / (dw * 8); + if (radius > dw * 5) { + opacity *= (6 * dw - radius) / dw + } + ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`; + ctx.beginPath(); + ctx.arc(x0, y0, radius, 0, 2 * Math.PI); + ctx.stroke(); + } + + function drawCircles(ctx, frame) { + for (let i = 6; i >= 0; i--) { + drawCircle(ctx, dw * i + speed * frame + 33); + } + } + + function createOffscreenCanvas(frame) { + let canvas = document.createElement("canvas"); + canvas.width = c.width; + canvas.height = c.height; + offscreenCanvases[shareMode][frame] = canvas; + let ctx = canvas.getContext('2d'); + drawCircles(ctx, frame); + } + + function drawFrame(frame) { + cCtx.clearRect(0, 0, w, h); + + if (!offscreenCanvases[shareMode][frame]) { + createOffscreenCanvas(frame); + } + cCtx.drawImage(offscreenCanvases[shareMode][frame], 0, 0); + } + + function startAnimating(fps) { + fpsInterval = 1000 / fps; + then = Date.now(); + animateBg(); + } + + function animateBg() { + requestAnimationFrame(animateBg); + + now = Date.now(); + elapsed = now - then; + // if not enough time has elapsed, do not draw the next frame -> abort + if (elapsed < fpsInterval) { + return; + } + + then = now - (elapsed % fpsInterval); + + if (animate) { + currentFrame = (currentFrame + 1) % (dw/speed); + drawFrame(currentFrame); + } + } + + function switchAnimation(state) { + animate = state; + console.debug(state) + } + + function redrawOnShareModeChange(active) { + shareMode = active + } + + init(); + startAnimating(30) // redraw canvas - Events.on('resize', _ => this.init()); - Events.on('redraw-canvas', _ => this.init()); - Events.on('translation-loaded', _ => this.init()); + Events.on('resize', _ => init()); + Events.on('redraw-canvas', _ => init()); + Events.on('translation-loaded', _ => init()); // ShareMode - Events.on('share-mode-changed', e => this.onShareModeChanged(e.detail.active)); + Events.on('share-mode-changed', e => redrawOnShareModeChange(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)); } async fadeIn() { - this.c.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; - if (this.h >= 800) this.offset += 10; - - if (oldW === this.w && oldH === this.h && oldOffset === this.offset) return; // nothing has changed - - 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; - - this.drawCircles(this.cCtx); - } - - onShareModeChanged(active) { - this.baseColor = active ? '165, 165, 255' : '165, 165, 165'; - this.baseOpacity = active ? 0.5 : 0.3; - this.drawCircles(this.cCtx); - } - - - 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(); - } - - 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); - } + this.canvas.classList.remove('opacity-0'); } } \ No newline at end of file diff --git a/public/scripts/ui.js b/public/scripts/ui.js index c988db7..835eaae 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() { From f0e72506175c43939bee38887a0acf794e2fffc3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sun, 16 Feb 2025 00:52:34 +0100 Subject: [PATCH 2/7] Use time based approach to smoothen reduced framerate --- public/scripts/ui-main.js | 91 ++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js index 682dbe3..a8c4a1f 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -344,15 +344,26 @@ class BackgroundCanvas { let x0, y0, w, h, dw, offset, baseColor, baseOpacity; - let offscreenCanvases; + let offscreenCanvases = {false: [], true: []}; let shareMode = false; + let startTime; let animate = true; - let currentFrame = 0; + let speed = 0.4; + let fps = 300; + let maxFrames = fps / speed; - let fpsInterval, now, then, elapsed; - - let speed = 1.5; + for (let frame = 0; frame < maxFrames; frame++) { + let canvas = document.createElement("canvas"); + offscreenCanvases[false][frame] = { + "redraw": true, + "canvas": canvas + }; + offscreenCanvases[true][frame] = { + "redraw": true, + "canvas": canvas + }; + } function init() { let oldW = w; @@ -360,8 +371,8 @@ class BackgroundCanvas { let oldOffset = offset w = document.documentElement.clientWidth; h = document.documentElement.clientHeight; - offset = $footer.offsetHeight - 33; - if (h > 800) offset += 16; + offset = $footer.offsetHeight - 28; + if (h > 800) offset += 11; if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed @@ -369,12 +380,15 @@ class BackgroundCanvas { c.height = h; x0 = w / 2; y0 = h - offset; - dw = Math.round(Math.max(w, h, 1000) / 12); + dw = Math.round(Math.max(w, h, 1000) / 15); - drawCircles(cCtx, 0); + drawFrame(currentFrame); // enforce redrawing of frames - offscreenCanvases = {true: [], false: []}; + for (let frame = 0; frame < maxFrames; frame++) { + offscreenCanvases[true][frame]["redraw"] = true; + offscreenCanvases[false][frame]["redraw"] = true; + } } function drawCircle(ctx, radius) { @@ -383,8 +397,11 @@ class BackgroundCanvas { baseColor = shareMode ? '168 168 255' : '168 168 168'; baseOpacity = shareMode ? 0.8 : 0.4; - let opacity = baseOpacity * radius / (dw * 8); - if (radius > dw * 5) { + let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h))); + if (radius < dw) { + opacity *= (radius - 33) / (dw - 33) + } + else if (radius > dw * 5) { opacity *= (6 * dw - radius) / dw } ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`; @@ -394,16 +411,16 @@ class BackgroundCanvas { } function drawCircles(ctx, frame) { - for (let i = 6; i >= 0; i--) { - drawCircle(ctx, dw * i + speed * frame + 33); + ctx.clearRect(0, 0, w, h); + for (let i = 5; i >= 0; i--) { + drawCircle(ctx, dw * i + speed * dw * frame / fps + 33); } } - function createOffscreenCanvas(frame) { - let canvas = document.createElement("canvas"); + function drawOffscreenCanvas(frame) { + let canvas = offscreenCanvases[shareMode][frame]["canvas"]; canvas.width = c.width; canvas.height = c.height; - offscreenCanvases[shareMode][frame] = canvas; let ctx = canvas.getContext('2d'); drawCircles(ctx, frame); } @@ -411,39 +428,45 @@ class BackgroundCanvas { function drawFrame(frame) { cCtx.clearRect(0, 0, w, h); - if (!offscreenCanvases[shareMode][frame]) { - createOffscreenCanvas(frame); + if (offscreenCanvases[shareMode][frame]["redraw"]) { + drawOffscreenCanvas(frame); } - cCtx.drawImage(offscreenCanvases[shareMode][frame], 0, 0); + cCtx.drawImage(offscreenCanvases[shareMode][frame]["canvas"], 0, 0); } - function startAnimating(fps) { - fpsInterval = 1000 / fps; - then = Date.now(); + function startAnimating() { + startTime = Date.now(); animateBg(); } + let currentFrame = 0; function animateBg() { - requestAnimationFrame(animateBg); + let now = Date.now(); - now = Date.now(); - elapsed = now - then; - // if not enough time has elapsed, do not draw the next frame -> abort - if (elapsed < fpsInterval) { + if (!animate) { + // Animation stopped -> don't draw next frame return; } - then = now - (elapsed % fpsInterval); + let timeSinceLastFullCycle = (now - startTime) % (1000 / speed); + let nextFrame = Math.trunc(fps * timeSinceLastFullCycle / 1000); - if (animate) { - currentFrame = (currentFrame + 1) % (dw/speed); - drawFrame(currentFrame); + // Only draw frame if it differs from current frame + if (nextFrame !== currentFrame) { + drawFrame(nextFrame); + currentFrame = nextFrame; } + + requestAnimationFrame(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; - console.debug(state) + animateBg(); } function redrawOnShareModeChange(active) { @@ -451,7 +474,7 @@ class BackgroundCanvas { } init(); - startAnimating(30) + startAnimating(); // redraw canvas Events.on('resize', _ => init()); From 16523843bd7bd6241854666a51ae3bff9155809d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sun, 16 Feb 2025 19:23:04 +0100 Subject: [PATCH 3/7] Use OffscreenCanvas on supported browsers to offload canvas drawing to a service worker thread --- public/scripts/canvas-worker.js | 135 ++++++++++++++++++ public/scripts/ui-main.js | 241 ++++++++++++++++++++------------ public/service-worker.js | 1 + 3 files changed, 288 insertions(+), 89 deletions(-) create mode 100644 public/scripts/canvas-worker.js diff --git a/public/scripts/canvas-worker.js b/public/scripts/canvas-worker.js new file mode 100644 index 0000000..e7e6bc7 --- /dev/null +++ b/public/scripts/canvas-worker.js @@ -0,0 +1,135 @@ +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 (h > 800) offset += 11; + + 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 + } + + 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/scripts/ui-main.js b/public/scripts/ui-main.js index a8c4a1f..3dbd612 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -333,45 +333,93 @@ class FooterUI { class BackgroundCanvas { constructor() { - this.canvas = $$('canvas'); + this.$canvas = $$('canvas'); + this.$footer = $$('footer'); + this.initAnimation(); } + async fadeIn() { + this.$canvas.classList.remove('opacity-0'); + } + initAnimation() { - let c = this.canvas; - let cCtx = c.getContext('2d'); - let $footer = $$('footer'); + this.baseColorNormal = '168 168 168'; + this.baseColorShareMode = '168 168 255'; + this.baseOpacityNormal = 0.4; + this.baseOpacityShareMode = 0.8; + this.speed = 0.4; + this.fps = 40; - let x0, y0, w, h, dw, offset, baseColor, baseOpacity; + // 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(); - let offscreenCanvases = {false: [], true: []}; - let shareMode = false; + init(); + startAnimation(); + + // 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)); + } + + 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; + + let c; + let cCtx; + + let x0, y0, w, h, dw, offset; let startTime; let animate = true; - let speed = 0.4; - let fps = 300; - let maxFrames = fps / speed; + let currentFrame = 0; + let lastFrame; + let baseColor; + let baseOpacity; - for (let frame = 0; frame < maxFrames; frame++) { - let canvas = document.createElement("canvas"); - offscreenCanvases[false][frame] = { - "redraw": true, - "canvas": canvas - }; - offscreenCanvases[true][frame] = { - "redraw": true, - "canvas": canvas - }; + 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 = document.documentElement.clientWidth; - h = document.documentElement.clientHeight; - offset = $footer.offsetHeight - 28; + let oldOffset = offset; + w = clientWidth; + h = clientHeight; + offset = footerOffsetHeight - 28; if (h > 800) offset += 11; if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed @@ -380,30 +428,39 @@ class BackgroundCanvas { c.height = h; x0 = w / 2; y0 = h - offset; - dw = Math.round(Math.max(w, h, 1000) / 15); + dw = Math.round(Math.min(Math.max(w, h), 800) / 10); drawFrame(currentFrame); + } - // enforce redrawing of frames - for (let frame = 0; frame < maxFrames; frame++) { - offscreenCanvases[true][frame]["redraw"] = true; - offscreenCanvases[false][frame]["redraw"] = true; + 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; - baseColor = shareMode ? '168 168 255' : '168 168 168'; - baseOpacity = shareMode ? 0.8 : 0.4; - let opacity = Math.max(0, baseOpacity * (1 - 1.2 * radius / Math.max(w, h))); - if (radius < dw) { - opacity *= (radius - 33) / (dw - 33) - } - else if (radius > dw * 5) { - opacity *= (6 * dw - radius) / dw + if (radius > dw * 7) { + opacity *= (8 * dw - radius) / dw } + ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`; ctx.beginPath(); ctx.arc(x0, y0, radius, 0, 2 * Math.PI); @@ -412,39 +469,21 @@ class BackgroundCanvas { function drawCircles(ctx, frame) { ctx.clearRect(0, 0, w, h); - for (let i = 5; i >= 0; i--) { + for (let i = 7; i >= 0; i--) { drawCircle(ctx, dw * i + speed * dw * frame / fps + 33); } } - function drawOffscreenCanvas(frame) { - let canvas = offscreenCanvases[shareMode][frame]["canvas"]; - canvas.width = c.width; - canvas.height = c.height; - let ctx = canvas.getContext('2d'); - drawCircles(ctx, frame); - } - function drawFrame(frame) { cCtx.clearRect(0, 0, w, h); - - if (offscreenCanvases[shareMode][frame]["redraw"]) { - drawOffscreenCanvas(frame); - } - cCtx.drawImage(offscreenCanvases[shareMode][frame]["canvas"], 0, 0); + drawCircles(cCtx, frame); } - function startAnimating() { - startTime = Date.now(); - animateBg(); - } - - let currentFrame = 0; function animateBg() { let now = Date.now(); - if (!animate) { - // Animation stopped -> don't draw next frame + if (!animate && currentFrame === lastFrame) { + // Animation stopped and cycle finished -> stop drawing frames return; } @@ -460,38 +499,62 @@ class BackgroundCanvas { requestAnimationFrame(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; - animateBg(); - } + createCanvas(); - function redrawOnShareModeChange(active) { - shareMode = active - } - - init(); - startAnimating(); - - // redraw canvas - Events.on('resize', _ => init()); - Events.on('redraw-canvas', _ => init()); - Events.on('translation-loaded', _ => init()); - - // ShareMode - Events.on('share-mode-changed', e => redrawOnShareModeChange(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)); + return {init, startAnimation, switchAnimation, onShareModeChange}; } - async fadeIn() { - this.canvas.classList.remove('opacity-0'); + 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/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/service-worker.js b/public/service-worker.js index c6420fe..f022b11 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -17,6 +17,7 @@ const relativePathsToCache = [ 'scripts/ui-main.js', 'scripts/util.js', 'scripts/zip.min.js', + 'scripts/canvas-worker.js', 'sounds/blop.mp3', 'sounds/blop.ogg', 'images/favicon-96x96.png', From ae68ede3f37445caa56b2b08ef307ff32490b38e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sun, 16 Feb 2025 19:29:14 +0100 Subject: [PATCH 4/7] Put worker and libraries in subfolders --- public/scripts/{ => libs}/heic2any.min.js | 0 public/scripts/{ => libs}/no-sleep.min.js | 0 public/scripts/{ => libs}/qr-code.min.js | 0 public/scripts/{ => libs}/zip.min.js | 0 public/scripts/main.js | 8 ++++---- public/scripts/ui-main.js | 2 +- public/scripts/{ => worker}/canvas-worker.js | 0 public/service-worker.js | 9 +++++---- 8 files changed, 10 insertions(+), 9 deletions(-) rename public/scripts/{ => libs}/heic2any.min.js (100%) rename public/scripts/{ => libs}/no-sleep.min.js (100%) rename public/scripts/{ => libs}/qr-code.min.js (100%) rename public/scripts/{ => libs}/zip.min.js (100%) rename public/scripts/{ => worker}/canvas-worker.js (100%) 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 8960ed2..6ab999a 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 3dbd612..f9ecbad 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -517,7 +517,7 @@ class BackgroundCanvas { let $footer = this.$footer; const offscreen = $canvas.transferControlToOffscreen(); - const worker = new Worker("scripts/canvas-worker.js"); + const worker = new Worker("scripts/worker/canvas-worker.js"); function createCanvas() { worker.postMessage({ diff --git a/public/scripts/canvas-worker.js b/public/scripts/worker/canvas-worker.js similarity index 100% rename from public/scripts/canvas-worker.js rename to public/scripts/worker/canvas-worker.js diff --git a/public/service-worker.js b/public/service-worker.js index f022b11..07e2c50 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -10,14 +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/canvas-worker.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', From 8eea54f8dd022fb67a4454ee0f93c7f8413cac86 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sun, 16 Feb 2025 23:20:43 +0100 Subject: [PATCH 5/7] Make sure older webkit/blink based browsers are able to render opacity of circles --- public/scripts/ui-main.js | 9 ++++++++- public/scripts/worker/canvas-worker.js | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js index f9ecbad..6a7dd44 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -461,7 +461,14 @@ class BackgroundCanvas { opacity *= (8 * dw - radius) / dw } - ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`; + 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(); diff --git a/public/scripts/worker/canvas-worker.js b/public/scripts/worker/canvas-worker.js index e7e6bc7..1428bc1 100644 --- a/public/scripts/worker/canvas-worker.js +++ b/public/scripts/worker/canvas-worker.js @@ -96,7 +96,15 @@ function drawCircle(ctx, radius) { opacity *= (8 * dw - radius) / dw } - ctx.strokeStyle = `rgb(${baseColor} / ${opacity})`; + 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(); From ec0012ecd1d27133051452b822eb96843a9290f8 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sun, 16 Feb 2025 23:23:27 +0100 Subject: [PATCH 6/7] Speed up animation slightly --- public/scripts/ui-main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/ui-main.js b/public/scripts/ui-main.js index 6a7dd44..812a4ae 100644 --- a/public/scripts/ui-main.js +++ b/public/scripts/ui-main.js @@ -348,7 +348,7 @@ class BackgroundCanvas { this.baseColorShareMode = '168 168 255'; this.baseOpacityNormal = 0.4; this.baseOpacityShareMode = 0.8; - this.speed = 0.4; + this.speed = 0.5; this.fps = 40; // if browser supports OffscreenCanvas From 09e4e5d289eb11793fea8b6fba6e54f9bfbe79e3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sun, 16 Feb 2025 23:47:35 +0100 Subject: [PATCH 7/7] Prevent background animation from being cut on devices with a notch --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 5dec40a..d1873b2 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ PairDrop | Transfer Files Cross-Platform. No Setup, No Signup. - +