Rectangle attraction 🧲

There are x amount of rectangles. Each iteration they are moved towards one or more attraction points until they collide with some other rectangle. The more iterations, the more likely they will "fit" better. There's also an option to allow variance in sizing to create some interesting outputs.

Note; if sizes are set too high and there are too many rectangles, it wont resolve pretty.

Log in to post a comment.

const seed = 1 // min=1, max=100, step=1
const totalAttractionPoints = 0 // min=0, max=5, step=1
const totalRectangles = 100 // min=5, max=1000, step=1
const totalIterations = 50 // min=0, max=300, step=1
const minSize = 2 // min=1, max=30, step=1
const maxSize = 20 // min=2, max=30, step=1
const canChangeWidth = 0 // min=0, max=1, step=1 (No,Yes)
const canChangeHeight = 0 // min=0, max=1, step=1 (No,Yes)
const useSpace = 1 // min=0, max=1, step=1 (No,Yes)

const random = new Random(seed)

const turtle = new Turtle()
const grid = Math.ceil(Math.sqrt(totalRectangles))

const attractionPoints = [...Array(totalAttractionPoints).keys()].map(i => [-75 + random.next() * 150, -75 + random.next() * 150])

const rects = [...Array(totalRectangles).keys()].map(i => [
    -100 + (i % grid) * (200 / grid) | 0, 
    -100 + (i / grid | 0) * (200 / grid) | 0,
    (minSize + random.next() * (maxSize - minSize)) | 0,  
    (minSize + random.next() * (maxSize - minSize)) | 0
])

const debug = 0 // min=0, max=1, step=1 (No,Yes)
if (debug) {
    if (!attractionPoints.length) (turtle.jump([0,-25]), turtle.circle(25))
    attractionPoints.forEach(p => (turtle.jump([p[0], p[1] - 25]), turtle.circle(25)))
}

const _ = [...Array(totalIterations).keys()].forEach(iteration => {
    // shuffle order of doing stuff
    random.shuffle(rects);
    
    const centerAtStart = getCenter(rects)
    rects.forEach(currentRect => {
        const attractionPoint = attractionPoints.length ? attractionPoints[random.next() * attractionPoints.length | 0] : getCenter(rects)
        let tries = 0
        const prev = [0,0,0,0]
        while (tries++ < 50) {
            copyRectFromTo(currentRect, prev)
            
            // move maybe horizontal or vertical
            if (random.next() > 0.5) {
                if (currentRect[0] < attractionPoint[0]) currentRect[0] += 1
                else currentRect[0] -= 1
            } 
            if (random.next() > 0.5) {
                if (currentRect[1] < attractionPoint[1]) currentRect[1] += 1
                else currentRect[1] -= 1
            }
            // change size maybe horizontal or vertical
            if (canChangeWidth && random.next() > 0.75) {
                currentRect[2] = Math.min(maxSize, Math.max(minSize, currentRect[2] + (random.next() > 0.5 ? -1 : 1)))
            }
            if (canChangeHeight && random.next() > 0.75) {
                currentRect[3] = Math.min(maxSize, Math.max(minSize, currentRect[3] + (random.next() > 0.5 ? -1 : 1)))
            }
            
            // test if collide
            for (let rect of rects) {
                if (rect != currentRect) {
                    if (rectToRect(currentRect, rect)) {
                        // put back to point where it didnt collide with something
                        copyRectFromTo(prev, currentRect)
                         // stop iteration
                        return
                    }
                }
            }
        }
    })
})

function walk() {
    drawRect(rects.shift())
    return rects.length
}

function rectToRect(r1, r2) {
    const [r1x,r1y,r1w,r1h] = r1
    const [r2x,r2y,r2w,r2h] = r2
    if (!(r1x > r2x + r2w || r1x + r1w < r2x || r1y > r2y + r2h || r1y + r1h < r2y)) {    // r1 bottom edge past r2 top
        return true
    }
    return false
}

function copyRectFromTo(from, to) {
    to[0] = from[0]
    to[1] = from[1]
    to[2] = from[2]
    to[3] = from[3]
}

function getCenter(rects) {
    const center = [0,0]
    const total = rects.length
    for(let rect of rects) {
        center[0] += (rect[0] + rect[2] / 2) / total
        center[1] += (rect[1] + rect[3] / 2) / total
    }
    return center
}

function drawRect(rect) {
    let [x,y,w,h] = rect
    if (!useSpace) {
        x -= 1
        y -= 1
        w += 1
        h += 1
    }
    turtle.jump(x,y)
    turtle.goto(x+w,y)
    turtle.goto(x+w,y+h)
    turtle.goto(x,y+h)
    turtle.goto(x,y)
}


// Seeded random - Mulberry32
function Random(seed) {
    class Random {
        constructor(seed) { 
            this.seed = seed
        }
        next() { 
            var t = this.seed += 0x6D2B79F5
            t = Math.imul(t ^ t >>> 15, t | 1)
            t ^= t + Math.imul(t ^ t >>> 7, t | 61)
            return ((t ^ t >>> 14) >>> 0) / 4294967296
        }
        shuffle(arr) {
            for (let i = arr.length - 1; i > 0; i--) {
                const j = Math.floor(this.next() * (i + 1));
                [arr[i], arr[j]] = [arr[j], arr[i]];
            }
            return arr
        }
    }
    return new Random(seed)
}