Thanks to some of the verbiage in the sidebar that showed up on my stackexchange question, I tried googling again with some different terms and found this:
http://www.worldwidewhat.net/2012/07/how-to-draw-bitmaps-using-javascript/
Which more or less answers my question. Still interested of others have suggestions or answers, though!
Update:
Here is the code that I ended up using. It is modified from the link above so that the loops over the grid are done bottom-top left-right so that no flipping is necessary, and I have cut out some of the ancillary functions. In my loop, I am adjusting my data to be centered around the mean and normalized to span 4 standard deviations (two positive, two negative).
var interpolation = this.interpolateGeoDataLayerToGrid();
var numFileBytes = this.getLittleEndianHex(interpolation.grid[0].length * interpolation.grid.length);
var w = this.getLittleEndianHex(interpolation.grid[0].length);
var h = this.getLittleEndianHex(interpolation.grid.length);
var header =
'BM' + // Signature
numFileBytes + // size of the file (bytes)*
'\x00\x00' + // reserved
'\x00\x00' + // reserved
'\x36\x00\x00\x00' + // offset of where BMP data lives (54 bytes)
'\x28\x00\x00\x00' + // number of remaining bytes in header from here (40 bytes)
w + // the width of the bitmap in pixels*
h + // the height of the bitmap in pixels*
'\x01\x00' + // the number of color planes (1)
'\x20\x00' + // 32 bits / pixel
'\x00\x00\x00\x00' + // No compression (0)
'\x00\x00\x00\x00' + // size of the BMP data (bytes)*
'\x13\x0B\x00\x00' + // 2835 pixels/meter - horizontal resolution
'\x13\x0B\x00\x00' + // 2835 pixels/meter - the vertical resolution
'\x00\x00\x00\x00' + // Number of colors in the palette (keep 0 for 32-bit)
'\x00\x00\x00\x00'; // 0 important colors (means all colors are important)
var imgdata = "";
for (var row=interpolation.grid.length-1; row >= 0; row--) {
for (var col=0; col<interpolation.grid[row].length; col++) {
var value = Math.min(255,Math.max(0,Math.floor(128 + 64*(interpolation.grid[row][col]-interpolation.mean)/interpolation.stdev)));
imgdata += String.fromCharCode(255-value, 0, value, 128);
}
}
var datauri = 'data:image/bmp;base64,';
if(window.btoa != undefined) {
datauri += btoa(header + imgdata);
}
else {
datauri += $.base64.encode(header + imgdata);
}
newOverlay = new google.maps.GroundOverlay(datauri,
new google.maps.LatLngBounds(
new google.maps.LatLng(this.GridDataSettings.latmin, this.GridDataSettings.longmin),
new google.maps.LatLng(this.GridDataSettings.latmax, this.GridDataSettings.longmax)
));
newOverlay.setMap(map);
And here is the lttleEndianHex helper function, as taken from the link above:
getLittleEndianHex: function(value) {
var result = [];
for (var bytes = 4; bytes > 0; bytes--) {
result.push(String.fromCharCode(value & 255));
value >>= 8;
}
return result.join('');
}