<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Infinite Mandelbrot Room (Portal v2)</title>
<style>
body, html { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #000; font-family: monospace; }
#canvas { width: 100%; height: 100%; display: block; }
.hud {
position: absolute; bottom: 20px; left: 20px; color: #00ff66;
background: rgba(0, 20, 10, 0.85); padding: 12px 18px; border: 1px solid #00ff66;
border-radius: 4px; pointer-events: none; box-shadow: 0 0 15px rgba(0,255,10,0.3);
font-size: 13px; line-height: 1.4;
}
.portal-back {
position: absolute; top: 20px; left: 20px; color: #fff; text-decoration: none;
background: rgba(20, 20, 20, 0.8); padding: 8px 14px; border: 1px solid #666;
border-radius: 4px; font-size: 12px; transition: all 0.2s;
}
.portal-back:hover { background: rgba(0, 255, 10, 0.2); border-color: #00ff66; color: #00ff66; }
</style>
</head>
<body>
<a href="../aletheia-observatory/walkable-3d.html" class="portal-back">← Return to Observatory Corridor</a>
<canvas id="canvas"></canvas>
<div class="hud">
INFINITE MANDELBROT ROOM // OPEN SKY PORTAL<br>
<span id="coords">Z: 1.00 | WAYPOINT: ACTIVE</span>
</div>
<script>
// In-page memory store (sandbox-safe replacement for localStorage)
const sessionMemory = {
waypointsVisited: 0,
logWaypoint(zoom) {
this.waypointsVisited++;
document.getElementById('coords').innerText = Z: ${zoom.toFixed(2)} | WAYPOINTS UNLOCKED: ${this.waypointsVisited};
}
};
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl');
if (!gl) { alert('WebGL not supported!'); }
// Vertex shader (Full-screen quad)
const vsSource = ;
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
// Fragment shader (Inline Raymarched Mandelbrot Room with Open Sky & Waypoints)
const fsSource = `
precision highp float;
uniform vec2 u_resolution;
uniform float u_time;
uniform vec2 u_mouse;
vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d) {
return a + b cos(6.28318 (c * t + d));
}
void main() {
vec2 st = (gl_FragCoord.xy - 0.5 * u_resolution.xy) / min(u_resolution.x, u_resolution.y);
// Camera rotation via mouse
float rot = u_mouse.x * 2.0;
mat2 rotMat = mat2(cos(rot), -sin(rot), sin(rot), cos(rot));
st *= rotMat;
float zoomSpeed = 0.2;
float scale = pow(2.5, mod(u_time * zoomSpeed, 12.0));
// Mandelbrot coordinate mapping
vec2 c = st * (3.0 / scale) + vec2(-0.7436438870371587, 0.1318259042053119);
vec2 z = c;
float n = 0.0;
float maxIter = 80.0;
for (float i = 0.0; i < 80.0; i++) {
if (dot(z, z) > 4.0) { n = i; break; }
z = vec2(z.x z.x - z.y z.y, 2.0 z.x z.y) + c;
}
vec3 color = vec3(0.0);
if (n > 0.0) {
float t = sqrt(n / maxIter);
color = palette(t + u_time * 0.05, vec3(0.5), vec3(0.5), vec3(1.0), vec3(0.0, 0.33, 0.67));
}
// Open sky top gradient header constraint
if (st.y > 0.65) {
vec3 starField = vec3(step(0.98, fract(sin(dot(st 15.0, vec2(12.9898, 78.233))) 43758.5453)));
color = mix(vec3(0.02, 0.02, 0.08), vec3(0.1, 0.15, 0.3), (st.y - 0.65) / 0.35) + starField * 0.8;
}
gl_FragColor = vec4(color, 1.0);
}
`;
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertShader = createShader(gl, gl.VERTEX_SHADER, vsSource);
const fragShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource);
const program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);
gl.useProgram(program);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1, -1, 1, -1, -1, 1,
-1, 1, 1, -1, 1, 1,
]), gl.STATIC_DRAW);
const aPositionLocation = gl.getAttribLocation(program, "a_position");
gl.enableVertexAttribArray(aPositionLocation);
gl.vertexAttribPointer(aPositionLocation, 2, gl.FLOAT, false, 0, 0);
const resLocation = gl.getUniformLocation(program, "u_resolution");
const timeLocation = gl.getUniformLocation(program, "u_time");
const mouseLocation = gl.getUniformLocation(program, "u_mouse");
let mouseX = 0.0, mouseY = 0.0;
window.addEventListener('mousemove', (e) => {
mouseX = (e.clientX / window.innerWidth) * 2.0 - 1.0;
mouseY = (e.clientY / window.innerHeight) * 2.0 - 1.0;
});
window.addEventListener('click', () => {
sessionMemory.logWaypoint(Math.random() * 10 + 1);
});
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
let startTime = Date.now();
function render() {
let currentTime = (Date.now() - startTime) * 0.001;
gl.uniform2f(resLocation, canvas.width, canvas.height);
gl.uniform1f(timeLocation, currentTime);
gl.uniform2f(mouseLocation, mouseX, mouseY);
gl.drawArrays(gl.TRIANGLES, 0, 6);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>