mirror of
https://github.com/scratchfoundation/scratch-render.git
synced 2025-08-14 07:19:08 -04:00
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:
parent
140c0fbf37
commit
f29ed34ddc
2 changed files with 150 additions and 14 deletions
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue