import {FractalRenderer} from './fractalRenderer.js';
import {asyncDelay, compareComplex} from "../global/utils";
import {DEFAULT_CONSOLE_GROUP_COLOR, EASE_TYPE, PI} from "../global/constants";
/**
* MandelbrotRenderer
*
* @author Radim Brnka
* @description This module defines a MandelbrotRenderer class that inherits from fractalRenderer, implements the shader fragment code for the Mandelbrot set fractal and sets preset zoom-ins.
* @extends FractalRenderer
*/
export class MandelbrotRenderer extends FractalRenderer {
constructor(canvas) {
super(canvas);
this.DEFAULT_PAN = [-0.5, 0];
this.pan = [...this.DEFAULT_PAN];
/** Mandelbrot-specific presets */
this.PRESETS = [
{
id: 0,
pan: this.DEFAULT_PAN,
zoom: this.DEFAULT_ZOOM,
rotation: this.DEFAULT_ROTATION,
title: 'Default View'
},
{id: 1, pan: [0.351424, 0.063866], zoom: 0.000049},
{id: 2, pan: [0.254998, 0.000568], zoom: 0.000045},
{id: 3, pan: [-0.164538, 1.038428], zoom: 0.000127},
{id: 4, pan: [-0.750700, 0.021415], zoom: 0.000110, title: 'Across the Seahorse Valley'},
{id: 5, pan: [-0.766863, -0.107475], zoom: 0.000196},
{id: 6, pan: [-0.8535686544080792, -0.21081423598149682], zoom: 0.000126},
{id: 7, pan: [0.337420, 0.047257], zoom: 0.000143, title: 'Minibrot'},
{id: 8, pan: [0.11650135661082159, -0.6635453818054073], zoom: 0.000104, title: 'Misiurewicz Point'},
{id: 9, pan: [-0.124797, 0.840309], zoom: 0.000628, title: 'The Rabbits'}
// {id: 9, pan: [-0.7469408211592985,0.10721648652717636], rotation: 0, zoom: 0.000985998295121236, title: 'Seahorse Valley'}
];
this.init();
}
/**
* @inheritDoc
* @override
*/
createFragmentShaderSource() {
/** Coloring algorithm */
const coloring = `float color = i / 100.0;
vec3 fractalColor = vec3(
sin(color * 3.1415),
sin(color * 6.283),
sin(color * 1.720)
) * u_colorPalette;
gl_FragColor = vec4(fractalColor, 1.0);
`;
return `
precision mediump float;
// Use uniforms for dynamic values.
uniform vec2 u_resolution;
uniform vec2 u_pan;
uniform float u_zoom;
uniform float u_iterations;
uniform vec3 u_colorPalette;
uniform float u_rotation; // Rotation in radians
void main() {
// Compute aspect ratio from the current resolution.
float aspect = u_resolution.x / u_resolution.y;
// Normalize coordinates based on the current resolution.
vec2 st = gl_FragCoord.xy / u_resolution;
st -= 0.5; // Center the coordinate system.
st.x *= aspect; // Adjust for the aspect ratio.
// Apply rotation.
float cosR = cos(u_rotation);
float sinR = sin(u_rotation);
vec2 rotated = vec2(
st.x * cosR - st.y * sinR,
st.x * sinR + st.y * cosR
);
// Scale and translate to fractal coordinates.
vec2 c = rotated * u_zoom + u_pan;
// Mandelbrot computation.
vec2 z = vec2(0.0, 0.0);
float i;
for (float n = 0.0; n < 2000.0; n++) {
if (n >= u_iterations || dot(z, z) > 4.0) {
i = n;
break;
}
z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;
}
// Color the pixel.
if (i >= u_iterations) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
} else {
${coloring}
}
}
`;
}
/**
* @inheritDoc
* @override
*/
draw() {
this.gl.useProgram(this.program);
const baseIters = Math.floor(5000 * Math.pow(2, -Math.log2(this.zoom)));
this.iterations = Math.min(2000, baseIters + this.extraIterations);
super.draw();
}
// region > ANIMATION METHODS --------------------------------------------------------------------------------------
/**
* Animates travel to a preset. It first zooms out to the default zoom, then rotates, then animates pan and zoom-in.
* If any of the final params is the same as the target, it won't animate it.
*
* @param {MANDELBROT_PRESET} preset An instance-specific object to define exact spot in the fractal
* @param {number} [zoomOutDuration] in ms
* @param {number} [zoomInDuration] in ms
* @return {Promise<void>}
*/
async animateTravelToPreset(preset, zoomOutDuration = 1000, zoomInDuration = 3500) {
console.groupCollapsed(`%c ${this.constructor.name}: animateTravelToPreset`, `color: ${DEFAULT_CONSOLE_GROUP_COLOR}`);
console.log(`Traveling to preset: ${JSON.stringify(preset)}`);
const targetRotation = preset.rotation || 0;
if (preset.zoom.toFixed(6) === this.zoom.toFixed(6)) {
// If only pan is changed, adjust pan
await this.animatePanTo(preset.pan, zoomOutDuration);
} else if (compareComplex(preset.pan, this.pan)) {
// If only zoom is changed, adjust zoom-in
await this.animateZoomTo(preset.zoom, zoomOutDuration);
} else {
// Otherwise zoom-out
await this.animateZoomTo(this.DEFAULT_ZOOM, zoomOutDuration);
}
await Promise.all([
this.animatePanThenZoomTo(preset.pan, preset.zoom, 500, zoomInDuration),
this.animateRotationTo(targetRotation, 500, EASE_TYPE.QUINT)
]);
this.currentPresetIndex = preset.id || 0;
console.log(`Travel complete.`);
console.groupEnd();
}
/**
* Animate travel to a preset with random rotation. This method waits for three stages:
* 1. Zoom-out to default zoom with rotation.
* 2. Pan transition.
* 3. Zoom-in with rotation.
*
* @param {MANDELBROT_PRESET} preset The target preset object with properties: pan, c, zoom, rotation.
* @param {number} zoomOutDuration Duration (ms) for the zoom-out stage.
* @param {number} panDuration Duration (ms) for the pan stage.
* @param {number} zoomInDuration Duration (ms) for the zoom-in stage.
*/
async animateTravelToPresetWithRandomRotation(preset, zoomOutDuration, panDuration, zoomInDuration) {
console.groupCollapsed(`%c ${this.constructor.name}: animateTravelToPresetWithRandomRotation`, `color: ${DEFAULT_CONSOLE_GROUP_COLOR}`);
// Generate random rotations for a more dynamic effect.
const zoomOutRotation = this.rotation + (Math.random() * PI * 2 - PI);
const zoomInRotation = zoomOutRotation + (Math.random() * PI * 2 - PI);
if (this.rotation !== this.DEFAULT_ROTATION) {
await this.animateZoomRotationTo(this.DEFAULT_ZOOM, zoomOutRotation, zoomOutDuration)
}
await this.animatePanTo(preset.pan, panDuration, EASE_TYPE.CUBIC);
await this.animateZoomRotationTo(preset.zoom, zoomInRotation, zoomInDuration);
this.currentPresetIndex = preset.id || 0;
console.groupEnd();
}
/**
* Animates infinite demo loop of traveling to the presets
* @param {boolean} random Determines whether presets are looped in order from 1-9 or ordered randomly
* @return {Promise<void>}
*/
async animateDemo(random = true) {
console.groupCollapsed(`%c ${this.constructor.name}: animateDemo`, `color: ${DEFAULT_CONSOLE_GROUP_COLOR}`);
this.stopAllNonColorAnimations();
if (!this.PRESETS.length) {
console.warn('No presets defined for Mandelbrot mode ');
return;
}
this.demoActive = true;
const getNextPresetIndex = (random) => {
if (random) {
let index;
do {
index = Math.floor(Math.random() * this.PRESETS.length);
} while (index === this.currentPresetIndex || index === 0);
return index;
} else {
// Sequential: increment index, but if it wraps to 0, skip to 1.
const nextIdx = (this.currentPresetIndex + 1) % this.PRESETS.length;
return nextIdx === 0 ? 1 : nextIdx;
}
};
// Continue cycling through presets while demo is active.
while (this.demoActive) {
this.currentPresetIndex = getNextPresetIndex(random);
const currentPreset = this.PRESETS[this.currentPresetIndex];
console.log(`Animating to preset ${this.currentPresetIndex}`);
// Animate to the current preset.
await this.animateTravelToPresetWithRandomRotation(currentPreset, 2000, 1000, 5000);
// Wait after the animation completes.
await asyncDelay(3500);
}
console.log(`Demo interrupted.`);
console.groupEnd();
}
// endregion--------------------------------------------------------------------------------------------------------
}