Mini art wall 🖼️

Grid with small classic generative pieces

Log in to post a comment.

const grid = 6; // min=2 max=20 step=1
const gap = 4.5;       // min=0 max=10 step=0.1
const space = 6;       // min=0 max=20 step=0.1
const size = 180; // min=50, max=200, step=5

const turtle = new Turtle();

function shuffler(length) {
    let arr = [];
    return () => {
        if (arr.length === 0) {
            arr = Array.from({length}, (_,i) => i);
            arr.sort(() => -0.5 + Math.random());
        }
        return arr.pop();
    }
}
const totalTypes = 17;
const getRandomType = shuffler(totalTypes);

function walk(i) {
    const row = (i % grid);
    const col = (i / grid | 0);
    const gs = gap + space;
    const tileSize = size / grid - gs;
    const x = -size/2 + (row / grid) * size + gs / 2;
    const y = -size/2 + (col / grid) * size + gs / 2;
    const tile = {
        x,y,
        w: tileSize, h: tileSize,
        cx: x + tileSize / 2, cy: y + tileSize / 2,
        type: getRandomType(),
        col,
        row,
    }
    drawTile(tile);
    return i < grid*grid - 1;
}

// Liang-Barsky line clipping to ensure we never draw outside the tile
const clipLine = (x1, y1, x2, y2, bounds) => {
    const {minX,minY,maxX,maxY} = bounds;
    let t0 = 0, t1 = 1;
    const dx = x2 - x1, dy = y2 - y1;
    const p = [-dx, dx, -dy, dy];
    const q = [x1 - minX, maxX - x1, y1 - minY, maxY - y1];

    for (let k = 0; k < 4; k++) {
        if (p[k] === 0) { if (q[k] < 0) return; } 
        else {
            const r = q[k] / p[k];
            if (p[k] < 0) { if (r > t1) return; else if (r > t0) t0 = r; } 
            else { if (r < t0) return; else if (r < t1) t1 = r; }
        }
    }
    if (t0 <= t1) {
        turtle.jump(x1 + t0 * dx, y1 + t0 * dy);
        turtle.goto(x1 + t1 * dx, y1 + t1 * dy);
    }
};

const globalAngle = (Math.random() * 36 | 0) * 10;

function drawTile(t) {
    const minX = t.x, maxX = t.x + t.w;
    const minY = t.y, maxY = t.y + t.h;
    const bounds = { minX, maxX, minY, maxY };

    if ([2,5,6,7,10,13].includes(t.type) || Math.random() < 0.2) {
        let s1 = space/2;
        turtle.jump(t.x-s1, t.y-s1);
        turtle.goto(t.x+t.w+s1, t.y-s1);
        turtle.goto(t.x+t.w+s1, t.y+t.h+s1);
        turtle.goto(t.x-s1, t.y+t.h+s1);
        turtle.goto(t.x-s1, t.y-s1);
        if (Math.random()>0.5) {
            s1+=1;
            turtle.jump(t.x-s1, t.y-s1);
            turtle.goto(t.x+t.w+s1, t.y-s1);
            turtle.goto(t.x+t.w+s1, t.y+t.h+s1);
            turtle.goto(t.x-s1, t.y+t.h+s1);
            turtle.goto(t.x-s1, t.y-s1);
        }
    }

    // Draw pattern based on type
    switch (t.type) {
        case 0: { // Circles (offset on odd/even)
            const s = 5+Math.random()*4|0;
            const r = t.w/40;
            for(let y=0;y<s;y++) for(let x=y%2?0.5:0;x<s;x++) {
                if(Math.random() > 0.1) {
                    turtle.jump(t.x+ x/s * t.w + r, t.y + y/s*t.h + r*1.5);
                    turtle.circle(r);
                }
            }
            break;
        }
        case 1: {// Stripes (hashed lines)
            const dist = Math.random()>0.5 ? 2 : 4;
            const rad = globalAngle * Math.PI / 180;
            const dirX = Math.cos(rad), dirY = Math.sin(rad);
            const perpX = -dirY, perpY = dirX;
            const diag = Math.hypot(t.w, t.h);
            for (let d = -diag; d < diag; d += dist) {
                const px = t.cx + d * perpX, py = t.cy + d * perpY;
                clipLine(px - diag * dirX, py - diag * dirY, px + diag * dirX, py + diag * dirY, bounds);
            }
            break;
        }
        case 2: { 
            // Connected (dots + lines) 
            const d = 0.25;
            const s = 7;
            const pts = Array.from({ length: s*s }, (_,i) => [minX + ((i%s)/(s-1)) * t.w + (-1+Math.random()*2)*t.w/s, minY +(i/s|0)/(s-1)  * t.h + (-1+Math.random()*2)*t.h/s]);
            //pts.forEach(p => { turtle.jump(p[0], p[1] - 0.5); turtle.circle(0.5); });
            for (let a of pts) for (let b of pts) {
                if (a !== b && Math.hypot(a[0] - b[0], a[1] - b[1]) < t.w * d)
                    clipLine(a[0], a[1], b[0], b[1], bounds);
            }
            break;
        }
        case 3: // Plus grid
            const s = t.w / 8;
            for (let px = minX + s / 2; px < maxX; px += s)
                for (let py = minY + s / 2; py < maxY; py += s) {
                    
                    if(Math.random() > 0.2)clipLine(px - s / 3, py, px + s / 3, py, bounds);
                    if(Math.random() > 0.2)clipLine(px, py - s / 3, px, py + s / 3, bounds);
                    
                }
            break;

        case 4: // Round (arcs from left)
            const dist = Math.random()>0.5 ? 2 : 4;
            const startX = minX + (maxX-minX)*Math.random()
            const startY = minY + (maxY-minY)*[-0.5, 0.5, 1.5][Math.floor(Math.random() * 3)];
            for (let rad = 0; rad < t.w * 2.5; rad += dist) {
                let lx = null, ly = null;
                for (let a = 0; a <= Math.PI*2; a += 0.2) {
                    const nx = startX + rad * Math.cos(a), ny = startY + rad * Math.sin(a);
                    if (lx !== null) clipLine(lx, ly, nx, ny, bounds);
                    lx = nx; ly = ny;
                }
                const nx = startX + rad * Math.cos(0), ny = startY + rad * Math.sin(0);
                clipLine(lx, ly, nx, ny, bounds);
            }
            break;

        case 5: // Outside-to-circle
            const circleRadius = t.h/[10,5][Math.random()*2|0];
            const totalLines = 6;
            for (let side = 0; side < 4; side++) {
                for (let k = 0; k < totalLines; k++) {
                    const tVal = k / (totalLines-1);
                   let sx, sy;
                    
                    if (side === 0) {
                        sx = minX + tVal * t.w;
                        sy = minY;
                    } else if (side === 1) {
                        sx = maxX;
                        sy = minY + tVal * t.h;
                    } else if (side === 2) {
                        sx = maxX - tVal * t.w;
                        sy = maxY;
                    } else {
                        sx = minX;
                        sy = maxY - tVal * t.h;
                    }
                    const theta = Math.atan2(sy - t.cy, sx - t.cx);
                    clipLine(sx, sy, t.cx + circleRadius * Math.cos(theta), t.cy + circleRadius * Math.sin(theta), bounds);
                }
            }
            break;

        case 6: // Isometric (triangular grid)
            [30, 90, 150].forEach(deg => {
                const rad = deg * Math.PI / 180;
                const dX = Math.cos(rad), dY = Math.sin(rad);
                const pX = -dY, pY = dX;
                const dia = Math.hypot(t.w, t.h);
                for (let d = -dia; d < dia; d += 6) {
                    const cx = t.cx + d * pX, cy = t.cy + d * pY;
                    clipLine(cx - dia * dX, cy - dia * dY, cx + dia * dX, cy + dia * dY, bounds);
                }
            });
            break;
            
        case 7: { // 10 PRINT (Random Diagonals)
            const step = t.w / 8;
            for (let x = minX; x < maxX - 1; x += step) {
                for (let y = minY; y < maxY - 1; y += step) {
                    if (Math.random() > 0.5) clipLine(x, y, x + step, y + step, bounds);
                    else clipLine(x + step, y, x, y + step, bounds);
                }
            }
            break;
        }
        case 8: { // Concentric Squares (Tunnel)
            const centerX = t.cx + (Math.random() - 0.5) * t.w * 0.5;
            const centerY = t.cy + (Math.random() - 0.5) * t.h * 0.5;
            for (let i = 0; i < t.w / 2; i += 2) {
                const f = i / (t.w / 2); // 0 to 1
                // Lerp from bounds to center point
                const x1 = minX * (1 - f) + centerX * f;
                const x2 = maxX * (1 - f) + centerX * f;
                const y1 = minY * (1 - f) + centerY * f;
                const y2 = maxY * (1 - f) + centerY * f;
                // Draw manual rect to ensure clipping
                clipLine(x1, y1, x2, y1, bounds);
                clipLine(x2, y1, x2, y2, bounds);
                clipLine(x2, y2, x1, y2, bounds);
                clipLine(x1, y2, x1, y1, bounds);
            }
            break;
        }
        case 9: { // Sine Waves
            const amp = t.h / 10;
            const freq = (Math.random() * 0.25) + 0.25;
            const steps = Math.random()>0.5 ? 2 : 4;
            const offset = Math.random() * t.w/10;
            for (let y = minY-offset; y < maxY + offset; y += steps) {
                let lx = minX;
                let ly = y + Math.sin((lx - minX) * freq) * amp; 
                for (let x = minX + 2; x <= maxX; x += 2) {
                    const ny = y + Math.sin((x - minX) * freq) * amp;
                    clipLine(lx, ly, x, ny, bounds);
                    lx = x; ly = ny;
                }
            }
            break;
        }
        case 10: { // empty box 
        
            break;
        }
        case 11: { // Truchet Arcs
            const step = t.w / 6;
            for (let x = minX; x < maxX - 0.1; x += step) {
                for (let y = minY; y < maxY - 0.1; y += step) {
                    const cx = x + step/2, cy = y + step/2;
                    const r = step/2;
                    // Randomly choose orientation
                    if (Math.random() > 0.5) {
                        // Top-Left & Bottom-Right
                         drawArc(x, y, r, 0, Math.PI/2, bounds); // approximate via small lines
                         drawArc(x+step, y+step, r, Math.PI, 1.5*Math.PI, bounds);
                    } else {
                        // Top-Right & Bottom-Left
                        drawArc(x+step, y, r, Math.PI/2, Math.PI, bounds);
                        drawArc(x, y+step, r, 1.5*Math.PI, 2*Math.PI, bounds);
                    }
                }
            }
            break;
        }
        
       case 12: { // ZigZags (Fixed)
            const d = 6;            // Width of each zig/zag
            const a = t.h / 12;     // Amplitude (height)
            const space = a * 2.5;  // Spacing between rows

            for (let y = minY + a; y < maxY; y += space) {
                let lx = minX; 
                let ly = y + a; // Start at the "High" point
                
                // Use a counter 'i' to guarantee perfect alternation
                for (let i = 0; lx < maxX; i++) {
                    const nx = lx + d;
                    // Toggle: Even = Low, Odd = High
                    const ny = y + (i % 2 === 0 ? -a : a);
                    
                    clipLine(lx, ly, nx, ny, bounds);
                    
                    lx = nx; 
                    ly = ny;
                }
            }
            break;
        }

        case 13: { // Sunburst / Radial Lines
            const rays = 12 + Math.random() * 10;
            const originX = t.cx + (Math.random() - 0.5) * t.w * 0.5;
            const originY = t.cy + (Math.random() - 0.5) * t.h * 0.5;
            const radius = Math.hypot(t.w, t.h); // Ensure it covers corners
            
            for (let i = 0; i < rays; i++) {
                const theta = (i / rays) * Math.PI * 2;
                const ex = originX + Math.cos(theta) * radius;
                const ey = originY + Math.sin(theta) * radius;
                clipLine(originX, originY, ex, ey, bounds);
            }
            break;
        }

        case 14: { // Brick Wall
            const rowH = t.h / 8-0.001;
            const brickW = t.w / 4;
            for (let y = minY; y < maxY; y += rowH) {
                // Horizontal mortar
                clipLine(minX, y, maxX, y, bounds);
                
                // Vertical joints (offset every other row)
                const offset = ((y ) / rowH | 0) % 2 === 0 ? 0 : brickW / 2;
                for (let x = minX + offset; x < maxX; x += brickW) {
                    clipLine(x - 1, y, x-1, y + rowH, bounds);
                }
            }
            break;
        }

        case 15: { // Rectangular Spiral
            let x = t.cx, y = t.cy;
            let len = 2;
            const gap = 1;
            // 0: up, 1: right, 2: down, 3: left
            let dir = 0; 
            
            // Limit loop to prevent infinite run if bounds are huge
            for(let i=0; i<50; i++) {
                let nx = x, ny = y;
                if (dir === 0) ny -= len;
                if (dir === 1) nx += len;
                if (dir === 2) ny += len;
                if (dir === 3) nx -= len;

                clipLine(x, y, nx, ny, bounds);
                
                x = nx; y = ny;
                len += gap; // Spiral gets wider
                dir = (dir + 1) % 4;
                
                // Stop if we are fully outside bounds (optimization)
                if(x < minX - gap && nx < minX - gap) break;
                if(x > maxX + gap && nx > maxX + gap) break;
                if(y < minY - gap && ny < minY - gap) break;
                if(y > maxY + gap && ny > maxY + gap) break;
            }
            break;
        }

        case 16: { // Nested Rotated Squares
            let size = Math.min(t.w, t.h);
            let cx = t.cx, cy = t.cy;
            let angle = 0;
            // Recursively draw squares shrinking in size
            while (size > 2) {
                const rad = angle;
                const c = Math.cos(rad) * size / 2;
                const s = Math.sin(rad) * size / 2;
                
                const p1x = cx - c - s, p1y = cy + s - c; // Top-Left-ish
                const p2x = cx + c - s, p2y = cy - s - c; // Top-Right-ish
                const p3x = cx + c + s, p3y = cy - s + c; // Bottom-Right-ish
                const p4x = cx - c + s, p4y = cy + s + c; // Bottom-Left-ish

                clipLine(p1x, p1y, p2x, p2y, bounds);
                clipLine(p2x, p2y, p3x, p3y, bounds);
                clipLine(p3x, p3y, p4x, p4y, bounds);
                clipLine(p4x, p4y, p1x, p1y, bounds);

                size *= 0.8; // Shrink
                angle += 0.15; // Rotate
            }
            break;
        }
        default:
            break;
    }
}

// Helper for Case 11 to draw clipped arcs
function drawArc(cx, cy, r, startAngle, endAngle, bounds) {
    const steps = 6;
    let la = startAngle;
    for(let i=1; i<=steps; i++) {
        const na = startAngle + (endAngle - startAngle) * (i/steps);
        clipLine(
            cx + Math.cos(la)*r, cy + Math.sin(la)*r,
            cx + Math.cos(na)*r, cy + Math.sin(na)*r,
            bounds
        );
        la = na;
    }
}