Twitter Joints and Project Goods

Bradley Momberger's project notes, & other software stuff that's interesting.

Art Bot Walkthrough 2: Failures at Refinement

Bradley Momberger •

0: the refresher

Last time we went through the process of prototyping an approximation of random Mondrian paintings. Using randomly spaced vertical lines and randomly spaced horizontal dividers, we got a close approximation. For reference, here’s another example of the process in action.

randomly generated Mondrian of some complexity

Now we have an issue to work through, in that a real Mondrian isn’t perfectly divided along the vertical lines. Let’s try to account for that. One possibility is to forgo the line drawing, and instead focus exclusively on drawing the rectangles.

1: Reimagining the paint process

First we’ll define a grid by making yparts the same as xparts.

var yparts = [];
while(asum(yparts) < 512) {
  yparts.push(Math.floor(Math.pow(Math.random() * 14, 2)) + 50);
}

Now we have a set of xparts (vertical lines) and a set of yparts (horizontal lines). The fills will now be changed so that every single grid item is a rectangle, either white or a primary-ish color.

var fills = [];
xparts.forEach(function() {
  fills.unshift([]);
  yparts.forEach(function() {
    fills[0].push("rgb(255, 255, 255)");
  });
});
var count = Math.floor(
  Math.random() * xparts.length * yparts.length / 2
) + 1;
var fillcolors = ["rgb(255, 255, 0)", "rgb(255, 0, 0)", "rgb(0, 0, 255)"];
var xp, yp, fill;
while(count--) {

  xp = Math.floor(Math.random() * xparts.length);
  yp = Math.floor(Math.random() * yparts.length);
  fill = fillcolors[Math.floor(Math.random() * fillcolors.length)];

  fills[xp][yp] = fill;
}

And our fills are now a bit simpler with the addition of a second forEach() call.

fills.forEach(function(xfills, i) {
  xfills.forEach(function(xyfill, j) {
    var xoff, yoff, w, h;

    context.fillStyle = xyfill;
    xoff = i && asum(xparts.slice(0, i)) + 7;
    yoff = j && asum(yparts.slice(0, j)) + 7;
    w = xparts[i] - (xoff ? 7 : 0);
    h = yparts[j] - (yoff ? 7 : 0);

    context.fillRect(xoff, yoff, w, h);
  });
});

And some sample output:

more rigid grid with some red and blue blocks

Now we’re looking at just a straight grid (horizontal and vertical lines both going full width/height), but otherwise it’s similar to our previous exercise. Here’s where it can be a bit more interesting, though; instead of introducing jitter within the grid by line offsets, let’s combine some of the blocks to the left and top.

2: Degridifying

Going back to how we defined fills, we have this line:

var fillcolors = ["rgb(255, 255, 0)", "rgb(255, 0, 0)", "rgb(0, 0, 255)"];

To this we can add, say “left” and “top”, which we’ll use when drawing fills to just continue the fill from the left or from above, except when we’re already at the left or top margin.

var fillcolors = [
  "rgb(255, 255, 0)", "rgb(255, 0, 0)", "rgb(0, 0, 255)", 
  "left", "left", "left", "top", "top", "top"];
var xp, yp, fill;
while(count--) {

  xp = Math.floor(Math.random() * xparts.length);
  yp = Math.floor(Math.random() * yparts.length);
  fill = fillcolors[
           Math.floor(
             Math.random() 
              * (xp * yp === 0 ? 3 : fillcolors.length)
            )
         ];

  fills[xp][yp] = fill;
}

And now we change the fill drawing to look like this:

fills.forEach(function(xfills, i) {
  xfills.forEach(function(xyfill, j) {
    var xoff, yoff, w, h;
    var offsetTop = j !== 0 && xyfill !== "top";
    var offsetLeft = i !== 0 && xyfill !== "left";

    if(xyfill === "left") {
      context.fillStyle = fills[i - 1][j];
    } else if(xyfill === "top") {
      //reuse last fill style
    } else {
      context.fillStyle = xyfill;    
    }

    xoff = asum(xparts.slice(0, i)) + (offsetLeft ? 7 : 0);
    yoff = asum(yparts.slice(0, j)) + (offsetTop ? 7 : 0);
    w = xparts[i] - (offsetLeft ? 7 : 0);
    h = yparts[j] - (offsetTop ? 7 : 0);

    context.fillRect(xoff, yoff, w, h);
  });
});

The major bit here is that we’ve now stopped using the offsets as the determinant for whether the grid cell should be offset by 7px (to show a black divider), and instead base it on whether the index is zero in one dimension or whether the fill style is “left” or “top”. So running this a few times so it’s illustrative (a problem soon to be addressed here), we get this output.

small number of grid cells shown

But we also get sample output looking like this.

full grid, only one blue square

This indicates that we need to tweak the randomizer somewhat so that it looks a little bit less mechanical.

3: Failed improvements

The rest of this post, as indicated by the title, is reserved for things which didn’t really work as expected.

3.1: More randomizer

var count = Math.floor(
  Math.random() * xparts.length * yparts.length * (1 - 1 / Math.E)
) + 4;

for the number of backtracking or colored fills makes for not much more interesting things. The count could be as little as 4 if the randomizer comes out small. A normal or poisson distribution with sqrt(xparts.length * yparts.length) * C + C for arbitrary constants C as the mean would likely be better in this case.

3.2: Backtracking over rows and columns

Sometimes if “left” and “top” are put in at just the right place, the result is a non-rectangular area where a top and a left extension are too close together.

showing a block that's irregular

To address this, I tried backtracking over every “left” and “top” cell until reaching something that was neither, eliminating the 7px left offset if a left was found, and the 7px top offset when a top was found. This changed the fill routine to the below.

fills.forEach(function(xfills, i) {
  xfills.forEach(function(xyfill, j) {
    var xoff, yoff, w, h, ref_i = i, ref_j = j;
    var offsetTop = j !== 0;
    var offsetLeft = i !== 0;

    while(xyfill === "left" || xyfill === "top") {
      if(xyfill === "left") {
        offsetLeft = false;
        xyfill = fills[ref_i -= 1][ref_j];
      }
      if(xyfill === "top") {
        offsetTop = false;
        xyfill = fills[ref_i][ref_j -= 1];
      }
    }
    context.fillStyle = xyfill;    

    xoff = asum(xparts.slice(0, i)) + (offsetLeft ? 7 : 0);
    yoff = asum(yparts.slice(0, j)) + (offsetTop ? 7 : 0);
    w = xparts[i] - (offsetLeft ? 7 : 0);
    h = yparts[j] - (offsetTop ? 7 : 0);

    context.fillRect(xoff, yoff, w, h);
  });
});

This has an unexpected side effect, though.

showing incomplete blocks

Still, it’s important to the final product that we are able to split up the lines with blocks spanning multiple columns. Another strategy is required here.

3.3: Second attempt at backtracking.

After some thought, incompletely bounded rectangles in part 3.2 was fairly easy to work out. Backtracking is still the method used in the code below, but instead of just drawing the current cell, we redraw the original top-left cell for the backtrack chain to be the width and height of all the blocks backtracked over.

fills.forEach(function(xfills, i) {
  xfills.forEach(function(xyfill, j) {
    var xoff = asum(xparts.slice(0, i)), 
        yoff = asum(yparts.slice(0, j)), 
        w = xparts[i], 
        h = yparts[j], ref_i = i, ref_j = j;

    while(xyfill === "left" || xyfill === "top") {
      if(xyfill === "left") {
        xyfill = fills[ref_i -= 1][ref_j];
        xoff -= xparts[ref_i];
        w += xparts[ref_i];
      }
      if(xyfill === "top") {
        xyfill = fills[ref_i][ref_j -= 1];
        yoff -= yparts[ref_j];
        h += yparts[ref_j];
      }
    }
    context.fillStyle = xyfill;    

    xoff += (ref_i ? 7 : 0);
    yoff += (ref_j ? 7 : 0);
    w -= (ref_i ? 7 : 0);
    h -= (ref_j ? 7 : 0);

    context.fillRect(xoff, yoff, w, h);
  });
});

But we have a problem now. Here’s a table showing the 2d “fills” from a run of the script, transposed so that x-fills read across and y-fills read down.

       
rgb(255, 0, 0) rgb(255, 255, 0) rgb(255, 255, 255) rgb(255, 255, 255)
rgb(0, 0, 255) rgb(255, 255, 255) rgb(255, 255, 255) left
rgb(0, 0, 255) rgb(255, 255, 255) rgb(255, 255, 255) rgb(255, 255, 255)
rgb(255, 255, 255) left top left
rgb(255, 255, 255) left top top
rgb(255, 0, 0) rgb(255, 255, 255) top rgb(255, 255, 255)

In the bottom right corner, a “top” in the bottom row and a “left” in the last column both wind up to the bolded cell at (2, 2), creating overlapping white rectangles. The output is predictably non-rectangular.

predictably non-rectangular output

It seems we’ll have to try a different strategy for crossing row and column boundaries in our fill drawing. To prevent the problem of multiple rectangles spanning from the same source, the source will have to be responsible for determining its row and column span. This will be covered in the next post.

comments powered by Disqus