// LL 2021

const rays = 70000; // min=100 max=200000 step=1
const objects = 15; // min=0 max=50 step=1
const object_radius = 10; // min=1 max=30 step=1
const reflection_factor = 0.9; // min=0 max=1 step=0.001

const seed=0; /// min=0 max=100 step=1

Canvas.setpenopacity(-Math.max(0.05, 1/rays));
//Canvas.setpenopacity(-0.1);

const turtle = new Turtle();

const ray_x = 0; // min=-100 max=100 step=1
const ray_y = 0; // min=-100 max=100 step=1
const ray_dir = -1; // min=0 max=1 step=0.001
const ray_spread = 0.01; // min=0 max=1 step=0.001

const origin_x = ray_x;
const origin_y = ray_y;
const origin_dir_x = Math.cos((ray_dir<0?random():ray_dir) * Math.PI * 2);
const origin_dir_y = Math.sin((ray_dir<0?random():ray_dir) * Math.PI * 2);
const origin_spread = Math.PI * (ray_spread**3) * 2;

const draw_objects=1; // min=0 max=1 step=1 (No,Yes)

var whole_scene;
var rng;
var current_ray = null;

console.clear();

function walk(i, t) {
    if (i==0) {
        rng = new RNG(seed);
        whole_scene = new Scene();
    }

    if (draw_objects) whole_scene.draw();

    if (current_ray === null || current_ray.done) {
        const dir_x = t<1 ? Math.cos(t * Math.PI * 2) : origin_dir_x;
        const dir_y = t<1 ? Math.sin(t * Math.PI * 2) : origin_dir_y;
        current_ray = new Ray(whole_scene, origin_x, origin_y, dir_x, dir_y, origin_spread);
    }
    
    whole_scene.shoot(current_ray);
    
    return i+1 < rays;
}

class Sphere {
    constructor(x, y, r) {
        this.x = x;
        this.y = y;
        this.r = r;
    }
    
    draw() {
        turtle.jump(this.x + this.r, this.y);
        const step = 1/20;
        for (var a=step; a<1+step*0.1; a+=step) {
            turtle.goto(this.x + Math.cos(a * Math.PI * 2) * this.r, this.y + Math.sin(a * Math.PI * 2) * this.r);
        }
    }
    
    getIntersection(ray) {
        const fx = ray.x - this.x, fy = ray.y - this.y;
        const a = dot(ray.dir_x, ray.dir_y, ray.dir_x, ray.dir_y);
        const b = 2 * dot(fx, fy, ray.dir_x, ray.dir_y);
        const c = dot(fx, fy, fx, fy) - this.r * this.r;
        
        const discriminant2 = b * b - 4 * a * c;
        if (discriminant2 < 0) {
            return false;
        } else {
            const discriminant = Math.sqrt(discriminant2);
            const t1 = (-b - discriminant) / (2 * a);
            const t2 = (-b + discriminant) / (2 * a);
            
            return (t1 > 0.1) ? t1 : (t2 > 0.1) ? t2 : false;
        }
    }
    
    getNormalAtPoint(x, y) {
        var nx = x - this.x, ny = y - this.y;
        const len = Math.hypot(nx, ny);
        if (len > 0.01) {
            nx /= len;
            ny /= len;
        }
        return [ nx, ny ];
    }
}

class Scene {
    constructor() {
        this.objects = [];
        
        const d = 65;
        const r = object_radius;
        for (var a=0; a<1; a+=1/objects) {
            this.objects.push(
                new Sphere(
                    d * Math.cos(a * Math.PI * 2 - Math.PI / 2),
                    d * Math.sin(a * Math.PI * 2 - Math.PI / 2),
                r)
            );
        }
    }
    
    draw() {
        this.objects.forEach(o => o.draw());
    }
    
    shoot(ray) {
        if (ray.done) return;
        
        var closest_object = null;
        var min_t = 1000;
        
        this.objects.forEach(o => {
            const t = o.getIntersection(ray);
            if (t && (t < min_t)) {
                min_t = t;
                closest_object = o;
            }
        });
        
        turtle.jump(ray.x, ray.y);
        const new_x = ray.x + ray.dir_x * min_t;
        const new_y = ray.y + ray.dir_y * min_t;
        turtle.goto(new_x, new_y);

        if (closest_object !== null) {
            const normal = closest_object.getNormalAtPoint(new_x, new_y);
            ray.reflect(new_x, new_y, normal[0], normal[1]);
        } else {
            ray.done = true;
        }
    }
}

class Ray {
    constructor(scene, x, y, dx, dy, ds) {
        this.x = x;
        this.y = y;
        this.done = false;
        this.scene = scene;
        const angle = random() * ds - ds / 2;
        this.dir_x = rotX(dx, dy, angle);
        this.dir_y = rotY(dx, dy, angle);
        const len = Math.hypot(this.dir_x, this.dir_y);
        if (len < 0.001) this.done = true;
        this.dir_x /= len;
        this.dir_y /= len;
    }
    
    reflect(x, y, nx, ny) {
        this.x = x;
        this.y = y;

        const dir = random() < reflection_factor ? 1 : -1;
        const dotDN = dot(this.dir_x, this.dir_y, nx, ny);
        const reflect_x = this.dir_x - 2 * dotDN * dir * nx;
        const reflect_y = this.dir_y - 2 * dotDN * dir * ny;
        this.dir_x = reflect_x;
        this.dir_y = reflect_y;

    }
}

function dot(v1x, v1y, v2x, v2y) { return v1x * v2x + v1y * v2y; }

function rotX(x, y, a) { return Math.cos(a) * x - Math.sin(a) * y; }
function rotY(x, y, a) { return Math.sin(a) * x + Math.cos(a) * y; }

function random() { return Math.random(); }
//function random() { return rng.nextFloat(); }

// Minified Random Number Generator from https://turtletoy.net/turtle/ab7a7e539e
function RNG(t){return new class{constructor(t){this.m=2147483648,this.a=1103515245,this.c=12345,this.state=t||Math.floor(Math.random()*(this.m-1))}nextFloat(){return this.state=(this.a*this.state+this.c)%this.m,this.state/(this.m-1)}}(t)}