diff --git a/receiver/colorful_spectrum/README.md b/receiver/colorful_spectrum/README.md index 7c076c5..ea18501 100644 --- a/receiver/colorful_spectrum/README.md +++ b/receiver/colorful_spectrum/README.md @@ -4,7 +4,7 @@ title: "OpenWebRX+ Receiver Plugin: Colorful Spectrum" permalink: /receiver/colorful_spectrum --- -This `receiver` plugin will colorify your spectrum analyzer. +This `receiver` plugin will colorify your spectrum analyzer and alow you to configure the analyzer's background opacity. It adds a drop-down menu inside the settings section of the receiver pane, allowing users to easily change color. When the droplet next to the drop-down is pressed, the color selector turns into a slider that configures the background opacity. ## Preview @@ -18,6 +18,12 @@ Add this line in your `init.js` file: Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/colorful_spectrum/colorful_spectrum.js'); ``` +## Configuration +You can configure the default waterfall color and background opacity by setting the parameters in your `init.js` file (before loading the plugin): +```js +window.SpectrumDefaultColor = 'waterfall'; //or whatever color you want +window.SpectrumBackgroundOpacity = 0.0; // Change this number between 0.0 (clear) and 1.0 (pitch black) +``` ## init.js Learn how to [load plugins](/openwebrxplus-plugins/#load-plugins). diff --git a/receiver/colorful_spectrum/colorful_spectrum.js b/receiver/colorful_spectrum/colorful_spectrum.js index dfe217b..8953f79 100644 --- a/receiver/colorful_spectrum/colorful_spectrum.js +++ b/receiver/colorful_spectrum/colorful_spectrum.js @@ -1,63 +1,310 @@ /* - * Plugin: colorify the spectrum analyzer. - * + * Plugin: Spectravue Style Spectrum Analyzer (Stable & Uncorrupted) * License: MIT - * Copyright (c) 2023 Stanislav Lechev [0xAF], LZ2SLL */ -// do not load CSS for this plugin Plugins.colorful_spectrum.no_css = true; Plugins.colorful_spectrum.init = async function () { - // Check if utils plugin is loaded if (!Plugins.isLoaded('utils', 0.4)) { - // try to load the utils plugin await Plugins.load('https://0xaf.github.io/openwebrxplus-plugins/receiver/utils/utils.js'); - // check again if it was loaded successfully if (!Plugins.isLoaded('utils', 0.4)) { console.error('colorful_spectrum plugin depends on "utils >= 0.4".'); return false; - } else { - Plugins._debug('Plugin "utils" has been loaded as dependency.'); } } - // wait for OWRX to initialize + // --- CONFIGURATION --- + const defaultColor = window.SpectrumDefaultColor || 'blue'; + + const defaultOpacity = window.SpectrumBackgroundOpacity !== undefined ? window.SpectrumBackgroundOpacity : 0.0; + + // Define custom Spectravue palettes. + const colorPalettes = { + 'waterfall': { dynamic: true }, + 'blue': { + stroke: "rgba(0, 255, 255, 1.0)", + top: "rgba(0, 255, 255, 0.7)", + bottom: "rgba(0, 0, 150, 0.5)" + }, + 'red': { + stroke: "rgba(255, 30, 30, 1.0)", + top: "rgba(255, 30, 30, 0.9)", + bottom: "rgba(100, 0, 0, 0.5)" + }, + 'green': { + stroke: "rgba(0, 255, 0, 1.0)", + top: "rgba(0, 255, 0, 0.9)", + bottom: "rgba(0, 80, 0, 0.5)" + }, + 'yellow': { + stroke: "rgba(255, 255, 0, 1.0)", + top: "rgba(255, 255, 0, 0.9)", + bottom: "rgba(120, 120, 0, 0.5)" + }, + 'pink': { + stroke: "rgba(255, 0, 150, 1.0)", + top: "rgba(255, 0, 150, 0.9)", + bottom: "rgba(100, 0, 50, 0.5)" + }, + 'purple': { + stroke: "rgba(180, 50, 255, 1.0)", + top: "rgba(180, 50, 255, 0.9)", + bottom: "rgba(60, 0, 100, 0.5)" + }, + 'grey': { + stroke: "rgba(220, 220, 220, 1.0)", + top: "rgba(180, 180, 180, 0.8)", + bottom: "rgba(50, 50, 50, 0.5)" + } + }; + Plugins.utils.on_ready(function () { - Plugins.utils.wrap_func( - 'draw', - function (orig, thisArg, args) { - return true; - }, - function (res, thisArg, args) { - var vis_freq = get_visible_freq_range(); - var vis_start = 0.5 - (center_freq - vis_freq.start) / bandwidth; - var vis_end = 0.5 - (center_freq - vis_freq.end) / bandwidth; - var data_start = Math.round(fft_size * vis_start); - var data_end = Math.round(fft_size * vis_end); - var data_width = data_end - data_start; - var data_height = Math.abs(thisArg.max - thisArg.min); - var spec_width = thisArg.el.offsetWidth; - var spec_height = thisArg.el.offsetHeight; - if (spec_width <= data_width) { - var x_ratio = data_width / spec_width; - var y_ratio = spec_height / data_height; - for (var x = 0; x < spec_width; x++) { - var data = (thisArg.data[data_start + ((x * x_ratio) | 0)]); - var y = (data - thisArg.min) * y_ratio; - thisArg.ctx.fillRect(x, spec_height, 1, -y); - if (data) { - var c = Waterfall.makeColor(data); - thisArg.ctx.fillStyle = "rgba(" + - c[0] + ", " + c[1] + ", " + c[2] + ", " + - (25 + y * 2) + "%)"; + + // --- 1. PRECISE UI INJECTION --- + function injectUI() { + if (document.getElementById('webrx-spectrum-color')) return; + + let waterfallSelect = null; + let allSelects = document.querySelectorAll('select'); + + for (let i = 0; i < allSelects.length; i++) { + let text = allSelects[i].textContent || allSelects[i].innerText; + if (text.includes('Turbo') && text.includes('Ocean') && text.includes('Eclipse')) { + waterfallSelect = allSelects[i]; + break; + } + } + + if (!waterfallSelect) { + setTimeout(injectUI, 500); + return; + } + + let wrapper = waterfallSelect.parentNode; + let newWrapper = wrapper.cloneNode(true); + + wrapper.style.display = 'inline-flex'; + wrapper.style.verticalAlign = 'middle'; + + newWrapper.style.display = 'inline-flex'; + newWrapper.style.verticalAlign = 'middle'; + newWrapper.style.marginLeft = '8px'; + newWrapper.style.width = '115px'; + newWrapper.style.position = 'relative'; + + let clonedSelects = newWrapper.querySelectorAll('select'); + for (let j = 1; j < clonedSelects.length; j++) { + clonedSelects[j].remove(); + } + + let spectrumSelect = clonedSelects[0]; + spectrumSelect.id = 'webrx-spectrum-color'; + spectrumSelect.title = 'Spectrum Fill Color'; + spectrumSelect.innerHTML = ''; + spectrumSelect.style.width = '100%'; + spectrumSelect.style.flex = '1'; + spectrumSelect.style.display = 'inline-block'; + spectrumSelect.style.marginLeft = '3px'; + + try { + let origStyle = window.getComputedStyle(waterfallSelect); + const stylesToCopy = [ + 'height', 'minHeight', 'maxHeight', + 'fontSize', 'fontFamily', 'fontWeight', + 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', + 'lineHeight', 'boxSizing', 'border', 'borderRadius' + ]; + stylesToCopy.forEach(prop => { + if(origStyle[prop]) spectrumSelect.style[prop] = origStyle[prop]; + }); + } catch(e) { + console.warn("Colorful Spectrum: Could not copy styles perfectly", e); + } + + for (const key in colorPalettes) { + let opt = document.createElement('option'); + opt.value = key; + opt.innerText = key.charAt(0).toUpperCase() + key.slice(1); + spectrumSelect.appendChild(opt); + } + + let savedColor = localStorage.getItem('owrx-spectrum-color') || defaultColor; + spectrumSelect.value = savedColor; + + spectrumSelect.addEventListener('change', function(e) { + localStorage.setItem('owrx-spectrum-color', e.target.value); + }); + + let opacitySlider = document.createElement('input'); + opacitySlider.type = 'range'; + opacitySlider.id = 'webrx-spectrum-opacity'; + opacitySlider.min = '0'; + opacitySlider.max = '1'; + opacitySlider.step = '0.05'; + opacitySlider.title = 'Background Darkness'; + opacitySlider.style.width = '100%'; + opacitySlider.style.flex = '1'; + opacitySlider.style.display = 'none'; + opacitySlider.style.verticalAlign = 'middle'; + opacitySlider.style.marginLeft = '3px'; + + let savedOpacity = localStorage.getItem('owrx-spectrum-opacity'); + if (savedOpacity !== null) { + window.SpectrumBackgroundOpacity = parseFloat(savedOpacity); + } else { + window.SpectrumBackgroundOpacity = defaultOpacity; + } + opacitySlider.value = window.SpectrumBackgroundOpacity; + + opacitySlider.addEventListener('input', function(e) { + let val = parseFloat(e.target.value); + window.SpectrumBackgroundOpacity = val; + localStorage.setItem('owrx-spectrum-opacity', val); + }); + + newWrapper.appendChild(opacitySlider); + + let isSliderVisible = false; + Array.from(newWrapper.children).forEach(child => { + if (child.tagName !== 'SELECT' && child.tagName !== 'INPUT') { + child.style.transform = 'scaleX(-1)'; + child.style.cursor = 'pointer'; + child.title = 'Toggle Opacity Slider'; + + child.addEventListener('click', function() { + isSliderVisible = !isSliderVisible; + if (isSliderVisible) { + spectrumSelect.style.display = 'none'; + opacitySlider.style.display = 'inline-block'; + child.title = 'Back to Color Menu'; + } else { + opacitySlider.style.display = 'none'; + spectrumSelect.style.display = 'inline-block'; + child.title = 'Toggle Opacity Slider'; } - } + }); } - }, - spectrum + }); + + wrapper.parentNode.insertBefore(newWrapper, wrapper.nextSibling); + } + + injectUI(); + + // --- 2. SPECTRAVUE-STYLE DRAWING LOGIC --- + Plugins.utils.wrap_func( + 'draw', + function (orig, thisArg, args) { return true; }, + function (res, thisArg, args) { + if (!thisArg.data) return; + + var vis_freq = get_visible_freq_range(); + var vis_start = 0.5 - (center_freq - vis_freq.start) / bandwidth; + var vis_end = 0.5 - (center_freq - vis_freq.end) / bandwidth; + var data_start = Math.round(fft_size * vis_start); + var data_end = Math.round(fft_size * vis_end); + var data_width = data_end - data_start; + var data_height = Math.abs(thisArg.max - thisArg.min); + var spec_width = thisArg.el.offsetWidth; + var spec_height = thisArg.el.offsetHeight; + + var x_ratio = data_width / spec_width; + var y_ratio = spec_height / data_height; + + let spectrumSelectUI = document.getElementById('webrx-spectrum-color'); + let selectedColorMode = spectrumSelectUI ? spectrumSelectUI.value : defaultColor; + + thisArg.ctx.clearRect(0, 0, spec_width, spec_height); + + const bgOpacity = window.SpectrumBackgroundOpacity !== undefined ? window.SpectrumBackgroundOpacity : 0.0; + if (bgOpacity > 0) { + thisArg.ctx.fillStyle = `rgba(0, 0, 0, ${bgOpacity})`; + thisArg.ctx.fillRect(0, 0, spec_width, spec_height); + } + + thisArg.ctx.save(); + thisArg.ctx.beginPath(); + + for (var x = 0; x < spec_width; x++) { + var data_idx = data_start + ((x * x_ratio) | 0); + + if (data_idx < 0) data_idx = 0; + if (data_idx >= thisArg.data.length) data_idx = thisArg.data.length - 1; + + var data = thisArg.data[data_idx]; + var y = (data - thisArg.min) * y_ratio; + + if (x === 0) { + thisArg.ctx.moveTo(x, spec_height - y); + } else { + thisArg.ctx.lineTo(x, spec_height - y); + } + } + + if (selectedColorMode === 'waterfall') { + + var fillGradient = thisArg.ctx.createLinearGradient(0, 0, 0, spec_height); + var strokeGradient = thisArg.ctx.createLinearGradient(0, 0, 0, spec_height); + + for (var i = 0; i <= 10; i++) { + var step = i / 10; + + var signal = thisArg.max - (step * (thisArg.max - thisArg.min)); + var originalC = Waterfall.makeColor(signal); + var c = [originalC[0], originalC[1], originalC[2]]; + + var fillAlpha = 0.8; + + var blend = Math.max(0, (step - 0.6) / 0.4); + if (blend > 0) { + // Pushed these numbers down for a much darker, deeper navy blue + c[0] = Math.round(c[0] * (1 - blend) + 20 * blend); + c[1] = Math.round(c[1] * (1 - blend) + 70 * blend); + c[2] = Math.round(c[2] * (1 - blend) + 160 * blend); + fillAlpha = 0.8 + (0.15 * blend); + } + + fillGradient.addColorStop(step, "rgba(" + c[0] + ", " + c[1] + ", " + c[2] + ", " + fillAlpha + ")"); + strokeGradient.addColorStop(step, "rgba(" + c[0] + ", " + c[1] + ", " + c[2] + ", 1.0)"); + } + + thisArg.ctx.lineWidth = 1.5; + thisArg.ctx.strokeStyle = strokeGradient; + thisArg.ctx.stroke(); + + thisArg.ctx.lineTo(spec_width, spec_height); + thisArg.ctx.lineTo(0, spec_height); + thisArg.ctx.closePath(); + + thisArg.ctx.fillStyle = fillGradient; + thisArg.ctx.fill(); + + } else { + + let palette = colorPalettes[selectedColorMode] || colorPalettes['blue']; + + thisArg.ctx.lineWidth = 1.5; + thisArg.ctx.strokeStyle = palette.stroke; + thisArg.ctx.stroke(); + + thisArg.ctx.lineTo(spec_width, spec_height); + thisArg.ctx.lineTo(0, spec_height); + thisArg.ctx.closePath(); + + var gradient = thisArg.ctx.createLinearGradient(0, 0, 0, spec_height); + gradient.addColorStop(0, palette.top); + gradient.addColorStop(1, palette.bottom); + + thisArg.ctx.fillStyle = gradient; + thisArg.ctx.fill(); + } + + thisArg.ctx.restore(); + }, + spectrum ); }); diff --git a/receiver/colorful_spectrum/colorful_spectrum.png b/receiver/colorful_spectrum/colorful_spectrum.png index da40b71..25d9ddd 100644 Binary files a/receiver/colorful_spectrum/colorful_spectrum.png and b/receiver/colorful_spectrum/colorful_spectrum.png differ diff --git a/receiver/colorful_spectrum/colorful_spectrum1.png b/receiver/colorful_spectrum/colorful_spectrum1.png new file mode 100644 index 0000000..ab91c20 Binary files /dev/null and b/receiver/colorful_spectrum/colorful_spectrum1.png differ diff --git a/receiver/colorful_spectrum/colorful_spectum2.png b/receiver/colorful_spectrum/colorful_spectum2.png new file mode 100644 index 0000000..9f6f72f Binary files /dev/null and b/receiver/colorful_spectrum/colorful_spectum2.png differ