Skip to content

Instantly share code, notes, and snippets.

@tafsiri
Last active January 29, 2024 07:21
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tafsiri/e9016e1b8d36bae56572 to your computer and use it in GitHub Desktop.
<html lang="en">
<head>
<meta charset="utf-8">
<title>2D Picking with canvas</title>
<meta name="description" content="">
<meta name="author" content="Yannick Assogba">
<script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r14/Stats.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.js"></script>
</head>
<body>
<div id='container'>
</div>
<script src="index.js"></script>
</body>
</html>
/**
* This example uses a hidden canvas to demonstrate a technique for
* simulating DOM click events while rendering to a canvas.
*
* The basic technique is to render your visual markers twice, the second
* time on a hidden canvas where each marker gets a unique color. We can then
* look up that color to get back to the data in question.
*
* Open your web console and click on the squares to see their original
* indices in the data array.
*/
window.addEventListener('load', function(){
var stats = new Stats();
stats.setMode( 0 ); // 0: fps, 1: ms, 2: mb
// align top-left
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.body.appendChild( stats.domElement );
var width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
var height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
height = height / 2;
var mainCanvas = document.createElement("canvas");
var hiddenCanvas = document.createElement("canvas");
var data = [];
// A dictionary to lookup nodes by color used in the hidden canvas.
// Note: if you use this code somewhere else, remember that you may need
// to clean up references to objects stored here if the objects are otherwised
// removed/deleted from your vis.
var colToNode = {};
mainCanvas.setAttribute('width', width);
mainCanvas.setAttribute('height', height);
hiddenCanvas.setAttribute('width', width);
hiddenCanvas.setAttribute('height', height);
hiddenCanvas.style.display = 'none';
var container = document.querySelector("#container");
container.appendChild(mainCanvas);
container.appendChild(hiddenCanvas); // Include this to see the hidden canvas.
//
var controls = {
count:100,
showHiddenCanvas: false,
animateHiddenCanvas: false,
lastClickedIndex: -1
};
var gui = new dat.GUI();
var countController = gui.add(controls, 'count', 0, 20000).step(100);
countController.onChange(function(value) {
data = makeData(value);
});
var hiddenCanvasControl = gui.add(controls, 'showHiddenCanvas', false);
hiddenCanvasControl.onChange(function(value) {
if(value){
controls.animateHiddenCanvas = true;
animateHidden.updateDisplay();
hiddenCanvas.style.display = 'block';
} else {
hiddenCanvas.style.display = 'none';
}
});
var animateHidden = gui.add(controls, 'animateHiddenCanvas', false);
var lastClicked = gui.add(controls, 'lastClickedIndex');
/*
Generate the data.
*/
function makeData(count) {
data = [];
for(var i = 0; i < count; i++) {
var w = 10 + Math.random() * 20;
var obj = {
x: Math.random() * (width - 20),
y: Math.random() * (height - 20),
xVel: (Math.random() * 0.5) * (Math.random() < 0.5 ? -1 : 1),
yVel: (Math.random() * 0.5) * (Math.random() < 0.5 ? -1 : 1),
width: w,
height: w,
index: i
};
data.push(obj);
}
return data;
}
/*
Updates the nodes on each frame to make them bounce around the screen.
*/
function update(data) {
var numElements = data.length;
for(var i = 0; i < numElements; i++) {
var node = data[i];
node.x += node.xVel;
node.y += node.yVel;
if(node.x > width || node.x < 0) {
node.xVel *= -1;
}
if(node.y > height || node.y < 0) {
node.yVel *= -1;
}
}
}
function shuffle(data) {
var numElements = data.length;
for(var i = 0; i < numElements; i++) {
node.x = Math.random * width;
node.y = Math.random * width;
}
}
/*
Generates the next color in the sequence, going from 0,0,0 to 255,255,255.
*/
var nextCol = 1;
function genColor(){
var ret = [];
// via http://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 += 100; // This is exagerated for this example and would ordinarily be 1.
}
var col = "rgb(" + ret.join(',') + ")";
return col;
}
function draw(data, canvas, hidden) {
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, width, height);
var numElements = data.length;
for(var i = 0; i < numElements; i++) {
var node = data[i];
if(node.renderCol) {
// Render clicked nodes in the color of their corresponding node
// on the hidden canvas.
ctx.fillStyle = node.renderCol;
} else {
ctx.fillStyle = 'RGBA(105, 105, 105, 0.8)';
}
if(hidden) {
if(node.__pickColor === undefined) {
// If we have never drawn the node to the hidden canvas get a new
// color for it and put it in the dictionary.
node.__pickColor = genColor();
colToNode[node.__pickColor] = node;
}
// On the hidden canvas each rectangle gets a unique color.
ctx.fillStyle = node.__pickColor;
}
drawMark(ctx, node);
}
}
function drawMark(ctx, node) {
// Draw the actual rectangle
// ctx.fillRect(node.x, node.y, node.width, node.height);
ctx.fillRect(node.x, node.y, node.width, node.height);
}
// Listen for clicks on the main canvas
mainCanvas.addEventListener("click", function(e){
draw(data, hiddenCanvas, true);
var mouseX = e.layerX;
var mouseY = e.layerY;
// Get the corresponding pixel color on the hidden canvas
// and look up the node in our map.
var ctx = hiddenCanvas.getContext("2d");
var col = ctx.getImageData(mouseX, mouseY, 1, 1).data;
var colString = "rgb(" + col[0] + "," + col[1] + ","+ col[2] + ")";
var node = colToNode[colString];
if(node) {
node.renderCol = node.__pickColor;
controls.lastClickedIndex = node.index;
lastClicked.updateDisplay();
animateHidden.updateDisplay();
console.log("Clicked on node with index:", node.index, node);
}
});
// Generate the data and start the draw loop.
data = makeData(100); // Increase this number to get more boxes
function animate() {
stats.begin();
draw(data, mainCanvas);
if(controls.animateHiddenCanvas){
draw(data, hiddenCanvas, true);
}
update(data);
stats.end();
window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);
}, false);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment