/**
* @module AxesOverlay
* @description Renders coordinate axes grid overlay for Riemann mode
* @author Radim Brnka
* @copyright Synaptory Fractal Traveler, 2025-2026
* @license MIT
*/
import {log} from '../global/constants';
let canvas = null;
let ctx = null;
let visible = false;
let renderer = null;
/**
* Initializes the axes overlay
* @param {HTMLCanvasElement} canvasElement
* @param {Object} fractalRenderer
*/
export function init(canvasElement, fractalRenderer) {
canvas = canvasElement;
renderer = fractalRenderer;
if (canvas) {
ctx = canvas.getContext('2d');
}
}
/**
* Sets the renderer reference
* @param {Object} fractalRenderer
*/
export function setRenderer(fractalRenderer) {
renderer = fractalRenderer;
}
/**
* Shows the axes overlay
*/
export function show() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.classList.remove('axes-hidden');
visible = true;
draw();
}
/**
* Hides the axes overlay
*/
export function hide() {
if (canvas) {
canvas.classList.add('axes-hidden');
}
visible = false;
}
/**
* Toggles the axes overlay
* @returns {boolean} New visibility state
*/
export function toggle() {
if (visible) {
hide();
} else {
show();
}
log(`Axes: ${visible ? 'ON' : 'OFF'}`);
return visible;
}
/**
* Updates the overlay (redraws if visible)
*/
export function update() {
if (!visible) return;
draw();
}
/**
* Resizes the canvas and redraws
*/
export function resize() {
if (!visible || !canvas) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
draw();
}
/**
* Returns current visibility state
* @returns {boolean}
*/
export function isVisible() {
return visible;
}
/**
* Calculates appropriate tick spacing based on zoom level
* @param {number} zoom
* @returns {number}
*/
function getTickSpacing(zoom) {
const idealTicks = 8;
const rawSpacing = zoom / idealTicks;
const magnitude = Math.pow(10, Math.floor(Math.log10(rawSpacing)));
const normalized = rawSpacing / magnitude;
if (normalized < 1.5) return magnitude;
if (normalized < 3.5) return 2 * magnitude;
if (normalized < 7.5) return 5 * magnitude;
return 10 * magnitude;
}
/**
* Formats axis number for display
* @param {number} n
* @returns {string}
*/
function formatAxisNumber(n) {
// Check if effectively an integer (within floating-point tolerance)
const rounded = Math.round(n);
if (Math.abs(n - rounded) < 1e-9) {
return rounded.toString();
}
// Use exponential only for very small or very large non-integers
if (Math.abs(n) < 0.01 || Math.abs(n) >= 100000) {
return n.toExponential(1);
}
return n.toFixed(2).replace(/\.?0+$/, '');
}
/**
* Draws the coordinate axes with numbers
*/
function draw() {
if (!ctx || !canvas || !renderer) return;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
ctx.clearRect(0, 0, width, height);
const pan = renderer.pan;
const zoom = renderer.zoom;
// Calculate visible range
const aspect = width / height;
const halfWidth = zoom * aspect / 2;
const halfHeight = zoom / 2;
const left = pan[0] - halfWidth;
const right = pan[0] + halfWidth;
const top = pan[1] + halfHeight;
const bottom = pan[1] - halfHeight;
const tickSpacing = getTickSpacing(zoom);
const scale = height / zoom;
// Calculate main axis positions
const realAxisY = centerY - (0 - pan[1]) * scale;
const imagAxisX = centerX + (0 - pan[0]) * scale;
// Clamp label positions
const labelPadding = 20;
const realAxisLabelY = Math.max(labelPadding, Math.min(height - labelPadding, realAxisY));
const imagAxisLabelX = Math.max(labelPadding + 30, Math.min(width - labelPadding - 30, imagAxisX));
// Grid lines style
ctx.strokeStyle = 'rgba(255, 255, 255, 0.12)';
ctx.lineWidth = 1;
ctx.font = '14px monospace';
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
// Vertical grid lines
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
const startX = Math.ceil(left / tickSpacing) * tickSpacing;
for (let x = startX; x <= right; x += tickSpacing) {
const screenX = centerX + (x - pan[0]) * scale;
ctx.beginPath();
ctx.moveTo(screenX, 0);
ctx.lineTo(screenX, height);
ctx.stroke();
if (Math.abs(x) > tickSpacing * 0.1) {
const label = formatAxisNumber(x);
ctx.fillText(label, screenX, realAxisLabelY + 5);
}
}
// Horizontal grid lines
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
const startY = Math.ceil(bottom / tickSpacing) * tickSpacing;
for (let y = startY; y <= top; y += tickSpacing) {
const screenY = centerY - (y - pan[1]) * scale;
ctx.beginPath();
ctx.moveTo(0, screenY);
ctx.lineTo(width, screenY);
ctx.stroke();
if (Math.abs(y) > tickSpacing * 0.1) {
const label = formatAxisNumber(y) + 'i';
ctx.fillText(label, imagAxisLabelX + 5, screenY);
}
}
// Main axes
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
ctx.lineWidth = 2;
// Real axis
ctx.beginPath();
ctx.moveTo(0, realAxisY);
ctx.lineTo(width, realAxisY);
ctx.stroke();
// Imaginary axis
ctx.beginPath();
ctx.moveTo(imagAxisX, 0);
ctx.lineTo(imagAxisX, height);
ctx.stroke();
// Origin label
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('0', imagAxisX + 5, realAxisY + 5);
}