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;
}