This is an implentation of the Bubble Cursor, which was originally introduced by Tovi Grossman and Ravin Balakrishnan at CHI 2005.
-
-
Save milroc/25ab205b6639095c9522 to your computer and use it in GitHub Desktop.
Bubble Cursor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<head> | |
<script type = "text/javascript" src="http://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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment