scratch-render/src/Silhouette.js

211 lines
7.0 KiB
JavaScript

/**
* @fileoverview
* A representation of a Skin's silhouette that can test if a point on the skin
* renders a pixel where it is drawn.
*/
/**
* <canvas> element used to update Silhouette data from skin bitmap data.
* @type {CanvasElement}
*/
let __SilhouetteUpdateCanvas;
/**
* Internal helper function (in hopes that compiler can inline). Get a pixel
* from silhouette data, or 0 if outside it's bounds.
* @private
* @param {Silhouette} silhouette - has data width and height
* @param {number} x - x
* @param {number} y - y
* @return {number} Alpha value for x/y position
*/
const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => {
// 0 if outside bouds, otherwise read from data.
if (x >= width || y >= height || x < 0 || y < 0) {
return 0;
}
return data[(((y * width) + x) * 4) + 3];
};
/**
* Memory buffers for doing 4 corner sampling for linear interpolation
*/
const __cornerWork = [
new Uint8ClampedArray(4),
new Uint8ClampedArray(4),
new Uint8ClampedArray(4),
new Uint8ClampedArray(4)
];
/**
* Get the color from a given silhouette at an x/y local texture position.
* @param {Silhouette} The silhouette to sample.
* @param {number} x X position of texture (0-1).
* @param {number} y Y position of texture (0-1).
* @param {Uint8ClampedArray} dst A color 4b space.
* @return {Uint8ClampedArray} The dst vector.
*/
const getColor4b = ({_width: width, _height: height, _colorData: data}, x, y, dst) => {
// 0 if outside bouds, otherwise read from data.
if (x >= width || y >= height || x < 0 || y < 0) {
return dst.fill(0);
}
const offset = ((y * width) + x) * 4;
dst[0] = data[offset];
dst[1] = data[offset + 1];
dst[2] = data[offset + 2];
dst[3] = data[offset + 3];
return dst;
};
class Silhouette {
constructor () {
/**
* The width of the data representing the current skin data.
* @type {number}
*/
this._width = 0;
/**
* The height of the data representing the current skin date.
* @type {number}
*/
this._height = 0;
/**
* The data representing a skin's silhouette shape.
* @type {Uint8ClampedArray}
*/
this._colorData = null;
this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
}
/**
* Update this silhouette with the bitmapData for a skin.
* @param {*} bitmapData An image, canvas or other element that the skin
* rendering can be queried from.
*/
update (bitmapData) {
let imageData;
if (bitmapData instanceof ImageData) {
// If handed ImageData directly, use it directly.
imageData = bitmapData;
this._width = bitmapData.width;
this._height = bitmapData.height;
} else {
// Draw about anything else to our update canvas and poll image data
// from that.
const canvas = Silhouette._updateCanvas();
const width = this._width = canvas.width = bitmapData.width;
const height = this._height = canvas.height = bitmapData.height;
const ctx = canvas.getContext('2d');
if (!(width && height)) {
return;
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(bitmapData, 0, 0, width, height);
imageData = ctx.getImageData(0, 0, width, height);
}
this._colorData = imageData.data;
// delete our custom overriden "uninitalized" color functions
// let the prototype work for itself
delete this.colorAtNearest;
delete this.colorAtLinear;
}
/**
* Sample a color from the silhouette at a given local position using
* "nearest neighbor"
* @param {twgl.v3} vec [x,y] texture space (0-1)
* @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes)
* @returns {Uint8ClampedArray} dst
*/
colorAtNearest (vec, dst) {
return getColor4b(
this,
Math.floor(vec[0] * (this._width - 1)),
Math.floor(vec[1] * (this._height - 1)),
dst
);
}
/**
* Sample a color from the silhouette at a given local position using
* "linear interpolation"
* @param {twgl.v3} vec [x,y] texture space (0-1)
* @param {Uint8ClampedArray} dst The memory buffer to store the value in. (4 bytes)
* @returns {Uint8ClampedArray} dst
*/
colorAtLinear (vec, dst) {
const x = vec[0] * (this._width - 1);
const y = vec[1] * (this._height - 1);
const x1D = x % 1;
const y1D = y % 1;
const x0D = 1 - x1D;
const y0D = 1 - y1D;
const xFloor = Math.floor(x);
const yFloor = Math.floor(y);
const x0y0 = getColor4b(this, xFloor, yFloor, __cornerWork[0]);
const x1y0 = getColor4b(this, xFloor + 1, yFloor, __cornerWork[1]);
const x0y1 = getColor4b(this, xFloor, yFloor + 1, __cornerWork[2]);
const x1y1 = getColor4b(this, xFloor + 1, yFloor + 1, __cornerWork[3]);
dst[0] = (x0y0[0] * x0D * y0D) + (x0y1[0] * x0D * y1D) + (x1y0[0] * x1D * y0D) + (x1y1[0] * x1D * y1D);
dst[1] = (x0y0[1] * x0D * y0D) + (x0y1[1] * x0D * y1D) + (x1y0[1] * x1D * y0D) + (x1y1[1] * x1D * y1D);
dst[2] = (x0y0[2] * x0D * y0D) + (x0y1[2] * x0D * y1D) + (x1y0[2] * x1D * y0D) + (x1y1[2] * x1D * y1D);
dst[3] = (x0y0[3] * x0D * y0D) + (x0y1[3] * x0D * y1D) + (x1y0[3] * x1D * y0D) + (x1y1[3] * x1D * y1D);
return dst;
}
/**
* Test if texture coordinate touches the silhouette using nearest neighbor.
* @param {twgl.v3} vec A texture coordinate.
* @return {boolean} If the nearest pixel has an alpha value.
*/
isTouchingNearest (vec) {
if (!this._colorData) return;
return getPoint(
this,
Math.floor(vec[0] * (this._width - 1)),
Math.floor(vec[1] * (this._height - 1))
) > 0;
}
/**
* Test to see if any of the 4 pixels used in the linear interpolate touch
* the silhouette.
* @param {twgl.v3} vec A texture coordinate.
* @return {boolean} Any of the pixels have some alpha.
*/
isTouchingLinear (vec) {
if (!this._colorData) return;
const x = Math.floor(vec[0] * (this._width - 1));
const y = Math.floor(vec[1] * (this._height - 1));
return getPoint(this, x, y) > 0 ||
getPoint(this, x + 1, y) > 0 ||
getPoint(this, x, y + 1) > 0 ||
getPoint(this, x + 1, y + 1) > 0;
}
/**
* Get the canvas element reused by Silhouettes to update their data with.
* @private
* @return {CanvasElement} A canvas to draw bitmap data to.
*/
static _updateCanvas () {
if (typeof __SilhouetteUpdateCanvas === 'undefined') {
__SilhouetteUpdateCanvas = document.createElement('canvas');
}
return __SilhouetteUpdateCanvas;
}
}
module.exports = Silhouette;