Source: ui/juliaSlidersController.js

/**
 * @module JuliaSlidersController
 * @author Radim Brnka
 * @description Controller of Julia slider elements directly interacting with the c-value of Julia fractal.
 * @copyright Synaptory Fractal Traveler, 2025-2026
 * @license MIT
 */

import {CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE} from "../global/constants";
import {isJuliaMode, resetPresetAndDiveButtonStates, updateInfo} from "./ui";
import {clearURLParams} from "../global/utils";
import {JuliaRenderer} from "../renderers/juliaRenderer";

/**
 * Throttle limit in milliseconds.
 * 50ms = 20 updates/sec - smooth enough for visual feedback, reduces DOM thrash during animations.
 * @type {number}
 */
const SLIDER_UPDATE_THROTTLE_LIMIT = 50;

/** Threshold for "near zero" visual feedback */
const ZERO_ZONE_THRESHOLD = 0.01;

/** Threshold for snapping to exactly zero */
const ZERO_SNAP_THRESHOLD = 0.005;

let sliderContainer;
let realSlider;
let imagSlider;
let realSliderValue;
let imagSliderValue;
let lastSliderUpdate = 0; // Tracks the last time the sliders were updated

let fractalApp;

/**
 * Initializes the elements and their event listeners
 * @param {FractalRenderer} app
 */
export function initJuliaSliders(app) {
    if (!(app instanceof JuliaRenderer)) {
        console.error(`%c initJuliaSliders: %c Can only attach to JuliaRenderer, not ${app}!`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
        return;
    }

    fractalApp = app;

    realSlider = document.getElementById('realSlider');
    realSliderValue = document.getElementById('realSliderValue');
    imagSlider = document.getElementById('imagSlider');
    imagSliderValue = document.getElementById('imagSliderValue');

    realSlider.addEventListener('input', onRealSliderChange);
    imagSlider.addEventListener('input', onImagSliderChange);

    sliderContainer = document.getElementById('sliders');
    sliderContainer.style.display = 'flex';

    console.log(`%c initJuliaSliders: %c Initialized.`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
}

function onSliderChangeFinished() {
    fractalApp.draw();
    updateInfo();
    clearURLParams();
    resetPresetAndDiveButtonStates();
    fractalApp.demoTime = 0;
}

/**
 * Updates slider visual state based on proximity to zero
 * @param {HTMLInputElement} slider
 * @param {number} value
 */
function updateZeroZoneState(slider, value) {
    const isNearZero = Math.abs(value) < ZERO_ZONE_THRESHOLD;
    slider.classList.toggle('near-zero', isNearZero);
}

/**
 * Snaps value to zero if within snap threshold
 * @param {number} value
 * @returns {number}
 */
function snapToZero(value) {
    return Math.abs(value) < ZERO_SNAP_THRESHOLD ? 0 : value;
}

const onRealSliderChange = () => {
    let cx = snapToZero(parseFloat(realSlider.value));
    fractalApp.c[0] = cx;
    realSliderValue.textContent = cx.toFixed(4);
    updateZeroZoneState(realSlider, cx);
    onSliderChangeFinished();
};

const onImagSliderChange = () => {
    let cy = snapToZero(parseFloat(imagSlider.value));
    fractalApp.c[1] = cy;
    imagSliderValue.textContent = `${cy.toFixed(4)}i`;
    updateZeroZoneState(imagSlider, cy);
    onSliderChangeFinished();
};

// region > External methods ------------------------------------------------------------------------------------------
/** Destructor. Removes listeners and hides elements. */
export function destroyJuliaSliders() {
    if (realSlider && imagSlider && sliderContainer) {
        realSlider.removeEventListener('input', onRealSliderChange);
        imagSlider.removeEventListener('input', onImagSliderChange);

        sliderContainer.style.display = 'none';

        console.log(`%c destroyJuliaSliders: %c Destroyed.`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
    } else {
        console.warn(`%c destroyJuliaSliders: %c Called on not initialized state!`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
    }
}

/** Resets sliders */
export function resetJuliaSliders() {
    if (!isJuliaMode()) {
        console.warn(`%c resetJuliaSliders: %c Not in Julia mode!`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
        return;
    }

    realSlider.value = parseFloat(fractalApp.c[0].toFixed(2));
    imagSlider.value = parseFloat(fractalApp.c[1].toFixed(2));
    realSliderValue.innerText = realSlider.value;
    imagSliderValue.innerText = imagSlider.value + 'i';
    fractalApp.demoTime = 0;
}

/** Updates the real/imaginary sliders appropriately */
export function updateJuliaSliders() {
    const now = performance.now();
    const timeSinceLastUpdate = now - lastSliderUpdate;

    if (timeSinceLastUpdate < SLIDER_UPDATE_THROTTLE_LIMIT) {
        return; // Skip update if called too soon
    }

    // Update the last update time
    lastSliderUpdate = now;
    realSliderValue.innerText = fractalApp.c[0].toFixed(4);
    imagSliderValue.innerText = fractalApp.c[1].toFixed(4) + 'i';
    realSlider.value = parseFloat(fractalApp.c[0].toFixed(4));
    imagSlider.value = parseFloat(fractalApp.c[1].toFixed(4));

    // Update zero zone visual feedback
    updateZeroZoneState(realSlider, fractalApp.c[0]);
    updateZeroZoneState(imagSlider, fractalApp.c[1]);
}

/** Disable slider controls */
export const disableJuliaSliders = () => {
    imagSlider.disabled = realSlider.disabled = true;
    realSlider.classList.add('thumbDisabled');
    imagSlider.classList.add('thumbDisabled');
};

/** Enable slider controls */
export const enableJuliaSliders = () => {
    imagSlider.disabled = realSlider.disabled = false;
    realSlider.classList.remove('thumbDisabled');
    imagSlider.classList.remove('thumbDisabled');
};

// endregion -----------------------------------------------------------------------------------------------------------