Tribute to Evades.io

A tribute to the wonderful Evades.io

Log in to post a comment.

Canvas.setpenopacity(0.65);

const gridLines = 18; // min=8 max=30 step=1 Perspective grid density
const players = 7; // min=3 max=10 step=1 Number of player orbs
const horizonY = -45; // min=-60 max=-30 step=5 Horizon line height
const safeZoneHeight = 18; // min=10 max=30 step=2 Yellow safe zone depth

const turtle = new Turtle();
turtle.pendown();

const steps = [];
const TAU = Math.PI * 2;

function pushPoint(kind, x, y) { steps.push({ kind, x, y }); }

// Draw a circle
function drawCircle(cx, cy, radius, segments) {
    segments = segments || Math.max(16, Math.floor(radius * 3));
    for (let i = 0; i <= segments; i++) {
        const angle = (i / segments) * TAU;
        const x = cx + Math.cos(angle) * radius;
        const y = cy + Math.sin(angle) * radius;
        pushPoint(i === 0 ? 'jump' : 'draw', x, y);
    }
}

// Draw filled circle with concentric rings
function drawFilledCircle(cx, cy, radius, rings) {
    rings = rings || 4;
    for (let r = 0; r < rings; r++) {
        const ringRadius = radius * (1 - r * 0.22);
        if (ringRadius > 0.3) {
            drawCircle(cx, cy, ringRadius, Math.max(12, Math.floor(ringRadius * 2.5)));
        }
    }
}

// Draw a crown above a player
function drawCrown(cx, cy, width, height) {
    const hw = width / 2;

    // Crown base band
    pushPoint('jump', cx - hw, cy + height * 0.2);
    pushPoint('draw', cx + hw, cy + height * 0.2);
    pushPoint('draw', cx + hw, cy);
    pushPoint('draw', cx - hw, cy);
    pushPoint('draw', cx - hw, cy + height * 0.2);

    // Three crown points
    const peakHeight = height * 0.9;
    const valleyHeight = height * 0.35;

    pushPoint('jump', cx - hw, cy);
    pushPoint('draw', cx - hw * 0.65, cy - peakHeight);
    pushPoint('draw', cx - hw * 0.3, cy - valleyHeight);
    pushPoint('draw', cx, cy - peakHeight);
    pushPoint('draw', cx + hw * 0.3, cy - valleyHeight);
    pushPoint('draw', cx + hw * 0.65, cy - peakHeight);
    pushPoint('draw', cx + hw, cy);
}

// Draw player orb (circle with optional crown)
function drawPlayer(cx, cy, radius, hasCrown, crownWidth) {
    drawFilledCircle(cx, cy, radius, 4);

    if (hasCrown) {
        const cw = crownWidth || radius * 1.2;
        const ch = radius * 0.6;
        drawCrown(cx, cy - radius - 1, cw, ch);
    }
}

// Perspective transformation
const bottomY = 65;

function perspectiveX(x, y) {
    const depth = (y - horizonY) / (bottomY - horizonY);
    const vanishX = 0;
    return vanishX + (x - vanishX) * depth;
}

// Draw perspective grid line from bottom to horizon
function drawPerspectiveLine(bottomX, topY) {
    const segments = 25;
    let first = true;
    for (let i = 0; i <= segments; i++) {
        const t = i / segments;
        const y = bottomY + (topY - bottomY) * t;
        const x = perspectiveX(bottomX, y);
        pushPoint(first ? 'jump' : 'draw', x, y);
        first = false;
    }
}

// Draw horizontal line with perspective
function drawHorizontalLine(y, leftX, rightX) {
    const segments = 40;
    let first = true;
    for (let i = 0; i <= segments; i++) {
        const t = i / segments;
        const baseX = leftX + (rightX - leftX) * t;
        const x = perspectiveX(baseX, y);
        pushPoint(first ? 'jump' : 'draw', x, y);
        first = false;
    }
}

// === BUILD THE SCENE ===

const gridTop = horizonY + 3;
const safeZoneTop = bottomY - safeZoneHeight;

// Horizon glow (bright line at vanishing point)
const glowY = horizonY;
for (let i = 0; i < 4; i++) {
    const spread = 12 - i * 2.5;
    pushPoint('jump', -spread, glowY + i * 0.4);
    pushPoint('draw', spread, glowY + i * 0.4);
}

// Small bright arc at horizon center
for (let i = 0; i <= 10; i++) {
    const angle = Math.PI + (i / 10) * Math.PI;
    const x = Math.cos(angle) * 6;
    const y = glowY + Math.sin(angle) * 2.5 + 1;
    pushPoint(i === 0 ? 'jump' : 'draw', x, y);
}

// Vertical perspective lines
for (let i = 0; i <= gridLines; i++) {
    const t = i / gridLines;
    const bottomX = -95 + t * 190;
    drawPerspectiveLine(bottomX, gridTop);
}

// Horizontal grid lines with perspective spacing
const hLineCount = 14;
for (let i = 0; i <= hLineCount; i++) {
    const t = i / hLineCount;
    // Non-linear spacing for perspective effect
    const tCurved = Math.pow(t, 1.4);
    const y = bottomY - (bottomY - gridTop) * tCurved;
    drawHorizontalLine(y, -95, 95);
}

// Safe zone boundary (brighter horizontal lines)
for (let thickness = 0; thickness < 3; thickness++) {
    drawHorizontalLine(safeZoneTop + thickness * 0.6, -95, 95);
}

// Frame edges (diagonal sides of the trapezoid)
pushPoint('jump', perspectiveX(-95, gridTop), gridTop);
pushPoint('draw', perspectiveX(-95, bottomY), bottomY);
pushPoint('jump', perspectiveX(95, gridTop), gridTop);
pushPoint('draw', perspectiveX(95, bottomY), bottomY);

// Bottom edge
pushPoint('jump', -95, bottomY);
pushPoint('draw', 95, bottomY);

// === PLAYERS ===

// Draw all players with perspective scaling
function drawPlayerWithPerspective(baseX, baseY, baseRadius, hasCrown) {
    const px = perspectiveX(baseX, baseY);
    const py = baseY;

    // Scale radius based on depth
    const depthFactor = (baseY - horizonY) / (bottomY - horizonY);
    const scaledRadius = baseRadius * Math.pow(depthFactor, 0.5);

    if (scaledRadius > 1) {
        drawPlayer(px, py, scaledRadius, hasCrown, scaledRadius * 1.2);
    }
}

// Front row (in safe zone - some with crowns) matching original image
const frontRowY = 55;
drawPlayerWithPerspective(-50, frontRowY, 8, false);   // Orange left (no crown)
drawPlayerWithPerspective(-17, frontRowY, 9, true);    // Pink with crown
drawPlayerWithPerspective(17, frontRowY, 8, true);     // Cyan with crown
drawPlayerWithPerspective(50, frontRowY, 8, true);     // Light blue right with crown

// Middle row
const midRowY = 30;
if (players > 3) {
    drawPlayerWithPerspective(-30, midRowY, 7, false);  // Red
    drawPlayerWithPerspective(5, midRowY, 7, false);    // Green
    drawPlayerWithPerspective(35, midRowY, 6, false);   // Another
}

// Back row (near horizon)
const backRowY = 5;
if (players > 5) {
    drawPlayerWithPerspective(-15, backRowY, 5, false);  // Cyan small
    drawPlayerWithPerspective(18, backRowY, 5, false);   // Dark green
}

// Far back (tiny at horizon)
if (players > 7) {
    drawPlayerWithPerspective(0, -15, 4, false);
}

// === TITLE TEXT "Evades" ===

const titlePos = { x: -36, y: -80 };
const titleScale = 2;

// BitmapText utility code. Created by Mark Knol 2020
// https://turtletoy.net/turtle/48b904349d
class BitmapText {
    constructor(font, letterSpacing = 1, lineSpacing = 2) {
        this.font = font;
        this.letterSpacing = letterSpacing;
        this.lineSpacing = lineSpacing;
        
		// decode
        const data = this.font.data;
        this.bits = [];
    	let odd = true;
    	for(let ii=0; ii<data.length; ii++) {
    		for(let c=0;c<data.charCodeAt(ii) - 40; c++) {
    			this.bits.push(odd);
    		}
    		odd = !odd;
    	}
    }
    iter(str, cb) {
        this.get(str).forEach(cb);
    }
	get(str) {
		const positions = [];
		let line = 0;
        let offset = 0;
		const cpl = this.font.charsPerLine;
        for (let i=0; i<str.length; i++) {
            if (str.charAt(i) == "\n") { line ++; offset = 0; continue; }
            let char = str.charCodeAt(i) - 32; // start from space
            if (char < 0 || char > 3 * 32) continue;
            let drawPos = [offset++ * (this.font.size[0] + this.letterSpacing), line * (this.font.size[1] + this.lineSpacing)];
            let charPos = [char % cpl * (this.font.size[0] + this.font.spacing[0]), Math.floor(char / cpl) * (this.font.size[1] + this.font.spacing[1])];
            for (let y=0; y<this.font.size[1]; y++){
                for (let x=0; x<this.font.size[0]; x++) {
                    let dotPos = [x, y];
                    if (this.bits[charPos[0] + dotPos[0] + (charPos[1] + dotPos[1]) * ((this.font.size[0] + this.font.spacing[0]) * cpl)]) {
                        positions.push([drawPos[0] + dotPos[0], drawPos[1] + dotPos[1]]);
                    }
                }
            }
        }
		return positions;
	}
}

const BitmapTextFonts = {
    // based on <https://robey.lag.net/2010/01/23/tiny-monospace-font.html>
    SMALL: {
        data: "(-)*)))))))**)),)+),)))+)))=+*)*+)+)))))+)+)+)+)+3)-)++.)*))))+)*,)))))*)+)+)+)+)8)))))*),)+)))))))+)-)))))))))*)+)+)*+*),).).)))***)+)/)+)*+)+-+.)*)))*)*+**)+)+)++))+)+1)1)*)6+)**)+))).)+)+)+)+)/)*)+)))*)*)-)+)+)))))+)))))+)*)+)+)*+*)3).)))*),)**/)))+)))-)0).+*)*+)++))+)++))+)+-)-)-),)«)+)**+*)**+)+**)))))++)))))))+)))))))*)**+)**+*)+)))))))))))))))))))))+)+-+*).)))))))))))))+)))))+)+)+)))*),)))))))++)+)))))))))))))))))),)*)))))))))))))))))))+)))+)-)))))-+)+)**)+))))+)+)+)+*),))**)++)+)))))**))))+*)+)*))))))))+*)+)+)*),),)1)+)))))))))+)))))+)+)))))))*)*)))))))))+))))+))))))++)*,)*)*)))*)*+))))*)*)+)-)+)2*)))))*+*)**+)),*)))))+*)*))))+))))))))*)*),*)))))*+)+**)*)))))))*)*+)+-+-+©)/)1)/)-),),)-*G)C**)**+*)+*).)1)***)+*))3),)7)+)*),*)+9))))+*)+)+)***+.*)*+***)))))+)+)*+),)))))*)*+)*+)*))))))))+)*+)*)))))))))))))))****))1)-+-)))))))))+))))*+),)))))*),))*+)*+)))))))))*+*)),**)*))))+)+*),))*+)+)+).+-+)*+******)***)))*)***))))+))))))))*)*)-)))+*+)+**)*+)))))**+***)**.+©",
        size: [3,5],
        spacing: [1,1],
        charsPerLine: 32,
    },
    // based on <https://opengameart.org/content/ascii-bitmap-font-oldschool>
    NORMAL: {
        data: "(1)-))),)))-),*.+-)/),)X+-)5)-))),))),,***)*)*)-).).),))))),)E)*)+)+*5)3-*)))/)+)))4)0),+-)D)+)***)))5)4))),+-)-)5)0)+-*-1-3),))))),)5)3-,)))+)-)))))2)0),+-).);)-**),)B)))+,+)***)*)4).),))))),).):).)+),)5)4)))-)/*+*))4),)<)6)4++-ħ+,+-*+-+++-++,+O+,+,++,,++)+)*)+)+)))+).)2)*)+)*)+),).)/)3)-)+)*)+)*)+)*)+)*)+).).)*)*)+).)1)+)+)*)+):),-,),)+)*))+*)+)*)+)*)1)-*+-*,+,-)-+,,9)7).)+)))))*-*,+)0)0)-)/)*)+)+)-)+).)3).),-,).),))+*)+)*)+)*)/)-)+)-)/)*)+)+)-)+).),).)/)3)4).)+)*)+)*)+)*-++.)+,,+,).+,+3)D)-++)+)*,,+ħ,+-*-+++)+)*-*-*)+)*).)+)*)+)+++,,++,,,*-*)+)*)+)*).).)+)*)+),)0)*)+)*).*)**)+)*)+)*)+)*)+)*)+)*)0),)+)*)+)*).).).)+),)0)*)*)+).)))))***)*)+)*)+)*)+)*)+)*)0),)+)*)+)*,+,+).-,)0)*+,).)+)*)))))*)+)*,+)+)*,,+-),)+)*)+)*).).)***)+),)0)*)*)+).)+)*)***)+)*).)))))*)+).),),)+)*)+)*).).)+)*)+),),)+)*)+)*).)+)*)+)*)+)*).)*)+)+).),),)+)*,+-*)/++)+)*-+++)+)*-*)+)*)+)+++)/*))*)+)*,-)-+ħ)+)*)+)*)+)*)+)*-,*3*.)4)4)9)3*2)+)*)+)*)+)*)+).),),)0)-)))4)3)9)2)4)+)*)+)+))),))).)-)-)/)B++,,+,,+++-+,*)+)*)+),).).).).).)E)*)+)*)+)*)+)*)+)+)-)+)*)+)*)))))+)))-)-)/)/)-)B,*)+)*).)+)*-+).,+)))+*)**)+),),)0)0),)A)+)*)+)*)+)*)+)*)/)1),),)+)*)+),),-,*3*3-2,*,,+,,+,+).+ħ)0)0)*).)`)P)<).)`)P,++.+*)*)+).*))+,,++,,,*))*,,*,+)+)*)+)*)+)*)+)*)+)*)+),)0)*+,).)))))*)+)*)+)*)+)*)+)***)*)/)-)+)*)+)*)+)+)))+)+)*)+),)0)*)*)+).)))))*)+)*)+)*,,,*)/+,)-)+)*)+)*)+),)-,*)+),),)+)*)+)*).)+)*)+)*)+)*)2)*)2)+)*)*)+)+)))+)))))+)))/)*)+)*-+++)+)+++)+)*)+)+++)2)*).,-*,,,)-)))+)+)++ı)-)-)).).)-,).).)-)*)),)/)/)+))*).).).))/).).)--)-)-)Ƌ",
        size: [5,7],
        spacing: [2,2],
        charsPerLine: 18,
    }
};

function drawBitmapTextToSteps(str, font, x, y, scale, diagonal1 = false, diagonal2 = false) {
    const text = new BitmapText(font);
    text.iter(str, pos => {
        const a = [x + pos[0] * scale, y + pos[1] * scale];
        const b = [a[0] + scale, a[1] + scale];
        pushPoint('jump', a[0], a[1]);
        pushPoint('draw', b[0], a[1]);
        pushPoint('draw', b[0], b[1]);
        pushPoint('draw', a[0], b[1]);
        pushPoint('draw', a[0], a[1]);
        if (diagonal1) pushPoint('draw', b[0], b[1]);
        if (diagonal2) {
            pushPoint('jump', a[0], b[1]);
            pushPoint('draw', b[0], a[1]);
        }
    });
}

// Draw title - "Evades" using bitmap font (with slight offset shadow)
drawBitmapTextToSteps("Evades", BitmapTextFonts.NORMAL, titlePos.x, titlePos.y, titleScale);
drawBitmapTextToSteps("Evades", BitmapTextFonts.NORMAL, titlePos.x + 0.6, titlePos.y + 0.6, titleScale);

function walk(i) {
    const s = steps[i];
    if (!s) return false;
    if (s.kind === 'jump') turtle.jump(s.x, s.y);
    else turtle.goto(s.x, s.y);
    return true;
}