block by milroc 25ab205b6639095c9522

Bubble Cursor

Full Screen

This is an implentation of the Bubble Cursor, which was originally introduced by Tovi Grossman and Ravin Balakrishnan at CHI 2005.

index.html

<!DOCTYPE html>
<meta charset="utf-8">

<head>
  <script type = "text/javascript" src="//d3js.org/d3.v3.js"></script>
</head>

<style>
body {
  background: #2C3E50;
}
</style>
<body>
  <div id="bubbleCursor">
  <script>
var backgroundColor = "#2C3E50";
var targetColor = "#E74C3C";
var bubbleColor = "#ECF0F1";
  // Number of targets
  var numTargets = 40;
  // Min/Max radius of targets
  var minRadius = 10, maxRadius = 30;
  // Min separation between targets
  var minSep = 20;

  var w = 960, h = 500;
  var svg = d3.select("#bubbleCursor")
	.append("svg:svg")
	.attr("width", w)
	.attr("height", h);

  // Make a white background rectangle

  function distance(ptA,ptB) {
      var diff = [ptB[0]-ptA[0],ptB[1]-ptA[1]];
      return Math.sqrt(diff[0]*diff[0] + diff[1]*diff[1]);
  }

  // Initialize position and radius of all targets.
  function initTargets(numTargets,minRadius,maxRadius,minSep) {
      var radRange = maxRadius - minRadius;
      var minX = maxRadius + 10, maxX = w-maxRadius-10, xRange = maxX-minX;
      var minY = maxRadius + 10, maxY = h-maxRadius-10, yRange = maxY-minY;

      // Make a vertices array storing position and radius of each
      // target point.
      var targets = [];
      for (var i = 0; i<numTargets;i++) {
	  var ptCollision = true;
	  while (ptCollision) {
	      // Randomly choose position and radius of new target pt.
	      var pt = [Math.random() * xRange + minX,
			Math.random() * yRange + minY];
	      var rad = Math.random()*radRange+minRadius;

	      // Check for collisions with all targets made earlier.
	      ptCollision = false;
	      for(var j = 0; j < targets.length && !ptCollision; j++) {
		  var ptJ = targets[j][0]
		  var radPtJ = targets[j][1];
		  var separation = distance(pt,ptJ);
		  if (separation < (rad+radPtJ+minSep)) {
		      ptCollision = true;
		  }
	      }

	      if(!ptCollision) {
		  targets.push([pt,rad]);
	      }
	  }
      }

      return targets;
  }


  function updateTargetsFill(currentCapturedTarget,clickTarget) {
      // Update the fillcolor of the targetcircles
      svg.selectAll(".targetCircles")
          .attr("fill",function(d,i){
	      var clr = bubbleColor
	      if(i === currentCapturedTarget) {
		  clr = bubbleColor
	      }
	      if(i === clickTarget)
		  clr = targetColor;
	      return clr;
	  });

  }

  function getTargetCapturedByBubbleCursor(mouse,targets) {
      // Compute distances from mouse to center, outermost, innermost
      // of each target and find currMinIdx and secondMinIdx;
      var mousePt = [mouse[0],mouse[1]];
      var dists=[], containDists=[], intersectDists=[];
      var currMinIdx = 0;
      for (var idx =0; idx < numTargets; idx++) {
	  var targetPt = targets[idx][0];
	  var currDist = distance(mousePt,targetPt);
	  dists.push(currDist);

	  targetRadius = targets[idx][1];
	  containDists.push(currDist+targetRadius);
	  intersectDists.push(currDist-targetRadius);


	  if(intersectDists[idx] < intersectDists[currMinIdx]) {
	      currMinIdx = idx;

	  }
      }

      // Find secondMinIdx
      var secondMinIdx = (currMinIdx+1)%numTargets;
      for (var idx =0; idx < numTargets; idx++) {
	  if (idx != currMinIdx &&
	      intersectDists[idx] < intersectDists[secondMinIdx]) {
	      secondMinIdx = idx;
	  }
      }

      var cursorRadius = Math.min(containDists[currMinIdx],
			       intersectDists[secondMinIdx]);
      svg.select(".cursorCircle")
	  .attr("cx",mouse[0])
	  .attr("cy",mouse[1])
	  .attr("r",cursorRadius);

      if(cursorRadius < containDists[currMinIdx]) {
	  svg.select(".cursorMorphCircle")
	      .attr("cx",targets[currMinIdx][0][0])
	      .attr("cy",targets[currMinIdx][0][1])
	      .attr("r",targets[currMinIdx][1]+5);
      } else {
	  svg.select(".cursorMorphCircle")
	      .attr("cx",0)
	      .attr("cy",0)
	      .attr("r",0);
      }

      return currMinIdx;
  }

  // Make the targets
  var targets = initTargets(numTargets,minRadius,maxRadius,minSep);
  // Choose the target that should be clicked
  var clickTarget = Math.floor(Math.random()*targets.length);

  // Add in cursorMorph circle  at 0,0 with 0 radius
  // We add it first so that it appears behind the targets
  svg.append("circle")
    .attr("class","cursorMorphCircle")
    .attr("cx",0)
    .attr("cy",0)
    .attr("r",0)
    .attr("fill",d3.hsl(backgroundColor).darker(0.5));

  // Add in the cursor circle at 0,0 with 0 radius
  // We add it first so that it appears behind the targets
  svg.append("circle")
    .attr("class","cursorCircle")
    .attr("cx",0)
    .attr("cy",0)
    .attr("r",0)
    .attr("fill",d3.hsl(backgroundColor).darker(0.5));

  // Add in the target circles
  svg.selectAll("targetCircles")
    .data(targets)
    .enter()
    .append("circle")
    .attr("class","targetCircles")
    .attr("cx",function(d,i){return d[0][0];})
    .attr("cy",function(d,i){return d[0][1];})
    .attr("r",function(d,i){return d[1]-1;})
    .attr("stroke-width",0)
    .attr("stroke",bubbleColor)
    .attr("fill",backgroundColor);

  // Update the fill color of the targets
  updateTargetsFill(-1,clickTarget);

  //Handle mousemove events
  svg.on("mousemove", function(d,i) {
      var capturedTargetIdx =
	  getTargetCapturedByBubbleCursor(d3.mouse(this),targets);

      // Update the fillcolor of the targetcircles
      updateTargetsFill(capturedTargetIdx,clickTarget);
  });

  // Handle mouse moving outside of svg window.
  svg.on("mouseout", function(d,i) {
      // Update the fillcolor of the targetcircles
      updateTargetsFill(-1,clickTarget);

      // Get rid of the grady cursor circles by setting size and pos to 0
      svg.select(".cursorCircle")
	  .attr("cx",0)
	  .attr("cy",0)
	  .attr("r",0);

      svg.select(".cursorMorphCircle")
	  .attr("cx",0)
	  .attr("cy",0)
	  .attr("r",0);
  });

  // Handle a mouse click
  svg.on("click", function(d,i) {
      var capturedTargetIdx =
	  getTargetCapturedByBubbleCursor(d3.mouse(this),targets);

      // If user clicked on the clickTarget then choose a new clickTarget
      if(capturedTargetIdx == clickTarget) {
	  var newClickTarget = clickTarget;
	  // Make sure newClickTarget is not the same as the current clickTarget
	  while (newClickTarget == clickTarget)
	      newClickTarget = Math.floor(Math.random()*targets.length);
	  clickTarget = newClickTarget;

	  // Update drawing of targets
	  updateTargetsFill(capturedTargetIdx,clickTarget);
      }

  });

  </script>
  </div>

</body>
</html>