From 0b413492adf2fef58b4c57d9b96520bee4e55dca Mon Sep 17 00:00:00 2001 From: Su-Yong Date: Wed, 25 Oct 2023 15:37:51 +0900 Subject: [PATCH] feat(ambient-mode): add config for `ambient-mode` plugin (#1349) --- src/config/defaults.ts | 11 +++- src/menu.ts | 2 + src/plugins/ambient-mode/back.ts | 6 ++- src/plugins/ambient-mode/config.ts | 4 ++ src/plugins/ambient-mode/front.ts | 47 ++++++++++++---- src/plugins/ambient-mode/menu.ts | 87 ++++++++++++++++++++++++++++++ src/plugins/ambient-mode/style.css | 29 ++++++++-- 7 files changed, 170 insertions(+), 16 deletions(-) create mode 100644 src/plugins/ambient-mode/config.ts create mode 100644 src/plugins/ambient-mode/menu.ts diff --git a/src/config/defaults.ts b/src/config/defaults.ts index f32a51fa..6b49c36a 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -81,7 +81,16 @@ const defaultConfig = { disableDefaultLists: false, }, 'album-color-theme': {}, - 'ambient-mode': {}, + 'ambient-mode': { + enabled: false, + quality: 50, + buffer: 30, + interpolationTime: 1500, + blur: 100, + size: 100, + opacity: 1, + fullscreen: false, + }, 'audio-compressor': {}, 'blur-nav-bar': {}, 'bypass-age-restrictions': {}, diff --git a/src/menu.ts b/src/menu.ts index 43095eff..5ec5fe20 100644 --- a/src/menu.ts +++ b/src/menu.ts @@ -8,6 +8,7 @@ import { startingPages } from './providers/extracted-data'; import promptOptions from './providers/prompt-options'; import adblockerMenu from './plugins/adblocker/menu'; +import ambientModeMenu from './plugins/ambient-mode/menu'; import captionsSelectorMenu from './plugins/captions-selector/menu'; import crossfadeMenu from './plugins/crossfade/menu'; import disableAutoplayMenu from './plugins/disable-autoplay/menu'; @@ -32,6 +33,7 @@ const betaPlugins = ['crossfade', 'lumiastream']; const pluginMenus = { 'adblocker': adblockerMenu, + 'ambient-mode': ambientModeMenu, 'disable-autoplay': disableAutoplayMenu, 'captions-selector': captionsSelectorMenu, 'crossfade': crossfadeMenu, diff --git a/src/plugins/ambient-mode/back.ts b/src/plugins/ambient-mode/back.ts index 0f501515..4a4c9e31 100644 --- a/src/plugins/ambient-mode/back.ts +++ b/src/plugins/ambient-mode/back.ts @@ -1,10 +1,14 @@ import { BrowserWindow } from 'electron'; +import config from './config'; import style from './style.css'; import { injectCSS } from '../utils'; +export default (win: BrowserWindow) => { + config.subscribeAll((newConfig) => { + win.webContents.send('ambient-mode:config-change', newConfig); + }); -export default (win: BrowserWindow) => { injectCSS(win.webContents, style); }; diff --git a/src/plugins/ambient-mode/config.ts b/src/plugins/ambient-mode/config.ts new file mode 100644 index 00000000..6ec7cc95 --- /dev/null +++ b/src/plugins/ambient-mode/config.ts @@ -0,0 +1,4 @@ +import { PluginConfig } from '../../config/dynamic'; + +const config = new PluginConfig('ambient-mode'); +export default config; diff --git a/src/plugins/ambient-mode/front.ts b/src/plugins/ambient-mode/front.ts index 253d32b0..1fae3640 100644 --- a/src/plugins/ambient-mode/front.ts +++ b/src/plugins/ambient-mode/front.ts @@ -1,9 +1,15 @@ +import { ipcRenderer } from 'electron'; + import { ConfigType } from '../../config/dynamic'; -export default (_: ConfigType<'ambient-mode'>) => { - const interpolationTime = 3000; // interpolation time (ms) - const framerate = 30; // frame - const qualityRatio = 50; // width size (pixel) +export default (config: ConfigType<'ambient-mode'>) => { + let interpolationTime = config.interpolationTime; // interpolation time (ms) + let buffer = config.buffer; // frame + let qualityRatio = config.quality; // width size (pixel) + let sizeRatio = config.size / 100; // size ratio (percent) + let blur = config.blur; // blur (pixel) + let opacity = config.opacity; // opacity (percent) + let isFullscreen = config.fullscreen; // fullscreen (boolean) let unregister: (() => void) | null = null; @@ -37,7 +43,7 @@ export default (_: ConfigType<'ambient-mode'>) => { context.globalAlpha = 1; if (lastImageData) { - const frameOffset = (1 / framerate) * (1000 / interpolationTime); + const frameOffset = (1 / buffer) * (1000 / interpolationTime); context.globalAlpha = 1 - (frameOffset * 2); // because of alpha value must be < 1 context.putImageData(lastImageData, 0, 0); context.globalAlpha = frameOffset; @@ -61,8 +67,18 @@ export default (_: ConfigType<'ambient-mode'>) => { blurCanvas.width = qualityRatio; blurCanvas.height = Math.floor(newHeight / newWidth * qualityRatio); - blurCanvas.style.width = `${newWidth}px`; - blurCanvas.style.height = `${newHeight}px`; + blurCanvas.style.width = `${newWidth * sizeRatio}px`; + blurCanvas.style.height = `${newHeight * sizeRatio}px`; + + if (isFullscreen) blurCanvas.classList.add('fullscreen'); + else blurCanvas.classList.remove('fullscreen'); + + const leftOffset = newWidth * (sizeRatio - 1) / 2; + const topOffset = newHeight * (sizeRatio - 1) / 2; + blurCanvas.style.setProperty('--left', `${-1 * leftOffset}px`); + blurCanvas.style.setProperty('--top', `${-1 * topOffset}px`); + blurCanvas.style.setProperty('--blur', `${blur}px`); + blurCanvas.style.setProperty('--opacity', `${opacity}`); }; const observer = new MutationObserver((mutations) => { @@ -75,10 +91,22 @@ export default (_: ConfigType<'ambient-mode'>) => { const resizeObserver = new ResizeObserver(() => { applyVideoAttributes(); }); + const onConfigSync = (_: Electron.IpcRendererEvent, newConfig: ConfigType<'ambient-mode'>) => { + if (typeof newConfig.interpolationTime === 'number') interpolationTime = newConfig.interpolationTime; + if (typeof newConfig.buffer === 'number') buffer = newConfig.buffer; + if (typeof newConfig.quality === 'number') qualityRatio = newConfig.quality; + if (typeof newConfig.size === 'number') sizeRatio = newConfig.size / 100; + if (typeof newConfig.blur === 'number') blur = newConfig.blur; + if (typeof newConfig.opacity === 'number') opacity = newConfig.opacity; + if (typeof newConfig.fullscreen === 'boolean') isFullscreen = newConfig.fullscreen; + applyVideoAttributes(); + }; + ipcRenderer.on('ambient-mode:config-change', onConfigSync); + /* hooking */ let canvasInterval: NodeJS.Timeout | null = null; - canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate))); + canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer))); applyVideoAttributes(); observer.observe(songVideo, { attributes: true }); resizeObserver.observe(songVideo); @@ -90,7 +118,7 @@ export default (_: ConfigType<'ambient-mode'>) => { }; const onPlay = () => { if (canvasInterval) clearInterval(canvasInterval); - canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / framerate))); + canvasInterval = setInterval(onSync, Math.max(1, Math.ceil(1000 / buffer))); }; songVideo.addEventListener('pause', onPause); songVideo.addEventListener('play', onPlay); @@ -107,6 +135,7 @@ export default (_: ConfigType<'ambient-mode'>) => { observer.disconnect(); resizeObserver.disconnect(); + ipcRenderer.off('ambient-mode:config-change', onConfigSync); window.removeEventListener('resize', applyVideoAttributes); wrapper.removeChild(blurCanvas); diff --git a/src/plugins/ambient-mode/menu.ts b/src/plugins/ambient-mode/menu.ts new file mode 100644 index 00000000..18a4e8da --- /dev/null +++ b/src/plugins/ambient-mode/menu.ts @@ -0,0 +1,87 @@ +import config from './config'; + +import { MenuTemplate } from '../../menu'; + +const interpolationTimeList = [0, 500, 1000, 1500, 2000, 3000, 4000, 5000]; +const qualityList = [10, 25, 50, 100, 200, 500, 1000]; +const sizeList = [100, 110, 125, 150, 175, 200, 300]; +const bufferList = [1, 5, 10, 20, 30]; +const blurAmountList = [0, 5, 10, 25, 50, 100, 150, 200, 500]; +const opacityList = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]; + +export default (): MenuTemplate => [ + { + label: 'Smoothness transition', + submenu: interpolationTimeList.map((interpolationTime) => ({ + label: `During ${interpolationTime / 1000}s`, + type: 'radio', + checked: config.get('interpolationTime') === interpolationTime, + click() { + config.set('interpolationTime', interpolationTime); + }, + })), + }, + { + label: 'Quality', + submenu: qualityList.map((quality) => ({ + label: `${quality} pixels`, + type: 'radio', + checked: config.get('quality') === quality, + click() { + config.set('quality', quality); + }, + })), + }, + { + label: 'Size', + submenu: sizeList.map((size) => ({ + label: `${size}%`, + type: 'radio', + checked: config.get('size') === size, + click() { + config.set('size', size); + }, + })), + }, + { + label: 'Buffer', + submenu: bufferList.map((buffer) => ({ + label: `${buffer}`, + type: 'radio', + checked: config.get('buffer') === buffer, + click() { + config.set('buffer', buffer); + }, + })), + }, + { + label: 'Opacity', + submenu: opacityList.map((opacity) => ({ + label: `${opacity * 100}%`, + type: 'radio', + checked: config.get('opacity') === opacity, + click() { + config.set('opacity', opacity); + }, + })), + }, + { + label: 'Blur amount', + submenu: blurAmountList.map((blur) => ({ + label: `${blur} pixels`, + type: 'radio', + checked: config.get('blur') === blur, + click() { + config.set('blur', blur); + }, + })), + }, + { + label: 'Using fullscreen', + type: 'checkbox', + checked: config.get('fullscreen'), + click(item) { + config.set('fullscreen', item.checked); + }, + }, +]; diff --git a/src/plugins/ambient-mode/style.css b/src/plugins/ambient-mode/style.css index 44f90164..dd765611 100644 --- a/src/plugins/ambient-mode/style.css +++ b/src/plugins/ambient-mode/style.css @@ -1,7 +1,26 @@ -#song-video canvas.html5-blur-canvas{ - position: absolute; - left: 0; - top: 0; +#song-video canvas.html5-blur-canvas { + filter: blur(var(--blur, 100px)); + opacity: var(--opacity, 1); - filter: blur(100px); + pointer-events: none; +} + +#song-video canvas.html5-blur-canvas:not(.fullscreen) { + position: absolute; + + left: var(--left, 0px); + top: var(--top, 0px); +} + +#song-video canvas.html5-blur-canvas.fullscreen { + position: fixed; + + width: 100% !important; + height: 100% !important; + left: 0 !important; + top: 0 !important; +} + +#song-video .html5-video-container > video { + top: 0 !important; }