Fractal Explorer
lfm2 24b a2b · basic

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 15,447 bytes · SHA-256 4e5f2ac174ae
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Interactive Fractal Explorer</title>
<style>
:root {--bg:#0f0f12;--fwd:#e0e0ff;--accent:#ff6b6b;--accent2:#ffd93d;--accent3:#ffc107;--text:#e0e0ff;--accent4:#ff8c00;}
* {box-sizing:border-box;}
body,html {margin:0;padding:0;height:100%;width:100%;font-family:sans-serif;overflow:hidden;}
#canvas {display:block;width:100vw;height:100vh;image-rendering:pixelated;}
#hud {position:fixed;bottom:0;left:0;right:0;background:rgba(0,0,0,0.7);color:#fff;padding:8px 12px;font-size:12px;z-index:10;display:flex;align-items:center;gap:8px;}
#hud span {margin-left:8px;}
.palette-swatches {display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;}
.palette-swatch {width:16px;height:16px;border-radius:2px;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.2);}
#controls {position:fixed;top:12px;left:12px;background:#111;padding:10px 16px;border-radius:8px;display:flex;flex-direction:column;gap:6px;}
.control-group {display:flex;align-items:center;flex-wrap:wrap;gap:6px;min-width:180px;}
label {font-size:11px;color:#e0e0ff;}
input[type=range] {width:120px;}
#zoom-btn,#reset-btn,#j-btn {background:#222;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-weight:bold;}
#zoom-btn:hover,#reset-btn:hover,#j-btn:hover {background:#444;}
#zoom-slider {width:120px;margin-left:6px;}
#back-btn {background:#222;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;}
#palette-select {font-size:13px;color:#fff;background:#111;padding:2px 6px;border-radius:4px;border:none;cursor:pointer;}
.active {background:#222;color:#fff;}
#view-info {position:absolute;bottom:12px;left:12px;font-size:11px;color:#bbb;z-index:10;}
#debug-panel {position:fixed;top:0;right:0;padding:8px 12px;background:rgba(0,0,0,.7);color:#fff;font-family:monospace;font-size:11px;display:none;z-index:20;}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="hud">
<span>Zoom: ×<span id="zoom">1e0</span></span>
<span>Center: (x,y)</span>
<span>Iterations: <span id="iter">0</span></span>
<span>FPS: <span id="fps">0</span></span>
</div>
<div id="controls">
<div class="control-group">
<label>Palette</label>
<div class="palette-swatches" id="palette-swatches"></div>
</div>
<div class="control-group">
<label>Julia c (click or J)</label>
<input type="text" id="j-c" placeholder="c real, c imag" style="width:60px;font-size:11px;">
<button id="j-btn">J</button>
</div>
<div class="control-group">
<label>Iterations</label>
<input type="range" id="iter-slider" min="1" max="200" value="100" style="width:120px;">
<span id="iter-label">100</span>
</div>
<div class="control-group">
<label>Max Iterations</label>
<input type="range" id="max-iter-slider" min="50" max="300" value="200" style="width:120px;">
<span id="max-iter-label">200</span>
</div>
<button id="zoom-btn" class="active">Zoom+</button>
<button id="reset-btn">Reset</button>
<div id="debug-panel">
<span>FPS: <span id="fps-debug">0</span></span>
<span>Iterations(0,0):<span id="iter-debug">0</span></span>
<span>View: <span id="view-info">1x @ 1e0</span></span>
</div>
</div>
<div id="view-info" style="position:absolute;bottom:12px;left:12px;font-size:11px;color:#bbb;z-index:10;"></div>
<script>
/* ==== CONFIG ====
* All constants are here for quick tuning and documented behavior
* Iteration count scales with zoom (log scale) to keep detail at high magnifications
* Max iterations is capped to avoid infinite loops while allowing deep zooms
* Smooth coloring uses fractional escape count: n + 1 - log2(log|z|)
*/
const CONFIG = {
CANVAS_WIDTH: window.innerWidth,
CANVAS_HEIGHT: window.innerHeight,
MAX_ITER_BASE: 200, // base iterations at base zoom
MAX_ITER_SCALE: 2.5, // iterates faster as zoom increases
MIN_ITER: 1,
MAX_ITER_MAX: 3000, // cap to avoid precision loss
ZOOM_SPEED: 0.85, // smooth zoom factor per wheel step
PAN_SPEED: 0.3, // px per step when dragging pan
PROGRESSIVE_PASSES: 2, // 1/4 then full resolution
FPS_LIMIT: 60,
DEBUG_FPS: true,
FRAME_TIME_MS: 1000 / CONFIG.FPS_LIMIT,
MAX_ZOOM: 1e13,
CURSOR_RADIUS: 8,
HISTORY_DEPTH: 10,
INTERNAL_PIXELS: 256, // coarsest pass resolution
ANIMATION_SMOOTH: true,
FRACTAL_COLORS: {
fire: [0xff0000,0xff7f00,0xffff00,0xffd700],
ocean: [0x001a33,0x0066cc,0x0099ff,0x66ddff],
monochrome: [0x222222,0x444444,0x666666,0x88aaaa],
psychedelic: [0xff00ff,0x00ffff,0xffff00,0xff7f00]
}
};
/* ==== STATE ====
* All mutable state lives here; updated via UI and math functions
*/
const state = {
center: {x: -0.5, y: 0}, // Mandelbrot c-reference
scale: 1,
zoom: 1,
maxIter: CONFIG.MAX_ITER_BASE,
iterations: 100,
palette: 'fire',
jc: null, // Julia c for preview
history: [],
activeBox: null, // {x1,y1,x2,y2}
isDraggingBox: false,
lastFps: 0,
lastTime: 0,
animationId: null,
lastRenderTime: 0,
frameCount: 0
};
/* ==== CANVAS SETUP ====
* Precompute pixel buffer and color palette lookup tables for speed
*/
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = CONFIG.CANVAS_WIDTH;
canvas.height = CONFIG.CANVAS_HEIGHT;
// Color palettes as lookup arrays (linear interpolation supported)
const PALETTES = CONFIG.FRACTAL_COLORS;
function paletteColor(iter, maxIter) {
if (iter >= maxIter || iter <= 0) return [0,0,0];
// smooth fractional iteration
const n = iter - 1;
const logz = Math.log(Math.abs(cj));
const log2logz = Math.log(Math.logz);
const t = n + 1 - Math.log2(log2logz);
// clamp to palette range
const idx = Math.min(Math.floor(t * (palette.length-1)), palette.length-1);
return palette[idx];
}
// Precompute palette lookup for fast coloring
const PALETTE_LOOKUP = {};
for (let i=0;i<4;i++) {
const p = CONFIG.FRACTAL_COLORS[Object.keys(CONFIG.FRACTAL_COLORS)[i]];
for (let j=0;j<p.length;j++) {
PALETTE_LOOKUP[p[j]] = [j,p[j]];
}
}
/* ==== RENDERING CORE ====
* All pixel work is done via ImageData and direct buffer writes
*/
function renderFrame() {
const w = canvas.width, h = canvas.height;
const imgData = ctx.createImageData(w, h);
const data = imgData.data;
// Coarse pass if requested (progressive refinement)
const coarseRes = Math.max(1, Math.floor(CONFIG.PROGRESSIVE_PASSES * state.zoom / CONFIG.MAX_ZOOM));
const stepX = w / coarseRes;
const stepY = h / coarseRes;
// For each pixel in coarse grid
for (let y = 0; y < coarseRes; ++y) {
for (let x = 0; x < coarseRes; ++x) {
// Map to complex plane
const cx = state.center.x + (x - .5) * state.scale / (coarseRes/2);
const cy = state.center.y + (y - .5) * state.scale / (coarseRes/2);
// Mandelbrot iteration with smooth coloring
let zx = 0, zy = 0;
let iter = 0;
let escaped = false;
const maxIter = Math.min(CONFIG.MAX_ITER_MAX, Math.floor(state.maxIter * state.zoom * CONFIG.ZOOM_SPEED * state.zoom));
while (iter < maxIter && !escaped) {
const zx2 = zx*zx - zy*zy + cx;
const zy2 = 2*zx*zy + cy;
zx = zx2; zy = zy2;
iter++;
if (zx*zx + zy*zy > 4) {
escaped = true;
}
}
// Color
const col = iter < maxIter ? paletteColor(iter, maxIter) : [0,0,0];
const idx = (y*w + x)*4;
data[idx] = col[0];
data[idx+1] = col[1];
data[idx+2] = col[2];
data[idx+3] = 255;
}
}
// Optional: draw selection overlay
if (state.activeBox) {
const [x1,y1,x2,y2] = state.activeBox;
ctx.save();
ctx.globalAlpha = 0.4;
ctx.strokeStyle = state.isDraggingBox ? '#ff8c00' : '#ff6b6b';
ctx.lineWidth = 2;
ctx.setLineDash([4,3]);
ctx.beginPath();
ctx.rect(x1, y1, x2-x1, y2-y1);
ctx.stroke();
ctx.setLineDash([]);
ctx.restore();
}
// Copy buffer to canvas
ctx.putImageData(imgData, 0, 0);
// Update HUD
updateHud();
}
/* ==== INTERACTION HANDLERS ====
* Mouse events for pan/zoom/drag selection
*/
canvas.addEventListener('mousedown', e => {
if (e.target !== canvas) return;
state.isDraggingBox = true;
state.activeBox = [e.offsetX, e.offsetY, e.offsetX, e.offsetY];
updateHud();
});
canvas.addEventListener('mousemove', e => {
if (!state.isDraggingBox) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
state.activeBox[2] = x; state.activeBox[3] = y;
updateHud();
});
canvas.addEventListener('mouseup', e => {
if (!state.isDraggingBox) return;
state.isDraggingBox = false;
const rect = canvas.getBoundingClientRect();
const x1 = Math.min(state.activeBox[0], state.activeBox[2]);
const y1 = Math.min(state.activeBox[1], state.activeBox[3]);
const x2 = Math.max(state.activeBox[0], state.activeBox[2]);
const y2 = Math.max(state.activeBox[1], state.activeBox[3]);
// Center on box center
const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
state.center = {
x: cx - (state.scale / 2),
y: cy - (state.scale / 2)
};
state.zoom = Math.max(1, Math.min(state.zoom*1.2, CONFIG.MAX_ZOOM));
state.history.push({center: {x:cx, y:cy}, scale: state.zoom, zoom: state.zoom, palette: state.palette, jc: state.jc});
if (state.history.length > state.HISTORY_DEPTH) state.history.shift();
updateHud();
});
canvas.addEventListener('wheel', e => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
// Center on mouse
const cx = state.center.x + (x - rect.width/2);
const cy = state.center.y + (y - rect.height/2);
const zoomFactor = e.deltaY < 0 ? CONFIG.ZOOM_SPEED : -CONFIG.ZOOM_SPEED;
// Clamp scale
let newScale = state.zoom * Math.exp(zoomFactor * 0.1);
newScale = Math.max(CONFIG.MIN_ZOOM, Math.min(newScale, CONFIG.MAX_ZOOM));
// Update view
state.center = {x:cx, y:cy};
state.zoom = newScale;
state.history.push({center: {x:cx, y:cy}, scale: newScale, zoom: newScale, palette: state.palette, jc: state.jc});
if (state.history.length > state.HISTORY_DEPTH) state.history.shift();
updateHud();
}, {passive:false});
document.getElementById('reset-btn').addEventListener('click', () => {
state.center = {x:-0.5, y:0};
state.scale = 1;
state.zoom = 1;
state.maxIter = CONFIG.MAX_ITER_BASE;
state.palette = 'fire';
state.jc = null;
state.history = [];
updateHud();
});
document.getElementById('j-btn').addEventListener('click', () => {
const cStr = document.getElementById('j-c').value.trim();
if (cStr) {
try {
const [cr, ci] = cStr.split(',').map(Number);
state.jc = {real: cr, imag: ci};
} catch(e) {}
}
});
document.getElementById('iter-slider').addEventListener('input', e => {
state.iterations = +e.target.value;
updateHud();
});
document.getElementById('max-iter-slider').addEventListener('input', e => {
state.maxIter = +e.target.value;
updateHud();
});
/* ==== PROGRESSIVE REFINEMENT ====
* Draw low-res first, then refine to full resolution
*/
function progressiveRender() {
const w = canvas.width, h = canvas.height;
let progress = 0, total = CONFIG.PROGRESSIVE_PASSES;
function renderStep() {
if (progress >= total) return;
const step = Math.ceil(CONFIG.PROGRESSIVE_PASSES * state.zoom / CONFIG.MAX_ZOOM);
const coarseRes = Math.max(1, Math.floor(step/2));
// Draw low-res pass
renderFrameLowRes(coarseRes);
progress += 1;
requestAnimationFrame(renderStep);
}
function renderFrameLowRes(res) {
const imgData = ctx.createImageData(w, h);
const data = imgData.data;
for (let y = 0; y < res; ++y) {
for (let x = 0; x < res; ++x) {
// Same math as above but at lower resolution
const cx = state.center.x + (x - .5) * state.scale / (res/2);
const cy = state.center.y + (y - .5) * state.zoom / (res/2);
let zx = 0, zy = 0, iter = 0;
while (iter < state.maxIter && !(zx*zx+zy*zy > 4)) {
const zx2 = zx*zx - zy*zy + cx;
const zy2 = 2*zx*zy + cy;
zx = zx2; zy = zy2;
iter++;
}
const col = iter < state.maxIter ? paletteColor(iter, state.maxIter) : [0,0,0];
const idx = (y*w + x)*4;
data[idx] = col[0]; data[idx+1] = col[1]; data[idx+2] = col[2]; data[idx+3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
if (progress < total) {
state.lastRenderTime = performance.now();
requestAnimationFrame(renderStep);
}
}
// Start progressive
requestAnimationFrame(renderStep);
}
/* ==== UTILS ====
* Utility helpers for math and rendering
*/
function updateHud() {
const fpsEl = document.getElementById('fps');
const iterEl = document.getElementById('iter');
const zoomEl = document.getElementById('zoom');
const viewEl = document.getElementById('view-info');
// FPS
if (state.lastRenderTime) {
state.frameCount++;
const dt = performance.now() - state.lastRenderTime;
if (dt > state.lastFrameTime) {
state.lastFrameTime = dt;
fpsEl.textContent = Math.round(1/dt*1000);
fpsEl.style.display = dt < 0.05 ? 'inline' : 'block';
}
} else {
state.lastFrameTime = performance.now();
}
// Iteration count
iterEl.textContent = state.iterations;
// Zoom display
zoomEl.textContent = `${state.zoom.toExponential(2)}`;
// View info
viewEl.innerHTML = `Center: (${state.center.x.toFixed(6)}, ${state.center.y.toFixed(6)})<br>Scale: ${state.scale.toExponential(1)}x`
}
/* ==== DEBUG & INIT ====
* Expose debug API for headless probing
*/
window.fractal = {
iterationsAt: (x, y) => {
// Returns iteration count at pixel (x,y)
const w = canvas.width, h = canvas.height;
const cx = state.center.x + (x - h/2) * state.scale / (w/2);
const cy = state.center.y + (y - h/2) * state.zoom / (h/2);
let zx = 0, zy = 0, iter = 0;
while (iter < state.maxIter && !(zx*zx+zy*zy > 4)) {
const zx2 = zx*zx - zy*zy + cx;
const zy2 = 2*zx*zy + cy;
zx = zx2; zy = zy2;
iter++;
}
return iter;
},
setView: ({center, scale, palette, jc}) => {
state.center = center || state.center;
state.scale = scale || state.scale;
state.palette = palette || state.palette;
state.jc = jc || null;
// Restore history if needed
if (state.history.length) state.history[state.history.length-1] = {center, scale, palette, jc};
updateHud();
},
saveState: () => JSON.stringify(state),
loadState: (json) => {
try {
const s = JSON.parse(json);
if (s.center) state.center = s.center;
if (s.scale) state.scale = s.scale;
if (s.palette) state.palette = s.palette;
if (s.jc) state.jc = s.jc;
updateHud();
} catch(e) {}
}
};
/* ==== START ====
* Resize canvas on window resize
*/
window.addEventListener('resize', () => {
canvas.width = CONFIG.CANVAS_WIDTH;
canvas.height = CONFIG.CANVAS_HEIGHT;
updateHud();
});
// Initial render
updateHud();
// Start progressive rendering loop
progressiveRender();
/* ==== END ==== */
</script>
</body>
</html>
<!-- agent-meta {"model":"liquid/lfm2-24b-a2b","provider":"lmstudio","persona":"basic","sessionId":"cf3d30ed-aab5-4414-bf0c-e2a7822b2192","tokensIn":1299,"tokensOut":5267,"tokensTotal":6566,"cost":0,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-01T04:48:54.411Z"} -->