Picking improvements

- Increased canvas border in demo.html to make it more obvious if future
  code forgets to compensate for the border.
- Fixed picking code to compensate for a border around the canvas.
- Completed support for picking with a touch larger than a single point.
This commit is contained in:
Christopher Willis-Ford 2016-05-31 14:00:39 -07:00
parent c02d2a1f5b
commit f6b78b2216
3 changed files with 120 additions and 28 deletions

View File

@ -5,7 +5,7 @@
<title>Scratch WebGL rendering demo</title>
</head>
<body>
<canvas id="scratch-stage" width="10" height="10" style="border:1px dashed black; margin-top: 9px;"></canvas>
<canvas id="scratch-stage" width="10" height="10" style="border:3px dashed black"></canvas>
<p>
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any" oninput="onFudgeChanged(this.value)" onchange="onFudgeChanged(this.value)">
</p>
@ -33,10 +33,44 @@
window.fudge = newValue;
}
// Adapted from code by Simon Sarris: http://stackoverflow.com/a/10450761
function getMousePos(event, element) {
var stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(element, null)['paddingLeft'], 10) || 0;
var stylePaddingTop = parseInt(document.defaultView.getComputedStyle(element, null)['paddingTop'], 10) || 0;
var styleBorderLeft = parseInt(document.defaultView.getComputedStyle(element, null)['borderLeftWidth'], 10) || 0;
var styleBorderTop = parseInt(document.defaultView.getComputedStyle(element, null)['borderTopWidth'], 10) || 0;
// Some pages have fixed-position bars at the top or left of the page
// They will mess up mouse coordinates and this fixes that
var html = document.body.parentNode;
var htmlTop = html.offsetTop;
var htmlLeft = html.offsetLeft;
// Compute the total offset. It's possible to cache this if you want
var offsetX = 0, offsetY = 0;
if (element.offsetParent !== undefined) {
do {
offsetX += element.offsetLeft;
offsetY += element.offsetTop;
} while ((element = element.offsetParent));
}
// Add padding and border style widths to offset
// Also add the <html> offsets in case there's a position:fixed bar
// This part is not strictly necessary, it depends on your styling
offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft;
offsetY += stylePaddingTop + styleBorderTop + htmlTop;
// We return a simple javascript object with x and y defined
return {
x: event.pageX - offsetX,
y: event.pageY - offsetY
};
}
canvas.onclick = function(event) {
var x = event.pageX - canvas.offsetLeft;
var y = event.pageY - canvas.offsetTop;
var pickID = renderer.pick(x,y);
var mousePos = getMousePos(event, canvas);
var pickID = renderer.pick(mousePos.x, mousePos.y);
console.log('You clicked on ' + (pickID < 0 ? 'nothing' : 'ID# ' + pickID));
};

View File

@ -442,13 +442,16 @@ Drawable.color4fFromID = function(id) {
* Calculate the ID number represented by the given color. If all components of
* the color are zero, the result will be Drawable.NONE; otherwise the result
* will be a valid ID.
* @param {Array.<int>} rgba An array of [r,g,b,a], each component a byte.
* @param {int} r The red value of the color, in the range [0,255].
* @param {int} g The green value of the color, in the range [0,255].
* @param {int} b The blue value of the color, in the range [0,255].
* @param {int} a The alpha value of the color, in the range [0,255].
* @returns {int} The ID represented by that color.
*/
Drawable.color4ubToID = function(rgba) {
Drawable.color4ubToID = function(r, g, b, a) {
var id;
id = (rgba[0] & 255) << 0;
id |= (rgba[1] & 255) << 8;
id |= (rgba[2] & 255) << 16;
id = (r & 255) << 0;
id |= (g & 255) << 8;
id |= (b & 255) << 16;
return id + Drawable.NONE;
};

View File

@ -44,6 +44,13 @@ function RenderWebGL(
this._createQueryBuffers();
}
/**
* Maximum touch size for a picking check.
* TODO: Figure out a reasonable max size. Maybe this should be configurable?
* @type {int[]}
*/
RenderWebGL.MAX_TOUCH_SIZE = [3, 3];
/**
* Inherit from EventEmitter
*/
@ -101,7 +108,7 @@ RenderWebGL.prototype.draw = function () {
var gl = this._gl;
twgl.bindFramebufferInfo(gl, null);
gl.viewport(0, 0, gl.canvas.clientWidth, gl.canvas.clientHeight);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor.apply(gl, this._backgroundColor);
gl.clear(gl.COLOR_BUFFER_BIT);
@ -245,30 +252,44 @@ RenderWebGL.prototype._createQueryBuffers = function () {
{format: gl.DEPTH_STENCIL }
];
// TODO: consider larger sizes for multi-sample touch picking
var pickBufferWidth = 1;
var pickBufferHeight = 1;
this._pickBufferInfo = twgl.createFramebufferInfo(
gl, attachments, pickBufferWidth, pickBufferHeight);
gl, attachments,
RenderWebGL.MAX_TOUCH_SIZE[0], RenderWebGL.MAX_TOUCH_SIZE[1]);
};
/**
* @param {int} centerX The canvas x coordinate of the picking location.
* @param {int} centerY The canvas y coordinate of the picking location.
* Detect which sprite, if any, is at the given location.
* @param {int} centerX The client x coordinate of the picking location.
* @param {int} centerY The client y coordinate of the picking location.
* @param {int} touchWidth The client width of the touch event (optional).
* @param {int} touchHeight The client height of the touch event (optional).
* @returns {int} The ID of the topmost Drawable under the picking location, or
* Drawable.NONE if there is no Drawable at that location.
*/
RenderWebGL.prototype.pick = function (centerX, centerY) {
RenderWebGL.prototype.pick = function (
centerX, centerY, touchWidth, touchHeight) {
var gl = this._gl;
// TODO: consider larger sizes for multi-sample touch picking
var touchWidth = 1;
var touchHeight = 1;
var pixelLeft = centerX - Math.floor(touchWidth / 2);
var pixelRight = centerX + Math.ceil(touchWidth / 2);
var pixelTop = centerY - Math.floor(touchHeight / 2);
var pixelBottom = centerY + Math.ceil(touchHeight / 2);
touchWidth = touchWidth || 1;
touchHeight = touchHeight || 1;
var clientToGLX = gl.canvas.width / gl.canvas.clientWidth;
var clientToGLY = gl.canvas.height / gl.canvas.clientHeight;
centerX *= clientToGLX;
centerY *= clientToGLY;
touchWidth *= clientToGLX;
touchHeight *= clientToGLY;
touchWidth =
Math.max(1, Math.min(touchWidth, RenderWebGL.MAX_TOUCH_SIZE[0]));
touchHeight =
Math.max(1, Math.min(touchHeight, RenderWebGL.MAX_TOUCH_SIZE[1]));
var pixelLeft = Math.floor(centerX - Math.floor(touchWidth / 2) + 0.5);
var pixelRight = Math.floor(centerX + Math.ceil(touchWidth / 2) + 0.5);
var pixelTop = Math.floor(centerY - Math.floor(touchHeight / 2) + 0.5);
var pixelBottom = Math.floor(centerY + Math.ceil(touchHeight / 2) + 0.5);
twgl.bindFramebufferInfo(gl, this._pickBufferInfo);
gl.viewport(0, 0, touchWidth, touchHeight);
@ -291,8 +312,42 @@ RenderWebGL.prototype.pick = function (centerX, centerY) {
this._drawExcept(Drawable.DRAW_MODE.pick, null, projection);
var pixels = new Uint8Array(1 * 1 * 4);
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
var pixels = new Uint8Array(touchWidth * touchHeight * 4);
gl.readPixels(
0, 0, touchWidth, touchHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return Drawable.color4ubToID(pixels);
// Uncomment this and make a canvas with id="pick-image" to debug picking
/*
var pickImage = document.getElementById('pick-image');
pickImage.width = touchWidth;
pickImage.height = touchHeight;
var context = pickImage.getContext('2d');
var imageData = context.getImageData(0, 0, touchWidth, touchHeight);
for (var i = 0, bytes = pixels.length; i < bytes; ++i) {
imageData.data[i] = pixels[i];
}
context.putImageData(imageData, 0, 0);
*/
var hits = {};
for (var pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) {
var pixelID = Drawable.color4ubToID(
pixels[pixelBase],
pixels[pixelBase + 1],
pixels[pixelBase + 2],
pixels[pixelBase + 3]);
hits[pixelID] = (hits[pixelID] || 0) + 1;
}
// Bias toward selecting anything over nothing
hits[Drawable.NONE] = 0;
var hit = Drawable.NONE;
for (var hitID in hits) {
if (hits.hasOwnProperty(hitID) && (hits[hitID] > hits[hit])) {
hit = hitID;
}
}
return hit | 0;
};