Merge pull request #557 from adroitwhiz/penskin-cleanup

Clean up PenSkin code
This commit is contained in:
DD Liu
2020-10-29 15:25:58 -04:00
committed by GitHub

View File

@@ -3,7 +3,6 @@ const twgl = require('twgl.js');
const RenderConstants = require('./RenderConstants');
const Skin = require('./Skin');
const Rectangle = require('./Rectangle');
const ShaderManager = require('./ShaderManager');
/**
@@ -31,44 +30,6 @@ const DefaultPenAttributes = {
*/
const __premultipliedColor = [0, 0, 0, 0];
/**
* Reused memory location for projection matrices.
* @type {FloatArray}
*/
const __projectionMatrix = twgl.m4.identity();
/**
* Reused memory location for translation matrix for building a model matrix.
* @type {FloatArray}
*/
const __modelTranslationMatrix = twgl.m4.identity();
/**
* Reused memory location for scaling matrix for building a model matrix.
* @type {FloatArray}
*/
const __modelScalingMatrix = twgl.m4.identity();
/**
* Reused memory location for a model matrix.
* @type {FloatArray}
*/
const __modelMatrix = twgl.m4.identity();
/**
* Reused memory location for a vector to create a translation matrix from.
* @type {FloatArray}
*/
const __modelTranslationVector = twgl.v3.create();
/**
* Reused memory location for a vector to create a scaling matrix from.
* @type {FloatArray}
*/
const __modelScalingVector = twgl.v3.create();
class PenSkin extends Skin {
/**
* Create a Skin which implements a Scratch pen layer.
@@ -86,21 +47,12 @@ class PenSkin extends Skin {
*/
this._renderer = renderer;
/** @type {HTMLCanvasElement} */
this._canvas = document.createElement('canvas');
/** @type {WebGLTexture} */
this._exportTexture = null;
/** @type {Array<number>} */
this._size = null;
/** @type {WebGLFramebuffer} */
this._framebuffer = null;
/** @type {WebGLFramebuffer} */
this._silhouetteBuffer = null;
/** @type {boolean} */
this._canvasDirty = false;
/** @type {boolean} */
this._silhouetteDirty = false;
@@ -117,23 +69,30 @@ class PenSkin extends Skin {
};
/** @type {object} */
this._toBufferDrawRegionId = {
enter: () => this._enterDrawToBuffer(),
exit: () => this._exitDrawToBuffer()
this._usePenBufferDrawRegionId = {
enter: () => this._enterUsePenBuffer(),
exit: () => this._exitUsePenBuffer()
};
/** @type {twgl.BufferInfo} */
this._lineBufferInfo = null;
this._lineBufferInfo = twgl.createBufferInfoFromArrays(this._renderer.gl, {
a_position: {
numComponents: 2,
data: [
1, 0,
0, 0,
1, 1,
1, 1,
0, 0,
0, 1
]
}
});
const NO_EFFECTS = 0;
/** @type {twgl.ProgramInfo} */
this._stampShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.default, NO_EFFECTS);
/** @type {twgl.ProgramInfo} */
this._lineShader = this._renderer._shaderManager.getShader(ShaderManager.DRAW_MODE.line, NO_EFFECTS);
this._createLineGeometry();
this.onNativeSizeChanged = this.onNativeSizeChanged.bind(this);
this._renderer.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
@@ -146,7 +105,6 @@ class PenSkin extends Skin {
dispose () {
this._renderer.removeListener(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged);
this._renderer.gl.deleteTexture(this._texture);
this._renderer.gl.deleteTexture(this._exportTexture);
this._texture = null;
super.dispose();
}
@@ -162,37 +120,29 @@ class PenSkin extends Skin {
* @return {Array<number>} the "native" size, in texels, of this skin. [width, height]
*/
get size () {
return [this._canvas.width, this._canvas.height];
return this._size;
}
/**
* @param {Array<number>} scale The X and Y scaling factors to be used, as percentages of this skin's "native" size.
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given size.
* @param {int} pixelsWide - The width that the skin will be rendered at, in GPU pixels.
* @param {int} pixelsTall - The height that the skin will be rendered at, in GPU pixels.
*/
// eslint-disable-next-line no-unused-vars
getTexture (pixelsWide, pixelsTall) {
if (this._canvasDirty) {
this._drawToBuffer();
}
return this._exportTexture;
getTexture (scale) {
return this._texture;
}
/**
* Clear the pen layer.
*/
clear () {
const gl = this._renderer.gl;
twgl.bindFramebufferInfo(gl, this._framebuffer);
this._renderer.enterDrawRegion(this._usePenBufferDrawRegionId);
/* Reset framebuffer to transparent black */
const gl = this._renderer.gl;
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
const ctx = this._canvas.getContext('2d');
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
this._silhouetteDirty = true;
}
@@ -203,7 +153,6 @@ class PenSkin extends Skin {
* @param {number} y - the Y coordinate of the point to draw.
*/
drawPoint (penAttributes, x, y) {
// Canvas renders a zero-length line as two end-caps back-to-back, which is what we want.
this.drawLine(penAttributes, x, y, x, y);
}
@@ -230,48 +179,23 @@ class PenSkin extends Skin {
this._silhouetteDirty = true;
}
/**
* Create 2D geometry for drawing lines to a framebuffer.
*/
_createLineGeometry () {
const quads = {
a_position: {
numComponents: 2,
data: [
1, 0,
0, 0,
1, 1,
1, 1,
0, 0,
0, 1
]
}
};
this._lineBufferInfo = twgl.createBufferInfoFromArrays(this._renderer.gl, quads);
}
/**
* Prepare to draw lines in the _lineOnBufferDrawRegionId region.
*/
_enterDrawLineOnBuffer () {
const gl = this._renderer.gl;
const bounds = this._bounds;
const currentShader = this._lineShader;
const projection = twgl.m4.ortho(0, bounds.width, 0, bounds.height, -1, 1, __projectionMatrix);
twgl.bindFramebufferInfo(gl, this._framebuffer);
gl.viewport(0, 0, bounds.width, bounds.height);
gl.viewport(0, 0, this._size[0], this._size[1]);
const currentShader = this._lineShader;
gl.useProgram(currentShader.program);
twgl.setBuffersAndAttributes(gl, currentShader, this._lineBufferInfo);
const uniforms = {
u_skin: this._texture,
u_projectionMatrix: projection
u_stageSize: this._size
};
twgl.setUniforms(currentShader, uniforms);
@@ -286,6 +210,20 @@ class PenSkin extends Skin {
twgl.bindFramebufferInfo(gl, null);
}
/**
* Prepare to do things with this PenSkin's framebuffer
*/
_enterUsePenBuffer () {
twgl.bindFramebufferInfo(this._renderer.gl, this._framebuffer);
}
/**
* Return to a base state
*/
_exitUsePenBuffer () {
twgl.bindFramebufferInfo(this._renderer.gl, null);
}
/**
* Draw a line on the framebuffer.
* Note that the point coordinates are in the following coordinate space:
@@ -323,8 +261,7 @@ class PenSkin extends Skin {
u_lineColor: __premultipliedColor,
u_lineThickness: penAttributes.diameter || DefaultPenAttributes.diameter,
u_lineLength: lineLength,
u_penPoints: [x0, -y0, lineDiffX, -lineDiffY],
u_stageSize: this.size
u_penPoints: [x0, -y0, lineDiffX, -lineDiffY]
};
twgl.setUniforms(currentShader, uniforms);
@@ -334,136 +271,6 @@ class PenSkin extends Skin {
this._silhouetteDirty = true;
}
/**
* Stamp an image onto the pen layer.
* @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} stampElement - the element to use as the stamp.
* @param {number} x - the X coordinate of the stamp to draw.
* @param {number} y - the Y coordinate of the stamp to draw.
*/
drawStamp (stampElement, x, y) {
const ctx = this._canvas.getContext('2d');
ctx.drawImage(stampElement, this._rotationCenter[0] + x, this._rotationCenter[1] - y);
this._canvasDirty = true;
this._silhouetteDirty = true;
}
/**
* Enter a draw region to draw a rectangle.
*
* Multiple calls with the same regionId skip the callback reducing the
* amount of GL state changes.
* @param {twgl.ProgramInfo} currentShader - program info to draw rectangle
* with
* @param {Rectangle} bounds - viewport bounds to draw in
* region
*/
_drawRectangleRegionEnter (currentShader, bounds) {
const gl = this._renderer.gl;
gl.viewport(0, 0, bounds.width, bounds.height);
gl.useProgram(currentShader.program);
twgl.setBuffersAndAttributes(gl, currentShader, this._renderer._bufferInfo);
}
/**
* Draw a rectangle.
* @param {twgl.ProgramInfo} currentShader - program info to draw rectangle
* with
* @param {WebGLTexture} texture - texture to draw
* @param {Rectangle} bounds - bounded area to draw in
* @param {number} x - centered at x
* @param {number} y - centered at y
*/
_drawRectangle (currentShader, texture, bounds, x = -this._canvas.width / 2, y = this._canvas.height / 2) {
const gl = this._renderer.gl;
const projection = twgl.m4.ortho(
bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1,
__projectionMatrix
);
const uniforms = {
u_skin: texture,
u_projectionMatrix: projection,
u_modelMatrix: twgl.m4.multiply(
twgl.m4.translation(twgl.v3.create(
-x - (bounds.width / 2),
-y + (bounds.height / 2),
0
), __modelTranslationMatrix),
twgl.m4.scaling(twgl.v3.create(
bounds.width,
bounds.height,
0
), __modelScalingMatrix),
__modelMatrix
)
};
twgl.setTextureParameters(gl, texture, {minMag: gl.NEAREST});
twgl.setUniforms(currentShader, uniforms);
twgl.drawBufferInfo(gl, this._renderer._bufferInfo, gl.TRIANGLES);
}
/**
* Prepare to draw a rectangle in the _toBufferDrawRegionId region.
*/
_enterDrawToBuffer () {
const gl = this._renderer.gl;
twgl.bindFramebufferInfo(gl, this._framebuffer);
this._drawRectangleRegionEnter(this._stampShader, this._bounds);
}
/**
* Return to a base state from _toBufferDrawRegionId.
*/
_exitDrawToBuffer () {
const gl = this._renderer.gl;
twgl.bindFramebufferInfo(gl, null);
}
/**
* Draw the input texture to the framebuffer.
* @param {WebGLTexture} texture - input texture to draw
* @param {number} x - texture centered at x
* @param {number} y - texture centered at y
*/
_drawToBuffer (texture = this._texture, x = -this._canvas.width / 2, y = this._canvas.height / 2) {
if (texture !== this._texture && this._canvasDirty) {
this._drawToBuffer();
}
const gl = this._renderer.gl;
// If the input texture is the one that represents the pen's canvas
// layer, update the texture with the canvas data.
if (texture === this._texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._canvas);
const ctx = this._canvas.getContext('2d');
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
this._canvasDirty = false;
}
const currentShader = this._stampShader;
const bounds = this._bounds;
this._renderer.enterDrawRegion(this._toBufferDrawRegionId);
this._drawRectangle(currentShader, texture, bounds, x, y);
this._silhouetteDirty = true;
}
/**
* React to a change in the renderer's native size.
* @param {object} event - The change event.
@@ -480,31 +287,15 @@ class PenSkin extends Skin {
_setCanvasSize (canvasSize) {
const [width, height] = canvasSize;
const gl = this._renderer.gl;
this._bounds = new Rectangle();
this._bounds.initFromBounds(width / 2, width / -2, height / 2, height / -2);
this._canvas.width = width;
this._canvas.height = height;
this._size = canvasSize;
this._rotationCenter[0] = width / 2;
this._rotationCenter[1] = height / 2;
const gl = this._renderer.gl;
this._texture = twgl.createTexture(
gl,
{
auto: true,
mag: gl.NEAREST,
min: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
src: this._canvas
}
);
this._exportTexture = twgl.createTexture(
gl,
{
auto: true,
mag: gl.NEAREST,
min: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
@@ -516,66 +307,36 @@ class PenSkin extends Skin {
const attachments = [
{
format: gl.RGBA,
attachment: this._exportTexture
attachment: this._texture
}
];
if (this._framebuffer) {
twgl.resizeFramebufferInfo(gl, this._framebuffer, attachments, width, height);
twgl.resizeFramebufferInfo(gl, this._silhouetteBuffer, [{format: gl.RGBA}], width, height);
} else {
this._framebuffer = twgl.createFramebufferInfo(gl, attachments, width, height);
this._silhouetteBuffer = twgl.createFramebufferInfo(gl, [{format: gl.RGBA}], width, height);
}
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
this._silhouettePixels = new Uint8Array(Math.floor(width * height * 4));
this._silhouetteImageData = this._canvas.getContext('2d').createImageData(width, height);
this._silhouetteImageData = new ImageData(width, height);
this._silhouetteDirty = true;
}
/**
* Set context state to match provided pen attributes.
* @param {CanvasRenderingContext2D} context - the canvas rendering context to be modified.
* @param {PenAttributes} penAttributes - the pen attributes to be used.
* @private
*/
_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 = color4f[3]; // Alpha is 0 to 1 (not 0 to 255 like r,g,b)
context.strokeStyle = `rgba(${r},${g},${b},${a})`;
context.lineCap = 'round';
context.lineWidth = diameter;
}
/**
* If there have been pen operations that have dirtied the canvas, update
* now before someone wants to use our silhouette.
*/
updateSilhouette () {
if (this._silhouetteDirty) {
if (this._canvasDirty) {
this._drawToBuffer();
}
// Render export texture to another framebuffer
const gl = this._renderer.gl;
this._renderer.enterDrawRegion(this._toBufferDrawRegionId);
this._renderer.enterDrawRegion(this._usePenBufferDrawRegionId);
// Sample the framebuffer's pixels into the silhouette instance
const gl = this._renderer.gl;
gl.readPixels(
0, 0,
this._canvas.width, this._canvas.height,
this._size[0], this._size[1],
gl.RGBA, gl.UNSIGNED_BYTE, this._silhouettePixels
);