block by ericcitaire 5408146

D3 JezzBall

Full Screen

A clone of JezzBall.

The goal is to fill at least 75% of the room. Click on any corner button or use mouse wheel to change the orientation of the wall builder. For more detailed instructions, see JezzBall Walkthrough/FAQ page on IGN.

Enjoy ! :)

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<title>D3 JezzBall</title>
<style>

body {
  padding: 0px;
  margin: 0px;
}

rect {
  fill: none;
  pointer-events: all;
}

.smallTextStroke {
  font-family: sans-serif;
  font-size: 12px;
  fill: none;
  stroke: black;
  stroke-width: 1px;
}
.smallText {
  font-family: sans-serif;
  font-size: 12px;
  fill: white;
}

.bigTextStroke {
  font-family: sans-serif;
  font-size: 24px;
  font-weight: bold;
  fill: none;
  stroke: black;
  stroke-width: 4px;
}

.bigText {
  font-family: sans-serif;
  font-size: 24px;
  font-weight: bold;
  fill: white;
}

.cell {
}

.cell.wall {
  fill: #000;
  stroke: #000;
}

.cell.air {
  fill: #ddd;
  stroke: #999;
}

.cell.blue {
  stroke: blue;
  stroke-opacity: .8;
}

.cell.red {
  stroke: red;
  stroke-opacity: .8;
}

.ball {
  fill: #f00;
  fill-opacity: 1;
  stroke: #333;
  stroke-opacity: .5;
}

.builder.head {
  stroke: #333;
  stroke-opacity: .5;
}

.builder.blue {
  fill: blue;
}

.builder.red {
  fill: red;
}

.builder.tail {
  fill-opacity: .4;
}

.switch {
  position: absolute;
  top: 0px;
  left: 0px;
  padding: 0px;
  width: 20px;
  height: 20px;
}

#nextLevelButton, #playAgainButton {
  position: absolute;
  width: 100px;
  visibility: hidden;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<form>
  <input id="switchOrientationButton1" class="switch" type="button" value="V" />
  <input id="switchOrientationButton2" class="switch" type="button" value="V" />
  <input id="switchOrientationButton3" class="switch" type="button" value="V" />
  <input id="switchOrientationButton4" class="switch" type="button" value="V" />
  <input id="nextLevelButton" type="button" value="Next level" />
  <input id="playAgainButton" type="button" value="Play again" />
</form>
<script type="text/javascript" src="jezzball.js"></script>

block.html


<!DOCTYPE html>
<meta charset="utf-8">
<title>D3 JezzBall</title>
<link rel="icon" href="/favicon.png">
<style>

@import url("//bl.ocks.org/style.css?20120730");

</style>

<header>
  <a href="/ericcitaire">ericcitaire</a>’s block <a href="https://gist.github.com/ericcitaire/5408146">#5408146</a>
</header>

<h1>D3 JezzBall</h1>
<p><aside style="margin-top:-43px;">April 29, 2013</aside>


<iframe src="index.html" marginwidth="0" marginheight="0" scrolling="no"></iframe>
<p><aside><a style="position:relative;top:6px;" href="index.html" target="_blank">Open in a new window.</a></aside>

<footer>
  <aside>April 29, 2013</aside>
  <a href="/ericcitaire">ericcitaire</a>’s block <a href="https://gist.github.com/ericcitaire/5408146">#5408146</a>
</footer>

jezzball.js



d3.selection.prototype.size = function() {
    var n = 0;
    this.each(function() { ++n; });
    return n;
  };

var level = 1;
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
		if (key == "level") {
		  level = +value;
		}
	});

var w = 960,
    h = 500,
    sz = 20,
    r = sz / 2,
    sr = r * r,
    ssz = sz * sz,
    v = 3,
    n = level + 1,
    t = 5000;

var rows = Math.ceil(h / sz);
var cols = Math.ceil(w / sz);

var s = false;
d3.selectAll(".switch")
  .on("click", function(e) { s = !s; d3.selectAll(".switch").attr("value", s ? "H" : "V"); });
d3.select("#switchOrientationButton2")
  .style("left", (w - 20) + "px");
d3.select("#switchOrientationButton3")
  .style("top", (h - 20) + "px");
d3.select("#switchOrientationButton4")
  .style("top", (h - 20) + "px")
  .style("left", (w - 20) + "px");
d3.select("#nextLevelButton")
  .style("top", Math.round(h / 2 + 20) + "px")
  .style("left", Math.round(w / 2 - 50) + "px")
  .on("click", function(e) { window.location.href = "?level=" + (level + 1); });
d3.select("#playAgainButton")
  .style("top", Math.round(h / 2 + 20) + "px")
  .style("left", Math.round(w / 2 - 50) + "px")
  .on("click", function(e) { window.location.href = "?level=1"; });

var cells = d3.range(0, rows * cols).map(function (d) {
  var col = d % cols;
  var row = (d - col) / cols;
  return {
    r: row,
    c: col,
    x: col * sz + r,
    y: row * sz + r
  };
});
var balls = d3.range(0, n).map(function (d) {
    var bx = (w - sz * 4) * Math.random() + sz * 2;
    var by = (h - sz * 4) * Math.random() + sz * 2;
    var ball = {
        x: bx,
        y: by,
        px: bx + v * (Math.random() > .5 ? 1 : -1),
        py: by + v * (Math.random() > .5 ? 1 : -1),
        id: d,
        isMoving: true
      };
    return ball;
  });

var svg = d3.select("body").append("svg")
    .attr("width", w)
    .attr("height", h);

var mousewheel = function(e) {
  s = !s;
  d3.selectAll(".switch").attr("value", s ? "H" : "V");
  var p = d3.mouse(this);
  var c1 = ballCell({x: p[0], y: p[1]});
  previewLocation(c1, p);
  d3.event.preventDefault();
};
svg.on("mousewheel.zoom", mousewheel)
  .on("DOMMouseScroll.zoom", mousewheel);

var rectx = function(d) { return d.x - r; };
var recty = function(d) { return d.y - r; };

var tailx = function(d) { return d.dx > 0 ? d.sx - r : rectx(d) - d.dx * sz; };
var taily = function(d) { return d.dy > 0 ? d.sy - r : recty(d) - d.dy * sz; };
var tailw = function(d) { return d.dx == 0 ? sz : d.sz = (d.x - d.sx) * d.dx; };
var tailh = function(d) { return d.dy == 0 ? sz : d.sz = (d.y - d.sy) * d.dy; };

var ballCell = function(b) {
  var row = (b.y - b.y % sz) / sz;
  var col = (b.x - b.x % sz) / sz;
  return cells[row * cols + col];
};

var topCell = function(c) { return cells[Math.max(0, c.r - 1) * cols + c.c]; };
var leftCell = function(c) { return cells[c.r * cols + Math.max(0, c.c - 1)]; };
var bottomCell = function(c) { return cells[Math.min(rows - 1, c.r + 1) * cols + c.c]; };
var rightCell = function(c) { return cells[c.r * cols + Math.min(cols - 1, c.c + 1)]; };

var topLeftCell = function(c) { return cells[Math.max(0, c.r - 1) * cols + Math.max(0, c.c - 1)]; };
var bottomLeftCell = function(c) { return cells[Math.min(rows - 1, c.r + 1) * cols + Math.max(0, c.c - 1)]; };
var bottomRightCell = function(c) { return cells[Math.min(rows - 1, c.r + 1) * cols + Math.min(cols - 1, c.c + 1)]; };
var topRightCell = function(c) { return cells[Math.max(0, c.r - 1) * cols + Math.min(cols - 1, c.c + 1)]; };

var cell = svg.selectAll(".cell")
  .data(cells)
  .enter().append("rect")
  .attr("class", function(d) { return "cell " + ((d.isWall = d.c == 0 || d.c == cols - 1 || d.r == 0 || d.r == rows - 1) ? "wall" : "air"); })
  .attr("x", rectx)
  .attr("y", recty)
  .attr("width", sz)
  .attr("height", sz)
  .each(function(d) {
    d.elnt = d3.select(this);
  });

function previewLocation(c1, p) {
  if (blue) blue.classed("blue", false);
  if (red) red.classed("red", false);
  var c2, d;
  if (s) {
    d = p[0] - c1.x;
    c2 = d > 0 ? rightCell(c1) : leftCell(c1);
  } else {
    d = p[1] - c1.y;
    c2 = d > 0 ? bottomCell(c1) : topCell(c1);
  }
  if (c1.isWall || c2.isWall) {
    blue = null;
    red = null;
  } else if (d > 0) {
    blue = c1.elnt;
    red = c2.elnt;
  } else {
    blue = c2.elnt;
    red = c1.elnt;
  }
  if (blue) blue.classed("blue", function(d) { return !d.isWall; });
  if (red) red.classed("red", function(d) { return !d.isWall; });
}

var blue, red;
svg.selectAll(".air")
  .on("mouseover", function(c1) {
    var p = d3.mouse(this);
    previewLocation(c1, p);
  }).on("mouseout", function() {
    if (blue) blue.classed("blue", false);
    if (red) red.classed("red", false);
  }).on("click", function() {
    if (percentageCleared < 75 && lives >= 0 && timeLeft > 0) {
      var dx = s ? 1 : 0;
      var dy = s ? 0 : 1; 
      if (blue) blue.each(function(d) { startWall(d, "blue", -dx, -dy); });
      if (red) red.each(function(d) { startWall(d, "red", dx, dy); });
    }
  });

var areaCleared = 0;
var totalArea = svg.selectAll(".air").size();
var percentageCleared = 0;

svg.append("text")
  .attr("x", w / 2 - 50)
  .attr("y", h - 5)
  .attr("class", "smallText")
  .attr("id", "areaFilledText")
  .text("Area cleared: 0%");

var lives = n;
svg.append("text")
  .attr("x", 50)
  .attr("y", 15)
  .attr("class", "smallText")
  .attr("id", "livesText")
  .text("Lives: " + lives);

var gameStartedAt = new Date().getTime();
var timeLeft = t;
svg.append("text")
  .attr("x", w - 130)
  .attr("y", 15)
  .attr("class", "smallText")
  .attr("id", "timeLeftText")
  .text("Time left: " + timeLeft);

var force = d3.layout.force()
  .gravity(0)
  .charge(0)
  .friction(1)
  .size([w, h]);

balls.forEach(function (b) {
  svg.append("svg:circle")
    .data([b])
    .attr("class", "ball")
    .attr("cx", function (d) {
    return d.x;
  })
    .attr("cy", function (d) {
    return d.y;
  })
    .attr("r", r);
  force.nodes().push(b);
});

force.on("tick", function () {
      
  var ball = svg.selectAll(".ball");
  ball.attr("cx", function (d) { return d.x; })
    .attr("cy", function (d) { return d.y; })
    .each(function(b) {
      detectCollisions(b);
      
      var cc = ballCell(b);
      var tc = topCell(cc);
      var lc = leftCell(cc);
      var bc = bottomCell(cc);
      var rc = rightCell(cc);
      if (cc.isWall || (tc.isWall && bc.isWall) || (lc.isWall && rc.isWall)) {
        cc.elnt
          .classed("air", true)
          .classed("wall", false);
        b.px = b.x = cc.x;
        b.py = b.y = cc.y;
        cc.isWall = b.isMoving = false;
      }
  });
  
  var head = svg.selectAll(".head");
  head.attr("x", function(d) { d.x += d.dx * (v * .4); return rectx(d); })
    .attr("y", function(d) { d.y += d.dy * (v * .4); return recty(d); })
    .each(function(d) {
      svg.select("." + d.cl + ".tail")
        .attr("x", tailx)
        .attr("y", taily)
        .attr("width", tailw)
        .attr("height", tailh);
    });
  
  var air = svg.selectAll(".air");
  var tail = svg.selectAll(".tail");
  var wallWasBuilt = false;
  head.filter(function(h) {
      var hc = ballCell(h);
      if (h.dy < 0) {
        var tc = topCell(hc);
        return tc.isWall && h.y - tc.y < sz;
      }
      if (h.dx < 0) {
        var lc = leftCell(hc);
        return lc.isWall && h.x - lc.x < sz;
      }
      if (h.dy > 0) {
        var bc = bottomCell(hc);
        return bc.isWall && bc.y - h.y < sz;
      }
      if (h.dx > 0) {
        var rc = rightCell(hc);
        return rc.isWall && rc.x - h.x < sz;
      }
    }).each(function(h) {
      air.filter(function(a) {
          return h.dx == 0 ? h.x == a.x && Math.min(h.sy, h.y) <= a.y && a.y <= Math.max(h.sy, h.y) : h.y == a.y && Math.min(h.sx, h.x) <= a.x && a.x <= Math.max(h.sx, h.x);
        })
        .classed("newWall", true)
        .classed("air", false)
        .each(function(d) { if (!d.isWall) { ++areaCleared; d.isWall = true; } });
      tail.filter("." + h.cl)
        .remove();
      wallWasBuilt = true;
    }).remove();
  
  if (wallWasBuilt) {
    fillEmptyRooms();
  }
  
  var timePlayed = Math.floor(((new Date().getTime()) - gameStartedAt) / 100);
  timeLeft = timePlayed > t ? 0 : t - timePlayed;
  svg.select("#timeLeftText")
    .text("Time left: " + timeLeft);
  if (percentageCleared < 75 && lives >= 0 && timeLeft > 0) {
    force.resume();
  } else {
    force.stop();
    var text, textWidth;
    if (percentageCleared >= 75 && lives >= 0 && timeLeft > 0) {
      text = "Level complete !";
      textWidth = 188;
      d3.select("#nextLevelButton")
        .style("visibility", "visible");
    } else {
      text = "Game over !";
      textWidth = 138;
      d3.select("#playAgainButton")
        .style("visibility", "visible");
    }
    svg.append("text")
      .attr("x", w / 2 - textWidth / 2)
      .attr("y", h / 2)
      .attr("class", "bigTextStroke")
      .text(text);
    svg.append("text")
      .attr("x", w / 2 - textWidth / 2)
      .attr("y", h / 2)
      .attr("class", "bigText")
      .text(text);
  }
});

force.start();

function detectCollisions(b) {
  var dx = b.x - b.px > 0 ? 1 : -1;
  var dy = b.y - b.py > 0 ? 1 : -1;
  var d, sd;

  // tail collision
  svg.selectAll(".tail").filter(function(t) {
      var w = tailw(t);
      var h = tailh(t);
      var x0 = tailx(t);
      var x1 = x0 + w;
      var y0 = taily(t);
      var y1 = y0 + h;
      
      return x0 - r < b.x && b.x < x1 - r && y0 - r < b.y && b.y < y1 + r;
    })
    .each(function(t) {
        --lives;
        svg.select("#livesText")
          .text("Lives: " + (lives < 0 ? 0 : lives));
        svg.selectAll(".head." + t.cl).remove();
      })
    .remove();
  
  // wall borders collision
  var cc = ballCell(b);
  var tc = topCell(cc);
  if (tc.isWall && dy < 0 && (d = b.y - tc.y) <= sz) {
    bounceY(b, sz, d, dy);
  }
  var lc = leftCell(cc);
  if (lc.isWall && dx < 0 && (d = b.x - lc.x) <= sz) {
    bounceX(b, sz, d, dx);
  }
  var bc = bottomCell(cc);
  if (bc.isWall && dy > 0 && (d = bc.y - b.y) <= sz) {
    bounceY(b, sz, d, dy);
  }
  var rc = rightCell(cc);
  if (rc.isWall && dx > 0 && (d = rc.x - b.x) <= sz) {
    bounceX(b, sz, d, dx);
  }
  svg.selectAll(".head").each(function(h) {
    if (h.y - r <= b.y && b.y <= h.y + r) {
      if (dx < 0 && (d = b.x - h.x) <= sz && d > 0) {
        bounceX(b, sz, d, dx);
      }
      if (dx > 0 && (d = h.x - b.x) <= sz && d > 0) {
        bounceX(b, sz, d, dx);
      }
    }
    if (h.x - r <= b.x && b.x <= h.x + r) {
      if (dy < 0 && (d = b.y - h.y) <= sz && d > 0) {
        bounceY(b, sz, d, dy);
      }
      if (dy > 0 && (d = h.y - b.y) <= sz && d > 0) {
        bounceY(b, sz, d, dy);
      }
    }
  });
  
  // wall corners collision
  var tlc = topLeftCell(cc);
  if (!tc.isWall && !lc.isWall && tlc.isWall && (sd = cornerSquareDistance(b.x, b.y, tlc.x + r, tlc.y + r)) <= sr) {
    d = Math.sqrt(sd);
    if (dx < 0) {
      bounceX(b, r, d, dx);
    }
    if (dy < 0) {
      bounceY(b, r, d, dy);
    }
  }
  var blc = bottomLeftCell(cc);
  if (!bc.isWall && !lc.isWall && blc.isWall && (sd = cornerSquareDistance(b.x, b.y, blc.x + r, blc.y - r)) <= sr) {
    d = Math.sqrt(sd);
    if (dx < 0) {
      bounceX(b, r, d, dx);
    }
    if (dy > 0) {
      bounceY(b, r, d, dy);
    }
  }
  var brc = bottomRightCell(cc);
  if (!bc.isWall && !rc.isWall && brc.isWall && (sd = cornerSquareDistance(b.x, b.y, brc.x - r, brc.y - r)) <= sr) {
    d = Math.sqrt(sd);
    if (dx > 0) {
      bounceX(b, r, d, dx);
    }
    if (dy > 0) {
      bounceY(b, r, d, dy);
    }
  }
  var trc = topRightCell(cc);
  if (!tc.isWall && !rc.isWall && trc.isWall && (sd = cornerSquareDistance(b.x, b.y, trc.x - r, trc.y + r)) <= sr) {
    d = Math.sqrt(sd);
    if (dx > 0) {
      bounceX(b, r, d, dx);
    }
    if (dy < 0) {
      bounceY(b, r, d, dy);
    }
  }
  svg.selectAll(".head").each(function(h) {
    if ((sd = cornerSquareDistance(b.x, b.y, h.x + r, h.y + r)) <= sr && sd > 0) {
      d = Math.sqrt(sd);
      if (dx < 0) {
        bounceX(b, r, d, dx);
      }
      if (dy < 0) {
        bounceY(b, r, d, dy);
      }
    }
    if ((sd = cornerSquareDistance(b.x, b.y, h.x + r, h.y - r)) <= sr && sd > 0) {
      d = Math.sqrt(sd);
      if (dx < 0) {
        bounceX(b, r, d, dx);
      }
      if (dy > 0) {
        bounceY(b, r, d, dy);
      }
    }
    if ((sd = cornerSquareDistance(b.x, b.y, h.x - r, h.y - r)) <= sr && sd > 0) {
      d = Math.sqrt(sd);
      if (dx > 0) {
        bounceX(b, r, d, dx);
      }
      if (dy > 0) {
        bounceY(b, r, d, dy);
      }
    }
    if ((sd = cornerSquareDistance(b.x, b.y, h.x - r, h.y + r)) <= sr && sd > 0) {
      d = Math.sqrt(sd);
      if (dx > 0) {
        bounceX(b, r, d, dx);
      }
      if (dy < 0) {
        bounceY(b, r, d, dy);
      }
    }
  });

  // ball collision
  svg.selectAll(".ball").each(function(b2) {
      if (b.id == b2.id) {
        return;
      }
      var sd = cornerSquareDistance(b.x, b.y, b2.x, b2.y);
      if (sd <= ssz) {
        var dx2 = b2.x - b2.px > 0 ? 1 : -1;
        var dy2 = b2.y - b2.py > 0 ? 1 : -1;
        var d = Math.sqrt(sd);
        if (b.isMoving && (b2.x - b.x) * dx > r / 2) {
          bounceX(b, sz, d, dx);
        }
        if (b2.isMoving && (b.x - b2.x) * dx2 > r / 2) {
          bounceX(b2, sz, d, dx2);
        }
        if (b.isMoving && (b2.y - b.y) * dy > r / 2) {
          bounceY(b, sz, d, dy);
        }
        if (b2.isMoving && (b.y - b2.y) * dy2 > r / 2) {
          bounceY(b2, sz, d, dy2);
        }
      }
    });
}

function bounceX(b, m, d, dx) {
  if (b.isMoving) {
    b.x -= (m - d) * dx;
    b.px = b.x + dx * v;
  }
}

function bounceY(b, m, d, dy) {
  if (b.isMoving) {
    b.y -= (m - d) * dy;
    b.py = b.y + dy * v;
  }
}

function cornerSquareDistance(x0, y0, x1, y1) {
  var w = x1 - x0;
  var h = y1 - y0;
  return (w * w + h * h);
}

function fillEmptyRooms() {
  var air = d3.selectAll(".air");
  
  air
    .each(function (d) {
      d.visited = false;
    });
  
  svg.selectAll(".ball")
    .each(function (b) {
      var cc = ballCell(b);
      cc.visited = true;
      visit(cc);
    });
  
  air
    .classed("newWall", function(d) { return !d.visited; })
    .classed("air", function(d) { return d.visited; })
    .each(function(d) { if (!d.visited && !d.isWall) { ++areaCleared; d.isWall = true; } });
  
  percentageCleared = Math.floor((areaCleared * 100) / totalArea);
  svg.select("#areaFilledText")
    .text("Area cleared: " + percentageCleared + "%");
  
  svg.selectAll(".newWall")
    .on("mouseover", null)
    .on("mouseout", null)
    .on("click", null)
    .classed("wall", true)
    .classed("blue", false)
    .classed("red", false);
}

function visit(c) {
  var tc = topCell(c);
  if (!tc.isWall && !tc.visited) {
    tc.visited = true;
    visit(tc);
  }
  var lc = leftCell(c);
  if (!lc.isWall && !lc.visited) {
    lc.visited = true;
    visit(lc);
  }
  var bc = bottomCell(c);
  if (!bc.isWall && !bc.visited) {
    bc.visited = true;
    visit(bc);
  }
  var rc = rightCell(c);
  if (!rc.isWall && !rc.visited) {
    rc.visited = true;
    visit(rc);
  }
}

function startWall(cell, cl, dx, dy) {
  var wallHead = {
    sx: cell.x,
    sy: cell.y,
    x: cell.x,
    y: cell.y,
    dx: dx,
    dy: dy,
    cl: cl
  };
  if (svg.selectAll("." + cl + ".head").empty()) {
    svg.selectAll("." + cl + ".head")
      .data([wallHead]).enter().append("rect")
      .classed("builder", true)
      .classed(cl, true)
      .classed("head", true)
      .attr("x", rectx)
      .attr("y", recty)
      .attr("width", sz)
      .attr("height", sz);
    var tail = svg.selectAll("." + cl + ".tail")
      .data([wallHead]);
    tail.enter().append("rect")
      .classed("builder", true)
      .classed(cl, true)
      .classed("tail", true)
      .attr("x", tailx)
      .attr("y", taily)
      .attr("width", tailw)
      .attr("height", tailh);
    tail.exit().remove();
  }
}