Custom Code Experiment •
Mandelbulb Distortion
Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mandelbulb Distortion</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
background: #0a0a0f;
overflow: hidden;
font-family: system-ui, sans-serif;
}
#dropzone {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
cursor: pointer;
border: 1px dashed #333;
margin: 32px;
border-radius: 12px;
transition: border-color 0.2s, background 0.2s;
color: #555;
}
#dropzone:hover, #dropzone.drag-over {
border-color: #7f77dd;
background: rgba(127,119,221,0.05);
}
#dropzone p { font-size: 15px; }
#dropzone span { font-size: 12px; color: #3a3a3a; letter-spacing: 0.05em; }
#dropArrow { font-size: 40px; line-height: 1; color: #333; }
#fileInput { display: none; }
.pick-btn {
background: transparent;
border: 1px solid #333;
color: #888;
padding: 8px 22px;
font-size: 12px;
letter-spacing: 0.08em;
cursor: pointer;
border-radius: 4px;
transition: border-color 0.2s, color 0.2s;
}
.pick-btn:hover { border-color: #7f77dd; color: #ccc; }
#canvas-wrap {
display: none;
position: fixed;
inset: 0;
}
canvas {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
#controls {
display: none;
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
background: rgba(10,10,15,0.72);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 14px 24px;
gap: 28px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
white-space: nowrap;
}
#controls.visible { display: flex; }
.ctrl {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.ctrl label {
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #555;
}
input[type=range] {
width: 110px;
accent-color: #7f77dd;
}
.val {
font-size: 12px;
color: #888;
min-width: 30px;
text-align: center;
}
.ctrl-btn {
background: transparent;
border: 1px solid rgba(255,255,255,0.12);
color: #888;
padding: 5px 16px;
font-size: 11px;
letter-spacing: 0.08em;
cursor: pointer;
border-radius: 4px;
transition: border-color 0.2s, color 0.2s;
}
.ctrl-btn:hover { border-color: #7f77dd; color: #ccc; }
.ctrl-btn.active { border-color: #7f77dd; color: #7f77dd; }
.sep {
width: 1px;
height: 36px;
background: rgba(255,255,255,0.08);
}
</style>
</head>
<body>
<div id="dropzone" onclick="fileInput.click()">
<div id="dropArrow">⇧</div>
<p>Drop an image here, or click to choose</p>
<span>JPG · PNG · GIF · WEBP</span>
<button class="pick-btn" onclick="event.stopPropagation();fileInput.click()">Choose image file</button>
</div>
<input type="file" id="fileInput" accept="image/*">
<div id="canvas-wrap">
<canvas id="c"></canvas>
</div>
<div id="controls">
<div class="ctrl">
<label>Power (n)</label>
<input type="range" id="nSlider" min="0.5" max="8" step="0.01" value="2">
<span class="val" id="nVal">2.00</span>
</div>
<div class="ctrl">
<label>Strength</label>
<input type="range" id="strSlider" min="0" max="1" step="0.01" value="0.5">
<span class="val" id="strVal">0.50</span>
</div>
<div class="ctrl">
<label>Speed</label>
<input type="range" id="spdSlider" min="0" max="3" step="0.1" value="1">
<span class="val" id="spdVal">1.0</span>
</div>
<div class="sep"></div>
<div class="ctrl">
<label>Animate</label>
<button class="ctrl-btn active" id="playBtn">Pause</button>
</div>
<div class="ctrl">
<label>Image</label>
<button class="ctrl-btn" onclick="fileInput.click()">Change</button>
</div>
</div>
<script>
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
const dropzone = document.getElementById("dropzone");
const canvasWrap = document.getElementById("canvas-wrap");
const controls = document.getElementById("controls");
const nSlider = document.getElementById("nSlider");
const strSlider = document.getElementById("strSlider");
const spdSlider = document.getElementById("spdSlider");
const nVal = document.getElementById("nVal");
const strVal = document.getElementById("strVal");
const spdVal = document.getElementById("spdVal");
const playBtn = document.getElementById("playBtn");
const fileInput = document.getElementById("fileInput");
let srcPixels = null;
let W = 0, H = 0;
let time = 0;
let animating = true;
let lastTimestamp = null;
let rafId = null;
function loadFromFile(file) {
if (!file || !file.type.startsWith("image/")) return;
const reader = new FileReader();
reader.onload = e => {
const image = new Image();
image.onload = () => {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
const off = document.createElement("canvas");
off.width = W; off.height = H;
const offCtx = off.getContext("2d");
const imgRatio = image.naturalWidth / image.naturalHeight;
const scrRatio = W / H;
let dw, dh, dx, dy;
if (imgRatio > scrRatio) {
dh = H; dw = dh * imgRatio;
dx = (W - dw) / 2; dy = 0;
} else {
dw = W; dh = dw / imgRatio;
dx = 0; dy = (H - dh) / 2;
}
offCtx.drawImage(image, dx, dy, dw, dh);
srcPixels = offCtx.getImageData(0, 0, W, H);
dropzone.style.display = "none";
canvasWrap.style.display = "block";
controls.classList.add("visible");
if (rafId) cancelAnimationFrame(rafId);
time = 0; lastTimestamp = null;
rafId = requestAnimationFrame(loop);
};
image.src = e.target.result;
};
reader.readAsDataURL(file);
}
fileInput.addEventListener("change", () => loadFromFile(fileInput.files[0]));
dropzone.addEventListener("dragover", e => { e.preventDefault(); dropzone.classList.add("drag-over"); });
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("drag-over"));
dropzone.addEventListener("drop", e => {
e.preventDefault();
dropzone.classList.remove("drag-over");
loadFromFile(e.dataTransfer.files[0]);
});
function mandelbulbPower(x, y, z, n) {
const r = Math.sqrt(x*x + y*y + z*z);
if (r === 0) return [0, 0, 0];
const theta = Math.atan2(Math.sqrt(x*x + y*y), z);
const phi = Math.atan2(y, x);
const rn = Math.pow(r, n);
const sinTN = Math.sin(theta * n);
return [
rn * sinTN * Math.cos(phi * n),
rn * sinTN * Math.sin(phi * n),
rn * Math.cos(theta * n)
];
}
function render(t) {
if (!srcPixels) return;
const n = parseFloat(nSlider.value);
const strength = parseFloat(strSlider.value);
const dest = ctx.createImageData(W, H);
const src = srcPixels.data;
const dst = dest.data;
const cx = W * 0.5;
const cy = H * 0.5;
const maxR = Math.sqrt(cx*cx + cy*cy);
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const nx = (px - cx) / maxR;
const ny = (py - cy) / maxR;
const nz = Math.sin(t * 0.8 + nx * 1.5 + ny * 1.5) * 0.4;
const [mx, my] = mandelbulbPower(nx, ny, nz, n);
const dx = (mx - nx) * strength * maxR;
const dy = (my - ny) * strength * maxR;
const sx = Math.round(px + dx);
const sy = Math.round(py + dy);
const di = (py * W + px) * 4;
if (sx >= 0 && sx < W && sy >= 0 && sy < H) {
const si = (sy * W + sx) * 4;
dst[di] = src[si];
dst[di+1] = src[si+1];
dst[di+2] = src[si+2];
dst[di+3] = src[si+3];
} else {
dst[di+3] = 255;
}
}
}
ctx.putImageData(dest, 0, 0);
}
function loop(ts) {
if (lastTimestamp === null) lastTimestamp = ts;
const dt = (ts - lastTimestamp) / 1000;
lastTimestamp = ts;
if (animating) time += dt * parseFloat(spdSlider.value);
render(time);
rafId = requestAnimationFrame(loop);
}
nSlider.addEventListener("input", () => { nVal.textContent = parseFloat(nSlider.value).toFixed(2); });
strSlider.addEventListener("input", () => { strVal.textContent = parseFloat(strSlider.value).toFixed(2); });
spdSlider.addEventListener("input", () => { spdVal.textContent = parseFloat(spdSlider.value).toFixed(1); });
playBtn.addEventListener("click", () => {
animating = !animating;
playBtn.textContent = animating ? "Pause" : "Play";
playBtn.classList.toggle("active", animating);
if (animating) lastTimestamp = null;
});
window.addEventListener("resize", () => {
if (!srcPixels) return;
fileInput.dispatchEvent(new Event("change"));
});
</script>
</body>
</html>