mirror of
https://github.com/informaticker/network-website.git
synced 2024-11-23 18:21:58 +01:00
188 lines
5.3 KiB
HTML
188 lines
5.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Mandelbulb ASCII Art</title>
|
|
<style>
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
white-space: pre;
|
|
background: #000;
|
|
color: #0f0;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
}
|
|
#asciiArt {
|
|
margin: auto;
|
|
overflow: hidden;
|
|
font-size: 7px;
|
|
line-height: 7px;
|
|
letter-spacing: 1px;
|
|
}
|
|
.controls {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: #0f0;
|
|
}
|
|
input[type='number'] {
|
|
width: 60px;
|
|
background: #222;
|
|
border: 1px solid #333;
|
|
color: #0f0;
|
|
margin: 2px 0;
|
|
}
|
|
label {
|
|
display: inline-block;
|
|
width: 100px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="controls">
|
|
<label>Power: <input type="number" id="power" min="2" max="12" step="0.1" value="8"></label><br>
|
|
<label>Detail: <input type="number" step="0.01" id="detail" value="0.25"></label><br>
|
|
<label>Columns: <input type="number" id="columns" value="80"></label>
|
|
</div>
|
|
<pre id="asciiArt"></pre>
|
|
|
|
<script>
|
|
'use strict';
|
|
|
|
// Mandelbulb Calculation and Rendering
|
|
class Mandelbulb {
|
|
constructor(power, detail, columns, rows) {
|
|
this.power = power;
|
|
this.detail = detail;
|
|
this.columns = columns;
|
|
this.rows = rows;
|
|
this.charset = " .:░▒▓█"; // Use extended charset for ASCII view detail
|
|
}
|
|
|
|
// Normalized rotation using yaw and pitch for 3D rotation
|
|
rotate(x, y, z, sinA, cosA, sinB, cosB) {
|
|
const yr = y * cosA - z * sinA;
|
|
const zr = y * sinA + z * cosA;
|
|
const xr = x * cosB - zr * sinB;
|
|
const zrr = x * sinB + zr * cosB;
|
|
return [xr, yr, zrr];
|
|
}
|
|
|
|
// 3D Distance Estimation iterative formula for the Mandelbulb fractal
|
|
computeMandelbulbDistance(x0, y0, z0, maxIterations, escapeRadius) {
|
|
let x = x0, y = y0, z = z0, dr = 1.0;
|
|
let r = 0;
|
|
for (let i = 0; i < maxIterations; i++) {
|
|
r = Math.sqrt(x * x + y * y + z * z);
|
|
if (r > escapeRadius) break;
|
|
|
|
// Convert to polar coordinates
|
|
const theta = Math.atan2(Math.sqrt(x * x + y * y), z);
|
|
const phi = Math.atan2(y, x);
|
|
const zr = Math.pow(r, this.power - 1);
|
|
dr = Math.pow(r, this.power - 1) * this.power * dr + 1.0;
|
|
const thetaR = theta * this.power;
|
|
const phiR = phi * this.power;
|
|
|
|
// Convert back to cartesian coordinates
|
|
x = zr * Math.sin(thetaR) * Math.cos(phiR) + x0;
|
|
y = zr * Math.sin(thetaR) * Math.sin(phiR) + y0;
|
|
z = zr * Math.cos(thetaR) + z0;
|
|
}
|
|
return 0.5 * Math.log(r) * r / dr;
|
|
}
|
|
|
|
render(angleX, angleY) {
|
|
const output = Array.from({ length: this.rows }, () => []);
|
|
const maxIterations = 256;
|
|
const escapeRadius = 10;
|
|
const voxelSize = 2.5 / Math.min(this.columns, this.rows);
|
|
const fov = 1.0 / Math.tan(0.5);
|
|
const cosA = Math.cos(angleX), sinA = Math.sin(angleX);
|
|
const cosB = Math.cos(angleY), sinB = Math.sin(angleY);
|
|
const cameraZ = -2; // Camera position tweak for better fractal appearance
|
|
|
|
for (let row = 0; row < this.rows; row++) {
|
|
for (let col = 0; col < this.columns; col++) {
|
|
const px = (col - this.columns / 2) * voxelSize;
|
|
const py = (row - this.rows / 2) * voxelSize;
|
|
const pz = fov;
|
|
|
|
const [dx, dy, dz] = this.rotate(px, py, pz, sinA, cosA, sinB, cosB);
|
|
const [sx, sy, sz] = this.rotate(0, 0, cameraZ, sinA, cosA, sinB, cosB);
|
|
|
|
let depth = this.rayMarch(sx, sy, sz, dx, dy, dz, maxIterations, escapeRadius);
|
|
output[row][col] = this.depthToAscii(depth);
|
|
}
|
|
}
|
|
return output.map(line => line.join('')).join('\n');
|
|
}
|
|
|
|
// Ray marching for the fractal
|
|
rayMarch(x, y, z, dx, dy, dz, maxIterations, escapeRadius) {
|
|
let depth = 0.0;
|
|
for (let i = 0; i < maxIterations; i++) {
|
|
const distance = this.computeMandelbulbDistance(x, y, z, maxIterations, escapeRadius);
|
|
if (distance < this.detail) break;
|
|
depth += distance;
|
|
x += dx * distance;
|
|
y += dy * distance;
|
|
z += dz * distance;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
// Map depth into ASCII character
|
|
depthToAscii(depth) {
|
|
const index = Math.min(Math.floor(depth * 7 / this.detail), this.charset.length - 1);
|
|
return this.charset[index];
|
|
}
|
|
}
|
|
|
|
let asciiArt = document.getElementById('asciiArt');
|
|
let powerControl = document.getElementById('power');
|
|
let detailControl = document.getElementById('detail');
|
|
let columnsControl = document.getElementById('columns');
|
|
|
|
// Initial values
|
|
let power = parseFloat(powerControl.value);
|
|
let detail = parseFloat(detailControl.value);
|
|
let columns = parseInt(columnsControl.value);
|
|
let rows = Math.floor(columns / 2);
|
|
|
|
let mandelbulb = new Mandelbulb(power, detail, columns, rows);
|
|
let angleX = 0;
|
|
let angleY = 0;
|
|
|
|
function render() {
|
|
asciiArt.textContent = mandelbulb.render(angleX, angleY);
|
|
angleX += 0.04;
|
|
angleY += 0.03;
|
|
requestAnimationFrame(render);
|
|
}
|
|
|
|
powerControl.addEventListener('input', () => {
|
|
power = parseFloat(powerControl.value);
|
|
mandelbulb = new Mandelbulb(power, detail, columns, rows);
|
|
});
|
|
|
|
detailControl.addEventListener('input', () => {
|
|
detail = parseFloat(detailControl.value);
|
|
mandelbulb = new Mandelbulb(power, detail, columns, rows);
|
|
});
|
|
|
|
columnsControl.addEventListener('input', () => {
|
|
columns = parseInt(columnsControl.value);
|
|
rows = Math.floor(columns / 2);
|
|
mandelbulb = new Mandelbulb(power, detail, columns, rows);
|
|
});
|
|
|
|
render();
|
|
</script>
|
|
|
|
</body>
|
|
</html> |