var canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d"),
w = canvas.width,
h = canvas.height,
cellsY = 14, // cells Y for triangles
cellsX = cellsY * 2, // cells X times two to overlap skew
cw = w / cellsX * 2, // cell width and height
ch = h / cellsY,
toggle, cx = 0, cy, // for circles
cells = 25, // cells for cirles + comp. (see below)
deltaY = 0.8660254037844386, // = sqrt(3) * 0.5
deltaYI = 1 / deltaY, // inverse deltaY
grid = new Uint8Array((cells * cells * deltaYI)|0), // circles "booleans"
i;
// Calc and Render Triangles ---
// main transform: skew
ctx.setTransform(1, 0, 0.51, 1, -cellsX * cw * 0.5, 0);
ctx.fillStyle = "rgb(90, 146, 176)";
// fill random cells based on likely cover:
var cover = 0.67, // how much of total area to cover
biasDiv = 0.6, // bias for splitting cell
biasUpper = 0.5, // bias for which part to draw
count = cellsX * cellsY * cover, // coverage
tris = [],
x, y, d, u, overlap; // generate cells
for (i = 0; i < count; i++) {
overlap = true;
while (overlap) { // if we have overlapping cells
x = (Math.random() * cellsX) | 0;
y = (Math.random() * cellsY) | 0;
overlap = hasCell(x, y);
if (!overlap) {
d = Math.random() < biasDiv; // divide cell?
u = Math.random() < biasUpper; // if divided, use upper part?
tris.push({
x: x,
y: y,
divide: d,
upper: u
})
}
}
}
function hasCell(x, y) {
for (var i = 0, c; c = tris[i++];) {
if (c.x === x && c.y === y) return true;
}
return false;
}
// render
for (i = 0; i < tris.length; i++) renderTri(tris[i]);
ctx.fill(); // fill all sub-paths
function renderTri(t) {
var x = t.x * cw, // convert to abs. position
y = t.y * ch;
if (t.divide) { // create triangle
ctx.moveTo(x + cw, y); // define common diagonal
ctx.lineTo(x, y + ch);
t.upper ? ctx.lineTo(x, y) : ctx.lineTo(x + cw, y + ch);
}
else {
ctx.rect(x, y, cw, ch); // fill complete cell
}
}
// Calc and Render Circles ---
cover = 0.5, // how much of total area to cover
count = Math.ceil(grid.length * cover); // coverage
cw = ch = w / cells;
ctx.setTransform(1,0,0,1,0,0); // reset transforms
ctx.fillStyle = "rgb(32, 141, 83)";
ctx.globalCompositeOperation = "multiply"; // blend mode instead of alpha
if (ctx.globalCompositeOperation !== "multiply") ctx.globalAlpha = 0.5; // for IE
for (i = 0; i < count; i++) {
overlap = true;
while (overlap) { // if we have overlapping cells
x = (Math.random() * cells) | 0; // x index
y = (Math.random() * cells * deltaYI) | 0; // calc y index + comp
overlap = hasCircle(x, y); // already has circle?
if (!overlap) {
grid[y * cells + x] = 1; // set "true"
}
}
}
function hasCircle(x, y) {
return grid[y * cells + x] === 1;
}
// render
ctx.beginPath();
cy = ch * 0.5; // start on Y axis
for (y = 0; y < (cells * deltaYI)|0; y++) { // iterate rows + comp.
toggle = !(y % 2); // toggle x offset
for (x = 0; x < cells; x++) { // columns
if (grid[y * cells + x]) { // has circle?
cx = x * cw + (toggle ? cw * 0.5 : 0); // calc x
ctx.moveTo(cx + cw * 0.5, cy); // creat sub-path
ctx.arc(cx, cy, cw * 0.5, 0, 2 * Math.PI); // add arc
ctx.closePath(); // close sub-path
}
}
cy += ch * deltaY; // add deltaY
}
ctx.fill(); // fill all at once
body {background:#777}
canvas {padding:50px;background: rgb(226, 226, 226)}
<canvas width=600 height=600></canvas>