Files
serversite/fractal.html
T
2026-06-09 09:49:35 -04:00

523 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>awesome johnruina site</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@600;800;900&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
background: #0f0f0f;
font-family: 'Inter', sans-serif;
display: flex;
align-items: center;
justify-content: center;
}
.fractal-layout {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
gap: 1.5rem;
padding: 2rem;
}
.fractal-sidebar {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 80vh;
overflow-y: auto;
flex-shrink: 0;
}
.fractal-sidebar button {
padding: 0.6rem 1.2rem;
background: transparent;
color: #666;
border: 1px solid #333;
border-radius: 6px;
font-family: 'Inter', sans-serif;
font-weight: 600;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.25s;
text-transform: uppercase;
letter-spacing: 0.5px;
text-align: left;
}
.fractal-sidebar button:hover { color: #ccc; border-color: #555; }
.fractal-sidebar button.active {
color: #fff;
border-color: #4a4a8a;
background: #1a1a2e;
box-shadow: 0 0 12px rgba(74,74,138,0.2);
}
.fractal-main {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
flex: 1;
min-width: 0;
}
.fractal-main canvas {
max-width: 100%;
max-height: 75vh;
aspect-ratio: 1;
border-radius: 12px;
box-shadow: 0 0 60px rgba(74,74,138,0.2);
image-rendering: pixelated;
}
.fractal-label {
color: #666;
font-family: 'Inter', sans-serif;
font-size: 0.7rem;
letter-spacing: 1px;
text-transform: uppercase;
}
.fractal-hint {
color: #444;
font-family: 'Inter', sans-serif;
font-size: 0.6rem;
letter-spacing: 0.5px;
text-align: center;
}
.top-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
z-index: 10;
pointer-events: none;
}
.top-bar > * { pointer-events: auto; }
.back-btn {
text-decoration: none;
color: #888;
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
transition: color 0.2s;
background: rgba(15,15,15,0.7);
padding: 0.4rem 0.8rem;
border-radius: 6px;
border: 1px solid #333;
}
.back-btn:hover { color: #fff; border-color: #555; }
.zoom-badge {
color: #555;
font-family: 'Inter', sans-serif;
font-size: 0.65rem;
font-weight: 700;
background: rgba(15,15,15,0.7);
padding: 0.4rem 0.8rem;
border-radius: 6px;
border: 1px solid #333;
}
@media (max-width: 700px) {
.fractal-layout { flex-direction: column; padding: 1rem; padding-top: 3rem; }
.fractal-sidebar { flex-direction: row; flex-wrap: wrap; justify-content: center; max-height: none; }
}
</style>
</head>
<body>
<div class="top-bar">
<a class="back-btn" href="index.html">← Back</a>
<span class="zoom-badge" id="fractalZoomBadge">1.00x</span>
</div>
<div class="fractal-layout">
<div class="fractal-sidebar" id="fractalTypes">
<button data-type="julia" class="active">Julia</button>
<button data-type="mandelbrot">Mandelbrot</button>
<button data-type="burning">Burning Ship</button>
<button data-type="tricorn">Tricorn</button>
<button data-type="newton">Newton</button>
<button data-type="celtic">Celtic</button>
<button data-type="cubic_mandel">Cubic M</button>
<button data-type="cubic_julia">Cubic J</button>
<button data-type="phoenix">Phoenix</button>
<button data-type="z4_mandel">z⁴ M</button>
</div>
<div class="fractal-main">
<canvas id="fractalCanvas"></canvas>
<div class="fractal-label" id="fractalLabel">∞ julia set</div>
<div class="fractal-hint">WASD to pan · Scroll to zoom · P to cycle palette · Esc to go back</div>
</div>
</div>
<script>
// ─── palette ───
const palettes = [
{ name: 'neon', stops: [
[0,0,0], [10,10,50], [20,40,120], [60,120,200],
[100,200,220], [200,220,80], [240,140,40], [200,40,80], [100,10,60]
]},
{ name: 'fire', stops: [
[0,0,0], [40,10,0], [100,20,0], [180,60,0],
[240,140,20], [255,220,80], [255,255,200]
]},
{ name: 'ocean', stops: [
[0,0,10], [5,20,60], [10,60,120], [40,120,180],
[80,180,200], [160,220,220], [200,240,240]
]},
{ name: 'forest', stops: [
[5,15,5], [10,40,15], [20,80,30], [60,140,50],
[120,200,80], [200,220,140], [240,240,200]
]},
{ name: 'psych', stops: [
[0,0,0], [80,0,80], [200,0,100], [255,100,0],
[255,255,0], [0,200,100], [0,100,200], [80,0,120]
]},
];
let currentPalette = 0;
function buildPalette(name) {
const p = palettes.find(x => x.name === name) || palettes[0];
const arr = [];
const segs = p.stops.length - 1;
for (let i = 0; i < 256; i++) {
const t = i / 255;
const seg = Math.min(segs - 1, Math.floor(t * segs));
const f = (t * segs) - seg;
const a = p.stops[seg];
const b = p.stops[seg + 1];
arr.push([
Math.round(a[0] + (b[0] - a[0]) * f),
Math.round(a[1] + (b[1] - a[1]) * f),
Math.round(a[2] + (b[2] - a[2]) * f),
]);
}
return arr;
}
let palette = buildPalette('neon');
function getPaletteColor(t) {
const i = Math.max(0, Math.min(255, Math.floor(t * 255)));
return palette[i];
}
function cyclePalette() {
currentPalette = (currentPalette + 1) % palettes.length;
palette = buildPalette(palettes[currentPalette].name);
}
// ─── key state ───
const keyState = {};
document.addEventListener('keydown', (e) => { keyState[e.key.toLowerCase()] = true; });
document.addEventListener('keyup', (e) => { keyState[e.key.toLowerCase()] = false; });
// ─── fractal ───
const canvas = document.getElementById('fractalCanvas');
const ctx = canvas.getContext('2d');
const label = document.getElementById('fractalLabel');
const typeBtns = document.querySelectorAll('#fractalTypes button');
const zoomBadge = document.getElementById('fractalZoomBadge');
let running = true;
let animId = null;
let startTime = 0;
let currentType = 'julia';
let fractalZoom = 1;
let fractalPanX = 0;
let fractalPanY = 0;
const renderers = {
julia(px, py, maxIter, cx, cy) {
let zx = px, zy = py, iter = 0;
while (zx * zx + zy * zy < 4 && iter < maxIter) {
const tmp = zx * zx - zy * zy + cx;
zy = 2 * zx * zy + cy;
zx = tmp;
iter++;
}
return { iter, zx, zy, maxIter };
},
mandelbrot(px, py, maxIter) {
let rz = 0, iz = 0, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const tmp = rz * rz - iz * iz + px;
iz = 2 * rz * iz + py;
rz = tmp;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
burning(px, py, maxIter) {
let rz = 0, iz = 0, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const tmp = rz * rz - iz * iz + px;
iz = Math.abs(2 * rz * iz) + py;
rz = Math.abs(tmp);
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
tricorn(px, py, maxIter) {
let rz = 0, iz = 0, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const tmp = rz * rz - iz * iz + px;
iz = -2 * rz * iz + py;
rz = tmp;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
newton(px, py, maxIter) {
let rz = px, iz = py, iter = 0;
const tol = 1e-6;
while (iter < maxIter) {
const r2 = rz * rz - iz * iz;
const i2 = 2 * rz * iz;
const r3 = r2 * rz - i2 * iz;
const i3 = r2 * iz + i2 * rz;
const denomR = 3 * r2;
const denomI = 3 * i2;
const denomMag = denomR * denomR + denomI * denomI;
if (denomMag < 1e-20) break;
const fR = r3 - 1;
const fI = i3;
const nR = rz - (fR * denomR + fI * denomI) / denomMag;
const nI = iz - (fI * denomR - fR * denomI) / denomMag;
const dr = nR - rz, di = nI - iz;
rz = nR; iz = nI; iter++;
if (dr * dr + di * di < tol) break;
}
return { iter, zx: rz, zy: iz, maxIter };
},
celtic(px, py, maxIter) {
let rz = 0, iz = 0, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const tmp = Math.abs(rz * rz - iz * iz) + px;
iz = 2 * Math.abs(rz * iz) + py;
rz = tmp;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
cubic_mandel(px, py, maxIter) {
let rz = 0, iz = 0, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const r2 = rz * rz - iz * iz;
const i2 = 2 * rz * iz;
const r3 = r2 * rz - i2 * iz;
const i3 = r2 * iz + i2 * rz;
rz = r3 + px;
iz = i3 + py;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
cubic_julia(px, py, maxIter, cx, cy) {
let rz = px, iz = py, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const r2 = rz * rz - iz * iz;
const i2 = 2 * rz * iz;
const r3 = r2 * rz - i2 * iz;
const i3 = r2 * iz + i2 * rz;
rz = r3 + cx;
iz = i3 + cy;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
phoenix(px, py, maxIter, cx, cy) {
let rz = px, iz = py, iter = 0;
let prevR = 0, prevI = 0;
const h = 0.4 + 0.2 * Math.sin(startTime * 0.05);
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const tmp = rz * rz - iz * iz + cx + h * prevR;
prevI = iz;
iz = 2 * rz * iz + cy + h * prevI;
prevR = rz;
rz = tmp;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
z4_mandel(px, py, maxIter) {
let rz = 0, iz = 0, iter = 0;
while (rz * rz + iz * iz < 4 && iter < maxIter) {
const r2 = rz * rz - iz * iz;
const i2 = 2 * rz * iz;
const r4 = r2 * r2 - i2 * i2;
const i4 = 2 * r2 * i2;
rz = r4 + px;
iz = i4 + py;
iter++;
}
return { iter, zx: rz, zy: iz, maxIter };
},
};
function renderFractal(time) {
const w = canvas.width;
const h = canvas.height;
const imageData = ctx.createImageData(w, h);
const data = imageData.data;
let cx, cy, zoom, maxIter;
if (currentType === 'julia') {
cx = -0.7269 + 0.15 * Math.sin(time * 0.08);
cy = 0.1889 + 0.15 * Math.cos(time * 0.11);
zoom = 1.5 + 0.3 * Math.sin(time * 0.04);
maxIter = 100 + Math.floor(40 * Math.sin(time * 0.06));
} else if (currentType === 'cubic_julia') {
cx = 0.4 + 0.3 * Math.sin(time * 0.07);
cy = 0.4 + 0.3 * Math.cos(time * 0.09);
zoom = 1.5 + 0.3 * Math.sin(time * 0.04);
maxIter = 80 + Math.floor(30 * Math.sin(time * 0.06));
} else if (currentType === 'phoenix') {
cx = -0.5 + 0.2 * Math.sin(time * 0.06);
cy = 0.2 + 0.2 * Math.cos(time * 0.08);
zoom = 1.5 + 0.3 * Math.sin(time * 0.04);
maxIter = 80 + Math.floor(30 * Math.sin(time * 0.06));
} else if (currentType === 'newton') {
cx = 0; cy = 0;
zoom = 2.0 + 0.4 * Math.sin(time * 0.03);
maxIter = 40 + Math.floor(20 * Math.sin(time * 0.05));
} else {
cx = -0.5 + 0.1 * Math.sin(time * 0.05);
cy = 0;
zoom = 1.8 + 0.4 * Math.sin(time * 0.03);
maxIter = 80 + Math.floor(30 * Math.sin(time * 0.07));
}
const effZoom = zoom / fractalZoom;
const aspect = w / h;
const xMin = -effZoom * aspect + fractalPanX;
const xMax = effZoom * aspect + fractalPanX;
const yMin = -effZoom + fractalPanY;
const yMax = effZoom + fractalPanY;
const renderFn = renderers[currentType];
const paletteCycle = time * 0.015;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const px = xMin + (x / w) * (xMax - xMin);
const py = yMin + (y / h) * (yMax - yMin);
const result = renderFn(px, py, maxIter, cx, cy);
const idx = (y * w + x) * 4;
if (currentType === 'newton') {
if (result.iter >= maxIter) {
data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0;
} else {
const t = ((result.iter / maxIter + paletteCycle) % 1.0);
const c = getPaletteColor(t);
data[idx] = c[0]; data[idx + 1] = c[1]; data[idx + 2] = c[2];
}
} else if (result.iter === maxIter) {
data[idx] = 0; data[idx + 1] = 0; data[idx + 2] = 0;
} else {
const smooth = result.iter + 1 - Math.log(Math.log2(Math.sqrt(result.zx * result.zx + result.zy * result.zy)));
const t = ((smooth / maxIter) + paletteCycle) % 1.0;
const c = getPaletteColor(t);
data[idx] = c[0]; data[idx + 1] = c[1]; data[idx + 2] = c[2];
}
data[idx + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
}
function resizeCanvas() {
const sidebarW = document.querySelector('.fractal-sidebar').offsetWidth;
const availW = window.innerWidth * 0.92 - sidebarW - 48;
const availH = window.innerHeight * 0.75;
let size = Math.min(availW, availH, 900);
size = Math.floor(size / 2) * 2;
canvas.width = size;
canvas.height = size;
}
function animate() {
if (!running) return;
const elapsed = (performance.now() - startTime) / 1000;
if (keyState['w']) fractalPanY -= 0.02 * (1.5 / fractalZoom);
if (keyState['s']) fractalPanY += 0.02 * (1.5 / fractalZoom);
if (keyState['a']) fractalPanX += 0.02 * (1.5 / fractalZoom);
if (keyState['d']) fractalPanX -= 0.02 * (1.5 / fractalZoom);
renderFractal(elapsed);
zoomBadge.textContent = fractalZoom.toFixed(2) + 'x';
animId = requestAnimationFrame(animate);
}
function startFractal() {
fractalZoom = 1;
fractalPanX = 0;
fractalPanY = 0;
startTime = performance.now();
resizeCanvas();
animate();
}
function setType(type) {
currentType = type;
typeBtns.forEach(b => b.classList.toggle('active', b.dataset.type === type));
const names = {
julia: 'julia set', mandelbrot: 'mandelbrot set', burning: 'burning ship',
tricorn: 'tricorn', newton: 'newton', celtic: 'celtic mandelbrot',
cubic_mandel: 'cubic mandelbrot', cubic_julia: 'cubic julia',
phoenix: 'phoenix julia', z4_mandel: 'z⁴ mandelbrot',
};
label.textContent = `${names[type] || type}`;
}
typeBtns.forEach(btn => {
btn.addEventListener('click', () => {
setType(btn.dataset.type);
fractalZoom = 1;
fractalPanX = 0;
fractalPanY = 0;
});
});
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
if (e.deltaY < 0) fractalZoom *= 1.15;
else fractalZoom /= 1.15;
fractalZoom = Math.max(0.05, Math.min(1000, fractalZoom));
}, { passive: false });
document.addEventListener('keydown', (e) => {
if (e.key === 'p' || e.key === 'P') cyclePalette();
if (e.key === 'Escape') window.location.href = 'index.html';
});
window.addEventListener('resize', resizeCanvas);
setType('julia');
startFractal();
</script>
</body>
</html>