Inspired by:
This Demo is based on Previous One and add interactive feature on canvas.
Moving mouse on canvas hovered circle will be highlighed. I create an offscreen canvas that has the same dimmension as the presentation canvas. Offscreen canvas will be rendered ahead of the presentation one so that we can detect which circle the mouse hovered by ctx.getImageData(mouseX, mouseY, 1, 1).
Click on canvasImg will reveal the current offscreen canvas in an <img> node.
Once count is changed that will generate new circles and update canvas. The performance monitor will show the rendering frequency.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js" integrity="sha256-+9Mf3cAVmxxudDsr1XwXUeRZFtvdWVYdq5/vcgiYyNU=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js" integrity="sha256-HJ7j+71YYw6Kcs8THwQV9lXmPOcR0eXlg7n8KRTZsyA=" crossorigin="anonymous"></script>
<script>
// stats.js - https://github.com/mrdoob/stats.js
(function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof define&&define.amd?define(e):f.Stats=e()})(this,function(){var f=function(){function e(a){c.appendChild(a.dom);return a}function u(a){for(var d=0;d<c.children.length;d++)c.children[d].style.display=d===a?"block":"none";l=a}var l=0,c=document.createElement("div");c.style.cssText="position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000";c.addEventListener("click",function(a){a.preventDefault();
u(++l%c.children.length)},!1);var k=(performance||Date).now(),g=k,a=0,r=e(new f.Panel("FPS","#0ff","#002")),h=e(new f.Panel("MS","#0f0","#020"));if(self.performance&&self.performance.memory)var t=e(new f.Panel("MB","#f08","#201"));u(0);return{REVISION:16,dom:c,addPanel:e,showPanel:u,begin:function(){k=(performance||Date).now()},end:function(){a++;var c=(performance||Date).now();h.update(c-k,200);if(c>g+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/
1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v);
b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f});
</script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
canvas, img {
outline: 1px solid gray;
display: block;
margin: 0.5em auto;
}
</style>
</head>
<body>
<canvas></canvas>
<img>
<script>
var w = 950, h = 300,
pos = [-100, -100], // mouse position
canvas = document.querySelector("canvas"),
ctx = canvas.getContext("2d");
d3.select(canvas).attr("width", w).attr("height", h)
.on("mouseleave", function () { pos = [-100, -100]; })
.on("mousemove", function () { pos = d3.mouse(this); });
var genColor = (function () {
var nextCol = 1, _step = 50;
var fn = function () {
var ret = [];
// via https://stackoverflow.com/a/15804183
if (nextCol < 16777215) {
ret.push(nextCol & 0xff); // R
ret.push((nextCol & 0xff00) >> 8); // G
ret.push((nextCol & 0xff0000) >> 16); // B
nextCol += _step;
}
var col = "rgb(" + ret.join(",") + ")";
return col;
};
fn.step = function (v) {
if (!v || v < 0) { return _step; }
_step = v;
return fn;
};
fn.clear = function () {
nextCol = 1;
return fn;
};
return fn;
})();
var customNode = document.createElement("custom:cc"),
dataContainer = d3.select(customNode),
// A shallow copy of offscreen canvas that has the same dimmension
hCanvas = canvas.cloneNode(),
hCtx = hCanvas.getContext("2d"),
img = d3.select("img").attr("width", w).attr("height", h),
circleMap = {},
p = {
count: 200,
canvasImg: function () {
img.attr("src", hCanvas.toDataURL());
},
data: genCircles(this.num)
};
function genCircles (n) {
if (!n) { n = 100; }
genColor.clear();
return d3.range(n)
.map(function (d, i) {
var color = genColor(),
node = {
x: Math.random() * w,
y: Math.random() * h,
r: Math.random() * 10 + 3,
_fill: color
};
circleMap[color] = node;
return node;
});
}
var gui = new dat.GUI();
gui.add(p, "count", 100, 10000)
.step(100)
.onChange(function (n) {
p.data = genCircles(n);
drawCustom();
});
gui.add(p, "canvasImg");
function drawCustom () {
var u = dataContainer.selectAll("c").data(p.data);
u.enter()
.append("c")
.attr("r", d => d.r)
.attr("x", Math.random() * w)
.attr("y", Math.random() * h)
.merge(u)
.transition()
.duration(2000)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("r", d => d.r);
u.exit().transition()
.duration(2000)
.attr("r", 0)
.remove();
}
function getCircleByColor (pos) {
var rgb = hCtx.getImageData(pos[0], pos[1], 1, 1).data;
rgb = "rgb(" + rgb.slice(0, 3).join(",") + ")"; // avoid alpha charnnel
return circleMap[rgb];
}
function drawCanvas (ctx) {
ctx.save();
ctx.clearRect(0, 0, w, h);
ctx.lineWidth = 1;
ctx.strokeStyle = "#335";
// pick a color spot and detect which circle under this spot.
var t = getCircleByColor(pos);
ctx.fillStyle = "rgba(70, 130, 180, 0.5)";
dataContainer.selectAll("c").each(function (d) {
var x = +this.getAttribute("x"),
y = +this.getAttribute("y"),
r = +this.getAttribute("r");
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
});
if (t) {
ctx.beginPath();
ctx.arc(t.x, t.y, t.r, 0, Math.PI * 2, false);
ctx.fillStyle = "red";
ctx.fill();
}
ctx.restore();
}
function drawHiddenCanvas (ctx) {
ctx.save();
ctx.clearRect(0, 0, w, h);
dataContainer.selectAll("c").each(function (d) {
var x = +this.getAttribute("x"),
y = +this.getAttribute("y"),
r = +this.getAttribute("r");
ctx.fillStyle = d._fill;
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2, false);
ctx.fill();
});
ctx.restore();
}
function imageText (ctx) {
ctx.save();
ctx.fillStyle = "gray";
ctx.font = "50px fantasy";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Click \"canvasImg\" to see", w / 2, 120);
ctx.fillText("what the offscreen canvas looks like", w / 2, 170);
ctx.restore();
}
// Performance Monitor
var stats = new Stats();
stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild( stats.dom );
var cb = function () {
stats.begin();
drawHiddenCanvas(hCtx);
drawCanvas(ctx);
stats.end();
};
imageText(hCtx);
p.canvasImg();
drawCustom();
d3.timer(cb);
d3.select(window.parent.document.querySelector("iframe"))
.style("height", "620px");
</script>
</body>
https://cdnjs.cloudflare.com/ajax/libs/d3/5.12.0/d3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js