xxxxxxxxxx
// Non-circular packing
// Pixel comparison used to draw nestled bananas 🍌 (like circle-packing, but for irregular shapes)
// uses png images with transparency
// see it in action here: https://twitter.com/ippsketch/status/1393210340608024582?s=20
// tutorial here: https://ippsketch.com/posts/non-circular-packing/
// for @sableRaph (Twitter) weekly coding challenge
// NOTE
// This code is not optimized
// it runs slowly in browser, especially when the canvas size is increased (bc it reads pixels)
// I think it's better for generating a final image or GIFs than viewing in real time
// there also might be a better way to do this, or to optimize what is here. I'm very open to feedback and suggestions.
// 🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌
// SELECT AND PRELOAD THE IMAGE 🖼️
let img; // preload the image file
function preload() { // TRY SOME DIFFERENT IMAGES ❗❗❗👋❗❗❗
img = loadImage('./banana_peeled.png'); // a peeled banana 🍌
// img = loadImage('./banana.png'); // an unpeeled banana 🍌
// img = loadImage('./kingSpades.png'); //i++sketch profile pic
// img = loadImage('./owl.png'); // killer birb
}
// set these based on size of image, and how big and small of bananas 🍌 you're comfortable with
let imageScaleMin = .1;
let imageScaleMax = .7;
let imageScaleNum = 10; // scale steps to try when fittting (more = slower but tighter fit)
let imageScaleDelta = (imageScaleMax - imageScaleMin) / imageScaleNum;
let gb; // graphics layer for banana
let ib = 0; // banana 🍌 index
let spacesFilled = false; // after the following number of attempts, stop looping
let attemptCount = 100; // how many times to try to fit an image before giving up
let banana = []; // array of bananas
function setup() {
createCanvas(500, 500); // windowHeight/Width isn't used b/c it can add a ton of pixels (not great for this method)
pixelDensity(1)
gb = createGraphics(height, width); // graphic layer for drawn bananas 🍌
}
// 🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌
// DRAW BANANAS! 🍌
function draw() {
// find locations where bananas 🍌 (or borders) already are
if (spacesFilled == false) {
gb.loadPixels() // get the pixels of the drawn bananas 🍌
let np = 0; // no pixel array index
let loc = []; // location array of current bananas/borders
let nopx = []; //array where there are no image pixels (blank space for placing next banana 🍌)
for (let p = 0; p < gb.pixels.length; p += 4) { // step through each position
let li = floor(p / 4); //location index
if (gb.pixels[p + 3] > 0) { //if pixel not transparent
loc[li] = 1; // mark this location with a '1'
} else { // if pixel is transparent, save the location into the nopx array
loc[li] = 0; // this is a transparent pixel, mark with '0'
nopx[np++] = li;
}
}
banana[ib] = new Banana(loc, nopx) // make new banana 🍌, pass the pixel location data
banana[ib].drawBanana(gb); // draw the banana 🍌 on the graphics layer
ib++;
}
fill(0); stroke(255); strokeWeight(1);
rect(0, 0, width, height); // draw a black background for the bananas 🍌
image(gb, 0, 0, width, height) // draw the banana 🍌 graphic layer
if (spacesFilled == true) noLoop(); // stop the animation when all spots are filled
}
// 🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌🍌
// BANANA CLASS 🍌
// Banana class 🍌
class Banana {
constructor(loc, nopx) { // loc (=1 where already banana), nopx (array of blank spots)
let gt = createGraphics(width, height);
let ripeFlag = false;
let iScale = 0;
let iTry = 0;
while (ripeFlag == false && iScale < imageScaleNum) { // grow banana 🍌 until fit
// pick random parameters for banana 🍌 location
if (iScale==0){
let location = random(nopx);
this.x = location % width;
this.y = floor(location / width) + 1;
this.theta = random(TWO_PI);
// this.theta = 0; // TRY KEEPING IMAGES ORIENTAED THE SAME WAY ❗❗❗👋❗❗❗
this.imageScale = imageScaleMin;
}
// Draw banana 🍌 on temporary graphics layer
gt.clear();
this.drawBanana(gt)
gt.loadPixels();
// iterate through pixels of new banana 🍌 and see if it overlaps old bananas 🍌
for (let p=0; p<gt.pixels.length; p+=4){
let li = floor(p / 4);
let x = li % width;
let y = floor(li / width);
let border = false;
if (x == 0 || x == gt.width - 1 || y == 0 || y == gt.height - 1) border = true;
if (gt.pixels[p + 3] > 0 && (border == true || loc[li] == 1)) { //collision with another 🍌 banana
if (iScale == 0) { // pick a new location to try
iScale = -1; // this resets iScale to zero after the while loops completes, so new location will be picked
iTry++;
if (iTry > attemptCount) {
spacesFilled = true;
}
} else { //if this was a collsion after the banana 🍌 was grown, it's a good location, but we can stop growing now
ripeFlag = true; // banana 🍌 is ripe, it may be drawn
}
break;
}
}
if (ripeFlag == true) this.imageScale = this.imageScale - imageScaleDelta; //make banana 🍌 a little smalller since it collided with another banana
else this.imageScale = this.imageScale + imageScaleDelta; // no collision, grow the banana 🍌 a litte bit
iScale++;
}
} // constructor
drawBanana(tg) { // draw a banana 🍌 onto target graphics (tg)
tg.push();
tg.translate(this.x, this.y);
tg.rotate(this.theta);
tg.imageMode(CENTER);
tg.scale(this.imageScale);
tg.image(img, 0, 0);
tg.pop();
}
} // class