imageTranslateDrawingAxis is Your Friend

I briefly mentioned imageTranslateDrawingAxis in my previous post about Fusebox flowcharts, but wanted to provide a more self-contained example of how it can be used to great effect.  The idea here is to draw a house with a window, and to do it in a relative manner, rather than an absolute manner.  Consider these two functions:

function drawHouse_absolute(img, x, y, width, height) {
  imageDrawRect(img, x, y + height * ROOF_PROPORTION, width, height * (1 - ROOF_PROPORTION));
  imageDrawLine(img, x + width / 2, y, x, y + height * ROOF_PROPORTION);
  imageDrawLine(img, x + width / 2, y, x + width, y + height * ROOF_PROPORTION);
}
function drawWindow_absolute(img, x, y, width, height) {
  imageDrawRect(img, x, y, width, height);
  imageDrawLine(img, x, y + height / 2, x + width, y + height / 2);
  imageDrawLine(img, x + width / 2, y + 0, x + width / 2, y + height);
}

If you were to use them like this:

<cfset drawHouse_absolute(img, 10, 10, 100, 100) />
<cfset drawWindow_absolute(img, 20, 60, 20, 20) />

You might get an image like this:

As you can see, we have to do a lot of math in the functions, and it's all x/y anchored.  Even worse, we have to specify absolute coordinates (the "20, 60″ in the argument list to drawWindow_absolute), rather than specifying them relative to the house.

Instead, consider these two functions that do the same thing:

ROOF_PROPORTION = 0.35;
function drawHouse(img, width, height) {
  imageDrawRect(img, 0, height * ROOF_PROPORTION, width, height * (1 - ROOF_PROPORTION));
  imageDrawLine(img, width / 2, 0, 0, height * ROOF_PROPORTION);
  imageDrawLine(img, width / 2, 0, width, height * ROOF_PROPORTION);
}
function drawWindow(img, width, height) {
  imageDrawRect(img, 0, 0, width, height);
  imageDrawLine(img, 0, height / 2, width, height / 2);
  imageDrawLine(img, width / 2, 0, width / 2, height);
}

I've removed the 'x' and 'y' arguments and simplified the calculations to all be origin anchored.  This makes things a bit easier to manage, both inside the function and outside.  To draw the same picture, you'd use them like this, combined with my friend the 'imageTranslateDrawingAxis' function:

<cfset imageTranslateDrawingAxis(img, 10, 10) />
<cfset drawHouse(img, 100, 100) />
<cfset imageTranslateDrawingAxis(img, 10, 50) />
<cfset drawWindow(img, 20, 20) />

Remember the 'x' and 'y' arguments we passed in the first example?  Well they're still here, they're just split up.  The (10, 10) we passed to drawHouse_absolute is now directly visible in the first line, and the (20, 60) we passed to drawWindow_absolute is visible in the third line (keeping in mind that translation is cumulative; 10 + 10 == 20 and 10 + 50 == 60).

So we're still drawing the same picture, but now we have simpler drawing routines, and the same ability to control where stuff lays out.  But the big payoff is when we nest translations.

Consider this final function (which delegates to drawWindow from above):

function drawHouseWithWindows(img, width, height) {
  var i = 0;
  imageDrawRect(img, 0, height * ROOF_PROPORTION, width, height * (1 - ROOF_PROPORTION));
  imageDrawLine(img, width / 2, 0, 0, height * ROOF_PROPORTION);
  imageDrawLine(img, width / 2, 0, width, height * ROOF_PROPORTION);
  for (i = 5; i LTE arrayLen(arguments); i += 2) {
    // we have pairs of window coordinates, so lets draw windows
    imageTranslateDrawingAxis(img, arguments[i - 1], arguments[i] + height * ROOF_PROPORTION);
    drawWindow(img, width / 5, width / 5); // they don't get to pick the size :)
    imageTranslateDrawingAxis(img, -1 * arguments[i - 1], -1 * (arguments[i] + height * ROOF_PROPORTION));
  }
}

Lines 3-5 are identical to drawHouse above, but I've added support for an arbitrary number of coordinate pairs specifying where windows ought to be in relation to the "wall" of the house.  With this single function, we can draw the same image a third time like this:

<cfset imageTranslateDrawingAxis(img, 10, 10) />
<cfset drawHouseWithWindows(img, 100, 100,
  10, 15
) />

Same translation to place the house at (10, 10), but this time we're letting drawHouseWithWindows take care of translation for drawing the windows, and we can specify the window location relative to the house itself.  This is hugely powerful, if you think about it.  When you're drawing a house, you don't draw the window based on global space, you draw it relative to the house.  As the house moves, the window moves too.

It might not be obvious, but this also lets us package up the offsets needed for the height of the roof in a totally transparent way, all for free.  Before we had to explicitly specify them in our positioning: the 50 in the second translation of the second example is really the 15 in this third example, plus a 35 for the roof height (which I manually figured out based on the height of the house (100) and ROOF_PROPORTION (0.35)).  So that 50 is really violating DRY, because it's specifying the height of the roof (which is defined in drawHouse).

If you're doing any sort of complex drawing, not having to pass around x/y coordinates everywhere will save you a significant amount of work, keep your code a lot simpler and easier to read, and let you avoid the double-encoding of certain types of relative data.

Most of the interesting code is already here, but check out drawing.cfm.txt if you want the full test script that I used for building the examples and generating the image above.  Just save the file with a .CFM extension and hit it in your browser.

2 responses to “imageTranslateDrawingAxis is Your Friend”

  1. Ben Nadel

    Very cool stuff. I have never used that function before, but this is a great use case; it really helps to keep the code for a given shape self-contained, irrelevant (or less so) of where the containing shape is on the canvas.

  2. Ben Nadel

    Oops, forgot to subscribe.