Multiball Exhibition

Arcade excitement on display.

Log in to post a comment.

// Forked from "Rasterball" by maraz
// https://turtletoy.net/turtle/ebef974b5d

const quality = 1.5; // min=0.15, max=5, step=0.15
const letterbox = 0; // min=0.0, max=1.0, step=0.01

const squareSize = 3.275;
const perspective = 8.5;
const xshift = -87;
const yspace = 5;
const canvasSize = 1024;
const coordSize = 200;
const step = coordSize/canvasSize/quality;
const halfCanvas = canvasSize/2;
const halfCoord = coordSize/2;
const topCoord = -halfCoord+letterbox*halfCoord;
const bottomCoord = halfCoord-letterbox*halfCoord;

const balls = [{
    size: 75,
    xPos: 93,
},
{
    size: 46,
    xPos: 44.75,
},
{
    size: 29.5,
    xPos: 7,
},
{
    size: 21.75,
    xPos: -22.75,
},
{
    size: 17.75,
    xPos: -55,
},
{
    size: 16,
    xPos: -84,
}]

const shadows = balls.map(b => ({
    shadow: true,
    size: b.size*0.75,
    xPos: b.xPos,
    yPos: b.size*0.4-1.25/b.size*halfCoord,
}))

Turtle.prototype.step = function(draw = false) {
    draw && this.pendown();
    this.forward(step);
    draw && this.penup();
}

// This gives us antialiasing for quality > 1.
Canvas.setpenopacity(-1+0.0875*(quality-1));
const turtle = new Turtle();
turtle.penup();
turtle.goto(-halfCoord, topCoord);
turtle.seth(0);

const skyStep = (xl, yl) => Math.abs(yl/3)%2!=0 && Math.abs(xl/3)%2!=0

function sky(x, y, perspective) {
    const sq = squareSize - 1.5*squareSize*perspective*y/halfCoord/2;
    const xl = Math.round(x/sq);
    const yl = Math.round(perspective*y/sq);
    turtle.step(skyStep(xl, yl));
}

const checkCircle = (circle, x, y) => {
    const ax = Math.abs(x);
    const ay = Math.abs(y);
    if (ay < circle.size && ax < circle.size) {
        const circlePos = Math.sqrt(x*x+y*y);
        if (circlePos < circle.size) {
            return circlePos;
        }
    }
    return false;
}

const checkShadows = (x, y) => {
    for (const shadow of shadows) {
        const _x = x - shadow.xPos - xshift;
        const _y = y - shadow.yPos - yspace;
        const shadowPos = checkCircle(shadow, _x, _y);
        if (shadowPos != false) {
            return { ...shadow, x: _x, y: _y, shadowPos };
        }
    }
    return false;
}

const groundStep = (xq, yq) => ((xq+1)%2==0 && (yq+1)%2==0) || (xq%2==0 && yq%2==0);

const groundShadowStep = (shadow) => Math.round((Math.random() + shadow.shadowPos/shadow.size)/2) 
    
function ground(x, y, perspective) {
    const sq = 2*squareSize - 2*squareSize*perspective*y/halfCoord/2;
    const xq = Math.round(x/sq);
    const yq = Math.round(perspective*y/sq);
    const shadow = checkShadows(x, y);
    if (shadow) {
        turtle.step(groundShadowStep(shadow, xq, yq) && groundStep(xq, yq));
    } else {
        turtle.step(groundStep(xq, yq));
    }
}

const checkBalls = (x, y) => {
    for (const ball of balls) {
        const _x = x - ball.xPos;
        const ballPos = checkCircle(ball, _x, y);
        if (ballPos != false) {
            return { ...ball, x: _x, y, ballPos };
        }
    }
    return false;
}

const skyReflectionStep = (xq, yq) => Math.abs(yq/11)%2!=0 && Math.abs((xq+12))%5!=0
       
const fadeStep = (y, intensity, length = intensity*5) => !!
    Math.round(
        Math.log10(
            (length - Math.abs(y))
            * Math.random()
        )
    ) % intensity

// Conditions for drawing the pattern.
const checkerStep = (_ball) => {
    const sq = _ball.size/10*squareSize - _ball.size/10.6*squareSize*(_ball.ballPos/_ball.size);
    const xq = Math.round(_ball.x*(1.5)/sq);
    const yq = Math.round(_ball.y*(1.5)/sq);
    return (
        // Draw the upper part normally.
        (_ball.y < _ball.size * 0.8) 
        ||
        // For lower part, create a noise shadow.
        // Increased probability here means lighter shade.
        Math.round(1-(Math.random()*((_ball.ballPos - 0.81*_ball.size)/(_ball.size*0.25))))==1
    ) 
    // Drawing the pattern.
    && groundStep(xq, yq)
}
     
function ball(_ball) {
    const sq = _ball.size/10*squareSize - _ball.size/10.5*squareSize*(_ball.ballPos/_ball.size);
    const xq = Math.round(_ball.x*0.5/sq);
    const yq = Math.round(_ball.y*2/sq);
    // Sky reflections.
    if (yq+yspace*(0.12+0.002*_ball.size) < 0 && _ball.y < 0) {
        turtle.step(skyReflectionStep(xq, yq));
    }
    // Reflection of night sky outside the exhibition.
    else if (_ball.y < 0)
        turtle.step(!fadeStep(yq, 2));
    // Horizon line.
    else if (_ball.y === 0)
        turtle.step(false);
    // Reflection of ground outside the exhibition.
    else if (_ball.y > 0 && yq-yspace*(0.12+0.002*_ball.size) < 0) 
        turtle.step(!fadeStep(yq, 5));
    // Checkerboard pattern on the balls.
    else {
        turtle.step(checkerStep(_ball));
    }
}

const lineMultiplier = 1/quality;
let stillMultiplying = 0;
let yStep = step/lineMultiplier;

function walk(i) {
    const x = turtle.x();
    const y = turtle.y();
    if (x > halfCoord) {
        if (quality < 1) {
            if (stillMultiplying >= lineMultiplier) {
                stillMultiplying = 0;
            } 
            stillMultiplying = stillMultiplying + 1;
            turtle.goto(-halfCoord, y+yStep);
        }
        else {
            turtle.goto(-halfCoord, y+step);
        }
        return true;
    }
    const maybeBall = y < 75 && y > -75 && checkBalls(x, y-yStep*stillMultiplying);
    if (maybeBall) {
        ball(maybeBall);
    }
    else {
        if (y < 0) {
            sky(x+xshift, y+yspace, perspective);               
        } else {
            ground(x+xshift, y-yspace, -perspective);   
        }
    }
    return y < bottomCoord;
}