Make API consistent from page and worker

RenderWebGL{Local,Worker} class methods return Promise instances from
all methods returning a value. This makes the API consistent from a
Web Worker vs. the local page, and also allows the possibility of
asynchronous render queries in the future.
This commit is contained in:
Christopher Willis-Ford 2016-06-16 12:59:48 -07:00
parent b00ec4c501
commit ef100bf91e
7 changed files with 530 additions and 308 deletions

View File

@ -18,7 +18,7 @@
<script src="render-webgl.js"></script>
<script>
var canvas = document.getElementById('scratch-stage');
var renderer = new RenderWebGL(canvas);
var renderer = new RenderWebGLLocal(canvas);
var fudgeInput = document.getElementById('fudge');
function onFudgeMinChanged(newValue) {

View File

@ -4,6 +4,7 @@ var Drawable = require('./Drawable');
var WorkerMessages = require('./WorkerMessages');
var ShaderManager = require('./ShaderManager');
class RenderWebGL {
/**
* Create a renderer for drawing Scratch sprites to a canvas using WebGL.
@ -61,6 +62,23 @@ RenderWebGL.MAX_TOUCH_SIZE = [3, 3];
*/
RenderWebGL.TOLERANCE_TOUCHING_COLOR = 2;
/********
* Functions called only locally: these are not available from a worker.
********/
/**
* Set the physical size of the stage in device-independent pixels.
* This will be multiplied by the device's pixel ratio on high-DPI displays.
* @param {int} pixelsWide The desired width in device-independent pixels.
* @param {int} pixelsTall The desired height in device-independent pixels.
*/
RenderWebGL.prototype.resize = function (pixelsWide, pixelsTall) {
var pixelRatio = window.devicePixelRatio || 1;
this._gl.canvas.width = pixelsWide * pixelRatio;
this._gl.canvas.height = pixelsTall * pixelRatio;
};
/**
* Set the background color for the stage. The stage will be cleared with this
* color each frame.
@ -72,6 +90,15 @@ RenderWebGL.prototype.setBackgroundColor = function(red, green, blue) {
this._backgroundColor = [red, green, blue, 1];
};
/**
* Tell the renderer to draw various debug information to the provided canvas
* during certain operations.
* @param {canvas} canvas The canvas to use for debug output.
*/
RenderWebGL.prototype.setDebugCanvas = function (canvas) {
this._debugCanvas = canvas;
};
/**
* Set logical size of the stage in Scratch units.
* @param {int} xLeft The left edge's x-coordinate. Scratch 2 uses -240.
@ -88,22 +115,41 @@ RenderWebGL.prototype.setStageSize = function (xLeft, xRight, yBottom, yTop) {
this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
};
/********
* Functions supporting RenderWebGL{Local,Worker}: access from those classes.
********/
/**
* Set the physical size of the stage in device-independent pixels.
* This will be multiplied by the device's pixel ratio on high-DPI displays.
* @param {int} pixelsWide The desired width in device-independent pixels.
* @param {int} pixelsTall The desired height in device-independent pixels.
* Create a new Drawable and add it to the scene.
* @returns {int} The ID of the new Drawable.
*/
RenderWebGL.prototype.resize = function (pixelsWide, pixelsTall) {
var pixelRatio = window.devicePixelRatio || 1;
this._gl.canvas.width = pixelsWide * pixelRatio;
this._gl.canvas.height = pixelsTall * pixelRatio;
RenderWebGL.prototype._createDrawable = function () {
var drawable = new Drawable(this._gl);
var drawableID = drawable.getID();
this._drawables.push(drawableID);
return drawableID;
};
/**
* Destroy a Drawable, removing it from the scene.
* @param {int} drawableID The ID of the Drawable to remove.
* @returns {Boolean} True iff the drawable was found and removed.
*/
RenderWebGL.prototype._destroyDrawable = function (drawableID) {
var index = this._drawables.indexOf(drawableID);
if (index >= 0) {
Drawable.getDrawableByID(drawableID).dispose();
this._drawables.splice(index, 1);
return true;
}
return false;
};
/**
* Draw all current drawables and present the frame on the canvas.
*/
RenderWebGL.prototype.draw = function () {
RenderWebGL.prototype._draw = function () {
var gl = this._gl;
twgl.bindFramebufferInfo(gl, null);
@ -115,247 +161,6 @@ RenderWebGL.prototype.draw = function () {
this._drawables, ShaderManager.DRAW_MODE.default, this._projection);
};
/**
* Draw all Drawables, with the possible exception of
* @param {int[]} drawables The Drawable IDs to draw, possibly this._drawables.
* @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
* @param {module:twgl/m4.Mat4} projection The projection matrix to use.
* @param {Drawable~idFilterFunc} [filter] An optional filter function.
* @param {Object.<string,*>} [extraUniforms] Extra uniforms for the shaders.
* @private
*/
RenderWebGL.prototype._drawThese = function(
drawables, drawMode, projection, filter, extraUniforms) {
var gl = this._gl;
var currentShader = null;
var numDrawables = drawables.length;
for (var drawableIndex = 0; drawableIndex < numDrawables; ++drawableIndex) {
var drawableID = drawables[drawableIndex];
// If we have a filter, check whether the ID fails
if (filter && !filter(drawableID)) continue;
var drawable = Drawable.getDrawableByID(drawableID);
// TODO: check if drawable is inside the viewport before anything else
var effectBits = drawable.getEnabledEffects();
var newShader = this._shaderManager.getShader(drawMode, effectBits);
if (currentShader != newShader) {
currentShader = newShader;
gl.useProgram(currentShader.program);
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
twgl.setUniforms(currentShader, {u_projectionMatrix: projection});
twgl.setUniforms(currentShader, {u_fudge: window.fudge || 0});
// TODO: should these be set after the Drawable's uniforms?
// That would allow Drawable-scope uniforms to be overridden...
if (extraUniforms) {
twgl.setUniforms(currentShader, extraUniforms);
}
}
twgl.setUniforms(currentShader, drawable.getUniforms());
twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo);
}
};
/**
* Create a new Drawable and add it to the scene.
* @returns {int} The ID of the new Drawable.
*/
RenderWebGL.prototype.createDrawable = function () {
var drawable = new Drawable(this._gl);
var drawableID = drawable.getID();
this._drawables.push(drawableID);
return drawableID;
};
/**
* Destroy a Drawable, removing it from the scene.
* @param {int} drawableID The ID of the Drawable to remove.
* @returns {Boolean} True iff the drawable was found and removed.
*/
RenderWebGL.prototype.destroyDrawable = function (drawableID) {
var index = this._drawables.indexOf(drawableID);
if (index >= 0) {
Drawable.getDrawableByID(drawableID).dispose();
this._drawables.splice(index, 1);
return true;
}
return false;
};
/**
* Retrieve the renderer's projection matrix.
* @returns {module:twgl/m4.Mat4} The projection matrix.
*/
RenderWebGL.prototype.getProjectionMatrix = function () {
return this._projection;
};
/**
* Build geometry (vertex and index) buffers.
* @private
*/
RenderWebGL.prototype._createGeometry = function () {
var quad = {
a_position: {
numComponents: 2,
data: [
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, 0.5,
0.5, -0.5,
0.5, 0.5
]
},
a_texCoord: {
numComponents: 2,
data: [
1, 0,
0, 0,
1, 1,
1, 1,
0, 0,
0, 1
]
}
};
this._bufferInfo = twgl.createBufferInfoFromArrays(this._gl, quad);
};
/**
* Create the frame buffers used for queries such as picking and color-touching.
* These buffers are fixed in size regardless of the size of the main render
* target. The fixed size allows (more) consistent behavior across devices and
* presentation modes.
* @private
*/
RenderWebGL.prototype._createQueryBuffers = function () {
var gl = this._gl;
var attachments = [
{format: gl.RGBA },
{format: gl.DEPTH_STENCIL }
];
this._pickBufferInfo = twgl.createFramebufferInfo(
gl, attachments,
RenderWebGL.MAX_TOUCH_SIZE[0], RenderWebGL.MAX_TOUCH_SIZE[1]);
// TODO: should we create this on demand to save memory?
// A 480x360 32-bpp buffer is 675 KiB.
this._queryBufferInfo = twgl.createFramebufferInfo(
gl, attachments, this._nativeSize[0], this._nativeSize[1]);
};
/**
* Tell the renderer to draw various debug information to the provided canvas
* during certain operations.
* @param {canvas} canvas The canvas to use for debug output.
*/
RenderWebGL.prototype.setDebugCanvas = function (canvas) {
this._debugCanvas = canvas;
};
/**
* 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).
* @param {int[]} candidateIDs The Drawable IDs to pick from, otherwise all.
* @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, touchWidth, touchHeight, candidateIDs) {
var gl = this._gl;
touchWidth = touchWidth || 1;
touchHeight = touchHeight || 1;
candidateIDs = candidateIDs || this._drawables;
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);
var noneColor = Drawable.color4fFromID(Drawable.NONE);
gl.clearColor.apply(gl, noneColor);
gl.clear(gl.COLOR_BUFFER_BIT);
var widthPerPixel = (this._xRight - this._xLeft) / this._gl.canvas.width;
var heightPerPixel = (this._yBottom - this._yTop) / this._gl.canvas.height;
var pickLeft = this._xLeft + pixelLeft * widthPerPixel;
var pickRight = this._xLeft + pixelRight * widthPerPixel;
var pickTop = this._yTop + pixelTop * heightPerPixel;
var pickBottom = this._yTop + pixelBottom * heightPerPixel;
var projection = twgl.m4.ortho(
pickLeft, pickRight, pickTop, pickBottom, -1, 1);
this._drawThese(
candidateIDs, ShaderManager.DRAW_MODE.silhouette, projection);
var pixels = new Buffer(touchWidth * touchHeight * 4);
gl.readPixels(
0, 0, touchWidth, touchHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
this._debugCanvas.width = touchWidth;
this._debugCanvas.height = touchHeight;
var context = this._debugCanvas.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.color4bToID(
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;
};
/**
* Check if a particular Drawable is touching a particular color.
* @param {int} drawableID The ID of the Drawable to check.
@ -363,7 +168,7 @@ RenderWebGL.prototype.pick = function (
* @param {int[]} [mask3b] Optionally mask the check to this part of Drawable.
* @returns {Boolean} True iff the Drawable is touching the color.
*/
RenderWebGL.prototype.isTouchingColor = function(drawableID, color3b, mask3b) {
RenderWebGL.prototype._isTouchingColor = function(drawableID, color3b, mask3b) {
var gl = this._gl;
@ -450,32 +255,213 @@ RenderWebGL.prototype.isTouchingColor = function(drawableID, color3b, mask3b) {
};
/**
* Handle an event (message) from the specified worker.
* @param {Worker} worker The originating worker for the event.
* @param {MessageEvent} message The event to be handled.
* 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).
* @param {int[]} candidateIDs The Drawable IDs to pick from, otherwise all.
* @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, touchWidth, touchHeight, candidateIDs) {
var gl = this._gl;
touchWidth = touchWidth || 1;
touchHeight = touchHeight || 1;
candidateIDs = candidateIDs || this._drawables;
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);
var noneColor = Drawable.color4fFromID(Drawable.NONE);
gl.clearColor.apply(gl, noneColor);
gl.clear(gl.COLOR_BUFFER_BIT);
var widthPerPixel = (this._xRight - this._xLeft) / this._gl.canvas.width;
var heightPerPixel = (this._yBottom - this._yTop) / this._gl.canvas.height;
var pickLeft = this._xLeft + pixelLeft * widthPerPixel;
var pickRight = this._xLeft + pixelRight * widthPerPixel;
var pickTop = this._yTop + pixelTop * heightPerPixel;
var pickBottom = this._yTop + pixelBottom * heightPerPixel;
var projection = twgl.m4.ortho(
pickLeft, pickRight, pickTop, pickBottom, -1, 1);
this._drawThese(
candidateIDs, ShaderManager.DRAW_MODE.silhouette, projection);
var pixels = new Buffer(touchWidth * touchHeight * 4);
gl.readPixels(
0, 0, touchWidth, touchHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
this._debugCanvas.width = touchWidth;
this._debugCanvas.height = touchHeight;
var context = this._debugCanvas.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.color4bToID(
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;
};
/********
* Truly internal functions: these support the functions above.
********/
/**
* Build geometry (vertex and index) buffers.
* @private
*/
RenderWebGL.prototype._onWorkerMessage = function(worker, message) {
if (message.data.drawableID != null) {
var drawable = Drawable.getDrawableByID(message.data.drawableID);
}
switch(message.data.id) {
case WorkerMessages.ToRenderer.Ping:
worker.postMessage(WorkerMessages.FromRenderer.Pong);
break;
case WorkerMessages.ToRenderer.CreateDrawable:
worker.postMessage({
id: WorkerMessages.FromRenderer.ResultValue,
token: message.data.token,
value: this.createDrawable()
});
break;
case WorkerMessages.ToRenderer.UpdateDrawableProperties:
drawable.updateProperties(message.data.properties);
break;
RenderWebGL.prototype._createGeometry = function () {
var quad = {
a_position: {
numComponents: 2,
data: [
-0.5, -0.5,
0.5, -0.5,
-0.5, 0.5,
-0.5, 0.5,
0.5, -0.5,
0.5, 0.5
]
},
a_texCoord: {
numComponents: 2,
data: [
1, 0,
0, 0,
1, 1,
1, 1,
0, 0,
0, 1
]
}
};
this._bufferInfo = twgl.createBufferInfoFromArrays(this._gl, quad);
};
/**
* Create the frame buffers used for queries such as picking and color-touching.
* These buffers are fixed in size regardless of the size of the main render
* target. The fixed size allows (more) consistent behavior across devices and
* presentation modes.
* @private
*/
RenderWebGL.prototype._createQueryBuffers = function () {
var gl = this._gl;
var attachments = [
{format: gl.RGBA },
{format: gl.DEPTH_STENCIL }
];
this._pickBufferInfo = twgl.createFramebufferInfo(
gl, attachments,
RenderWebGL.MAX_TOUCH_SIZE[0], RenderWebGL.MAX_TOUCH_SIZE[1]);
// TODO: should we create this on demand to save memory?
// A 480x360 32-bpp buffer is 675 KiB.
this._queryBufferInfo = twgl.createFramebufferInfo(
gl, attachments, this._nativeSize[0], this._nativeSize[1]);
};
/**
* Draw all Drawables, with the possible exception of
* @param {int[]} drawables The Drawable IDs to draw, possibly this._drawables.
* @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
* @param {module:twgl/m4.Mat4} projection The projection matrix to use.
* @param {Drawable~idFilterFunc} [filter] An optional filter function.
* @param {Object.<string,*>} [extraUniforms] Extra uniforms for the shaders.
* @private
*/
RenderWebGL.prototype._drawThese = function(
drawables, drawMode, projection, filter, extraUniforms) {
var gl = this._gl;
var currentShader = null;
var numDrawables = drawables.length;
for (var drawableIndex = 0; drawableIndex < numDrawables; ++drawableIndex) {
var drawableID = drawables[drawableIndex];
// If we have a filter, check whether the ID fails
if (filter && !filter(drawableID)) continue;
var drawable = Drawable.getDrawableByID(drawableID);
// TODO: check if drawable is inside the viewport before anything else
var effectBits = drawable.getEnabledEffects();
var newShader = this._shaderManager.getShader(drawMode, effectBits);
if (currentShader != newShader) {
currentShader = newShader;
gl.useProgram(currentShader.program);
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
twgl.setUniforms(currentShader, {u_projectionMatrix: projection});
twgl.setUniforms(currentShader, {u_fudge: window.fudge || 0});
// TODO: should these be set after the Drawable's uniforms?
// That would allow Drawable-scope uniforms to be overridden...
if (extraUniforms) {
twgl.setUniforms(currentShader, extraUniforms);
}
}
twgl.setUniforms(currentShader, drawable.getUniforms());
twgl.drawBufferInfo(gl, gl.TRIANGLES, this._bufferInfo);
}
};
/********
* Worker interface
* TODO: Consider moving this to a separate "dispatcher" class or similar.
********/
/**
* Listen for messages from a worker.
* The renderer will post a message to this worker with data='rendererConnected'
@ -490,3 +476,63 @@ RenderWebGL.prototype.connectWorker = function(worker) {
});
worker.postMessage({id: WorkerMessages.FromRenderer.RendererConnected});
};
/**
* Post a ResultValue message to a worker in reply to a particular message.
* The outgoing message's reply token will be copied from the provided message.
* @param {Worker} worker The worker to receive the ResultValue message.
* @param {Object} message The originating message to which this is a reply.
* @param {*} value The value to send as a result.
* @private
*/
RenderWebGL.prototype._postResultValue = function(worker, message, value) {
worker.postMessage({
id: WorkerMessages.FromRenderer.ResultValue,
token: message.data.token,
value: value
});
};
/**
* Handle an event (message) from the specified worker.
* @param {Worker} worker The originating worker for the event.
* @param {MessageEvent} message The event to be handled.
* @private
*/
RenderWebGL.prototype._onWorkerMessage = function(worker, message) {
switch(message.data.id) {
case WorkerMessages.ToRenderer.Ping:
worker.postMessage(WorkerMessages.FromRenderer.Pong);
break;
case WorkerMessages.ToRenderer.CreateDrawable:
this._postResultValue(worker, message, this._createDrawable());
break;
case WorkerMessages.ToRenderer.DestroyDrawable:
this._postResultValue(worker, message,
this._destroyDrawable(message.data.drawableID));
break;
case WorkerMessages.ToRenderer.Draw:
this._draw();
break;
case WorkerMessages.ToRenderer.IsTouchingColor:
this._postResultValue(worker, message,
this._isTouchingColor(
message.data.drawableID,
message.data.color3b,
message.data.mask3b));
break;
case WorkerMessages.ToRenderer.Pick:
this._postResultValue(worker, message,
this._pick(
message.data.centerX,
message.data.centerY,
message.data.touchWidth,
message.data.touchHeight,
message.data.candidateIDs));
break;
case WorkerMessages.ToRenderer.UpdateDrawableProperties:
var drawable = Drawable.getDrawableByID(message.data.drawableID);
drawable.updateProperties(message.data.properties);
break;
}
};

81
src/RenderWebGLLocal.js Normal file
View File

@ -0,0 +1,81 @@
var RenderWebGL = require('./RenderWebGL');
var Drawable = require('./Drawable');
class RenderWebGLLocal extends RenderWebGL {
// inherit constructor
}
module.exports = RenderWebGLLocal;
/**
* Create a new Drawable and add it to the scene.
* @returns {Promise.<int>} The ID of the new Drawable.
*/
RenderWebGLLocal.prototype.createDrawable = function () {
var drawableID = this._createDrawable();
return Promise.resolve(drawableID);
};
/**
* Destroy a Drawable, removing it from the scene.
* @param {int} drawableID The ID of the Drawable to remove.
* @returns {Promise.<Boolean>} True iff the drawable was found and removed.
*/
RenderWebGLLocal.prototype.destroyDrawable = function (drawableID) {
var wasRemoved = this._destroyDrawable(drawableID);
return Promise.resolve(wasRemoved);
};
/**
* Draw all current drawables and present the frame on the canvas.
*/
RenderWebGLLocal.prototype.draw = function () {
this._draw();
};
/**
* Check if a particular Drawable is touching a particular color.
* @param {int} drawableID The ID of the Drawable to check.
* @param {int[]} color3b Test if the Drawable is touching this color.
* @param {int[]} [mask3b] Optionally mask the check to this part of Drawable.
* @returns {Promise.<Boolean>} True iff the Drawable is touching the color.
*/
RenderWebGLLocal.prototype.isTouchingColor = function(
drawableID, color3b, mask3b) {
var isTouching = this._isTouchingColor(drawableID, color3b, mask3b);
return Promise.resolve(isTouching);
};
/**
* 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).
* @param {int[]} candidateIDs The Drawable IDs to pick from, otherwise all.
* @returns {int} The ID of the topmost Drawable under the picking location, or
* Drawable.NONE if there is no Drawable at that location.
*/
RenderWebGLLocal.prototype.pick = function (
centerX, centerY, touchWidth, touchHeight, candidateIDs) {
var drawableID =
this._pick(centerX, centerY, touchWidth, touchHeight, candidateIDs);
return Promise.resolve(drawableID);
};
/**
* Update the position, direction, scale, or effect properties of this Drawable.
* @param {int} drawableID The ID of the Drawable to update.
* @param {Object.<string,*>} properties The new property values to set.
*/
RenderWebGLLocal.prototype.updateDrawableProperties = function (
drawableID, properties) {
var drawable = Drawable.getDrawableByID(drawableID);
drawable.updateProperties(properties);
};

View File

@ -27,10 +27,129 @@ class RenderWebGLWorker {
module.exports = RenderWebGLWorker;
/**
* Create a new Drawable and add it to the scene.
* @returns {Promise.<int>} The ID of the new Drawable.
*/
RenderWebGLWorker.prototype.createDrawable = function() {
return this._postForPromise({
id: WorkerMessages.ToRenderer.CreateDrawable
});
};
/**
* Destroy a Drawable, removing it from the scene.
* @param {int} drawableID The ID of the Drawable to remove.
* @returns {Promise.<Boolean>} True iff the drawable was found and removed.
*/
RenderWebGLWorker.prototype.destroyDrawable = function (drawableID) {
return this._postForPromise({
id: WorkerMessages.ToRenderer.DestroyDrawable,
drawableID: drawableID
});
};
/**
* Draw all current drawables and present the frame on the canvas.
*/
RenderWebGLWorker.prototype.draw = function () {
self.postMessage({
id: WorkerMessages.ToRenderer.Draw
});
};
/**
* Check if a particular Drawable is touching a particular color.
* @param {int} drawableID The ID of the Drawable to check.
* @param {int[]} color3b Test if the Drawable is touching this color.
* @param {int[]} [mask3b] Optionally mask the check to this part of Drawable.
* @returns {Promise.<Boolean>} True iff the Drawable is touching the color.
*/
RenderWebGLWorker.prototype.isTouchingColor = function(
drawableID, color3b, mask3b) {
var messageData = {
id: WorkerMessages.ToRenderer.IsTouchingColor,
drawableID: drawableID,
color3b: color3b
};
if (mask3b != undefined) {
messageData.mask3b = mask3b;
}
return this._postForPromise(messageData);
};
/**
* 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).
* @param {int[]} candidateIDs The Drawable IDs to pick from, otherwise all.
* @returns {int} The ID of the topmost Drawable under the picking location, or
* Drawable.NONE if there is no Drawable at that location.
*/
RenderWebGLWorker.prototype.pick = function (
centerX, centerY, touchWidth, touchHeight, candidateIDs) {
var messageData = {
id: WorkerMessages.ToRenderer.Pick,
centerX: centerX,
centerY: centerY
};
if (touchWidth > 1) {
messageData.touchWidth = touchWidth;
}
if (touchHeight > 1) {
messageData.touchHeight = touchHeight;
}
if (candidateIDs != undefined) {
messageData.candidateIDs = candidateIDs;
}
return this._postForPromise(messageData);
};
/**
* Update the position, direction, scale, or effect properties of this Drawable.
* @param {int} drawableID The ID of the Drawable to update.
* @param {Object.<string,*>} properties The new property values to set.
*/
RenderWebGLWorker.prototype.updateDrawableProperties = function (
drawableID, properties) {
self.postMessage({
id: WorkerMessages.ToRenderer.UpdateDrawableProperties,
drawableID: drawableID,
properties: properties
});
};
/**
* Retrieve a unique token for use in a message which requests a reply.
* @returns {string}
* @private
*/
RenderWebGLWorker.prototype._getToken = function() {
return (this._nextToken++) + '';
};
/**
* Post a message to the renderer and return a Promise awaiting the result.
* WARNING: The messageData object will be modified by this function. The
* `token` property of messageData will be set to the message's reply token.
* @param {Object} messageData The contents of the message.
* @private
*/
RenderWebGLWorker.prototype._postForPromise = function(messageData) {
var instance = this;
return new Promise(function (resolve) {
var token = instance._getToken();
instance._pendingTokens[token] = resolve;
messageData.token = token;
self.postMessage(messageData);
});
};
/**
* Actually handle a message from this worker's host.
* @param {MessageEvent} message The message to be handled.
@ -51,34 +170,3 @@ RenderWebGLWorker.prototype._onmessage = function(message) {
break;
}
};
/**
* Create a new Drawable and add it to the scene.
* @returns {Promise.<int>} The ID of the new Drawable.
*/
RenderWebGLWorker.prototype.createDrawable = function() {
var instance = this;
return new Promise(function (resolve) {
var token = instance._getToken();
instance._pendingTokens[token] = resolve;
self.postMessage({
id: WorkerMessages.ToRenderer.CreateDrawable,
token: token
});
});
};
/**
* Update the position, direction, scale, or effect properties of this Drawable.
* @param {int} drawableID The ID of the Drawable to update.
* @param {Object.<string,*>} properties The new property values to set.
*/
RenderWebGLWorker.prototype.updateDrawableProperties = function (
drawableID, properties) {
self.postMessage({
id: WorkerMessages.ToRenderer.UpdateDrawableProperties,
drawableID: drawableID,
properties: properties
});
};

View File

@ -15,11 +15,18 @@ var WorkerMessages = {
* If the renderer replies to the message, the 'token' property will be
* copied into the reply message. If a message generates no reply, the
* 'token' property is optional.
* In general these messages correspond to a function on RenderWebGLLocal,
* and in particular each argument in the RenderWebGLLocal method can be
* encoded as a property on the message data object with the same name.
* @enum {string}
*/
ToRenderer: {
Ping: 'Ping',
CreateDrawable: 'CreateDrawable',
DestroyDrawable: 'DestroyDrawable',
Draw: 'Draw',
IsTouchingColor: 'IsTouchingColor',
Pick: 'Pick',
UpdateDrawableProperties: 'UpdateDrawableProperties'
},

View File

@ -1,6 +1,6 @@
var RenderWebGL = require('./RenderWebGL');
var RenderWebGLLocal = require('./RenderWebGLLocal');
/**
* Export for use in a web page
*/
window.RenderWebGL = RenderWebGL;
window.RenderWebGLLocal = RenderWebGLLocal;

View File

@ -1,6 +1,6 @@
var RenderWebGL = require('./RenderWebGL');
var RenderWebGLLocal = require('./RenderWebGLLocal');
/**
* Export for NPM / Node.js
*/
module.exports = RenderWebGL;
module.exports = RenderWebGLLocal;