Files
intercept/static/js/components/radio-knob.js
Smittix 007400d2a7 Release v2.9.0 - iNTERCEPT rebrand and UI overhaul
- Rebrand from INTERCEPT to iNTERCEPT
- New logo design with 'i' and signal wave brackets
- Add animated landing page with "See the Invisible" tagline
- Fix tuning dial audio issues with debouncing and restart prevention
- Fix Listening Post scanner with proper signal hit logging
- Update setup script for apt-based Python package installation
- Add Instagram promo video template
- Add full-size logo assets for external use

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 01:00:17 +00:00

227 lines
7.0 KiB
JavaScript

/**
* Intercept - Radio Knob Component
* Interactive rotary knob control with drag-to-rotate
*/
class RadioKnob {
constructor(element, options = {}) {
this.element = element;
this.value = parseFloat(element.dataset.value) || 0;
this.min = parseFloat(element.dataset.min) || 0;
this.max = parseFloat(element.dataset.max) || 100;
this.step = parseFloat(element.dataset.step) || 1;
this.rotation = this.valueToRotation(this.value);
this.isDragging = false;
this.startY = 0;
this.startRotation = 0;
this.sensitivity = options.sensitivity || 1.5;
this.onChange = options.onChange || null;
this.bindEvents();
this.updateVisual();
}
valueToRotation(value) {
const range = this.max - this.min;
const normalized = (value - this.min) / range;
return normalized * 270 - 135; // -135 to +135 degrees
}
rotationToValue(rotation) {
const normalized = (rotation + 135) / 270;
let value = this.min + normalized * (this.max - this.min);
// Snap to step
value = Math.round(value / this.step) * this.step;
return Math.max(this.min, Math.min(this.max, value));
}
bindEvents() {
// Mouse events
this.element.addEventListener('mousedown', (e) => this.startDrag(e));
document.addEventListener('mousemove', (e) => this.drag(e));
document.addEventListener('mouseup', () => this.endDrag());
// Touch support
this.element.addEventListener('touchstart', (e) => {
e.preventDefault();
this.startDrag(e.touches[0]);
}, { passive: false });
document.addEventListener('touchmove', (e) => {
if (this.isDragging) {
e.preventDefault();
this.drag(e.touches[0]);
}
}, { passive: false });
document.addEventListener('touchend', () => this.endDrag());
// Scroll wheel support
this.element.addEventListener('wheel', (e) => this.handleWheel(e), { passive: false });
// Double-click to reset
this.element.addEventListener('dblclick', () => this.reset());
}
startDrag(e) {
this.isDragging = true;
this.startY = e.clientY;
this.startRotation = this.rotation;
this.element.style.cursor = 'grabbing';
this.element.classList.add('active');
// Play click sound if available
if (typeof playClickSound === 'function') {
playClickSound();
}
}
drag(e) {
if (!this.isDragging) return;
const deltaY = this.startY - e.clientY;
let newRotation = this.startRotation + deltaY * this.sensitivity;
// Clamp rotation
newRotation = Math.max(-135, Math.min(135, newRotation));
this.rotation = newRotation;
this.value = this.rotationToValue(this.rotation);
this.updateVisual();
this.dispatchChange();
}
endDrag() {
if (!this.isDragging) return;
this.isDragging = false;
this.element.style.cursor = 'grab';
this.element.classList.remove('active');
}
handleWheel(e) {
e.preventDefault();
const delta = e.deltaY > 0 ? -this.step : this.step;
const multiplier = e.shiftKey ? 5 : 1; // Faster with shift key
this.setValue(this.value + delta * multiplier);
// Play click sound if available
if (typeof playClickSound === 'function') {
playClickSound();
}
}
setValue(value, silent = false) {
this.value = Math.max(this.min, Math.min(this.max, value));
this.rotation = this.valueToRotation(this.value);
this.updateVisual();
if (!silent) {
this.dispatchChange();
}
}
getValue() {
return this.value;
}
reset() {
const defaultValue = parseFloat(this.element.dataset.default) ||
(this.min + this.max) / 2;
this.setValue(defaultValue);
}
updateVisual() {
this.element.style.transform = `rotate(${this.rotation}deg)`;
// Update associated value display
const valueDisplayId = this.element.id.replace('Knob', 'Value');
const valueDisplay = document.getElementById(valueDisplayId);
if (valueDisplay) {
valueDisplay.textContent = Math.round(this.value);
}
// Update data attribute
this.element.dataset.value = this.value;
}
dispatchChange() {
// Custom callback
if (this.onChange) {
this.onChange(this.value, this);
}
// Custom event
this.element.dispatchEvent(new CustomEvent('knobchange', {
detail: { value: this.value, knob: this },
bubbles: true
}));
}
}
/**
* Tuning Dial - Larger rotary control for frequency tuning
*/
class TuningDial extends RadioKnob {
constructor(element, options = {}) {
super(element, {
sensitivity: options.sensitivity || 0.8,
...options
});
this.fineStep = options.fineStep || 0.025;
this.coarseStep = options.coarseStep || 0.2;
}
handleWheel(e) {
e.preventDefault();
const step = e.shiftKey ? this.fineStep : this.coarseStep;
const delta = e.deltaY > 0 ? -step : step;
this.setValue(this.value + delta);
}
// Override to not round to step for smooth tuning
rotationToValue(rotation) {
const normalized = (rotation + 135) / 270;
let value = this.min + normalized * (this.max - this.min);
return Math.max(this.min, Math.min(this.max, value));
}
updateVisual() {
this.element.style.transform = `rotate(${this.rotation}deg)`;
// Update associated value display with decimals
const valueDisplayId = this.element.id.replace('Dial', 'Value');
const valueDisplay = document.getElementById(valueDisplayId);
if (valueDisplay) {
valueDisplay.textContent = this.value.toFixed(3);
}
this.element.dataset.value = this.value;
}
}
/**
* Initialize all radio knobs on the page
*/
function initRadioKnobs() {
// Initialize standard knobs
document.querySelectorAll('.radio-knob').forEach(element => {
if (!element._knob) {
element._knob = new RadioKnob(element);
}
});
// Initialize tuning dials
document.querySelectorAll('.tuning-dial').forEach(element => {
if (!element._dial) {
element._dial = new TuningDial(element);
}
});
}
// Auto-initialize on DOM ready
document.addEventListener('DOMContentLoaded', initRadioKnobs);
// Export for use in modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { RadioKnob, TuningDial, initRadioKnobs };
}