/**
* @module JuliaPreview
* @author Radim Brnka
* @description Manages the floating Julia set preview shown on middle-click in Mandelbrot mode.
* @copyright Synaptory Fractal Traveler, 2025-2026
* @license MIT
*/
import {CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE} from "../global/constants";
import {JuliaPreviewRenderer} from "../renderers/juliaPreviewRenderer";
/** @type {HTMLCanvasElement} */
let floatingCanvas = null;
/** @type {JuliaPreviewRenderer} */
let previewRenderer = null;
/** @type {boolean} */
let previewActive = false;
/** @type {Float32Array|null} */
let pendingInnerStops = null;
/** Preview canvas size in pixels */
const PREVIEW_WIDTH = 250;
const PREVIEW_HEIGHT = 200;
/** Offset from cursor position */
const CURSOR_OFFSET_X = 20;
const CURSOR_OFFSET_Y = 20;
/**
* Initializes the Julia preview system.
* Should be called once during app initialization.
*/
export function initJuliaPreview() {
floatingCanvas = document.getElementById('floatingCanvas');
if (!floatingCanvas) {
console.error('floatingCanvas element not found!');
return;
}
// Set canvas dimensions (physical pixels for rendering)
floatingCanvas.width = PREVIEW_WIDTH * window.devicePixelRatio;
floatingCanvas.height = PREVIEW_HEIGHT * window.devicePixelRatio;
// Set CSS dimensions (display size)
floatingCanvas.style.width = `${PREVIEW_WIDTH}px`;
floatingCanvas.style.height = `${PREVIEW_HEIGHT}px`;
}
/**
* Shows the Julia preview at the given screen position with the specified c parameter.
* @param {number} screenX - Screen X coordinate (clientX)
* @param {number} screenY - Screen Y coordinate (clientY)
* @param {number} cx - Real part of Julia c parameter
* @param {number} cy - Imaginary part of Julia c parameter
*/
export function showJuliaPreview(screenX, screenY, cx, cy) {
if (!floatingCanvas) {
initJuliaPreview();
}
// Create renderer on first show
if (!previewRenderer) {
previewRenderer = new JuliaPreviewRenderer(floatingCanvas);
// Use lower iteration count for responsiveness
previewRenderer.MAX_ITER = 500;
previewRenderer.extraIterations = 0;
console.log(`%c JuliaPreview: %c Created preview renderer`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
// Apply any pending palette that was set before renderer was created
if (pendingInnerStops) {
previewRenderer.innerStops = pendingInnerStops;
pendingInnerStops = null;
}
}
// Update c parameter
previewRenderer.c = [cx, cy];
previewRenderer.markOrbitDirty();
// Position the canvas near cursor
updatePreviewPosition(screenX, screenY);
// Show the canvas
floatingCanvas.style.display = 'block';
previewActive = true;
// Render the Julia set
previewRenderer.draw();
}
/**
* Updates the Julia preview position and c parameter as the mouse moves.
* @param {number} screenX - Screen X coordinate (clientX)
* @param {number} screenY - Screen Y coordinate (clientY)
* @param {number} cx - Real part of Julia c parameter
* @param {number} cy - Imaginary part of Julia c parameter
*/
export function updateJuliaPreview(screenX, screenY, cx, cy) {
if (!previewActive || !previewRenderer) return;
// Update c parameter
previewRenderer.c = [cx, cy];
previewRenderer.markOrbitDirty();
// Update position
updatePreviewPosition(screenX, screenY);
// Re-render
previewRenderer.draw();
}
/**
* Updates the floating canvas position relative to cursor.
* Ensures the preview stays within viewport bounds.
* @param {number} screenX - Screen X coordinate
* @param {number} screenY - Screen Y coordinate
*/
function updatePreviewPosition(screenX, screenY) {
if (!floatingCanvas) return;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// Default position: below and to the right of cursor
let left = screenX + CURSOR_OFFSET_X;
let top = screenY + CURSOR_OFFSET_Y;
// Flip to left side if would overflow right edge
if (left + PREVIEW_WIDTH > viewportWidth) {
left = screenX - PREVIEW_WIDTH - CURSOR_OFFSET_X;
}
// Flip to above cursor if would overflow bottom edge
if (top + PREVIEW_HEIGHT > viewportHeight) {
top = screenY - PREVIEW_HEIGHT - CURSOR_OFFSET_Y;
}
// Clamp to viewport bounds
left = Math.max(0, Math.min(left, viewportWidth - PREVIEW_WIDTH));
top = Math.max(0, Math.min(top, viewportHeight - PREVIEW_HEIGHT));
floatingCanvas.style.left = `${left}px`;
floatingCanvas.style.top = `${top}px`;
}
/**
* Hides the Julia preview.
*/
export function hideJuliaPreview() {
if (!previewActive) return;
previewActive = false;
if (floatingCanvas) {
floatingCanvas.style.display = 'none';
}
console.log(`%c JuliaPreview: %c Preview hidden`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
}
/**
* Recolors the Julia preview with a new palette.
* @param {number[]} palette - RGB palette [r, g, b] in range [0,1]
*/
export function recolorJuliaPreview(palette) {
if (!palette || palette.length < 3) return;
// Convert 3-float RGB palette to 15-float inner stops for Julia shader
// Pattern matches Cosmos palette: black -> color -> black -> bright -> dark
const innerStops = new Float32Array([
0, 0, 0, // stop 0: black
palette[0], palette[1], palette[2], // stop 1: the color
0, 0, 0, // stop 2: black (keeps dark background)
palette[0] * 1.2, palette[1] * 1.2, palette[2] * 1.2, // stop 3: brighter highlight
0.1, 0.1, 0.1 // stop 4: dark version
]);
if (previewRenderer) {
// Renderer exists - update immediately
previewRenderer.animateColorPaletteTransition(innerStops).then();
} else {
// Store for later when renderer is created
pendingInnerStops = innerStops;
}
}
/**
* Resets the Julia preview renderer to default state.
*/
export function resetJuliaPreview() {
if (!previewRenderer) {
console.warn(`%c JuliaPreview: %c Cannot reset - renderer not initialized`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
return;
}
previewRenderer.reset();
}
/**
* Destroys the Julia preview renderer and cleans up resources.
* Call this when switching away from Mandelbrot mode.
*/
export function destroyJuliaPreview() {
hideJuliaPreview();
if (previewRenderer) {
previewRenderer.destroy();
previewRenderer = null;
console.log(`%c JuliaPreview: %c Preview renderer destroyed`, CONSOLE_GROUP_STYLE, CONSOLE_MESSAGE_STYLE);
}
}
/**
* Returns whether the preview is currently active.
* @returns {boolean}
*/
export function isJuliaPreviewActive() {
return previewActive;
}