Implement pen point, line, and stamp

Also clean up the code around our calls to `readPixels`:
- Use `Uint8Array` instead of `Buffer` to hold the pixels
- Use `ImageData.data.set` instead of a `for` loop when copying pixels
  to a canvas
This commit is contained in:
Christopher Willis-Ford 2017-01-10 11:43:26 -08:00
parent 140c0fbf37
commit f29ed34ddc
2 changed files with 150 additions and 14 deletions

View file

@ -4,6 +4,23 @@ const RenderConstants = require('./RenderConstants');
const Skin = require('./Skin');
/**
* Attributes to use when drawing with the pen
* @typedef {object} PenAttributes
* @property {number} [diameter] - The size (diameter) of the pen.
* @property {number[]} [color4f] - The pen color as an array of [r,g,b,a], each component in the range [0,1].
*/
/**
* The pen attributes to use when unspecified.
* @type {PenAttributes}
*/
const DefaultPenAttributes = {
color4f: [0, 0, 1, 1],
diameter: 1
};
class PenSkin extends Skin {
/**
* Create a Skin which implements a Scratch pen layer.
@ -66,6 +83,43 @@ class PenSkin extends Skin {
return this._texture;
}
/**
* Draw a point on the pen layer.
* @param {[number, number]} location - where the point should be drawn.
* @param {PenAttributes} penAttributes - how the point should be drawn.
*/
drawPoint (location, penAttributes) {
// Canvas renders a zero-length line as two end-caps back-to-back, which is what we want.
this.drawLine(location, location, penAttributes);
}
/**
* Draw a point on the pen layer.
* @param {[number, number]} location0 - where the line should start.
* @param {[number, number]} location1 - where the line should end.
* @param {PenAttributes} penAttributes - how the line should be drawn.
*/
drawLine (location0, location1, penAttributes) {
const ctx = this._canvas.getContext('2d');
this._setAttributes(ctx, penAttributes);
ctx.moveTo(location0[0], location0[1]);
ctx.beginPath();
ctx.lineTo(location1[0], location1[1]);
ctx.stroke();
this._canvasDirty = true;
}
/**
* Stamp an image onto the pen layer.
* @param {[number, number]} location - where the stamp should be drawn.
* @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} stampElement - the element to use as the stamp.
*/
drawStamp (location, stampElement) {
const ctx = this._canvas.getContext('2d');
ctx.drawImage(stampElement, location[0], location[1]);
this._canvasDirty = true;
}
/**
* React to a change in the renderer's native size.
* @param {object} event - The change event.
@ -85,6 +139,8 @@ class PenSkin extends Skin {
const gl = this._renderer.gl;
this._canvas.width = width;
this._canvas.height = height;
this._rotationCenter[0] = width / 2;
this._rotationCenter[1] = height / 2;
this._texture = twgl.createTexture(
gl,
{
@ -118,6 +174,21 @@ class PenSkin extends Skin {
ctx.lineTo(width / 10, height / 5);
ctx.stroke();
}
_setAttributes (context, penAttributes) {
penAttributes = penAttributes || DefaultPenAttributes;
const color4f = penAttributes.color4f || DefaultPenAttributes.color4f;
const diameter = penAttributes.diameter || DefaultPenAttributes.diameter;
const r = Math.round(color4f[0] * 255);
const g = Math.round(color4f[1] * 255);
const b = Math.round(color4f[2] * 255);
const a = Math.round(color4f[3] * 255);
context.fillStyle = `rgba(${r},${g},${b},${a})`;
context.lineCap = 'round';
context.lineWidth = diameter;
}
}
module.exports = PenSkin;

View file

@ -78,6 +78,9 @@ class RenderWebGL extends EventEmitter {
this._shaderManager = new ShaderManager(gl);
/** @type {HTMLCanvasElement} */
this._tempCanvas = document.createElement('canvas');
this._createGeometry();
this.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
@ -416,7 +419,6 @@ class RenderWebGL extends EventEmitter {
return;
}
// Limit size of viewport to the bounds around the target Drawable,
// and create the projection matrix for the draw.
gl.viewport(0, 0, bounds.width, bounds.height);
@ -459,7 +461,7 @@ class RenderWebGL extends EventEmitter {
gl.disable(gl.STENCIL_TEST);
}
const pixels = new Buffer(bounds.width * bounds.height * 4);
const pixels = new Uint8Array(bounds.width * bounds.height * 4);
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
@ -467,9 +469,7 @@ class RenderWebGL extends EventEmitter {
this._debugCanvas.height = bounds.height;
const context = this._debugCanvas.getContext('2d');
const imageData = context.getImageData(0, 0, bounds.width, bounds.height);
for (let i = 0, bytes = pixels.length; i < bytes; ++i) {
imageData.data[i] = pixels[i];
}
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
}
@ -538,7 +538,7 @@ class RenderWebGL extends EventEmitter {
gl.disable(gl.STENCIL_TEST);
}
const pixels = new Buffer(bounds.width * bounds.height * 4);
const pixels = new Uint8Array(bounds.width * bounds.height * 4);
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
@ -546,9 +546,7 @@ class RenderWebGL extends EventEmitter {
this._debugCanvas.height = bounds.height;
const context = this._debugCanvas.getContext('2d');
const imageData = context.getImageData(0, 0, bounds.width, bounds.height);
for (let i = 0, bytes = pixels.length; i < bytes; ++i) {
imageData.data[i] = pixels[i];
}
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
}
@ -617,7 +615,7 @@ class RenderWebGL extends EventEmitter {
this._drawThese(candidateIDs, ShaderManager.DRAW_MODE.silhouette, projection);
const pixels = new Buffer(touchWidth * touchHeight * 4);
const pixels = new Uint8Array(touchWidth * touchHeight * 4);
gl.readPixels(0, 0, touchWidth, touchHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
@ -625,9 +623,7 @@ class RenderWebGL extends EventEmitter {
this._debugCanvas.height = touchHeight;
const context = this._debugCanvas.getContext('2d');
const imageData = context.getImageData(0, 0, touchWidth, touchHeight);
for (let i = 0, bytes = pixels.length; i < bytes; ++i) {
imageData.data[i] = pixels[i];
}
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
}
@ -734,6 +730,75 @@ class RenderWebGL extends EventEmitter {
drawable.updateProperties(properties);
}
/**
* Draw a point on a pen layer.
* @param {int} penSkinID - the unique ID of a Pen Skin.
* @param {[number, number]} location - where the point should be drawn.
* @param {PenAttributes} penAttributes - how the point should be drawn.
*/
penPoint (penSkinID, location, penAttributes) {
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
skin.drawPoint(location, penAttributes);
}
/**
* Draw a line on a pen layer.
* @param {int} penSkinID - the unique ID of a Pen Skin.
* @param {[number, number]} location0 - where the line should start.
* @param {[number, number]} location1 - where the line should end.
* @param {PenAttributes} penAttributes - how the line should be drawn.
*/
penLine (penSkinID, location0, location1, penAttributes) {
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
skin.drawLine(location0, location1, penAttributes);
}
/**
* Draw a point on a pen layer.
* @param {int} penSkinID - the unique ID of a Pen Skin.
* @param {[number, number]} location - where the point should be drawn.
* @param {int} stampID - the unique ID of the Drawable to use as the stamp.
*/
penStamp (penSkinID, location, stampID) {
const bounds = this._touchingBounds(stampID);
if (!bounds) {
return;
}
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
// Limit size of viewport to the bounds around the stamp Drawable and create the projection matrix for the draw.
gl.viewport(0, 0, bounds.width, bounds.height);
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.bottom, bounds.top, -1, 1);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
try {
gl.disable(gl.BLEND);
this._drawThese([stampID], ShaderManager.DRAW_MODE.default, projection);
} finally {
gl.enable(gl.BLEND);
}
const stampPixels = new Uint8Array(bounds.width * bounds.height * 4);
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, stampPixels);
const stampCanvas = this._tempCanvas;
stampCanvas.width = bounds.width;
stampCanvas.height = bounds.height;
const stampContext = stampCanvas.getContext('2d');
const stampImageData = stampContext.createImageData(bounds.width, bounds.height);
stampImageData.data.set(stampPixels);
stampContext.putImageData(stampImageData, 0, 0);
skin.drawStamp(location, stampCanvas);
}
/* ******
* Truly internal functions: these support the functions above.
********/
@ -892,7 +957,7 @@ class RenderWebGL extends EventEmitter {
{u_modelMatrix: modelMatrix}
);
const pixels = new Buffer(width * height * 4);
const pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// Known boundary points on left/right edges of pixels.