mirror of
https://github.com/scratchfoundation/scratch-render.git
synced 2025-07-16 17:22:21 -04:00
This makes the code messier but I'm not sure what else to do since the texture filtering method to be used depends on the drawable's properties (e.g. transform, enabled effects). We still need to pass in the scale separately because in the main rendering path, we multiply it by the screen-space scale factor.
235 lines
7.4 KiB
JavaScript
235 lines
7.4 KiB
JavaScript
const EventEmitter = require('events');
|
|
|
|
const twgl = require('twgl.js');
|
|
|
|
const RenderConstants = require('./RenderConstants');
|
|
const Silhouette = require('./Silhouette');
|
|
|
|
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
|
|
*/
|
|
constructor (id) {
|
|
super();
|
|
|
|
/** @type {int} */
|
|
this._id = id;
|
|
|
|
/** @type {Vec3} */
|
|
this._rotationCenter = twgl.v3.create(0, 0);
|
|
|
|
/** @type {WebGLTexture} */
|
|
this._texture = null;
|
|
|
|
/**
|
|
* 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 {Array<number>}
|
|
*/
|
|
u_skinSize: [0, 0],
|
|
|
|
/**
|
|
* The actual WebGL texture object for the skin.
|
|
* @type {WebGLTexture}
|
|
*/
|
|
u_skin: null
|
|
};
|
|
|
|
/**
|
|
* A silhouette to store touching data, skins are responsible for keeping it up to date.
|
|
* @private
|
|
*/
|
|
this._silhouette = new Silhouette();
|
|
|
|
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 {Array<number>} the "native" size, in texels, of this skin.
|
|
*/
|
|
get size () {
|
|
return [0, 0];
|
|
}
|
|
|
|
/**
|
|
* Should this skin's texture be filtered with nearest-neighbor or linear interpolation at the given scale?
|
|
* @param {?Array<Number>} scale The screen-space X and Y scaling factors at which this skin's texture will be
|
|
* displayed, as percentages (100 means 1 "native size" unit is 1 screen pixel; 200 means 2 screen pixels, etc).
|
|
* @param {Drawable} drawable The drawable that this skin's texture will be applied to.
|
|
* @return {boolean} True if this skin's texture, as returned by {@link getTexture}, should be filtered with
|
|
* nearest-neighbor interpolation.
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
useNearest (scale, drawable) {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get the center of the current bounding box
|
|
* @return {Array<number>} the center of the current bounding box
|
|
*/
|
|
calculateRotationCenter () {
|
|
return [this.size[0] / 2, this.size[1] / 2];
|
|
}
|
|
|
|
/**
|
|
* @abstract
|
|
* @param {Array<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 this._emptyImageTexture;
|
|
}
|
|
|
|
/**
|
|
* Get the bounds of the drawable for determining its fenced position.
|
|
* @param {Array<number>} drawable - The Drawable instance this skin is using.
|
|
* @param {?Rectangle} result - Optional destination for bounds calculation.
|
|
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB.
|
|
*/
|
|
getFenceBounds (drawable, result) {
|
|
return drawable.getAABB(result);
|
|
}
|
|
|
|
/**
|
|
* Update and returns the uniforms for this skin.
|
|
* @param {Array<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;
|
|
}
|
|
|
|
/**
|
|
* If the skin defers silhouette operations until the last possible minute,
|
|
* this will be called before isTouching uses the silhouette.
|
|
* @abstract
|
|
*/
|
|
updateSilhouette () {}
|
|
|
|
/**
|
|
* Set this skin's texture to the given image.
|
|
* @param {ImageData|HTMLCanvasElement} textureData - The canvas or image data to set the texture to.
|
|
*/
|
|
_setTexture (textureData) {
|
|
const gl = this._renderer.gl;
|
|
|
|
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
|
// Premultiplied alpha is necessary for proper blending.
|
|
// See http://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
|
|
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
|
|
this._silhouette.update(textureData);
|
|
}
|
|
|
|
/**
|
|
* Set the contents of this skin to an empty skin.
|
|
* @fires Skin.event:WasAltered
|
|
*/
|
|
setEmptyImageData () {
|
|
// Free up the current reference to the _texture
|
|
this._texture = null;
|
|
|
|
if (!this._emptyImageData) {
|
|
// Create a transparent pixel
|
|
this._emptyImageData = new ImageData(1, 1);
|
|
|
|
// Create a new texture and update the silhouette
|
|
const gl = this._renderer.gl;
|
|
|
|
const textureOptions = {
|
|
auto: true,
|
|
wrap: gl.CLAMP_TO_EDGE,
|
|
src: this._emptyImageData
|
|
};
|
|
|
|
// Note: we're using _emptyImageTexture here instead of _texture
|
|
// so that we can cache this empty texture for later use as needed.
|
|
// this._texture can get modified by other skins (e.g. BitmapSkin
|
|
// and SVGSkin, so we can't use that same field for caching)
|
|
this._emptyImageTexture = twgl.createTexture(gl, textureOptions);
|
|
}
|
|
|
|
this._rotationCenter[0] = 0;
|
|
this._rotationCenter[1] = 0;
|
|
|
|
this._silhouette.update(this._emptyImageData);
|
|
this.emit(Skin.Events.WasAltered);
|
|
}
|
|
|
|
/**
|
|
* Does this point touch an opaque or translucent point on this skin?
|
|
* Nearest Neighbor version
|
|
* The caller is responsible for ensuring this skin's silhouette is up-to-date.
|
|
* @see updateSilhouette
|
|
* @see Drawable.updateCPURenderAttributes
|
|
* @param {twgl.v3} vec A texture coordinate.
|
|
* @return {boolean} Did it touch?
|
|
*/
|
|
isTouchingNearest (vec) {
|
|
return this._silhouette.isTouchingNearest(vec);
|
|
}
|
|
|
|
/**
|
|
* Does this point touch an opaque or translucent point on this skin?
|
|
* Linear Interpolation version
|
|
* The caller is responsible for ensuring this skin's silhouette is up-to-date.
|
|
* @see updateSilhouette
|
|
* @see Drawable.updateCPURenderAttributes
|
|
* @param {twgl.v3} vec A texture coordinate.
|
|
* @return {boolean} Did it touch?
|
|
*/
|
|
isTouchingLinear (vec) {
|
|
return this._silhouette.isTouchingLinear(vec);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* These are the events which can be emitted by instances of this class.
|
|
* @enum {string}
|
|
*/
|
|
Skin.Events = {
|
|
/**
|
|
* Emitted when anything about the Skin has been altered, such as the appearance or rotation center.
|
|
* @event Skin.event:WasAltered
|
|
*/
|
|
WasAltered: 'WasAltered'
|
|
};
|
|
|
|
module.exports = Skin;
|