//#region vars
//----------------------------------------------------------------------------------------------------------------------
// WebGPU variables
/**
* WebGPU Adapter
* @type {GPUAdapter}
* */
let adapter;
/**
* WebGPU Device
* @type {GPUDevice}
* */
let device;
/**
* WebGPU Canvas Context
* @type {GPUCanvasContext}
* */
let context;
/**
* Swap chain format for WebGPU
* @type {GPUTextureFormat}
*/
let swapchain_format;
/**
* Render pass descriptor for WebGPU
* @type {GPURenderPassDescriptor}
* */
let renderpass_descriptor;
/**
* Pipeline for rendering
* @type {GPURenderPipeline}
* */
let pipeline;
/**
* Uniform buffer for camera
* @type {GPUBuffer}
* */
let uniforms_camera_buffer;
/**
* Uniform buffer for parameters
* @type {GPUBuffer}
* */
let uniforms_parameters_buffer;
/**
* Pipeline bind group
* @type {GPUBindGroup}
* */
let pipeline_bindgroup;
/**
* Uniform data for camera
* @type {Float32Array}
* */
let uniforms_camera;
/**
* Uniform data for parameters
* @type {Float32Array}
* */
let uniforms_parameters;
// UI variables
/**
* Surface for rendering
* @type {HTMLCanvasElement}
* */
let surface;
/** @type {HTMLInputElement}*/
let range_scale;
/** @type {HTMLInputElement}*/
let range_scale_text;
/** @type {HTMLInputElement}*/
let range_epsilon;
/** @type {HTMLInputElement}*/
let range_epsilon_text;
/** @type {HTMLInputElement}*/
let range_max_iterations;
/** @type {HTMLInputElement}*/
let range_max_iterations_text;
/** @type {HTMLInputElement}*/
let range_power;
/** @type {HTMLInputElement}*/
let range_power_text;
/** @type {HTMLInputElement}*/
let range_bailout;
/** @type {HTMLInputElement}*/
let range_bailout_text;
let color_near;
let color_far;
let button_reset;
// Visualization data
/** pathData is an array of sections, each section representing a continuous movement period
* Each section has the following structure:
* {
* startTime: [timestamp of the start of the section],
* endTime: [timestamp of the end of the section],
* camera: [{
* pos: [x1, y1, z1], // Position of the camera at each second
* rot: [pitch, 0, yaw],
* ...
* }],
* parameters: [
* { epsilon: value, max_iter: value, power: value, bailout: value }, // Parameters at each second
* ...
* ]
* }
*/
let pathData = [];
/**
* Flag to indicate if recording is active
* @type {boolean}
* */
let isRecording = false;
/**
* Current section being recorded
* @type {Object}
* */
let currentSection = null;
/**
* Last recorded time
* @type {number}
* */
let lastTime = 0;
/**
* Interval for saving data (in milliseconds)
* @type {number}
* */
const saveTime = 1000;
/**
* Next start time for a section
* @type {number}
* */
let nextStartTime = 0;
// Auxiliary variables
/**
* Timestamp for the current frame
* @type {number}
* */
let ts;
/**
* Delta time between frames
* @type {number}
* */
let dt;
/**
* Flag to enable camera updates
* @type {boolean}
* */
let camera_enabled = false;
/**
* State of parameters (0 - default, 1 - changed, 2 - awaiting upload)
* @type {number}
* */
let state_parameters = 1;
// Other variables
/**
* Input controls
* @type {Object}
* */
let input = {};
/**
* Camera object
* @type {Camera}
* */
let camera;
// parameters
/** @type {number}*/
let MAX_RAY_LENGTH = 10.0;
/** @type {number}*/
let MAX_SCALE = 1;
/** @type {number}*/
let MAX_ITER = 128;
/** @type {number}*/
let MAX_POWER = 16;
/** @type {number}*/
let MAX_BAILOUT = 10;
/** @type {number}*/
let MIN_EPSILON = 0.0000001;
/** @type {number}*/
let MIN_ITER = 5;
/** @type {number}*/
let MIN_POWER = 1;
/** @type {number}*/
let MIN_BAILOUT = 1.25;
/** @type {number}*/
let epsilon = 0.0026175;
/** @type {number}*/
let max_iter = 4.99;
/** @type {number}*/
let power = 8.0;
/** @type {number}*/
let bailout = 1.25;
//----------------------------------------------------------------------------------------------------------------------
//#endregion
//#region main
//----------------------------------------------------------------------------------------------------------------------
/**
* Main loop function which updates and renders each frame.
*/
function run() {
let now = performance.now();
dt = (now - ts)/1000;
ts = now;
update();
draw(device, context, pipeline);
requestAnimationFrame(run);
}
/**
* Updates the logic for each frame, including camera and input processing.
*/
function update() {
// input
if(camera_enabled) {
camera.update(dt, input);
const currentTime = performance.now();
// update path data
if(isRecording && currentSection && (currentTime - lastTime >= saveTime)) {
const cameraSettings = {
pos: Array.from(camera.position()),
rot: Array.from(camera.rotation())
};
currentSection.camera.push(cameraSettings);
const settings = {
epsilon: uniforms_parameters[0],
max_iter: uniforms_parameters[1],
power: uniforms_parameters[2],
bailout: uniforms_parameters[3]
}
currentSection.parameters.push(settings);
currentSection.endTime += 1;
lastTime = currentTime;
}
}
adapt();
reset_mouse_accumulation();
update_uniforms();
}
/**
* Handles the rendering of each frame, updating GPU resources and executing the render pass.
*/
function draw() {
// uniforms | cpu -> gpu
upload_uniforms();
// drawing
renderpass_descriptor.colorAttachments[0].view = context.getCurrentTexture().createView();
const commandEncoder = device.createCommandEncoder();
const renderpass = commandEncoder.beginRenderPass(renderpass_descriptor);
renderpass.setViewport(0, 0, surface.width, surface.height, 0, 1);
renderpass.setPipeline(pipeline);
renderpass.setBindGroup(0, pipeline_bindgroup);
renderpass.draw(3);
renderpass.end();
device.queue.submit([commandEncoder.finish()]);
}
//----------------------------------------------------------------------------------------------------------------------
//#endregion
//#region init
//----------------------------------------------------------------------------------------------------------------------
/**
* Initializes the application. Sets up UI, events, WebGPU, and D3 visualization.
*/
function init() {
// ui
init_ui();
// aux
ts = performance.now();
// other
camera = new Camera(surface.width, surface.height);
reset_mouse_accumulation();
// events
init_events();
// webgpu
reset_user();
setup_uniforms();
surface_resized();
init_webgpu().then(() => {
console.log('webgpu initialized!');
});
// d3 visualization
renderVisualization();
renderRecordingState();
}
/**
* Initializes WebGPU components including device, context, pipeline, and uniforms.
* @returns {Promise<void>} A promise that resolves when WebGPU is fully initialized.
*/
async function init_webgpu() {
adapter = await navigator.gpu.requestAdapter();
device = await adapter.requestDevice();
context = surface.getContext('webgpu');
swapchain_format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format: swapchain_format,
alphaMode: 'premultiplied',
});
// pipeline
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: "uniform",
},
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: "uniform",
},
},
],
});
const pipelineLayout = device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
});
pipeline = device.createRenderPipeline({
layout: pipelineLayout,
vertex: {
module: device.createShaderModule({
code: bulb_vert,
}),
entryPoint: 'main',
},
fragment: {
module: device.createShaderModule({
code: bulb_frag,
}),
entryPoint: 'main',
targets: [{
format: swapchain_format,
}],
},
primitive: {
topology: 'triangle-list',
},
});
// uniforms
uniforms_camera_buffer = device.createBuffer({
size: uniforms_camera.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
uniforms_parameters_buffer = device.createBuffer({
size: uniforms_parameters.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
pipeline_bindgroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniforms_camera_buffer
}
},
{
binding: 1,
resource: {
buffer: uniforms_parameters_buffer
}
},
],
});
// renderpass
renderpass_descriptor = {
colorAttachments: [{
view: undefined,
clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}],
};
requestAnimationFrame(run);
}
//----------------------------------------------------------------------------------------------------------------------
//#endregion
//#region ui & events
//----------------------------------------------------------------------------------------------------------------------
/**
* Initializes UI elements by querying and storing references to DOM elements.
*/
function init_ui() {
surface = document.getElementById('surface');
range_scale = document.getElementById('range_scale');
range_scale_text = document.getElementById('range_scale_value');
range_epsilon = document.getElementById('range_epsilon');
range_epsilon_text = document.getElementById('range_epsilon_value');
range_max_iterations = document.getElementById('range_max_iterations');
range_max_iterations_text = document.getElementById('range_max_iterations_value');
range_power = document.getElementById('range_power');
range_power_text = document.getElementById('range_power_value');
range_bailout = document.getElementById('range_bailout');
range_bailout_text = document.getElementById('range_bailout_value');
color_near = document.getElementById('color_near');
color_far = document.getElementById('color_far');
button_reset = document.getElementById('button_reset');
}
/**
* Sets up event listeners for UI interactions and other user inputs.
*/
function init_events() {
surface.addEventListener('resize', surface_resized);
surface.addEventListener('mousemove', surface_mousemove);
surface.addEventListener('mousedown', surface_mousedown);
document.addEventListener('keydown', keydown);
document.addEventListener('keyup', keyup);
range_scale.addEventListener('input', on_scale_changed);
range_epsilon.addEventListener('input', on_epsilon_changed);
range_max_iterations.addEventListener('input', on_max_iter_changed);
range_power.addEventListener('input', on_power_changed);
range_bailout.addEventListener('input', on_bailout_changed);
button_reset.addEventListener('click', reset_user);
update_parameter_tooltips();
}
/**
* Handles the resize event for the surface.
* Updates the dimensions of the rendering surface and the camera aspect ratio.
*/
function surface_resized() {
const devicePixelRatio = window.devicePixelRatio || 1;
surface.width = surface.clientWidth * devicePixelRatio;
surface.height = surface.clientHeight * devicePixelRatio;
camera.resized(surface.width, surface.height);
}
/**
* Handles mouse movement over the surface.
* @param {MouseEvent} e - The mouse event object.
*/
function surface_mousemove(e) {
input['x'] += e.movementX;
input['y'] += e.movementY;
}
/**
* Handles mouse button press on the surface.
* Toggles camera control and manages recording state.
*/
function surface_mousedown() {
camera_enabled = !camera_enabled;
if(camera_enabled) {
surface.requestPointerLock();
// start recording current section
if(isRecording) {
currentSection = {
startTime: nextStartTime,
endTime: nextStartTime,
camera: [],
parameters: []
};
}
} else {
document.exitPointerLock();
// save recorded section
if (isRecording && currentSection) {
pathData.push(currentSection);
nextStartTime = currentSection.endTime;
currentSection = null;
}
}
}
/**
* Handles changes to the scale range input.
* Updates the camera scale and UI tooltip.
*/
function on_scale_changed() {
camera.scale = parseFloat(range_scale.value);
range_scale.setAttribute('title', camera.scale);
}
function on_epsilon_changed() {
range_epsilon.setAttribute('title', epsilon);
}
function on_max_iter_changed() {
range_max_iterations.setAttribute('title', max_iter);
}
function on_power_changed() {
range_power.setAttribute('title', power);
}
function on_bailout_changed() {
range_bailout.setAttribute('title', bailout);
}
/**
* Updates the text content of UI text to reflect current parameter values.
*/
function update_parameter_tooltips() {
range_scale_text.textContent = parseFloat(camera.scale).toFixed(2);
range_epsilon_text.textContent = parseFloat(epsilon).toFixed(7);
range_max_iterations_text.textContent = parseFloat(max_iter).toFixed(1);
range_power_text.textContent = parseFloat(power).toFixed(0);
range_bailout_text.textContent = parseFloat(bailout).toFixed(2);
}
/**
* Resets the user settings to default values.
* Resets camera position, rotation, and UI elements to their default states.
*/
function reset_user() {
camera.setPosition(0, -3, 0);
camera.setRotation(-90, 180);
range_scale.value = 0.5;
range_epsilon.value = 0.1;
range_max_iterations.value = 0.5;
range_power.value = 11/14;
range_bailout.value = 0;
color_near.value = "#7f1e5d";
color_far.value = "#e5974d";
}
/**
* Handles keydown events for the document.
* Manages recording state and other key-based interactions.
* @param {KeyboardEvent} e - The keyboard event object.
*/
function keydown(e) {
input[e.code] = true;
// start recording
if(e.code === "KeyR" && !isRecording) {
isRecording = true;
pathData = [];
currentSection = {
startTime: 0,
endTime: 0,
camera: [],
parameters: []
};
renderRecordingState();
}
// stop recording
else if(e.code === "KeyR" && isRecording) {
// push last data
if(currentSection) {
pathData.push(currentSection);
currentSection = null;
}
isRecording = false;
renderVisualization();
renderRecordingState();
}
}
/**
* Handles keyup events for the document.
* Updates the input state based on the released key.
* @param {KeyboardEvent} e - The keyboard event object.
*/
function keyup(e) {
input[e.code] = false;
}
//----------------------------------------------------------------------------------------------------------------------
//#endregion
//#region aux
//----------------------------------------------------------------------------------------------------------------------
/**
* Converts a hex color string to an RGB object.
* @param {string} hex - The hex color string.
* @returns {{r: number, g: number, b: number} | null} RGB color object or null if invalid.
*/
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
/**
* Sets up uniform buffers for camera and parameters.
*/
function setup_uniforms() {
uniforms_camera = new Float32Array(16);
uniforms_parameters = new Float32Array(12);
update_uniforms();
}
/**
* Updates the uniform buffers with current camera and parameter data.
*/
function update_uniforms() {
// camera
uniforms_camera.set(camera.position(), 0);
uniforms_camera.set(vec4.mulScalar(camera.right(), surface.clientWidth/surface.clientHeight), 4);
uniforms_camera.set(camera.up(), 8);
uniforms_camera.set(camera.forward(), 12);
// parameters
if(state_parameters == 1) {
uniforms_parameters[0] = parseFloat(epsilon);
uniforms_parameters[1] = parseFloat(max_iter);
uniforms_parameters[2] = parseFloat(power);
uniforms_parameters[3] = parseFloat(bailout);
let nearColorHex = hexToRgb(color_near.value);
let farColorHex = hexToRgb(color_far.value);
let nearColor = vec3.fromValues(nearColorHex.r/255, nearColorHex.g/255, nearColorHex.b/255);
let farColor = vec3.fromValues(farColorHex.r/255, farColorHex.g/255, farColorHex.b/255);
uniforms_parameters.set(nearColor, 4);
uniforms_parameters.set(farColor, 8);
state_parameters = 2;
}
}
/**
* Uploads the updated uniforms to the GPU.
*/
function upload_uniforms() {
device.queue.writeBuffer(uniforms_camera_buffer, 0, uniforms_camera);
if(state_parameters == 2) {
device.queue.writeBuffer(uniforms_parameters_buffer, 0, uniforms_parameters);
state_parameters = 0;
}
}
/**
* Resets mouse movement accumulation.
*/
function reset_mouse_accumulation() {
input['x'] = 0;
input['y'] = 0;
}
/**
* Computes the Signed Distance Function (SDF) for the Mandelbulb fractal at a given position.
* @param {Float32Array} pos - The position to compute the SDF at.
* @returns {number} The computed distance.
*/
function mandelbulb_sdf(pos) {
var z = Float32Array.from(pos);
var dr = 1.0; // derivative
var r = 0.0;
var maxIter = parseInt(uniforms_parameters[1])
var power = uniforms_parameters[2];
var bailout = uniforms_parameters[3];
for(var i = 0; i < maxIter; i++) {
r = vec3.length(z);
if(r > bailout) {
break;
}
// to polar
var theta = Math.acos(z[2] / r);
var phi = Math.atan2(z[1], z[0]);
dr = Math.pow(r, power - 1.0) * power * dr + 1.0;
// scale and rotate
var zr = Math.pow(r, power);
theta = theta * power;
phi = phi * power;
// to cartesian
var delta = vec3.fromValues(Math.sin(theta) * Math.cos(phi), Math.sin(phi) * Math.sin(theta), Math.cos(theta));
vec3.add(vec3.mulScalar(delta, zr), pos, z);
}
return 0.5 * Math.log(r) * r / dr;
}
/**
* Performs ray marching to compute the distance from a ray origin to the Mandelbulb fractal surface.
* @param {Float32Array} ray_origin - The origin of the ray.
* @param {Float32Array} ray_dir - The direction of the ray.
* @returns {number} The distance from the ray origin to the fractal surface.
*/
function ray_marching(ray_origin, ray_dir) {
var d = mandelbulb_sdf(ray_origin);
var pos = vec3.add(ray_origin, vec3.mulScalar(ray_dir, d));
var distance = d;
var steps = 1;
var epsilon = uniforms_parameters[0];
while(steps < 10 && d > epsilon) {
d = mandelbulb_sdf(pos);
vec3.add(pos, vec3.mulScalar(ray_dir, d), pos);
distance += d;
steps++;
}
return (isNaN(distance) || distance <= 0) ? 0.0000001 : distance;
}
/**
* Adapts camera and rendering parameters based on the distance to the fractal surface.
*/
function adapt() {
// distance to fractal surface
var distance = ray_marching(Float32Array.from(camera.position()), Float32Array.from(camera.forward()));
// parameters (manually tweaked)
camera.scale = Math.max(1.0 / MAX_SCALE, Math.pow(1.0 / distance, 1.2) * (1.0 / (parseFloat(range_scale.value) + 0.0000001)));
epsilon = MIN_EPSILON + Math.max(Math.pow(distance, 0.9) * 15 * (parseFloat(1.0 - range_epsilon.value) * 0.0001), 0);
max_iter = Math.min(MAX_ITER, MIN_ITER + Math.log10(2.0 / distance) * 7 * parseFloat(range_max_iterations.value));
power = parseInt(MIN_POWER + (MAX_POWER - MIN_POWER - 1) * parseFloat(range_power.value));
bailout = MIN_BAILOUT + (MAX_BAILOUT - MIN_BAILOUT) * parseFloat(range_bailout.value);
// notify parameters changed
state_parameters = 1;
update_parameter_tooltips();
}
//----------------------------------------------------------------------------------------------------------------------
//#endregion
//#region D3 visualiztion
//----------------------------------------------------------------------------------------------------------------------
// Margins and dimensions for the D3 visualization
const margin = { top: 40, right: 200, bottom: 40, left: 60 },
width = 1100 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
/**
* Renders the visualization using D3.
*/
function renderVisualization() {
d3.select("#visualization").selectAll("*").remove();
const data = preprocessData();
const svg = d3.select("#visualization")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("background-color", "white");
// X Axis
const xScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.x))
.range([0, width]);
svg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
svg.append("text")
.attr("text-anchor", "end")
.attr("x", width)
.attr("y", height + margin.top)
.text("time (s)")
.attr("fill", "#EEEEEE");
// Y Axis
const rangeY = d3.extent(data, d=>d.y);
const yScale = d3.scaleLinear()
.domain([0, rangeY[1]])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(yScale));
svg.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("y", -margin.left + 20)
.attr("x", -margin.top + 50)
.text("Distance to Fractal")
.attr("fill", "#EEEEEE");
// Line
const colorScale = d3.scaleLinear()
.domain([2, 32])
.range(["#b7e8ff", "#1b2b33"]);
data.forEach((point, i) => {
if (i < data.length - 1) {
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveMonotoneX);
svg.append("path")
.datum([point, data[i + 1]])
.attr("fill", "none")
.attr("stroke", colorScale(point.max_iter))
.attr("stroke-width", 5)
.attr("stroke-linecap", "round")
.attr("stroke-dasharray", lineSpacing(point.epsilon))
.attr("d", line);
}
});
// seperate sections
pathData.forEach(section => {
const startX = xScale(section.startTime);
const startY = yScale(data[section.startTime].y)
svg.append("line")
.attr("x1", startX)
.attr("x2", startX)
.attr("y1", startY - 20)
.attr("y2", startY + 20)
.attr("stroke", "#ff0000") // Red vertical line for visibility
.attr("stroke-width", 1)
})
// hover text
const bisect = d3.bisector(function(d) {return d.x;}).left;
const focus = svg.append("g")
.append("circle")
.style("fill", "none")
.attr("stroke", "#E5974D")
.attr("r", 8.5)
.style("opacity", 0);
const focusText = svg.append("g")
.append("text")
.style("opacity", 0)
.attr("text-anchor", "left")
.attr("aligment-baseline", "middle")
const updateText = (d) => {
focusText.selectAll("*").remove();
const textX = xScale(d.x) - 25;
const textY = yScale(d.y) - 60;
focusText
.html("")
.attr("x", textX)
.attr("y", textY)
.style("font-size", "12px")
.attr("fill", "#EEEEEE")
.style("opacity", 1)
.append("tspan")
.text("\u03B5: " + Number(d.epsilon).toFixed(7))
.attr("x", textX)
.attr("dy", 0)
.append("tspan")
.text("max iter: " + Number(d.max_iter).toFixed(2))
.attr("x", textX)
.attr("dy", "1.2em")
.append("tspan")
.text("power: " + Number(d.power).toFixed(0))
.attr("x", textX)
.attr("dy", "1.2em");
};
// append circles for click event
svg.selectAll("circle.data-point")
.data(data)
.enter()
.append("circle")
.attr("class", "data-point")
.attr("cx", d => xScale(d.x))
.attr("cy", d => yScale(d.y))
.attr("r", 5)
.style("opacity", 0)
.on("mouseover", function(event, d) {
focus.style("opacity", 1)
focus
.attr("cx", xScale(d.x))
.attr("cy", yScale(d.y));
updateText(d);
})
.on("mouseout", function() {
focus.style("opacity", 0);
focusText.style("opacity", 0);
})
.on("click", function(event, d) {
jumpTo(d);
});
}
/**
* Renders the state of recording in the visualization.
*/
function renderRecordingState() {
const svg = d3.select("#visualization").select("svg");
svg.select("#recording-state").remove();
const state = svg.select("g").append("g")
.attr("id", "recording-state")
.attr("transform", `translate(${width + margin.right/2}, ${height + margin.bottom - 120})`);
state.append("circle")
.attr("r", 15)
.attr("stroke", "white")
.attr("fill", isRecording ? "#CD2693" : "none");
const text = state.append("text")
.attr("text-anchor", "middle")
//.attr("aligment-baseline", "middle")
.html("")
.attr("x", 0)
.attr("y", 50)
.style("font-size", "12px")
.attr("fill", "white")
.style("opacity", 1)
.append("tspan")
.text("press 'R'")
.attr("x", 0)
.attr("dy", 0)
.append("tspan")
.text(isRecording ? "to stop" : "to record")
.attr("x", 0)
.attr("dy", "1.2em")
}
/**
* Preprocesses data for D3 visualization.
* @returns {Array} The preprocessed data array.
*/
function preprocessData() {
let data = [];
let time = 0;
pathData.forEach(section => {
section.camera.forEach((setting, i) => {
data.push({
x: time + i,
y: distanceToBulb(setting.pos),
pos: setting.pos,
rot: setting.rot,
epsilon: section.parameters[i].epsilon,
max_iter: section.parameters[i].max_iter,
power: section.parameters[i].power,
bailout: section.parameters[i].bailout,
});
});
time = section.endTime;
let duration = (section.endTime - section.startTime) / 1000
time += Math.round(duration);
});
return data;
}
/**
* Adjusts camera and rendering parameters based on the selected data point.
* @param {Object} data - Data point containing the parameters to jump to.
*/
function jumpTo(data) {
camera.setPosition(data.pos[0], data.pos[1], data.pos[2]);
camera.setRotation(data.rot[0], data.rot[2]);
uniforms_parameters[0] = data.epsilon;
uniforms_parameters[1] = data.max_iter;
uniforms_parameters[2] = data.power;
uniforms_parameters[3] = data.bailout;
}
/**
* Calculates the distance to the fractal bulb.
* @param {Array} pos - Position array [x, y, z].
* @returns {number} The calculated distance.
*/
function distanceToBulb(pos) {
return Math.sqrt(pos[0] * pos[0] + pos[1] * pos[1] + pos[2] * pos[2]);
}
/**
* Determines the line spacing for the D3 visualization based on epsilon.
* @param {number} epsilon - The epsilon value used for line spacing.
* @returns {string} The dash array string for SVG path.
*/
function lineSpacing(epsilon) {
if(epsilon >= 0.001) return "1, 0"; // solid line
const minDash = 50;
const maxDash = 2;
let normEpsilon = 1 - (epsilon - 0.0000001) / (0.001 - 0.0000001);
let dash = minDash + (maxDash - minDash) * normEpsilon;
let gap = 10;
return `${dash}, ${gap}`;
}
//----------------------------------------------------------------------------------------------------------------------
//#endregion