mirror of
https://github.com/smittix/intercept.git
synced 2026-05-01 10:09:59 -07:00
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>
This commit is contained in:
226
static/js/components/radio-knob.js
Normal file
226
static/js/components/radio-knob.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user