Physics Lab
Fractals & Chaos
Explore self-similar patterns emerging from chaos. Visualize Mandelbrot, Julia, Newton fractals and the legendary Chaos Game.
Iterated Function System (IFS)
Start at a random point and repeatedly apply random transformations. A fractal emerges from the chaos!
2.0M
Points Plotted
0
Generations
0
Time (ms)
0
Mandelbrot Set
The most famous fractal: z = z² + c. Click+drag to zoom, right-click to zoom out. Double-click to zoom in on a point.
128
Center (Re)
-0.5
Center (Im)
0
Zoom
1x
Julia Sets
Same formula as Mandelbrot, but fix c and vary z. Drag the c point on the mini Mandelbrot map to explore different Julia sets.
128
c = -0.7+0.27i
c (Real)
-0.7
c (Imag)
0.27
Newton Fractal
Newton's method for z³ - 1 = 0. Colors show which root each point converges to. Stunning spiraling boundaries!
64
Polynomial
z³ - 1
Roots
3
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.dataset.tab;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tabName).classList.add('active');
});
});
// ============ CHAOS GAME ============
const chaosCanvas = document.getElementById('chaosCanvas');
const chaosCtx = chaosCanvas.getContext('2d', { willReadFrequently: true });
const chaosStatus = document.getElementById('chaosStatus');
const chaosPresets = {
sierpinski: [
{ a: 0.5, b: 0, c: 0, d: 0.5, e: 0, f: 0 },
{ a: 0.5, b: 0, c: 0, d: 0.5, e: 0.5, f: 0 },
{ a: 0.5, b: 0, c: 0, d: 0.5, e: 0.25, f: 0.433 }
],
fern: [
{ a: 0, b: 0, c: 0, d: 0.16, e: 0, f: 0, prob: 0.01 },
{ a: 0.85, b: 0.02, c: -0.02, d: 0.83, e: 0, f: 1.6, prob: 0.85 },
{ a: 0.2, b: -0.26, c: 0.23, d: 0.22, e: 0, f: 1.6, prob: 0.07 },
{ a: -0.15, b: 0.28, c: 0.26, d: 0.24, e: 0, f: 0.44, prob: 0.07 }
],
dragon: [
{ a: -0.5, b: -0.5, c: 0.5, d: -0.5, e: 0.5, f: 0.5 },
{ a: 0.5, b: 0.5, c: -0.5, d: 0.5, e: 0.5, f: 0.5 }
],
carpet: [
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 0, f: 0 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 1/3, f: 0 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 2/3, f: 0 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 0, f: 1/3 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 2/3, f: 1/3 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 0, f: 2/3 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 1/3, f: 2/3 },
{ a: 1/3, b: 0, c: 0, d: 1/3, e: 2/3, f: 2/3 }
]
};
const chaosColorPalettes = {
'transform': ['#ff2200', '#00c896', '#f5c518', '#1e90ff', '#ff69b4', '#00d9ff'],
'frequency': null, // density based
'rainbow': null,
'fire': null
};
let chaosCurrentPreset = 'sierpinski';
document.querySelectorAll('[data-preset]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-preset]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
chaosCurrentPreset = btn.dataset.preset;
});
});
document.getElementById('chaosSamples').addEventListener('input', (e) => {
document.getElementById('chaosSamplesValue').textContent = e.target.value + 'M';
});
function applyTransform(x, y, transform) {
return {
x: transform.a * x + transform.b * y + transform.e,
y: transform.c * x + transform.d * y + transform.f
};
}
function generateChaosGame() {
const samples = parseFloat(document.getElementById('chaosSamples').value) * 1_000_000;
const preset = chaosPresets[chaosCurrentPreset];
const colorScheme = document.getElementById('chaosColorScheme').value;
const speed = document.getElementById('chaosSpeed').value;
const skipRate = speed === 'fast' ? 10 : speed === 'slow' ? 0.1 : 1;
const actualSamples = Math.floor(samples * skipRate);
chaosStatus.style.display = 'block';
chaosStatus.textContent = 'Generating...';
const start = performance.now();
chaosCtx.fillStyle = '#000';
chaosCtx.fillRect(0, 0, chaosCanvas.width, chaosCanvas.height);
const imgData = chaosCtx.getImageData(0, 0, chaosCanvas.width, chaosCanvas.height);
const data = imgData.data;
let x = Math.random(), y = Math.random();
const width = chaosCanvas.width;
const height = chaosCanvas.height;
for (let i = 0; i < actualSamples; i++) {
const transform = preset[Math.floor(Math.random() * preset.length)];
const p = applyTransform(x, y, transform);
x = p.x;
y = p.y;
if (x >= 0 && x <= 1 && y >= 0 && y <= 1) {
const px = Math.floor(x * width);
const py = Math.floor((1 - y) * height);
const idx = (py * width + px) * 4;
if (colorScheme === 'transform') {
const transIdx = preset.indexOf(transform);
const colorPalette = chaosColorPalettes['transform'];
const hexColor = colorPalette[transIdx % colorPalette.length];
const rgb = parseInt(hexColor.slice(1), 16);
data[idx] = (rgb >> 16) & 255;
data[idx + 1] = (rgb >> 8) & 255;
data[idx + 2] = rgb & 255;
data[idx + 3] = 255;
} else {
data[idx] += 1;
data[idx + 1] += 1;
data[idx + 2] += 1;
data[idx + 3] = 255;
}
}
}
chaosCtx.putImageData(imgData, 0, 0);
const end = performance.now();
document.getElementById('chaosPointsValue').textContent = actualSamples.toLocaleString();
document.getElementById('chaosGenValue').textContent = actualSamples.toLocaleString();
document.getElementById('chaosTimeValue').textContent = Math.round(end - start);
chaosStatus.classList.add('success');
chaosStatus.textContent = `✓ Rendered ${actualSamples.toLocaleString()} points in ${Math.round(end - start)}ms`;
}
document.getElementById('chaosGenerate').addEventListener('click', generateChaosGame);
document.getElementById('chaosReset').addEventListener('click', () => {
chaosCtx.fillStyle = '#000';
chaosCtx.fillRect(0, 0, chaosCanvas.width, chaosCanvas.height);
});
document.getElementById('chaosSave').addEventListener('click', () => {
const link = document.createElement('a');
link.href = chaosCanvas.toDataURL();
link.download = `chaos-${chaosCurrentPreset}-${Date.now()}.png`;
link.click();
});
// ============ MANDELBROT ============
const mandelbrotCanvas = document.getElementById('mandelbrotCanvas');
const mandelbrotCtx = mandelbrotCanvas.getContext('2d', { willReadFrequently: true });
const mandelbrotStatus = document.getElementById('mandelbrotStatus');
let mandelbrotState = {
centerRe: -0.5,
centerIm: 0,
zoom: 1
};
const mandelbrotLocations = {
'full': { centerRe: -0.5, centerIm: 0, zoom: 1 },
'seahorse': { centerRe: -0.7489, centerIm: 0.1008, zoom: 50 },
'elephant': { centerRe: -0.7533, centerIm: 0.1138, zoom: 50 },
'spiral': { centerRe: -0.747, centerIm: 0.1, zoom: 100 }
};
document.querySelectorAll('[data-location]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-location]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const loc = mandelbrotLocations[btn.dataset.location];
mandelbrotState = { ...loc };
renderMandelbrot();
});
});
document.getElementById('mandelbrotIterations').addEventListener('input', (e) => {
document.getElementById('mandelbrotIterValue').textContent = e.target.value;
});
function mandelbrotColor(iterations, maxIter) {
const t = iterations / maxIter;
const hue = (t * 360) % 360;
return `hsl(${hue}, 100%, 50%)`;
}
function renderMandelbrot() {
const maxIter = parseInt(document.getElementById('mandelbrotIterations').value);
const colorScheme = document.getElementById('mandelbrotColorScheme').value;
const zoomLevel = parseFloat(document.getElementById('mandelbrotZoom').value);
mandelbrotState.zoom = zoomLevel;
mandelbrotStatus.style.display = 'block';
mandelbrotStatus.textContent = 'Rendering...';
const start = performance.now();
const width = mandelbrotCanvas.width;
const height = mandelbrotCanvas.height;
const imgData = mandelbrotCtx.createImageData(width, height);
const data = imgData.data;
const pixelWidth = 4 / (mandelbrotState.zoom * Math.min(width, height));
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const re = mandelbrotState.centerRe + (px - width / 2) * pixelWidth;
const im = mandelbrotState.centerIm + (py - height / 2) * pixelWidth;
let zre = 0, zim = 0, iter = 0;
let zre2 = 0, zim2 = 0;
while (iter < maxIter && zre2 + zim2 < 4) {
const temp = zre * zre - zim * zim + re;
zim = 2 * zre * zim + im;
zre = temp;
zre2 = zre * zre;
zim2 = zim * zim;
iter++;
}
const idx = (py * width + px) * 4;
if (iter === maxIter) {
data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255;
} else {
const hue = (iter / maxIter * 360) % 360;
const rgb = hslToRgb(hue, 100, 50);
data[idx] = rgb.r; data[idx + 1] = rgb.g; data[idx + 2] = rgb.b; data[idx + 3] = 255;
}
}
}
mandelbrotCtx.putImageData(imgData, 0, 0);
const end = performance.now();
document.getElementById('mandelbrotCenterRe').textContent = mandelbrotState.centerRe.toFixed(4);
document.getElementById('mandelbrotCenterIm').textContent = mandelbrotState.centerIm.toFixed(4);
document.getElementById('mandelbrotZoomValue').textContent = (mandelbrotState.zoom * 100).toFixed(0) + '%';
mandelbrotStatus.classList.add('success');
mandelbrotStatus.textContent = `✓ Rendered in ${Math.round(end - start)}ms`;
}
function hslToRgb(h, s, l) {
const c = (100 - Math.abs(2 * l - 100)) * s / 100;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l / 100 - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; }
else if (h < 120) { r = x; g = c; }
else if (h < 180) { g = c; b = x; }
else if (h < 240) { g = x; b = c; }
else if (h < 300) { r = x; b = c; }
else { r = c; b = x; }
return {
r: Math.round((r + m) * 255),
g: Math.round((g + m) * 255),
b: Math.round((b + m) * 255)
};
}
document.getElementById('mandelbrotRender').addEventListener('click', renderMandelbrot);
document.getElementById('mandelbrotReset').addEventListener('click', () => {
mandelbrotState = { centerRe: -0.5, centerIm: 0, zoom: 1 };
document.getElementById('mandelbrotZoom').value = 1;
renderMandelbrot();
});
document.getElementById('mandelbrotSave').addEventListener('click', () => {
const link = document.createElement('a');
link.href = mandelbrotCanvas.toDataURL();
link.download = `mandelbrot-${Date.now()}.png`;
link.click();
});
// ============ JULIA ============
const juliaCanvas = document.getElementById('juliaCanvas');
const juliaCtx = juliaCanvas.getContext('2d', { willReadFrequently: true });
const juliaMandelbrotMap = document.getElementById('juliaMandelbrotMap');
const juliaMandelbrotCtx = juliaMandelbrotMap.getContext('2d', { willReadFrequently: true });
const juliaStatus = document.getElementById('juliaStatus');
let juliaState = {
cRe: -0.7,
cIm: 0.27
};
const juliaPresets = {
'classic': { cRe: -0.7, cIm: 0.27 },
'spiral': { cRe: -0.162, cIm: 1.04 },
'dendrite': { cRe: -0.8, cIm: 0.156 },
'seahorse': { cRe: -0.162, cIm: 1.04 }
};
document.querySelectorAll('[data-juliapreset]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-juliapreset]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const preset = juliaPresets[btn.dataset.juliapreset];
juliaState = { ...preset };
renderJulia();
renderJuliaMandelbrotMap();
});
});
document.getElementById('juliaIterations').addEventListener('input', (e) => {
document.getElementById('juliaIterValue').textContent = e.target.value;
});
function renderJulia() {
const maxIter = parseInt(document.getElementById('juliaIterations').value);
juliaStatus.style.display = 'block';
juliaStatus.textContent = 'Rendering...';
const start = performance.now();
const width = juliaCanvas.width;
const height = juliaCanvas.height;
const imgData = juliaCtx.createImageData(width, height);
const data = imgData.data;
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const zRe = (px / width) * 4 - 2;
const zIm = (py / height) * 4 - 2;
let re = zRe, im = zIm, iter = 0;
while (iter < maxIter && re * re + im * im < 4) {
const temp = re * re - im * im + juliaState.cRe;
im = 2 * re * im + juliaState.cIm;
re = temp;
iter++;
}
const idx = (py * width + px) * 4;
if (iter === maxIter) {
data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255;
} else {
const rgb = hslToRgb((iter / maxIter * 360) % 360, 100, 50);
data[idx] = rgb.r; data[idx + 1] = rgb.g; data[idx + 2] = rgb.b; data[idx + 3] = 255;
}
}
}
juliaCtx.putImageData(imgData, 0, 0);
const end = performance.now();
document.getElementById('juliaCReal').textContent = juliaState.cRe.toFixed(3);
document.getElementById('juliaCImag').textContent = juliaState.cIm.toFixed(3);
document.getElementById('juliaC').textContent = `${juliaState.cRe.toFixed(2)}${juliaState.cIm >= 0 ? '+' : ''}${juliaState.cIm.toFixed(2)}i`;
juliaStatus.classList.add('success');
juliaStatus.textContent = `✓ Rendered in ${Math.round(end - start)}ms`;
}
function renderJuliaMandelbrotMap() {
const width = juliaMandelbrotMap.width;
const height = juliaMandelbrotMap.height;
const imgData = juliaMandelbrotCtx.createImageData(width, height);
const data = imgData.data;
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
const cRe = (px / width) * 4 - 2.5;
const cIm = (py / height) * 4 - 2;
let re = 0, im = 0, iter = 0;
while (iter < 64 && re * re + im * im < 4) {
const temp = re * re - im * im + cRe;
im = 2 * re * im + cIm;
re = temp;
iter++;
}
const idx = (py * width + px) * 4;
if (iter === 64) {
data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0; data[idx + 3] = 255;
} else {
const rgb = hslToRgb((iter / 64 * 360) % 360, 100, 50);
data[idx] = rgb.r; data[idx + 1] = rgb.g; data[idx + 2] = rgb.b; data[idx + 3] = 255;
}
}
}
juliaMandelbrotCtx.putImageData(imgData, 0, 0);
// Draw marker at current c
const cReMapped = ((juliaState.cRe + 2.5) / 4) * width;
const cImMapped = ((juliaState.cIm + 2) / 4) * height;
juliaMandelbrotCtx.strokeStyle = '#ff2200';
juliaMandelbrotCtx.lineWidth = 2;
juliaMandelbrotCtx.beginPath();
juliaMandelbrotCtx.arc(cReMapped, cImMapped, 5, 0, Math.PI * 2);
juliaMandelbrotCtx.stroke();
}
juliaMandelbrotMap.addEventListener('click', (e) => {
const rect = juliaMandelbrotMap.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width;
const y = (e.clientY - rect.top) / rect.height;
juliaState.cRe = x * 4 - 2.5;
juliaState.cIm = y * 4 - 2;
renderJulia();
renderJuliaMandelbrotMap();
});
document.getElementById('juliaRender').addEventListener('click', () => {
renderJulia();
renderJuliaMandelbrotMap();
});
document.getElementById('juliaReset').addEventListener('click', () => {
juliaState = { cRe: -0.7, cIm: 0.27 };
renderJulia();
renderJuliaMandelbrotMap();
});
document.getElementById('juliaSave').addEventListener('click', () => {
const link = document.createElement('a');
link.href = juliaCanvas.toDataURL();
link.download = `julia-${Date.now()}.png`;
link.click();
});
// ============ NEWTON ============
const newtonCanvas = document.getElementById('newtonCanvas');
const newtonCtx = newtonCanvas.getContext('2d', { willReadFrequently: true });
const newtonStatus = document.getElementById('newtonStatus');
let newtonPower = 3;
let newtonZoom = 1;
document.getElementById('newtonPowerInput').addEventListener('change', (e) => {
newtonPower = parseInt(e.target.value);
document.getElementById('newtonPolyDisplay').textContent = `z^${newtonPower} - 1`;
document.getElementById('newtonRootsCount').textContent = newtonPower;
});
document.getElementById('newtonIterations').addEventListener('input', (e) => {
document.getElementById('newtonIterValue').textContent = e.target.value;
});
document.getElementById('newtonZoom').addEventListener('input', (e) => {
newtonZoom = parseFloat(e.target.value);
});
function complexMult(a, b) {
return { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re };
}
function complexDiv(a, b) {
const denom = b.re * b.re + b.im * b.im;
return { re: (a.re * b.re + a.im * b.im) / denom, im: (a.im * b.re - a.re * b.im) / denom };
}
function polyValue(z, n) {
let result = { re: 1, im: 0 };
for (let i = 1; i < n; i++) {
result = complexMult(result, z);
}
result.re -= 1;
return result;
}
function polyDerivative(z, n) {
let result = { re: 0, im: 0 };
for (let i = 1; i < n; i++) {
let term = { re: 1, im: 0 };
for (let j = 1; j < i; j++) {
term = complexMult(term, z);
}
result.re += i * term.re;
result.im += i * term.im;
}
return result;
}
function renderNewton() {
const maxIter = parseInt(document.getElementById('newtonIterations').value);
newtonStatus.style.display = 'block';
newtonStatus.textContent = 'Rendering...';
const start = performance.now();
const width = newtonCanvas.width;
const height = newtonCanvas.height;
const imgData = newtonCtx.createImageData(width, height);
const data = imgData.data;
const rootColors = ['#ff2200', '#00c896', '#1e90ff', '#f5c518', '#ff69b4', '#00d9ff', '#90ee90', '#dda0dd'];
for (let py = 0; py < height; py++) {
for (let px = 0; px < width; px++) {
let z = {
re: (px - width / 2) / (width / 4) / newtonZoom,
im: (py - height / 2) / (height / 4) / newtonZoom
};
let root = 0;
for (let iter = 0; iter < maxIter; iter++) {
const f = polyValue(z, newtonPower);
const fp = polyDerivative(z, newtonPower);
const delta = complexDiv(f, fp);
z.re -= delta.re;
z.im -= delta.im;
if (Math.sqrt(delta.re * delta.re + delta.im * delta.im) < 1e-6) {
const angle = Math.atan2(z.im, z.re);
root = Math.round((angle / (2 * Math.PI) + 0.5) * newtonPower) % newtonPower;
break;
}
}
const idx = (py * width + px) * 4;
const colorHex = rootColors[root % rootColors.length];
const rgb = parseInt(colorHex.slice(1), 16);
data[idx] = (rgb >> 16) & 255;
data[idx + 1] = (rgb >> 8) & 255;
data[idx + 2] = rgb & 255;
data[idx + 3] = 255;
}
}
newtonCtx.putImageData(imgData, 0, 0);
const end = performance.now();
newtonStatus.classList.add('success');
newtonStatus.textContent = `✓ Rendered in ${Math.round(end - start)}ms`;
}
document.getElementById('newtonRender').addEventListener('click', renderNewton);
document.getElementById('newtonReset').addEventListener('click', () => {
newtonZoom = 1;
document.getElementById('newtonZoom').value = 1;
renderNewton();
});
document.getElementById('newtonSave').addEventListener('click', () => {
const link = document.createElement('a');
link.href = newtonCanvas.toDataURL();
link.download = `newton-z${newtonPower}-${Date.now()}.png`;
link.click();
});
// Initial renders
window.addEventListener('load', () => {
generateChaosGame();
renderMandelbrot();
renderJulia();
renderJuliaMandelbrotMap();
renderNewton();
});