This is an implentation of the Bubble Cursor, which was originally introduced by Tovi Grossman and Ravin Balakrishnan at CHI 2005.
xxxxxxxxxx
<meta charset="utf-8">
<head>
<script type = "text/javascript" src="https://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>
Modified http://d3js.org/d3.v3.js to a secure url
https://d3js.org/d3.v3.js