Skip to content

Instantly share code, notes, and snippets.

@magrawala
Last active August 29, 2015 13:57
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save magrawala/9716298 to your computer and use it in GitHub Desktop.
Save magrawala/9716298 to your computer and use it in GitHub Desktop.
Bubble Cursor

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

<!DOCTYPE html>
<meta charset="utf-8">
<head>
<script type = "text/javascript" src="http://d3js.org/d3.v3.js"></script>
</head>
<body>
<div id="bubbleCursor">
<script>
// 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
svg.append("rect")
.attr("class","backgroundRect")
.attr("width", w)
.attr("height", h)
.attr("fill","white");
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 = "white";
if(i === currentCapturedTarget) {
clr = "limegreen";
}
if(i === clickTarget)
clr = "salmon";
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 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","lightgray");
// 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","lightgray");
// 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",2)
.attr("stroke","limegreen")
.attr("fill","white");
// 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment