const canvas = document.getElementById('pt-canvas');
const ctx = canvas.getContext('2d');
const W = 48, H = 32;
canvas.width = W; canvas.height = H;
const sum = new Float32Array(W * H * 3);
let samples = 0;
let startedAt = performance.now();
const sphereC = [0, -0.05, -1.6], sphereR = 0.62;
const floorY = -0.62;
function normalize(v) { const l = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / l, v[1] / l, v[2] / l]; }
function hitSphere(o, d) {
const ox = o[0] - sphereC[0], oy = o[1] - sphereC[1], oz = o[2] - sphereC[2];
const b = 2 * (d[0] * ox + d[1] * oy + d[2] * oz);
const c = ox * ox + oy * oy + oz * oz - sphereR * sphereR;
const disc = b * b - 4 * c;
if (disc < 0) return null;
const t = (-b - Math.sqrt(disc)) / 2;
return t > 0.001 ? t : null;
}
function hitFloor(o, d) {
if (Math.abs(d[1]) < 1e-4) return null;
const t = (floorY - o[1]) / d[1];
return t > 0.001 ? t : null;
}
function sky(d) {
const up = Math.max(0, d[1]);
const b = 0.15 + up * 0.95;
return [b * 0.85, b * 0.9, b];
}
function cosineSample(n) {
const r1 = Math.random(), r2 = Math.random();
const r = Math.sqrt(r1), theta = 2 * Math.PI * r2;
const x = r * Math.cos(theta), y = r * Math.sin(theta), z = Math.sqrt(Math.max(0, 1 - r1));
const a = Math.abs(n[0]) > 0.9 ? [0, 1, 0] : [1, 0, 0];
const tt = normalize([a[1] * n[2] - a[2] * n[1], a[2] * n[0] - a[0] * n[2], a[0] * n[1] - a[1] * n[0]]);
const bz = [n[1] * tt[2] - n[2] * tt[1], n[2] * tt[0] - n[0] * tt[2], n[0] * tt[1] - n[1] * tt[0]];
return [tt[0] * x + bz[0] * y + n[0] * z, tt[1] * x + bz[1] * y + n[1] * z, tt[2] * x + bz[2] * y + n[2] * z];
}
function trace(o, d) {
const ts = hitSphere(o, d), tf = hitFloor(o, d);
let t = null, isSphere = false;
if (ts !== null && (tf === null || ts < tf)) { t = ts; isSphere = true; }
else if (tf !== null) { t = tf; isSphere = false; }
if (t === null) return sky(d);
const p = [o[0] + d[0] * t, o[1] + d[1] * t, o[2] + d[2] * t];
let n, albedo;
if (isSphere) { n = normalize([p[0] - sphereC[0], p[1] - sphereC[1], p[2] - sphereC[2]]); albedo = [0.85, 0.55, 0.35]; }
else { n = [0, 1, 0]; albedo = [0.72, 0.72, 0.78]; }
const bounceDir = cosineSample(n);
const bo = [p[0] + n[0] * 0.001, p[1] + n[1] * 0.001, p[2] + n[2] * 0.001];
const t2s = hitSphere(bo, bounceDir), t2f = hitFloor(bo, bounceDir);
const light = (t2s !== null || t2f !== null) ? [0.06, 0.06, 0.07] : sky(bounceDir);
return [albedo[0] * light[0], albedo[1] * light[1], albedo[2] * light[2]];
}
function render(now) {
if (now - startedAt > 4200) { sum.fill(0); samples = 0; startedAt = now; }
samples++;
const aspect = W / H;
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const px = ((x + Math.random()) / W) * 2 - 1;
const py = 1 - ((y + Math.random()) / H) * 2;
const dir = normalize([px * aspect * 0.62, py * 0.62, -1]);
const c = trace([0, 0.15, 0.9], dir);
const i = (y * W + x) * 3;
sum[i] += c[0]; sum[i + 1] += c[1]; sum[i + 2] += c[2];
}
}
const img = ctx.createImageData(W, H);
for (let p = 0; p < W * H; p++) {
const i = p * 3;
img.data[p * 4] = Math.min(255, (sum[i] / samples) * 255 * 1.15);
img.data[p * 4 + 1] = Math.min(255, (sum[i + 1] / samples) * 255 * 1.15);
img.data[p * 4 + 2] = Math.min(255, (sum[i + 2] / samples) * 255 * 1.15);
img.data[p * 4 + 3] = 255;
}
ctx.putImageData(img, 0, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);