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