Each pixel on the canvas is a starting point in the complex plane. Newton's method runs from there until it lands close to one of the three roots of z3 − 1 = 0 (the cube roots of unity). The pixel is colored red, green, or blue by whichever root it reaches — darker if it takes more iterations to get there.
<p class="hint">Each pixel is a starting point in the complex plane. Newton's method iterates until it converges to one of the three cube roots of z³ − 1 = 0. Color = which root; brightness = how fast it got there. Drag to pan, scroll to zoom.</p>
<canvas id="cv" width="380" height="300"></canvas>
<div class="controls">
<button id="btnReset" type="button">Reset view</button>
<span class="info" id="info">z³ − 1 = 0 | 3 roots</span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; cursor: crosshair; border-radius: 8px;
border: 1px solid #cdd9e3; max-width: 100%; touch-action: none; }
.controls { display: flex; align-items: center; gap: .6rem; margin-top: .55rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
.info { font-size: .82rem; color: #555; }
// Newton's method on f(z) = z^3 - 1.
// The three roots are 1, ω = e^(2πi/3), ω² = e^(4πi/3).
// For each pixel (starting complex number z0) we iterate:
// z_{n+1} = z_n - f(z_n)/f'(z_n) = z_n - (z_n^3 - 1)/(3*z_n^2)
// = (2*z_n^3 + 1) / (3*z_n^2) [simplified]
// until |z - root_k| < ε or max iterations reached.
// Color by root index; darken by iteration count.
const CV = document.getElementById('cv');
const CTX = CV.getContext('2d');
const W = CV.width, H = CV.height;
const MAX_ITER = 60;
const TOL = 1e-6;
// The three cube roots of unity in the complex plane.
const ROOTS = [
{ re: 1, im: 0 },
{ re: -0.5, im: Math.sqrt(3)/2 },
{ re: -0.5, im: -Math.sqrt(3)/2 }
];
// Base colors for each root (RGB)
const COLORS = [
[220, 60, 60], // red
[60, 180, 80], // green
[60, 100, 220] // blue
];
// View state: centre (cx,cy) in complex plane, scale (units per pixel)
let cx = 0, cy = 0, scale = 2.4 / Math.min(W, H);
function cmul(a, b) { return { re: a.re*b.re - a.im*b.im, im: a.re*b.im + a.im*b.re }; }
function cadd(a, b) { return { re: a.re+b.re, im: a.im+b.im }; }
function csub(a, b) { return { re: a.re-b.re, im: a.im-b.im }; }
function cabs2(z) { return z.re*z.re + z.im*z.im; }
function cdivReal(z, r) { return { re: z.re/r, im: z.im/r }; }
// Newton step for f(z) = z^3 - 1: z_new = (2*z^3 + 1) / (3*z^2)
function newtonStep(z) {
const z2 = cmul(z, z);
const z3 = cmul(z2, z);
const num = cadd({ re: 2*z3.re+1, im: 2*z3.im }, { re: 0, im: 0 });
// num = 2z³ + 1
const numC = { re: 2*z3.re + 1, im: 2*z3.im };
const den3 = { re: 3*z2.re, im: 3*z2.im };
// divide: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
const d2 = cabs2(den3);
if (d2 < 1e-30) return z; // avoid division by zero near origin
return {
re: (numC.re*den3.re + numC.im*den3.im) / d2,
im: (numC.im*den3.re - numC.re*den3.im) / d2
};
}
function iterate(re0, im0) {
let z = { re: re0, im: im0 };
for (let i = 0; i < MAX_ITER; i++) {
// Check convergence to each root
for (let k = 0; k < 3; k++) {
const d = csub(z, ROOTS[k]);
if (cabs2(d) < TOL*TOL) return { root: k, iter: i };
}
z = newtonStep(z);
}
// Didn't converge — assign to nearest root
let best = 0, bestD = Infinity;
for (let k = 0; k < 3; k++) {
const d = cabs2(csub(z, ROOTS[k]));
if (d < bestD) { bestD = d; best = k; }
}
return { root: best, iter: MAX_ITER };
}
function draw() {
const img = CTX.createImageData(W, H);
const data = img.data;
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const re = cx + (px - W/2) * scale;
const im = cy - (py - H/2) * scale; // y flipped
const { root, iter } = iterate(re, im);
const bright = 1 - 0.7 * (iter / MAX_ITER);
const [r, g, b] = COLORS[root];
const idx = (py*W + px) * 4;
data[idx] = Math.round(r * bright);
data[idx+1] = Math.round(g * bright);
data[idx+2] = Math.round(b * bright);
data[idx+3] = 255;
}
}
CTX.putImageData(img, 0, 0);
}
// Pan via drag
let dragging = false, dragX = 0, dragY = 0;
CV.addEventListener('mousedown', e => { dragging=true; dragX=e.offsetX; dragY=e.offsetY; });
window.addEventListener('mouseup', () => dragging = false);
window.addEventListener('mousemove', e => {
if (!dragging) return;
cx -= (e.offsetX - dragX) * scale;
cy += (e.offsetY - dragY) * scale;
dragX = e.offsetX; dragY = e.offsetY;
draw();
});
// Zoom via scroll
CV.addEventListener('wheel', e => {
e.preventDefault();
const factor = e.deltaY > 0 ? 1.25 : 0.8;
// zoom toward mouse position
const mx = cx + (e.offsetX - W/2) * scale;
const my = cy - (e.offsetY - H/2) * scale;
scale *= factor;
cx = mx - (e.offsetX - W/2) * scale;
cy = my + (e.offsetY - H/2) * scale;
draw();
}, { passive: false });
// Touch pan
let lastTouch = null;
CV.addEventListener('touchstart', e => { e.preventDefault(); lastTouch = e.touches[0]; }, { passive: false });
CV.addEventListener('touchmove', e => {
e.preventDefault();
const t = e.touches[0];
cx -= (t.clientX - lastTouch.clientX) * scale;
cy += (t.clientY - lastTouch.clientY) * scale;
lastTouch = t;
draw();
}, { passive: false });
document.getElementById('btnReset').addEventListener('click', () => {
cx = 0; cy = 0; scale = 2.4 / Math.min(W, H);
draw();
});
draw();
<p class="hint">Each pixel is a starting point in the complex plane. Newton's method iterates until it converges to one of the three cube roots of z³ − 1 = 0. Color = which root; brightness = how fast it got there. Drag to pan, scroll to zoom.</p>
<canvas id="cv" width="380" height="300"></canvas>
<div class="controls">
<button id="btnReset" type="button">Reset view</button>
<span class="info" id="info">z³ − 1 = 0 | 3 roots</span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; cursor: crosshair; border-radius: 8px;
border: 1px solid #cdd9e3; max-width: 100%; touch-action: none; }
.controls { display: flex; align-items: center; gap: .6rem; margin-top: .55rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
.info { font-size: .82rem; color: #555; }
// Newton's method on f(z) = z^3 - 1.
// The three roots are 1, ω = e^(2πi/3), ω² = e^(4πi/3).
// For each pixel (starting complex number z0) we iterate:
// z_{n+1} = z_n - f(z_n)/f'(z_n) = z_n - (z_n^3 - 1)/(3*z_n^2)
// = (2*z_n^3 + 1) / (3*z_n^2) [simplified]
// until |z - root_k| < ε or max iterations reached.
// Color by root index; darken by iteration count.
const CV = document.getElementById('cv');
const CTX = CV.getContext('2d');
const W = CV.width, H = CV.height;
const MAX_ITER = 60;
const TOL = 1e-6;
// The three cube roots of unity in the complex plane.
const ROOTS = [
{ re: 1, im: 0 },
{ re: -0.5, im: Math.sqrt(3)/2 },
{ re: -0.5, im: -Math.sqrt(3)/2 }
];
// Base colors for each root (RGB)
const COLORS = [
[220, 60, 60], // red
[60, 180, 80], // green
[60, 100, 220] // blue
];
// View state: centre (cx,cy) in complex plane, scale (units per pixel)
let cx = 0, cy = 0, scale = 2.4 / Math.min(W, H);
function cmul(a, b) { return { re: a.re*b.re - a.im*b.im, im: a.re*b.im + a.im*b.re }; }
function cadd(a, b) { return { re: a.re+b.re, im: a.im+b.im }; }
function csub(a, b) { return { re: a.re-b.re, im: a.im-b.im }; }
function cabs2(z) { return z.re*z.re + z.im*z.im; }
function cdivReal(z, r) { return { re: z.re/r, im: z.im/r }; }
// Newton step for f(z) = z^3 - 1: z_new = (2*z^3 + 1) / (3*z^2)
function newtonStep(z) {
const z2 = cmul(z, z);
const z3 = cmul(z2, z);
const num = cadd({ re: 2*z3.re+1, im: 2*z3.im }, { re: 0, im: 0 });
// num = 2z³ + 1
const numC = { re: 2*z3.re + 1, im: 2*z3.im };
const den3 = { re: 3*z2.re, im: 3*z2.im };
// divide: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
const d2 = cabs2(den3);
if (d2 < 1e-30) return z; // avoid division by zero near origin
return {
re: (numC.re*den3.re + numC.im*den3.im) / d2,
im: (numC.im*den3.re - numC.re*den3.im) / d2
};
}
function iterate(re0, im0) {
let z = { re: re0, im: im0 };
for (let i = 0; i < MAX_ITER; i++) {
// Check convergence to each root
for (let k = 0; k < 3; k++) {
const d = csub(z, ROOTS[k]);
if (cabs2(d) < TOL*TOL) return { root: k, iter: i };
}
z = newtonStep(z);
}
// Didn't converge — assign to nearest root
let best = 0, bestD = Infinity;
for (let k = 0; k < 3; k++) {
const d = cabs2(csub(z, ROOTS[k]));
if (d < bestD) { bestD = d; best = k; }
}
return { root: best, iter: MAX_ITER };
}
function draw() {
const img = CTX.createImageData(W, H);
const data = img.data;
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const re = cx + (px - W/2) * scale;
const im = cy - (py - H/2) * scale; // y flipped
const { root, iter } = iterate(re, im);
const bright = 1 - 0.7 * (iter / MAX_ITER);
const [r, g, b] = COLORS[root];
const idx = (py*W + px) * 4;
data[idx] = Math.round(r * bright);
data[idx+1] = Math.round(g * bright);
data[idx+2] = Math.round(b * bright);
data[idx+3] = 255;
}
}
CTX.putImageData(img, 0, 0);
}
// Pan via drag
let dragging = false, dragX = 0, dragY = 0;
CV.addEventListener('mousedown', e => { dragging=true; dragX=e.offsetX; dragY=e.offsetY; });
window.addEventListener('mouseup', () => dragging = false);
window.addEventListener('mousemove', e => {
if (!dragging) return;
cx -= (e.offsetX - dragX) * scale;
cy += (e.offsetY - dragY) * scale;
dragX = e.offsetX; dragY = e.offsetY;
draw();
});
// Zoom via scroll
CV.addEventListener('wheel', e => {
e.preventDefault();
const factor = e.deltaY > 0 ? 1.25 : 0.8;
// zoom toward mouse position
const mx = cx + (e.offsetX - W/2) * scale;
const my = cy - (e.offsetY - H/2) * scale;
scale *= factor;
cx = mx - (e.offsetX - W/2) * scale;
cy = my + (e.offsetY - H/2) * scale;
draw();
}, { passive: false });
// Touch pan
let lastTouch = null;
CV.addEventListener('touchstart', e => { e.preventDefault(); lastTouch = e.touches[0]; }, { passive: false });
CV.addEventListener('touchmove', e => {
e.preventDefault();
const t = e.touches[0];
cx -= (t.clientX - lastTouch.clientX) * scale;
cy += (t.clientY - lastTouch.clientY) * scale;
lastTouch = t;
draw();
}, { passive: false });
document.getElementById('btnReset').addEventListener('click', () => {
cx = 0; cy = 0; scale = 2.4 / Math.min(W, H);
draw();
});
draw();
Notice how, far from the origin, each region is a clean wedge of color. But zoom toward any boundary and the wedges break into infinite tendrils. No matter how close two starting points look, if they sit on opposite sides of the fractal boundary they can land on entirely different roots. That is sensitive dependence on initial conditions — the hallmark of chaos.
Comments
Loading comments...