mirror of
https://github.com/scratchfoundation/scratch-render.git
synced 2025-08-28 22:30:04 -04:00
Merge pull request #67 from cwillisf/skin-classes
Move `Skin` functionality out of `Drawable` into its own classes
This commit is contained in:
commit
51e8aa9b1a
7 changed files with 591 additions and 279 deletions
103
src/BitmapSkin.js
Normal file
103
src/BitmapSkin.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
const twgl = require('twgl.js');
|
||||||
|
|
||||||
|
const Skin = require('./Skin');
|
||||||
|
|
||||||
|
class BitmapSkin extends Skin {
|
||||||
|
/**
|
||||||
|
* Create a new Bitmap Skin.
|
||||||
|
* @param {!int} id - The ID for this Skin.
|
||||||
|
* @param {!RenderWebGL} renderer - The renderer which will use this skin.
|
||||||
|
*/
|
||||||
|
constructor (id, renderer) {
|
||||||
|
super(id);
|
||||||
|
|
||||||
|
/** @type {!int} */
|
||||||
|
this._costumeResolution = 1;
|
||||||
|
|
||||||
|
/** @type {!RenderWebGL} */
|
||||||
|
this._renderer = renderer;
|
||||||
|
|
||||||
|
/** @type {WebGLTexture} */
|
||||||
|
this._texture = null;
|
||||||
|
|
||||||
|
/** @type {[int, int]} */
|
||||||
|
this._textureSize = [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of this object. Do not use it after calling this method.
|
||||||
|
*/
|
||||||
|
dispose () {
|
||||||
|
if (this._texture) {
|
||||||
|
this._renderer.gl.deleteTexture(this._texture);
|
||||||
|
this._texture = null;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {[number,number]} the "native" size, in texels, of this skin.
|
||||||
|
*/
|
||||||
|
get size () {
|
||||||
|
return [this._textureSize[0] / this._costumeResolution, this._textureSize[1] / this._costumeResolution];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {[number,number]} scale - The scaling factors to be used.
|
||||||
|
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
getTexture (scale) {
|
||||||
|
return this._texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the contents of this skin to a snapshot of the provided bitmap data.
|
||||||
|
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin.
|
||||||
|
* @param {int} [costumeResolution=1] - The resolution to use for this bitmap.
|
||||||
|
*/
|
||||||
|
setBitmap (bitmapData, costumeResolution) {
|
||||||
|
const gl = this._renderer.gl;
|
||||||
|
|
||||||
|
if (this._texture) {
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmapData);
|
||||||
|
} else {
|
||||||
|
const textureOptions = {
|
||||||
|
auto: true,
|
||||||
|
mag: gl.NEAREST,
|
||||||
|
min: gl.NEAREST, // TODO: mipmaps, linear (except pixelate)
|
||||||
|
wrap: gl.CLAMP_TO_EDGE,
|
||||||
|
src: bitmapData
|
||||||
|
};
|
||||||
|
|
||||||
|
this._texture = twgl.createTexture(gl, textureOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do these last in case any of the above throws an exception
|
||||||
|
this._costumeResolution = costumeResolution || 1;
|
||||||
|
this._textureSize = BitmapSkin._getBitmapSize(bitmapData);
|
||||||
|
|
||||||
|
this.emit(Skin.Events.WasAltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - bitmap data to inspect.
|
||||||
|
* @returns {[int,int]} the width and height of the bitmap data, in pixels.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static _getBitmapSize (bitmapData) {
|
||||||
|
if (bitmapData instanceof HTMLImageElement) {
|
||||||
|
return [bitmapData.naturalWidth || bitmapData.width, bitmapData.naturalHeight || bitmapData.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmapData instanceof HTMLVideoElement) {
|
||||||
|
return [bitmapData.videoWidth || bitmapData.width, bitmapData.videoHeight || bitmapData.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageData or HTMLCanvasElement
|
||||||
|
return [bitmapData.width, bitmapData.height];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = BitmapSkin;
|
282
src/Drawable.js
282
src/Drawable.js
|
@ -1,9 +1,9 @@
|
||||||
const twgl = require('twgl.js');
|
const twgl = require('twgl.js');
|
||||||
const xhr = require('xhr');
|
|
||||||
|
|
||||||
const Rectangle = require('./Rectangle');
|
const Rectangle = require('./Rectangle');
|
||||||
const SvgRenderer = require('./svg-quirks-mode/svg-renderer');
|
const RenderConstants = require('./RenderConstants');
|
||||||
const ShaderManager = require('./ShaderManager');
|
const ShaderManager = require('./ShaderManager');
|
||||||
|
const Skin = require('./Skin');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback Drawable~idFilterFunc
|
* @callback Drawable~idFilterFunc
|
||||||
|
@ -16,15 +16,12 @@ class Drawable {
|
||||||
/**
|
/**
|
||||||
* An object which can be drawn by the renderer.
|
* An object which can be drawn by the renderer.
|
||||||
* TODO: double-buffer all rendering state (position, skin, effects, etc.)
|
* TODO: double-buffer all rendering state (position, skin, effects, etc.)
|
||||||
* @param {WebGLRenderingContext} gl The OpenGL context.
|
* @param {!int} id - This Drawable's unique ID.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor (gl) {
|
constructor (id) {
|
||||||
this._id = Drawable._nextDrawable++;
|
/** @type {!int} */
|
||||||
Drawable._allDrawables[this._id] = this;
|
this._id = id;
|
||||||
|
|
||||||
/** @type {WebGLRenderingContext} */
|
|
||||||
this._gl = gl;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The uniforms to be used by the vertex and pixel shaders.
|
* The uniforms to be used by the vertex and pixel shaders.
|
||||||
|
@ -39,19 +36,6 @@ class Drawable {
|
||||||
*/
|
*/
|
||||||
u_modelMatrix: twgl.m4.identity(),
|
u_modelMatrix: twgl.m4.identity(),
|
||||||
|
|
||||||
/**
|
|
||||||
* The nominal (not necessarily current) size of the current skin.
|
|
||||||
* This is scaled by _costumeResolution.
|
|
||||||
* @type {number[]}
|
|
||||||
*/
|
|
||||||
u_skinSize: [0, 0],
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The actual WebGL texture object for the skin.
|
|
||||||
* @type {WebGLTexture}
|
|
||||||
*/
|
|
||||||
u_skin: null,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color to use in the silhouette draw mode.
|
* The color to use in the silhouette draw mode.
|
||||||
* @type {number[]}
|
* @type {number[]}
|
||||||
|
@ -69,48 +53,24 @@ class Drawable {
|
||||||
|
|
||||||
this._position = twgl.v3.create(0, 0);
|
this._position = twgl.v3.create(0, 0);
|
||||||
this._scale = twgl.v3.create(100, 100);
|
this._scale = twgl.v3.create(100, 100);
|
||||||
this._rotationCenter = twgl.v3.create(0, 0);
|
|
||||||
this._direction = 90;
|
this._direction = 90;
|
||||||
this._transformDirty = true;
|
this._transformDirty = true;
|
||||||
this._visible = true;
|
this._visible = true;
|
||||||
this._effectBits = 0;
|
this._effectBits = 0;
|
||||||
|
|
||||||
|
// TODO: move convex hull functionality, maybe bounds functionality overall, to Skin classes
|
||||||
this._convexHullPoints = null;
|
this._convexHullPoints = null;
|
||||||
this._convexHullDirty = true;
|
this._convexHullDirty = true;
|
||||||
|
|
||||||
// Create a transparent 1x1 texture for temporary use
|
this._skinWasAltered = this._skinWasAltered.bind(this);
|
||||||
const tempTexture = twgl.createTexture(gl, {src: [0, 0, 0, 0]});
|
|
||||||
this._useSkin(tempTexture, 0, 0, 1, true);
|
|
||||||
|
|
||||||
// Load a real skin
|
|
||||||
this.setSkin(Drawable._DEFAULT_SKIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An invalid Drawable ID which can be used to signify absence, etc.
|
|
||||||
* @type {int}
|
|
||||||
*/
|
|
||||||
static get NONE () {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a Drawable by its ID number.
|
|
||||||
* @param {int} drawableID The ID of the Drawable to fetch.
|
|
||||||
* @returns {?Drawable} The specified Drawable if found, otherwise null.
|
|
||||||
*/
|
|
||||||
static getDrawableByID (drawableID) {
|
|
||||||
return Drawable._allDrawables[drawableID];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose of this Drawable. Do not use it after calling this method.
|
* Dispose of this Drawable. Do not use it after calling this method.
|
||||||
*/
|
*/
|
||||||
dispose () {
|
dispose () {
|
||||||
this.setSkin(null);
|
// Use the setter: disconnect events
|
||||||
if (this._id >= 0) {
|
this.skin = null;
|
||||||
delete Drawable[this._id];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -122,60 +82,40 @@ class Drawable {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the ID for this Drawable.
|
|
||||||
* @returns {number} The ID for this Drawable.
|
* @returns {number} The ID for this Drawable.
|
||||||
*/
|
*/
|
||||||
getID () {
|
get id () {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set this Drawable's skin.
|
* @returns {Skin} the current skin for this Drawable.
|
||||||
* The Drawable will continue using the existing skin until the new one loads.
|
|
||||||
* If there is no existing skin, the Drawable will use a 1x1 transparent image.
|
|
||||||
* @param {string} skinUrl The URL of the skin.
|
|
||||||
* @param {number=} optCostumeResolution Optionally, a resolution for the skin.
|
|
||||||
*/
|
*/
|
||||||
setSkin (skinUrl, optCostumeResolution) {
|
get skin () {
|
||||||
// TODO: cache Skins instead of loading each time. Ref count them?
|
return this._skin;
|
||||||
// TODO: share Skins across Drawables - see also destroy()
|
}
|
||||||
if (skinUrl) {
|
|
||||||
const ext = skinUrl.substring(skinUrl.lastIndexOf('.') + 1);
|
/**
|
||||||
switch (ext) {
|
* @param {Skin} newSkin - A new Skin for this Drawable.
|
||||||
case 'svg':
|
*/
|
||||||
case 'svg/get/':
|
set skin (newSkin) {
|
||||||
case 'svgz':
|
if (this._skin !== newSkin) {
|
||||||
case 'svgz/get/':
|
if (this._skin) {
|
||||||
this._setSkinSVG(skinUrl);
|
this._skin.removeListener(Skin.Events.WasAltered, this._skinWasAltered);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this._setSkinBitmap(skinUrl, optCostumeResolution);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
this._skin = newSkin;
|
||||||
this._useSkin(null, 0, 0, 1, true);
|
if (this._skin) {
|
||||||
|
this._skin.addListener(Skin.Events.WasAltered, this._skinWasAltered);
|
||||||
|
}
|
||||||
|
this._skinWasAltered();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use a skin if it is the currently-pending skin, or if skipPendingCheck==true.
|
* @returns {[number,number]} the current scaling percentages applied to this Drawable. [100,100] is normal size.
|
||||||
* If the passed skin is used (for either reason) _pendingSkin will be cleared.
|
|
||||||
* @param {WebGLTexture} skin The skin to use.
|
|
||||||
* @param {int} width The width of the skin.
|
|
||||||
* @param {int} height The height of the skin.
|
|
||||||
* @param {int} costumeResolution The resolution to use for this skin.
|
|
||||||
* @param {boolean} [skipPendingCheck] If true, don't compare to _pendingSkin.
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
_useSkin (skin, width, height, costumeResolution, skipPendingCheck) {
|
get scale () {
|
||||||
if (skipPendingCheck || (skin === this._pendingSkin)) {
|
return [this._scale[0], this._scale[1]];
|
||||||
this._pendingSkin = null;
|
|
||||||
if (this._uniforms.u_skin && (this._uniforms.u_skin !== skin)) {
|
|
||||||
this._gl.deleteTexture(this._uniforms.u_skin);
|
|
||||||
}
|
|
||||||
this._setSkinSize(width, height, costumeResolution);
|
|
||||||
this._uniforms.u_skin = skin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -185,79 +125,6 @@ class Drawable {
|
||||||
return this._effectBits;
|
return this._effectBits;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a bitmap skin. Supports the same formats as the Image element.
|
|
||||||
* @param {string} skinMd5ext The MD5 and file extension of the bitmap skin.
|
|
||||||
* @param {number=} optCostumeResolution Optionally, a resolution for the skin.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setSkinBitmap (skinMd5ext, optCostumeResolution) {
|
|
||||||
const url = skinMd5ext;
|
|
||||||
this._setSkinCore(url, optCostumeResolution);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load an SVG-based skin. This still needs quite a bit of work to match the
|
|
||||||
* level of quality found in Scratch 2.0:
|
|
||||||
* - We should detect when a skin is being scaled up and render the SVG at a
|
|
||||||
* higher resolution in those cases.
|
|
||||||
* - Colors seem a little off. This may be browser-specific.
|
|
||||||
* - This method works in Chrome, Firefox, Safari, and Edge but causes a
|
|
||||||
* security error in IE.
|
|
||||||
* @param {string} skinMd5ext The MD5 and file extension of the SVG skin.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setSkinSVG (skinMd5ext) {
|
|
||||||
const url = skinMd5ext;
|
|
||||||
|
|
||||||
const svgCanvas = document.createElement('canvas');
|
|
||||||
const svgRenderer = new SvgRenderer(svgCanvas);
|
|
||||||
|
|
||||||
const gotSVG = (err, response, body) => {
|
|
||||||
if (!err) {
|
|
||||||
svgRenderer.fromString(body, () => {
|
|
||||||
this._setSkinCore(svgCanvas, svgRenderer.getDrawRatio());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.get({
|
|
||||||
useXDR: true,
|
|
||||||
url: url
|
|
||||||
}, gotSVG);
|
|
||||||
// TODO: if there's no current u_skin, install *something* before returning
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common code for setting all skin types.
|
|
||||||
* @param {string|Image} source The source of image data for the skin.
|
|
||||||
* @param {int} costumeResolution The resolution to use for this skin.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setSkinCore (source, costumeResolution) {
|
|
||||||
const callback = (err, texture, sourceInCallback) => {
|
|
||||||
if (!err && (this._pendingSkin === texture)) {
|
|
||||||
this._useSkin(texture, sourceInCallback.width, sourceInCallback.height, costumeResolution);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const gl = this._gl;
|
|
||||||
const options = {
|
|
||||||
auto: true,
|
|
||||||
mag: gl.NEAREST,
|
|
||||||
min: gl.NEAREST, // TODO: mipmaps, linear (except pixelate)
|
|
||||||
wrap: gl.CLAMP_TO_EDGE,
|
|
||||||
src: source
|
|
||||||
};
|
|
||||||
const willCallCallback = typeof source === 'string';
|
|
||||||
this._pendingSkin = twgl.createTexture(gl, options, willCallCallback ? callback : null);
|
|
||||||
|
|
||||||
// If we won't get a callback, start using the skin immediately.
|
|
||||||
// This will happen if the data is already local.
|
|
||||||
if (!willCallCallback) {
|
|
||||||
callback(null, this._pendingSkin, source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {object.<string, *>} the shader uniforms to be used when rendering this Drawable.
|
* @returns {object.<string, *>} the shader uniforms to be used when rendering this Drawable.
|
||||||
*/
|
*/
|
||||||
|
@ -281,10 +148,6 @@ class Drawable {
|
||||||
*/
|
*/
|
||||||
updateProperties (properties) {
|
updateProperties (properties) {
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
if ('skin' in properties) {
|
|
||||||
this.setSkin(properties.skin, properties.costumeResolution);
|
|
||||||
this.setConvexHullDirty();
|
|
||||||
}
|
|
||||||
if ('position' in properties && (
|
if ('position' in properties && (
|
||||||
this._position[0] !== properties.position[0] ||
|
this._position[0] !== properties.position[0] ||
|
||||||
this._position[1] !== properties.position[1])) {
|
this._position[1] !== properties.position[1])) {
|
||||||
|
@ -303,13 +166,6 @@ class Drawable {
|
||||||
this._scale[1] = properties.scale[1];
|
this._scale[1] = properties.scale[1];
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
if ('rotationCenter' in properties && (
|
|
||||||
this._rotationCenter[0] !== properties.rotationCenter[0] ||
|
|
||||||
this._rotationCenter[1] !== properties.rotationCenter[1])) {
|
|
||||||
this._rotationCenter[0] = properties.rotationCenter[0];
|
|
||||||
this._rotationCenter[1] = properties.rotationCenter[1];
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
if ('visible' in properties) {
|
if ('visible' in properties) {
|
||||||
this._visible = properties.visible;
|
this._visible = properties.visible;
|
||||||
this.setConvexHullDirty();
|
this.setConvexHullDirty();
|
||||||
|
@ -337,33 +193,6 @@ class Drawable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the dimensions of this Drawable's skin.
|
|
||||||
* @param {int} width The width of the new skin.
|
|
||||||
* @param {int} height The height of the new skin.
|
|
||||||
* @param {int} [costumeResolution] The resolution to use for this skin.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_setSkinSize (width, height, costumeResolution) {
|
|
||||||
costumeResolution = costumeResolution || 1;
|
|
||||||
width /= costumeResolution;
|
|
||||||
height /= costumeResolution;
|
|
||||||
if (this._uniforms.u_skinSize[0] !== width || this._uniforms.u_skinSize[1] !== height) {
|
|
||||||
this._uniforms.u_skinSize[0] = width;
|
|
||||||
this._uniforms.u_skinSize[1] = height;
|
|
||||||
this.setTransformDirty();
|
|
||||||
}
|
|
||||||
this.setConvexHullDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the size of the Drawable's current skin.
|
|
||||||
* @return {Array.<number>} Skin size, width and height.
|
|
||||||
*/
|
|
||||||
getSkinSize () {
|
|
||||||
return this._uniforms.u_skinSize.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the transform to use when rendering this Drawable.
|
* Calculate the transform to use when rendering this Drawable.
|
||||||
* @private
|
* @private
|
||||||
|
@ -377,18 +206,14 @@ class Drawable {
|
||||||
const rotation = (270 - this._direction) * Math.PI / 180;
|
const rotation = (270 - this._direction) * Math.PI / 180;
|
||||||
twgl.m4.rotateZ(modelMatrix, rotation, modelMatrix);
|
twgl.m4.rotateZ(modelMatrix, rotation, modelMatrix);
|
||||||
|
|
||||||
|
|
||||||
// Adjust rotation center relative to the skin.
|
// Adjust rotation center relative to the skin.
|
||||||
const rotationAdjusted = twgl.v3.subtract(
|
const rotationAdjusted = twgl.v3.subtract(this.skin.rotationCenter, twgl.v3.divScalar(this.skin.size, 2));
|
||||||
this._rotationCenter,
|
|
||||||
twgl.v3.divScalar(this._uniforms.u_skinSize, 2)
|
|
||||||
);
|
|
||||||
rotationAdjusted[1] *= -1; // Y flipped to Scratch coordinate.
|
rotationAdjusted[1] *= -1; // Y flipped to Scratch coordinate.
|
||||||
rotationAdjusted[2] = 0; // Z coordinate is 0.
|
rotationAdjusted[2] = 0; // Z coordinate is 0.
|
||||||
|
|
||||||
twgl.m4.translate(modelMatrix, rotationAdjusted, modelMatrix);
|
twgl.m4.translate(modelMatrix, rotationAdjusted, modelMatrix);
|
||||||
|
|
||||||
const scaledSize = twgl.v3.divScalar(twgl.v3.multiply(this._uniforms.u_skinSize, this._scale), 100);
|
const scaledSize = twgl.v3.divScalar(twgl.v3.multiply(this.skin.size, this._scale), 100);
|
||||||
scaledSize[2] = 0; // was NaN because the vectors have only 2 components.
|
scaledSize[2] = 0; // was NaN because the vectors have only 2 components.
|
||||||
twgl.m4.scale(modelMatrix, scaledSize, modelMatrix);
|
twgl.m4.scale(modelMatrix, scaledSize, modelMatrix);
|
||||||
|
|
||||||
|
@ -438,7 +263,7 @@ class Drawable {
|
||||||
// transform. This allows us to skip recalculating the convex hull
|
// transform. This allows us to skip recalculating the convex hull
|
||||||
// for many Drawable updates, including translation, rotation, scaling.
|
// for many Drawable updates, including translation, rotation, scaling.
|
||||||
const projection = twgl.m4.ortho(-1, 1, -1, 1, -1, 1);
|
const projection = twgl.m4.ortho(-1, 1, -1, 1, -1, 1);
|
||||||
const skinSize = this._uniforms.u_skinSize;
|
const skinSize = this.skin.size;
|
||||||
const tm = twgl.m4.multiply(this._uniforms.u_modelMatrix, projection);
|
const tm = twgl.m4.multiply(this._uniforms.u_modelMatrix, projection);
|
||||||
const transformedHullPoints = [];
|
const transformedHullPoints = [];
|
||||||
for (let i = 0; i < this._convexHullPoints.length; i++) {
|
for (let i = 0; i < this._convexHullPoints.length; i++) {
|
||||||
|
@ -494,14 +319,23 @@ class Drawable {
|
||||||
return this.getAABB();
|
return this.getAABB();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond to an internal change in the current Skin.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_skinWasAltered () {
|
||||||
|
this.setConvexHullDirty();
|
||||||
|
this.setTransformDirty();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate a color to represent the given ID number. At least one component of
|
* Calculate a color to represent the given ID number. At least one component of
|
||||||
* the resulting color will be non-zero if the ID is not Drawable.NONE.
|
* the resulting color will be non-zero if the ID is not RenderConstants.ID_NONE.
|
||||||
* @param {int} id The ID to convert.
|
* @param {int} id The ID to convert.
|
||||||
* @returns {number[]} An array of [r,g,b,a], each component in the range [0,1].
|
* @returns {number[]} An array of [r,g,b,a], each component in the range [0,1].
|
||||||
*/
|
*/
|
||||||
static color4fFromID (id) {
|
static color4fFromID (id) {
|
||||||
id -= Drawable.NONE;
|
id -= RenderConstants.ID_NONE;
|
||||||
const r = ((id >> 0) & 255) / 255.0;
|
const r = ((id >> 0) & 255) / 255.0;
|
||||||
const g = ((id >> 8) & 255) / 255.0;
|
const g = ((id >> 8) & 255) / 255.0;
|
||||||
const b = ((id >> 16) & 255) / 255.0;
|
const b = ((id >> 16) & 255) / 255.0;
|
||||||
|
@ -510,7 +344,7 @@ class Drawable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the ID number represented by the given color. If all components of
|
* 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
|
* the color are zero, the result will be RenderConstants.ID_NONE; otherwise the result
|
||||||
* will be a valid ID.
|
* will be a valid ID.
|
||||||
* @param {int} r The red value of the color, in the range [0,255].
|
* @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} g The green value of the color, in the range [0,255].
|
||||||
|
@ -522,30 +356,8 @@ class Drawable {
|
||||||
id = (r & 255) << 0;
|
id = (r & 255) << 0;
|
||||||
id |= (g & 255) << 8;
|
id |= (g & 255) << 8;
|
||||||
id |= (b & 255) << 16;
|
id |= (b & 255) << 16;
|
||||||
return id + Drawable.NONE;
|
return id + RenderConstants.ID_NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The ID to be assigned next time the Drawable constructor is called.
|
|
||||||
* @type {number}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Drawable._nextDrawable = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All current Drawables, by ID.
|
|
||||||
* @type {Object.<int, Drawable>}
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
Drawable._allDrawables = {};
|
|
||||||
|
|
||||||
// TODO: fall back on a built-in skin to protect against network problems
|
|
||||||
Drawable._DEFAULT_SKIN = {
|
|
||||||
squirrel: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/',
|
|
||||||
bus: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/66895930177178ea01d9e610917f8acf.png/get/',
|
|
||||||
scratch_cat: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/09dc888b0b7df19f70d81588ae73420e.svg/get/',
|
|
||||||
gradient: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/a49ff276b9b8f997a1ae163992c2c145.png/get/'
|
|
||||||
}.squirrel;
|
|
||||||
|
|
||||||
module.exports = Drawable;
|
module.exports = Drawable;
|
||||||
|
|
31
src/RenderConstants.js
Normal file
31
src/RenderConstants.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
const DEFAULT_SKIN = {
|
||||||
|
squirrel: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/',
|
||||||
|
bus: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/66895930177178ea01d9e610917f8acf.png/get/',
|
||||||
|
scratch_cat: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/09dc888b0b7df19f70d81588ae73420e.svg/get/',
|
||||||
|
gradient: 'https://cdn.assets.scratch.mit.edu/internalapi/asset/a49ff276b9b8f997a1ae163992c2c145.png/get/'
|
||||||
|
}.squirrel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Various constants meant for use throughout the renderer.
|
||||||
|
* @type {object}
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* The ID value to use for "no item" or when an object has been disposed.
|
||||||
|
* @type {int}
|
||||||
|
*/
|
||||||
|
ID_NONE: -1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The URL to use as the default skin for a Drawable.
|
||||||
|
* TODO: Remove this in favor of falling back on a built-in skin.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
DEFAULT_SKIN: DEFAULT_SKIN,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize for fewer than this number of Drawables sharing the same Skin.
|
||||||
|
* Going above this may cause middleware warnings or a performance penalty but should otherwise behave correctly.
|
||||||
|
*/
|
||||||
|
SKIN_SHARE_SOFT_LIMIT: 300
|
||||||
|
};
|
|
@ -1,8 +1,12 @@
|
||||||
const hull = require('hull.js');
|
const hull = require('hull.js');
|
||||||
const twgl = require('twgl.js');
|
const twgl = require('twgl.js');
|
||||||
|
const xhr = require('xhr');
|
||||||
|
|
||||||
|
const BitmapSkin = require('./BitmapSkin');
|
||||||
const Drawable = require('./Drawable');
|
const Drawable = require('./Drawable');
|
||||||
|
const RenderConstants = require('./RenderConstants');
|
||||||
const ShaderManager = require('./ShaderManager');
|
const ShaderManager = require('./ShaderManager');
|
||||||
|
const SVGSkin = require('./SVGSkin');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum touch size for a picking check.
|
* Maximum touch size for a picking check.
|
||||||
|
@ -37,13 +41,30 @@ class RenderWebGL {
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor (canvas, xLeft, xRight, yBottom, yTop) {
|
constructor (canvas, xLeft, xRight, yBottom, yTop) {
|
||||||
// TODO: remove?
|
/** @type {Drawable[]} */
|
||||||
twgl.setDefaults({crossOrigin: true});
|
this._allDrawables = [];
|
||||||
|
|
||||||
|
/** @type {Skin[]} */
|
||||||
|
this._allSkins = [];
|
||||||
|
|
||||||
|
/** @type {int[]} */
|
||||||
|
this._drawList = [];
|
||||||
|
|
||||||
|
/** @type {WebGLRenderingContext} */
|
||||||
this._gl = twgl.getWebGLContext(canvas, {alpha: false, stencil: true});
|
this._gl = twgl.getWebGLContext(canvas, {alpha: false, stencil: true});
|
||||||
this._drawables = [];
|
|
||||||
|
/** @type {int} */
|
||||||
|
this._nextDrawableId = RenderConstants.ID_NONE + 1;
|
||||||
|
|
||||||
|
/** @type {int} */
|
||||||
|
this._nextSkinId = RenderConstants.ID_NONE + 1;
|
||||||
|
|
||||||
|
/** @type {module:twgl/m4.Mat4} */
|
||||||
this._projection = twgl.m4.identity();
|
this._projection = twgl.m4.identity();
|
||||||
|
|
||||||
|
/** @type {Object.<string,int>} */
|
||||||
|
this._skinUrlMap = {};
|
||||||
|
|
||||||
this._createGeometry();
|
this._createGeometry();
|
||||||
|
|
||||||
this.setBackgroundColor(1, 1, 1);
|
this.setBackgroundColor(1, 1, 1);
|
||||||
|
@ -58,6 +79,13 @@ class RenderWebGL {
|
||||||
this._shaderManager = new ShaderManager(gl);
|
this._shaderManager = new ShaderManager(gl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {WebGLRenderingContext} the WebGL rendering context associated with this renderer.
|
||||||
|
*/
|
||||||
|
get gl () {
|
||||||
|
return this._gl;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the physical size of the stage in device-independent pixels.
|
* 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.
|
* This will be multiplied by the device's pixel ratio on high-DPI displays.
|
||||||
|
@ -106,30 +134,134 @@ class RenderWebGL {
|
||||||
this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
|
this._projection = twgl.m4.ortho(xLeft, xRight, yBottom, yTop, -1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a skin by loading a bitmap or vector image from a URL, or reuse an existing skin created this way.
|
||||||
|
* WARNING: This method is deprecated and will be removed in the near future.
|
||||||
|
* Use `createBitmapSkin` or `createSVGSkin` instead.
|
||||||
|
* @param {!string} skinUrl The URL of the skin.
|
||||||
|
* @param {!int} [costumeResolution] Optional: resolution for the skin. Ignored unless creating a new Bitmap skin.
|
||||||
|
* @returns {!int} The ID of the Skin.
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
|
createSkinFromURL (skinUrl, costumeResolution) {
|
||||||
|
if (this._skinUrlMap.hasOwnProperty(skinUrl)) {
|
||||||
|
const existingId = this._skinUrlMap[skinUrl];
|
||||||
|
|
||||||
|
// Make sure the "existing" skin hasn't been destroyed
|
||||||
|
if (this._allSkins[existingId]) {
|
||||||
|
return existingId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const skinId = this._nextSkinId++;
|
||||||
|
this._skinUrlMap[skinUrl] = skinId;
|
||||||
|
|
||||||
|
let newSkin;
|
||||||
|
let isVector;
|
||||||
|
|
||||||
|
const ext = skinUrl.substring(skinUrl.lastIndexOf('.') + 1);
|
||||||
|
switch (ext) {
|
||||||
|
case 'svg':
|
||||||
|
case 'svg/get/':
|
||||||
|
case 'svgz':
|
||||||
|
case 'svgz/get/':
|
||||||
|
isVector = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
isVector = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isVector) {
|
||||||
|
newSkin = new SVGSkin(skinId, this);
|
||||||
|
xhr.get({
|
||||||
|
useXDR: true,
|
||||||
|
url: skinUrl
|
||||||
|
}, (err, response, body) => {
|
||||||
|
if (!err) {
|
||||||
|
newSkin.setSVG(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newSkin = new BitmapSkin(skinId, this);
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
img.onload = () => {
|
||||||
|
newSkin.setBitmap(img, costumeResolution);
|
||||||
|
};
|
||||||
|
img.src = skinUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._allSkins[skinId] = newSkin;
|
||||||
|
return skinId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new bitmap skin from a snapshot of the provided bitmap data.
|
||||||
|
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin.
|
||||||
|
* @param {!int} [costumeResolution=1] - The resolution to use for this bitmap.
|
||||||
|
* @returns {!int} the ID for the new skin.
|
||||||
|
*/
|
||||||
|
createBitmapSkin (bitmapData, costumeResolution) {
|
||||||
|
const skinId = this._nextSkinId++;
|
||||||
|
const newSkin = new BitmapSkin(skinId, this);
|
||||||
|
newSkin.setBitmap(bitmapData, costumeResolution);
|
||||||
|
this._allSkins[skinId] = newSkin;
|
||||||
|
return skinId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new SVG skin.
|
||||||
|
* @param {!string} svgData - new SVG to use.
|
||||||
|
* @returns {!int} the ID for the new skin.
|
||||||
|
*/
|
||||||
|
createSVGSkin (svgData) {
|
||||||
|
const skinId = this._nextSkinId++;
|
||||||
|
const newSkin = new SVGSkin(skinId, this);
|
||||||
|
newSkin.setSVG(svgData);
|
||||||
|
this._allSkins[skinId] = newSkin;
|
||||||
|
return skinId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy an existing skin. Do not use the skin or its ID after calling this.
|
||||||
|
* @param {!int} skinId - The ID of the skin to destroy.
|
||||||
|
*/
|
||||||
|
destroySkin (skinId) {
|
||||||
|
const oldSkin = this._allSkins[skinId];
|
||||||
|
oldSkin.dispose();
|
||||||
|
delete this._allSkins[skinId];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Drawable and add it to the scene.
|
* Create a new Drawable and add it to the scene.
|
||||||
* @returns {int} The ID of the new Drawable.
|
* @returns {int} The ID of the new Drawable.
|
||||||
*/
|
*/
|
||||||
createDrawable () {
|
createDrawable () {
|
||||||
const drawable = new Drawable(this._gl);
|
const drawableID = this._nextDrawableId++;
|
||||||
const drawableID = drawable.getID();
|
const drawable = new Drawable(drawableID, this);
|
||||||
this._drawables.push(drawableID);
|
this._allDrawables[drawableID] = drawable;
|
||||||
|
this._drawList.push(drawableID);
|
||||||
|
|
||||||
|
const defaultSkinId = this.createSkinFromURL(RenderConstants.DEFAULT_SKIN);
|
||||||
|
drawable.skin = this._allSkins[defaultSkinId];
|
||||||
|
|
||||||
return drawableID;
|
return drawableID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Destroy a Drawable, removing it from the scene.
|
* Destroy a Drawable, removing it from the scene.
|
||||||
* @param {int} drawableID The ID of the Drawable to remove.
|
* @param {int} drawableID The ID of the Drawable to remove.
|
||||||
* @returns {boolean} True iff the drawable was found and removed.
|
|
||||||
*/
|
*/
|
||||||
destroyDrawable (drawableID) {
|
destroyDrawable (drawableID) {
|
||||||
const index = this._drawables.indexOf(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
if (index >= 0) {
|
drawable.dispose();
|
||||||
Drawable.getDrawableByID(drawableID).dispose();
|
delete this._allDrawables[drawableID];
|
||||||
this._drawables.splice(index, 1);
|
|
||||||
return true;
|
let index;
|
||||||
|
while ((index = this._drawList.indexOf(drawableID)) >= 0) {
|
||||||
|
this._drawList.splice(index, 1);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -146,10 +278,10 @@ class RenderWebGL {
|
||||||
* @return {?number} New order if changed, or null.
|
* @return {?number} New order if changed, or null.
|
||||||
*/
|
*/
|
||||||
setDrawableOrder (drawableID, order, optIsRelative, optMin) {
|
setDrawableOrder (drawableID, order, optIsRelative, optMin) {
|
||||||
const oldIndex = this._drawables.indexOf(drawableID);
|
const oldIndex = this._drawList.indexOf(drawableID);
|
||||||
if (oldIndex >= 0) {
|
if (oldIndex >= 0) {
|
||||||
// Remove drawable from the list.
|
// Remove drawable from the list.
|
||||||
const drawable = this._drawables.splice(oldIndex, 1)[0];
|
const drawable = this._drawList.splice(oldIndex, 1)[0];
|
||||||
// Determine new index.
|
// Determine new index.
|
||||||
let newIndex = order;
|
let newIndex = order;
|
||||||
if (optIsRelative) {
|
if (optIsRelative) {
|
||||||
|
@ -160,8 +292,8 @@ class RenderWebGL {
|
||||||
}
|
}
|
||||||
newIndex = Math.max(newIndex, 0);
|
newIndex = Math.max(newIndex, 0);
|
||||||
// Insert at new index.
|
// Insert at new index.
|
||||||
this._drawables.splice(newIndex, 0, drawable);
|
this._drawList.splice(newIndex, 0, drawable);
|
||||||
return this._drawables.indexOf(drawable);
|
return this._drawList.indexOf(drawable);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -177,7 +309,7 @@ class RenderWebGL {
|
||||||
gl.clearColor.apply(gl, this._backgroundColor);
|
gl.clearColor.apply(gl, this._backgroundColor);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
this._drawThese(this._drawables, ShaderManager.DRAW_MODE.default, this._projection);
|
this._drawThese(this._drawList, ShaderManager.DRAW_MODE.default, this._projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -186,7 +318,7 @@ class RenderWebGL {
|
||||||
* @return {object} Bounds for a tight box around the Drawable.
|
* @return {object} Bounds for a tight box around the Drawable.
|
||||||
*/
|
*/
|
||||||
getBounds (drawableID) {
|
getBounds (drawableID) {
|
||||||
const drawable = Drawable.getDrawableByID(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
// Tell the Drawable about its updated convex hull, if necessary.
|
// Tell the Drawable about its updated convex hull, if necessary.
|
||||||
if (drawable.needsConvexHullPoints()) {
|
if (drawable.needsConvexHullPoints()) {
|
||||||
const points = this._getConvexHullPointsForDrawable(drawableID);
|
const points = this._getConvexHullPointsForDrawable(drawableID);
|
||||||
|
@ -218,8 +350,8 @@ class RenderWebGL {
|
||||||
* @return {Array.<number>} Skin size, width and height.
|
* @return {Array.<number>} Skin size, width and height.
|
||||||
*/
|
*/
|
||||||
getSkinSize (drawableID) {
|
getSkinSize (drawableID) {
|
||||||
const drawable = Drawable.getDrawableByID(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
return drawable.getSkinSize();
|
return drawable.skin.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -237,7 +369,7 @@ class RenderWebGL {
|
||||||
if (!bounds) {
|
if (!bounds) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const candidateIDs = this._filterCandidatesTouching(drawableID, this._drawables, bounds);
|
const candidateIDs = this._filterCandidatesTouching(drawableID, this._drawList, bounds);
|
||||||
if (!candidateIDs) {
|
if (!candidateIDs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -321,7 +453,7 @@ class RenderWebGL {
|
||||||
* @returns {boolean} True iff the Drawable is touching one of candidateIDs.
|
* @returns {boolean} True iff the Drawable is touching one of candidateIDs.
|
||||||
*/
|
*/
|
||||||
isTouchingDrawables (drawableID, candidateIDs) {
|
isTouchingDrawables (drawableID, candidateIDs) {
|
||||||
candidateIDs = candidateIDs || this._drawables;
|
candidateIDs = candidateIDs || this._drawList;
|
||||||
|
|
||||||
const gl = this._gl;
|
const gl = this._gl;
|
||||||
|
|
||||||
|
@ -341,7 +473,7 @@ class RenderWebGL {
|
||||||
gl.viewport(0, 0, bounds.width, bounds.height);
|
gl.viewport(0, 0, bounds.width, bounds.height);
|
||||||
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.bottom, bounds.top, -1, 1);
|
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.bottom, bounds.top, -1, 1);
|
||||||
|
|
||||||
const noneColor = Drawable.color4fFromID(Drawable.NONE);
|
const noneColor = Drawable.color4fFromID(RenderConstants.ID_NONE);
|
||||||
gl.clearColor.apply(gl, noneColor);
|
gl.clearColor.apply(gl, noneColor);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
|
||||||
|
|
||||||
|
@ -383,7 +515,7 @@ class RenderWebGL {
|
||||||
pixels[pixelBase],
|
pixels[pixelBase],
|
||||||
pixels[pixelBase + 1],
|
pixels[pixelBase + 1],
|
||||||
pixels[pixelBase + 2]);
|
pixels[pixelBase + 2]);
|
||||||
if (pixelID > Drawable.NONE) {
|
if (pixelID > RenderConstants.ID_NONE) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -399,14 +531,14 @@ class RenderWebGL {
|
||||||
* @param {int} touchHeight The client height 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.
|
* @param {int[]} candidateIDs The Drawable IDs to pick from, otherwise all.
|
||||||
* @returns {int} The ID of the topmost Drawable under the picking location, or
|
* @returns {int} The ID of the topmost Drawable under the picking location, or
|
||||||
* Drawable.NONE if there is no Drawable at that location.
|
* RenderConstants.ID_NONE if there is no Drawable at that location.
|
||||||
*/
|
*/
|
||||||
pick (centerX, centerY, touchWidth, touchHeight, candidateIDs) {
|
pick (centerX, centerY, touchWidth, touchHeight, candidateIDs) {
|
||||||
const gl = this._gl;
|
const gl = this._gl;
|
||||||
|
|
||||||
touchWidth = touchWidth || 1;
|
touchWidth = touchWidth || 1;
|
||||||
touchHeight = touchHeight || 1;
|
touchHeight = touchHeight || 1;
|
||||||
candidateIDs = candidateIDs || this._drawables;
|
candidateIDs = candidateIDs || this._drawList;
|
||||||
|
|
||||||
const clientToGLX = gl.canvas.width / gl.canvas.clientWidth;
|
const clientToGLX = gl.canvas.width / gl.canvas.clientWidth;
|
||||||
const clientToGLY = gl.canvas.height / gl.canvas.clientHeight;
|
const clientToGLY = gl.canvas.height / gl.canvas.clientHeight;
|
||||||
|
@ -427,7 +559,7 @@ class RenderWebGL {
|
||||||
twgl.bindFramebufferInfo(gl, this._pickBufferInfo);
|
twgl.bindFramebufferInfo(gl, this._pickBufferInfo);
|
||||||
gl.viewport(0, 0, touchWidth, touchHeight);
|
gl.viewport(0, 0, touchWidth, touchHeight);
|
||||||
|
|
||||||
const noneColor = Drawable.color4fFromID(Drawable.NONE);
|
const noneColor = Drawable.color4fFromID(RenderConstants.ID_NONE);
|
||||||
gl.clearColor.apply(gl, noneColor);
|
gl.clearColor.apply(gl, noneColor);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
@ -467,9 +599,9 @@ class RenderWebGL {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bias toward selecting anything over nothing
|
// Bias toward selecting anything over nothing
|
||||||
hits[Drawable.NONE] = 0;
|
hits[RenderConstants.ID_NONE] = 0;
|
||||||
|
|
||||||
let hit = Drawable.NONE;
|
let hit = RenderConstants.ID_NONE;
|
||||||
for (const hitID in hits) {
|
for (const hitID in hits) {
|
||||||
if (hits.hasOwnProperty(hitID) && (hits[hitID] > hits[hit])) {
|
if (hits.hasOwnProperty(hitID) && (hits[hitID] > hits[hit])) {
|
||||||
hit = hitID;
|
hit = hitID;
|
||||||
|
@ -485,7 +617,11 @@ class RenderWebGL {
|
||||||
* @return {?Rectangle} Rectangle bounds for touching query, or null.
|
* @return {?Rectangle} Rectangle bounds for touching query, or null.
|
||||||
*/
|
*/
|
||||||
_touchingBounds (drawableID) {
|
_touchingBounds (drawableID) {
|
||||||
const drawable = Drawable.getDrawableByID(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
|
|
||||||
|
// TODO: remove this once URL-based skin setting is removed.
|
||||||
|
if (!drawable.skin || !drawable.skin.getTexture([100, 100])) return null;
|
||||||
|
|
||||||
const bounds = drawable.getFastBounds();
|
const bounds = drawable.getFastBounds();
|
||||||
|
|
||||||
// Limit queries to the stage size.
|
// Limit queries to the stage size.
|
||||||
|
@ -517,7 +653,7 @@ class RenderWebGL {
|
||||||
candidateIDs = candidateIDs.filter(testID => {
|
candidateIDs = candidateIDs.filter(testID => {
|
||||||
if (testID === drawableID) return false;
|
if (testID === drawableID) return false;
|
||||||
// Only draw items which could possibly overlap target Drawable.
|
// Only draw items which could possibly overlap target Drawable.
|
||||||
const candidate = Drawable.getDrawableByID(testID);
|
const candidate = this._allDrawables[testID];
|
||||||
const candidateBounds = candidate.getFastBounds();
|
const candidateBounds = candidate.getFastBounds();
|
||||||
return bounds.intersects(candidateBounds);
|
return bounds.intersects(candidateBounds);
|
||||||
});
|
});
|
||||||
|
@ -534,7 +670,25 @@ class RenderWebGL {
|
||||||
* @param {object.<string,*>} properties The new property values to set.
|
* @param {object.<string,*>} properties The new property values to set.
|
||||||
*/
|
*/
|
||||||
updateDrawableProperties (drawableID, properties) {
|
updateDrawableProperties (drawableID, properties) {
|
||||||
const drawable = Drawable.getDrawableByID(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
|
if (!drawable) {
|
||||||
|
// TODO: fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
||||||
|
// Right now this happens so much on some projects that a warning or exception here can hang the browser.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: remove this after fully deprecating URL-based skin paths
|
||||||
|
if ('skin' in properties) {
|
||||||
|
const {skin, costumeResolution} = properties;
|
||||||
|
const skinId = this.createSkinFromURL(skin, costumeResolution);
|
||||||
|
drawable.skin = this._allSkins[skinId];
|
||||||
|
}
|
||||||
|
if ('skinId' in properties) {
|
||||||
|
drawable.skin = this._allSkins[properties.skinId];
|
||||||
|
}
|
||||||
|
if ('rotationCenter' in properties) {
|
||||||
|
const newRotationCenter = properties.rotationCenter;
|
||||||
|
drawable.skin.setRotationCenter(newRotationCenter[0], newRotationCenter[1]);
|
||||||
|
}
|
||||||
drawable.updateProperties(properties);
|
drawable.updateProperties(properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -597,7 +751,7 @@ class RenderWebGL {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw all Drawables, with the possible exception of
|
* Draw all Drawables, with the possible exception of
|
||||||
* @param {int[]} drawables The Drawable IDs to draw, possibly this._drawables.
|
* @param {int[]} drawables The Drawable IDs to draw, possibly this._drawList.
|
||||||
* @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
|
* @param {ShaderManager.DRAW_MODE} drawMode Draw normally, silhouette, etc.
|
||||||
* @param {module:twgl/m4.Mat4} projection The projection matrix to use.
|
* @param {module:twgl/m4.Mat4} projection The projection matrix to use.
|
||||||
* @param {Drawable~idFilterFunc} [filter] An optional filter function.
|
* @param {Drawable~idFilterFunc} [filter] An optional filter function.
|
||||||
|
@ -615,12 +769,17 @@ class RenderWebGL {
|
||||||
// If we have a filter, check whether the ID fails
|
// If we have a filter, check whether the ID fails
|
||||||
if (filter && !filter(drawableID)) continue;
|
if (filter && !filter(drawableID)) continue;
|
||||||
|
|
||||||
const drawable = Drawable.getDrawableByID(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
// TODO: check if drawable is inside the viewport before anything else
|
// TODO: check if drawable is inside the viewport before anything else
|
||||||
|
|
||||||
// Hidden drawables (e.g., by a "hide" block) are never drawn.
|
// Hidden drawables (e.g., by a "hide" block) are never drawn.
|
||||||
if (!drawable.getVisible()) continue;
|
if (!drawable.getVisible()) continue;
|
||||||
|
|
||||||
|
const drawableScale = drawable.scale;
|
||||||
|
|
||||||
|
// If the texture isn't ready yet, skip it.
|
||||||
|
if (!drawable.skin.getTexture(drawableScale)) continue;
|
||||||
|
|
||||||
const effectBits = drawable.getEnabledEffects();
|
const effectBits = drawable.getEnabledEffects();
|
||||||
const newShader = this._shaderManager.getShader(drawMode, effectBits);
|
const newShader = this._shaderManager.getShader(drawMode, effectBits);
|
||||||
if (currentShader !== newShader) {
|
if (currentShader !== newShader) {
|
||||||
|
@ -631,6 +790,7 @@ class RenderWebGL {
|
||||||
twgl.setUniforms(currentShader, {u_fudge: window.fudge || 0});
|
twgl.setUniforms(currentShader, {u_fudge: window.fudge || 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
twgl.setUniforms(currentShader, drawable.skin.getUniforms(drawableScale));
|
||||||
twgl.setUniforms(currentShader, drawable.getUniforms());
|
twgl.setUniforms(currentShader, drawable.getUniforms());
|
||||||
|
|
||||||
// Apply extra uniforms after the Drawable's, to allow overwriting.
|
// Apply extra uniforms after the Drawable's, to allow overwriting.
|
||||||
|
@ -651,7 +811,7 @@ class RenderWebGL {
|
||||||
* @return {Array.<Array.<number>>} points Convex hull points, as [[x, y], ...]
|
* @return {Array.<Array.<number>>} points Convex hull points, as [[x, y], ...]
|
||||||
*/
|
*/
|
||||||
_getConvexHullPointsForDrawable (drawableID) {
|
_getConvexHullPointsForDrawable (drawableID) {
|
||||||
const drawable = Drawable.getDrawableByID(drawableID);
|
const drawable = this._allDrawables[drawableID];
|
||||||
const [width, height] = drawable._uniforms.u_skinSize;
|
const [width, height] = drawable._uniforms.u_skinSize;
|
||||||
// No points in the hull if invisible or size is 0.
|
// No points in the hull if invisible or size is 0.
|
||||||
if (!drawable.getVisible() || width === 0 || height === 0) {
|
if (!drawable.getVisible() || width === 0 || height === 0) {
|
||||||
|
@ -663,8 +823,8 @@ class RenderWebGL {
|
||||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||||
gl.viewport(0, 0, width, height);
|
gl.viewport(0, 0, width, height);
|
||||||
|
|
||||||
// Clear the canvas with Drawable.NONE.
|
// Clear the canvas with RenderConstants.ID_NONE.
|
||||||
const noneColor = Drawable.color4fFromID(Drawable.NONE);
|
const noneColor = Drawable.color4fFromID(RenderConstants.ID_NONE);
|
||||||
gl.clearColor.apply(gl, noneColor);
|
gl.clearColor.apply(gl, noneColor);
|
||||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
@ -692,7 +852,7 @@ class RenderWebGL {
|
||||||
* Helper method to look up a pixel.
|
* Helper method to look up a pixel.
|
||||||
* @param {int} x X coordinate of the pixel in `pixels`.
|
* @param {int} x X coordinate of the pixel in `pixels`.
|
||||||
* @param {int} y Y coordinate of the pixel in `pixels`.
|
* @param {int} y Y coordinate of the pixel in `pixels`.
|
||||||
* @return {int} Known ID at that pixel, or Drawable.NONE.
|
* @return {int} Known ID at that pixel, or RenderConstants.ID_NONE.
|
||||||
*/
|
*/
|
||||||
const _getPixel = (x, y) => {
|
const _getPixel = (x, y) => {
|
||||||
const pixelBase = ((width * y) + x) * 4;
|
const pixelBase = ((width * y) + x) * 4;
|
||||||
|
@ -704,14 +864,14 @@ class RenderWebGL {
|
||||||
for (let y = 0; y <= height; y++) {
|
for (let y = 0; y <= height; y++) {
|
||||||
// Scan from left.
|
// Scan from left.
|
||||||
for (let x = 0; x < width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
if (_getPixel(x, y) > Drawable.NONE) {
|
if (_getPixel(x, y) > RenderConstants.ID_NONE) {
|
||||||
boundaryPoints.push([x, y]);
|
boundaryPoints.push([x, y]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scan from right.
|
// Scan from right.
|
||||||
for (let x = width - 1; x >= 0; x--) {
|
for (let x = width - 1; x >= 0; x--) {
|
||||||
if (_getPixel(x, y) > Drawable.NONE) {
|
if (_getPixel(x, y) > RenderConstants.ID_NONE) {
|
||||||
boundaryPoints.push([x, y]);
|
boundaryPoints.push([x, y]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
79
src/SVGSkin.js
Normal file
79
src/SVGSkin.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
const twgl = require('twgl.js');
|
||||||
|
|
||||||
|
const Skin = require('./Skin');
|
||||||
|
const SvgRenderer = require('./svg-quirks-mode/svg-renderer');
|
||||||
|
|
||||||
|
class SVGSkin extends Skin {
|
||||||
|
/**
|
||||||
|
* Create a new SVG skin.
|
||||||
|
* @param {!int} id - The ID for this Skin.
|
||||||
|
* @param {!RenderWebGL} renderer - The renderer which will use this skin.
|
||||||
|
*/
|
||||||
|
constructor (id, renderer) {
|
||||||
|
super(id);
|
||||||
|
|
||||||
|
/** @type {RenderWebGL} */
|
||||||
|
this._renderer = renderer;
|
||||||
|
|
||||||
|
/** @type {SvgRenderer} */
|
||||||
|
this._svgRenderer = new SvgRenderer();
|
||||||
|
|
||||||
|
/** @type {WebGLTexture} */
|
||||||
|
this._texture = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of this object. Do not use it after calling this method.
|
||||||
|
*/
|
||||||
|
dispose () {
|
||||||
|
if (this._texture) {
|
||||||
|
this._renderer.gl.deleteTexture(this._texture);
|
||||||
|
this._texture = null;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {[number,number]} the "native" size, in texels, of this skin.
|
||||||
|
*/
|
||||||
|
get size () {
|
||||||
|
return [this._svgRenderer.canvas.width, this._svgRenderer.canvas.height];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {[number,number]} scale - The scaling factors to be used.
|
||||||
|
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
getTexture (scale) {
|
||||||
|
// TODO: re-render a scaled version if the requested scale is significantly larger than the current render
|
||||||
|
return this._texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the contents of this skin to a snapshot of the provided SVG data.
|
||||||
|
* @param {string} svgData - new SVG to use.
|
||||||
|
*/
|
||||||
|
setSVG (svgData) {
|
||||||
|
this._svgRenderer.fromString(svgData, () => {
|
||||||
|
const gl = this._renderer.gl;
|
||||||
|
if (this._texture) {
|
||||||
|
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
||||||
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas);
|
||||||
|
} else {
|
||||||
|
const textureOptions = {
|
||||||
|
auto: true,
|
||||||
|
mag: gl.NEAREST,
|
||||||
|
min: gl.NEAREST, // TODO: mipmaps, linear (except pixelate)
|
||||||
|
wrap: gl.CLAMP_TO_EDGE,
|
||||||
|
src: this._svgRenderer.canvas
|
||||||
|
};
|
||||||
|
|
||||||
|
this._texture = twgl.createTexture(gl, textureOptions);
|
||||||
|
}
|
||||||
|
this.emit(Skin.Events.WasAltered);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SVGSkin;
|
119
src/Skin.js
Normal file
119
src/Skin.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
const twgl = require('twgl.js');
|
||||||
|
|
||||||
|
const RenderConstants = require('./RenderConstants');
|
||||||
|
|
||||||
|
class Skin extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Create a Skin, which stores and/or generates textures for use in rendering.
|
||||||
|
* @param {int} id - The unique ID for this Skin.
|
||||||
|
*/
|
||||||
|
constructor (id) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
/** @type {int} */
|
||||||
|
this._id = id;
|
||||||
|
|
||||||
|
/** @type {Vec3} */
|
||||||
|
this._rotationCenter = twgl.v3.create(0, 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The uniforms to be used by the vertex and pixel shaders.
|
||||||
|
* Some of these are used by other parts of the renderer as well.
|
||||||
|
* @type {Object.<string,*>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._uniforms = {
|
||||||
|
/**
|
||||||
|
* The nominal (not necessarily current) size of the current skin.
|
||||||
|
* @type {number[]}
|
||||||
|
*/
|
||||||
|
u_skinSize: [0, 0],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual WebGL texture object for the skin.
|
||||||
|
* @type {WebGLTexture}
|
||||||
|
*/
|
||||||
|
u_skin: null
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setMaxListeners(RenderConstants.SKIN_SHARE_SOFT_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispose of this object. Do not use it after calling this method.
|
||||||
|
*/
|
||||||
|
dispose () {
|
||||||
|
this._id = RenderConstants.ID_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {int} the unique ID for this Skin.
|
||||||
|
*/
|
||||||
|
get id () {
|
||||||
|
return this._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Vec3} the origin, in object space, about which this Skin should rotate.
|
||||||
|
*/
|
||||||
|
get rotationCenter () {
|
||||||
|
return this._rotationCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
* @return {[number,number]} the "native" size, in texels, of this skin.
|
||||||
|
*/
|
||||||
|
get size () {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the origin, in object space, about which this Skin should rotate.
|
||||||
|
* @param {number} x - The x coordinate of the new rotation center.
|
||||||
|
* @param {number} y - The y coordinate of the new rotation center.
|
||||||
|
*/
|
||||||
|
setRotationCenter (x, y) {
|
||||||
|
if (x !== this._rotationCenter[0] || y !== this._rotationCenter[1]) {
|
||||||
|
this._rotationCenter[0] = x;
|
||||||
|
this._rotationCenter[1] = y;
|
||||||
|
this.emit(Skin.Events.WasAltered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @abstract
|
||||||
|
* @param {[number,number]} scale - The scaling factors to be used.
|
||||||
|
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given size.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
getTexture (scale) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update and returns the uniforms for this skin.
|
||||||
|
* @param {[number,number]} scale - The scaling factors to be used.
|
||||||
|
* @returns {object.<string, *>} the shader uniforms to be used when rendering with this Skin.
|
||||||
|
*/
|
||||||
|
getUniforms (scale) {
|
||||||
|
this._uniforms.u_skin = this.getTexture(scale);
|
||||||
|
this._uniforms.u_skinSize = this.size;
|
||||||
|
return this._uniforms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the events which can be emitted by instances of this class.
|
||||||
|
* @type {object.<string,string>}
|
||||||
|
*/
|
||||||
|
Skin.Events = {
|
||||||
|
/**
|
||||||
|
* Emitted when anything about the Skin has been altered, such as the appearance or rotation center.
|
||||||
|
*/
|
||||||
|
WasAltered: 'WasAltered'
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Skin;
|
|
@ -30,12 +30,20 @@ document.body.insertBefore(documentStyleTag, document.body.firstChild);
|
||||||
class SvgRenderer {
|
class SvgRenderer {
|
||||||
/**
|
/**
|
||||||
* Create a quirks-mode SVG renderer for a particular canvas.
|
* Create a quirks-mode SVG renderer for a particular canvas.
|
||||||
* @param {!HTMLCanvasElement} canvas A canvas element to draw to.
|
* @param {HTMLCanvasElement} [canvas] An optional canvas element to draw to. If this is not provided, the renderer
|
||||||
|
* will create a new canvas.
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
constructor (canvas) {
|
constructor (canvas) {
|
||||||
this._canvas = canvas;
|
this._canvas = canvas || document.createElement('canvas');
|
||||||
this._context = canvas.getContext('2d');
|
this._context = this._canvas.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {!HTMLCanvasElement} this renderer's target canvas.
|
||||||
|
*/
|
||||||
|
get canvas () {
|
||||||
|
return this._canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue