Touching color implementation ()

* Touching color draft implementation

* New constant for CPU/GPU render split determined, removing query param

* Small fix for pick tests
This commit is contained in:
Mx Corey Frang 2018-08-08 14:20:35 -04:00 committed by GitHub
parent 62b4a1d5a2
commit c54a928f0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 574 additions and 88 deletions

View file

@ -6,8 +6,41 @@ const ShaderManager = require('./ShaderManager');
const Skin = require('./Skin');
const EffectTransform = require('./EffectTransform');
/**
* An internal workspace for calculating texture locations from world vectors
* this is REUSED for memory conservation reasons
* @type {twgl.v3}
*/
const __isTouchingPosition = twgl.v3.create();
/**
* Convert a scratch space location into a texture space float. Uses the
* internal __isTouchingPosition as a return value, so this should be copied
* if you ever need to get two local positions and store both. Requires that
* the drawable inverseMatrix is up to date.
*
* @param {Drawable} drawable The drawable to get the inverse matrix and uniforms from
* @param {twgl.v3} vec [x,y] scratch space vector
* @return {twgl.v3} [x,y] texture space float vector - transformed by effects and matrix
*/
const getLocalPosition = (drawable, vec) => {
// Transfrom from world coordinates to Drawable coordinates.
const localPosition = __isTouchingPosition;
const v0 = vec[0];
const v1 = vec[1];
const m = drawable._inverseMatrix;
// var v2 = v[2];
const d = (v0 * m[3]) + (v1 * m[7]) + m[15];
// The RenderWebGL quad flips the texture's X axis. So rendered bottom
// left is 1, 0 and the top right is 0, 1. Flip the X axis so
// localPosition matches that transformation.
localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d);
localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5;
// Apply texture effect transform.
EffectTransform.transformPoint(drawable, localPosition, localPosition);
return localPosition;
};
class Drawable {
/**
* An object which can be drawn by the renderer.
@ -381,36 +414,7 @@ class Drawable {
return false;
}
if (this._transformDirty) {
this._calculateTransform();
}
// Get the inverse of the model matrix or update it.
const inverse = this._inverseMatrix;
if (this._inverseTransformDirty) {
const model = twgl.m4.copy(this._uniforms.u_modelMatrix, inverse);
// The normal matrix uses a z scaling of 0 causing model[10] to be
// 0. Getting a 4x4 inverse is impossible without a scaling in x, y,
// and z.
model[10] = 1;
twgl.m4.inverse(model, model);
this._inverseTransformDirty = false;
}
// Transfrom from world coordinates to Drawable coordinates.
const localPosition = twgl.m4.transformPoint(inverse, vec, __isTouchingPosition);
// Transform into texture coordinates. 0, 0 is the bottom left. 1, 1 is
// the top right.
localPosition[0] += 0.5;
localPosition[1] += 0.5;
// The RenderWebGL quad flips the texture's X axis. So rendered bottom
// left is 1, 0 and the top right is 0, 1. Flip the X axis so
// localPosition matches that transformation.
localPosition[0] = 1 - localPosition[0];
// Apply texture effect transform.
EffectTransform.transformPoint(this, localPosition, localPosition);
const localPosition = getLocalPosition(this, vec);
if (this.useNearest) {
return this.skin.isTouchingNearest(localPosition);
@ -460,6 +464,7 @@ class Drawable {
bounds.initFromPointsAABB(transformedHullPoints);
return bounds;
}
/**
* Get the precise bounds for the upper 8px slice of the Drawable.
* Used for calculating where to position a text bubble.
@ -514,6 +519,7 @@ class Drawable {
* @return {!Rectangle} Bounds for the Drawable.
*/
getFastBounds () {
this.updateMatrix();
if (!this.needsConvexHullPoints()) {
return this.getBounds();
}
@ -545,6 +551,27 @@ class Drawable {
return transformedHullPoints;
}
/**
* Update the transform matrix and calculate it's inverse for collision
* and local texture position purposes.
*/
updateMatrix () {
if (this._transformDirty) {
this._calculateTransform();
}
// Get the inverse of the model matrix or update it.
if (this._inverseTransformDirty) {
const inverse = this._inverseMatrix;
twgl.m4.copy(this._uniforms.u_modelMatrix, inverse);
// The normal matrix uses a z scaling of 0 causing model[10] to be
// 0. Getting a 4x4 inverse is impossible without a scaling in x, y,
// and z.
inverse[10] = 1;
twgl.m4.inverse(inverse, inverse);
this._inverseTransformDirty = false;
}
}
/**
* Respond to an internal change in the current Skin.
* @private
@ -586,6 +613,28 @@ class Drawable {
id |= (b & 255) << 16;
return id + RenderConstants.ID_NONE;
}
/**
* Sample a color from a drawable's texture.
* @param {twgl.v3} vec The scratch space [x,y] vector
* @param {Drawable} drawable The drawable to sample the texture from
* @param {Uint8ClampedArray} dst The "color4b" representation of the texture at point.
* @returns {Uint8ClampedArray} The dst object filled with the color4b
*/
static sampleColor4b (vec, drawable, dst) {
const localPosition = getLocalPosition(drawable, vec);
if (localPosition[0] < 0 || localPosition[1] < 0 ||
localPosition[0] > 1 || localPosition[1] > 1) {
dst[3] = 0;
return dst;
}
const textColor =
// commenting out to only use nearest for now
// drawable.useNearest ?
drawable.skin._silhouette.colorAtNearest(localPosition, dst);
// : drawable.skin._silhouette.colorAtLinear(localPosition, dst);
return EffectTransform.transformColor(drawable, textColor, textColor);
}
}
module.exports = Drawable;

View file

@ -20,7 +20,165 @@ const CENTER_X = 0.5;
*/
const CENTER_Y = 0.5;
// color conversions grabbed from https://gist.github.com/mjackson/5311256
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param {number} r The red color value
* @param {number} g The green color value
* @param {number} b The blue color value
* @return {Array} The HSL representation
*/
const rgbToHsl = ([r, g, b]) => {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h;
let s;
const l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = ((g - b) / d) + (g < b ? 6 : 0); break;
case g: h = ((b - r) / d) + 2; break;
case b: h = ((r - g) / d) + 4; break;
}
h /= 6;
}
return [h, s, l];
};
/**
* Helper function for hslToRgb is called with varying 't' values to get
* red green and blue values from the p/q/t color space calculations
* @param {number} p vector coordinates
* @param {number} q vector coordinates
* @param {number} t vector coordinates
* @return {number} amount of r/g/b byte
*/
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + ((q - p) * 6 * t);
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + ((q - p) * ((2 / 3) - t) * 6);
return p;
};
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param {number} h The hue
* @param {number} s The saturation
* @param {number} l The lightness
* @return {Array} The RGB representation
*/
const hslToRgb = ([h, s, l]) => {
let r;
let g;
let b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - (l * s);
const p = (2 * l) - q;
r = hue2rgb(p, q, h + (1 / 3));
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - (1 / 3));
}
return [r * 255, g * 255, b * 255];
};
class EffectTransform {
/**
* Transform a color given the drawables effect uniforms. Will apply
* Ghost and Color and Brightness effects.
* @param {Drawable} drawable The drawable to get uniforms from.
* @param {Uint8ClampedArray} color4b The initial color.
* @param {Uint8ClampedArary} [dst] Working space to save the color in (is returned)
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
* @returns {Uint8ClampedArray} dst filled with the transformed color
*/
static transformColor (drawable, color4b, dst, effectMask) {
dst = dst || new Uint8ClampedArray(4);
effectMask = effectMask || 0xffffffff;
dst.set(color4b);
if (dst[3] === 0) {
return dst;
}
const uniforms = drawable.getUniforms();
const effects = drawable.getEnabledEffects() & effectMask;
if ((effects & ShaderManager.EFFECT_INFO.ghost.mask) !== 0) {
// gl_FragColor.a *= u_ghost
dst[3] *= uniforms.u_ghost;
}
const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;
const enableBrightness = (effects & ShaderManager.EFFECT_INFO.brightness.mask) !== 0;
if (enableColor || enableBrightness) {
// vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
const hsl = rgbToHsl(dst);
if (enableColor) {
// this code forces grayscale values to be slightly saturated
// so that some slight change of hue will be visible
// const float minLightness = 0.11 / 2.0;
const minL = 0.11 / 2.0;
// const float minSaturation = 0.09;
const minS = 0.09;
// if (hsl.z < minLightness) hsl = vec3(0.0, 1.0, minLightness);
if (hsl[2] < minL) {
hsl[0] = 0;
hsl[1] = 1;
hsl[2] = minL;
// else if (hsl.y < minSaturation) hsl = vec3(0.0, minSaturation, hsl.z);
} else if (hsl[1] < minS) {
hsl[0] = 0;
hsl[1] = minS;
}
// hsl.x = mod(hsl.x + u_color, 1.0);
// if (hsl.x < 0.0) hsl.x += 1.0;
hsl[0] = (uniforms.u_color + hsl[0] + 1) % 1;
}
if (enableBrightness) {
// hsl.z = clamp(hsl.z + u_brightness, 0.0, 1.0);
hsl[2] = Math.min(1, hsl[2] + uniforms.u_brightness);
}
// gl_FragColor.rgb = convertHSL2RGB(hsl);
dst.set(hslToRgb(hsl));
}
return dst;
}
/**
* Transform a texture coordinate to one that would be select after applying shader effects.
* @param {Drawable} drawable The drawable whose effects to emulate.
@ -56,7 +214,7 @@ class EffectTransform {
const offsetX = dst[0] - CENTER_X;
const offsetY = dst[1] - CENTER_Y;
// float offsetMagnitude = length(offset);
const offsetMagnitude = twgl.v3.length(dst);
const offsetMagnitude = Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
// float whirlFactor = max(1.0 - (offsetMagnitude / kRadius), 0.0);
const whirlFactor = Math.max(1.0 - (offsetMagnitude / RADIUS), 0.0);
// float whirlActual = u_whirl * whirlFactor * whirlFactor;
@ -69,14 +227,14 @@ class EffectTransform {
// cosWhirl, -sinWhirl,
// sinWhirl, cosWhirl
// );
const rot00 = cosWhirl;
const rot10 = -sinWhirl;
const rot01 = sinWhirl;
const rot11 = cosWhirl;
const rot1 = cosWhirl;
const rot2 = -sinWhirl;
const rot3 = sinWhirl;
const rot4 = cosWhirl;
// texcoord0 = rotationMatrix * offset + kCenter;
dst[0] = (rot00 * offsetX) + (rot10 * offsetY) + CENTER_X;
dst[1] = (rot01 * offsetX) + (rot11 * offsetY) + CENTER_Y;
dst[0] = (rot1 * offsetX) + (rot3 * offsetY) + CENTER_X;
dst[1] = (rot2 * offsetX) + (rot4 * offsetY) + CENTER_Y;
}
if ((effects & ShaderManager.EFFECT_INFO.fisheye.mask) !== 0) {
// vec2 vec = (texcoord0 - kCenter) / kCenter;

View file

@ -16,6 +16,12 @@ const log = require('./util/log');
const __isTouchingDrawablesPoint = twgl.v3.create();
const __candidatesBounds = new Rectangle();
const __touchingColor = new Uint8ClampedArray(4);
const __blendColor = new Uint8ClampedArray(4);
// More pixels than this and we give up to the GPU and take the cost of readPixels
// Width * Height * Number of drawables at location
const __cpuTouchingColorPixelCount = 4e4;
/**
* @callback RenderWebGL#idFilterFunc
@ -32,26 +38,40 @@ const __candidatesBounds = new Rectangle();
const MAX_TOUCH_SIZE = [3, 3];
/**
* "touching {color}?" or "{color} touching {color}?" tests will be true if the
* target is touching a color whose components are each within this tolerance of
* the corresponding component of the query color.
* between 0 (exact matches only) and 255 (match anything).
* @type {object.<string,int>}
* @memberof RenderWebGL
* Passed to the uniforms for mask in touching color
*/
const TOLERANCE_TOUCHING_COLOR = {
R: 7,
G: 7,
B: 15,
Mask: 2
};
const MASK_TOUCHING_COLOR_TOLERANCE = 2;
/**
* Constant used for masking when detecting the color white
* @type {Array<int>}
* @memberof RenderWebGL
* Determines if the mask color is "close enough" (only test the 6 top bits for
* each color). These bit masks are what scratch 2 used to use, so we do the same.
* @param {Uint8Array} a A color3b or color4b value.
* @param {Uint8Array} b A color3b or color4b value.
* @returns {boolean} If the colors match within the parameters.
*/
const COLOR_BLACK = [0, 0, 0, 1];
const maskMatches = (a, b) => (
// has some non-alpha component to test against
a[3] > 0 &&
(a[0] & 0b11111100) === (b[0] & 0b11111100) &&
(a[1] & 0b11111100) === (b[1] & 0b11111100) &&
(a[2] & 0b11111100) === (b[2] & 0b11111100)
);
/**
* Determines if the given color is "close enough" (only test the 5 top bits for
* red and green, 4 bits for blue). These bit masks are what scratch 2 used to use,
* so we do the same.
* @param {Uint8Array} a A color3b or color4b value.
* @param {Uint8Array} b A color3b or color4b value / or a larger array when used with offsets
* @param {number} offset An offset into the `b` array, which lets you use a larger array to test
* multiple values at the same time.
* @returns {boolean} If the colors match within the parameters.
*/
const colorMatches = (a, b, offset) => (
(a[0] & 0b11111000) === (b[offset + 0] & 0b11111000) &&
(a[1] & 0b11111000) === (b[offset + 1] & 0b11111000) &&
(a[2] & 0b11110000) === (b[offset + 2] & 0b11110000)
);
/**
* Sprite Fencing - The number of pixels a sprite is required to leave remaining
@ -667,9 +687,6 @@ class RenderWebGL extends EventEmitter {
* @returns {boolean} True iff the Drawable is touching the color.
*/
isTouchingColor (drawableID, color3b, mask3b) {
const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
const candidates = this._candidatesTouching(drawableID, this._visibleDrawList);
if (candidates.length === 0) {
return false;
@ -677,7 +694,43 @@ class RenderWebGL extends EventEmitter {
const bounds = this._candidatesBounds(candidates);
const candidateIDs = candidates.map(({id}) => id);
// if there are just too many pixels to CPU render efficently, we
// need to let readPixels happen
if (bounds.width * bounds.height * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b);
}
const drawable = this._allDrawables[drawableID];
const point = __isTouchingDrawablesPoint;
const color = __touchingColor;
const hasMask = Boolean(mask3b);
for (let y = bounds.bottom; y <= bounds.top; y++) {
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom);
}
// Scratch Space - +y is top
for (let x = bounds.left; x <= bounds.right; x++) {
point[1] = y;
point[0] = x;
if (
// if we use a mask, check our sample color
(hasMask ?
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
drawable.isTouching(point)) &&
// and the target color is drawn at this pixel
colorMatches(RenderWebGL.sampleColor3b(point, candidates, color), color3b, 0)
) {
return true;
}
}
}
return false;
}
_isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) {
const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
// Limit size of viewport to the bounds around the target Drawable,
// and create the projection matrix for the draw.
@ -689,7 +742,7 @@ class RenderWebGL extends EventEmitter {
// When using masking such that the background fill color will showing through, ensure we don't
// fill using the same color that we are trying to detect!
if (color3b[0] > 196 && color3b[1] > 196 && color3b[2] > 196) {
fillBackgroundColor = COLOR_BLACK;
fillBackgroundColor = [0, 0, 0, 255];
}
gl.clearColor.apply(gl, fillBackgroundColor);
@ -699,7 +752,7 @@ class RenderWebGL extends EventEmitter {
if (mask3b) {
extraUniforms = {
u_colorMask: [mask3b[0] / 255, mask3b[1] / 255, mask3b[2] / 255],
u_colorMaskTolerance: TOLERANCE_TOUCHING_COLOR.Mask / 255
u_colorMaskTolerance: MASK_TOUCHING_COLOR_TOLERANCE / 255
};
}
@ -730,27 +783,24 @@ class RenderWebGL extends EventEmitter {
gl.colorMask(true, true, true, true);
gl.disable(gl.STENCIL_TEST);
}
}
const pixels = new Uint8Array(Math.floor(bounds.width * bounds.height * 4));
gl.readPixels(0, 0, bounds.width, bounds.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
_isTouchingColorGpuFin (bounds, color3b, stop) {
const gl = this._gl;
const pixels = new Uint8Array(Math.floor(bounds.width * (bounds.height - stop) * 4));
gl.readPixels(0, 0, bounds.width, (bounds.height - stop), gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (this._debugCanvas) {
this._debugCanvas.width = bounds.width;
this._debugCanvas.height = bounds.height;
const context = this._debugCanvas.getContext('2d');
const imageData = context.getImageData(0, 0, bounds.width, bounds.height);
const imageData = context.getImageData(0, 0, bounds.width, bounds.height - stop);
imageData.data.set(pixels);
context.putImageData(imageData, 0, 0);
}
for (let pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) {
const pixelDistanceR = Math.abs(pixels[pixelBase] - color3b[0]);
const pixelDistanceG = Math.abs(pixels[pixelBase + 1] - color3b[1]);
const pixelDistanceB = Math.abs(pixels[pixelBase + 2] - color3b[2]);
if (pixelDistanceR <= TOLERANCE_TOUCHING_COLOR.R &&
pixelDistanceG <= TOLERANCE_TOUCHING_COLOR.G &&
pixelDistanceB <= TOLERANCE_TOUCHING_COLOR.B) {
if (colorMatches(color3b, pixels, pixelBase)) {
return true;
}
}
@ -883,7 +933,12 @@ class RenderWebGL extends EventEmitter {
candidateIDs = (candidateIDs || this._drawList).filter(id => {
const drawable = this._allDrawables[id];
// default pick list ignores visible and ghosted sprites.
return drawable.getVisible() && drawable.getUniforms().u_ghost !== 0;
if (drawable.getVisible() && drawable.getUniforms().u_ghost !== 0) {
drawable.updateMatrix();
drawable.skin.updateSilhouette();
return true;
}
return false;
});
if (candidateIDs.length === 0) {
return false;
@ -1085,6 +1140,8 @@ class RenderWebGL extends EventEmitter {
/** @todo remove this once URL-based skin setting is removed. */
if (!drawable.skin || !drawable.skin.getTexture([100, 100])) return null;
drawable.updateMatrix();
drawable.skin.updateSilhouette();
const bounds = drawable.getFastBounds();
// Limit queries to the stage size.
@ -1110,24 +1167,31 @@ class RenderWebGL extends EventEmitter {
*/
_candidatesTouching (drawableID, candidateIDs) {
const bounds = this._touchingBounds(drawableID);
if (!bounds) {
return [];
const result = [];
if (bounds === null) {
return result;
}
return candidateIDs.reduce((result, id) => {
// iterate through the drawables list BACKWARDS - we want the top most item to be the first we check
for (let index = candidateIDs.length - 1; index >= 0; index--) {
const id = candidateIDs[index];
if (id !== drawableID) {
const drawable = this._allDrawables[id];
const candidateBounds = drawable.getFastBounds();
if (bounds.intersects(candidateBounds)) {
result.push({
id,
drawable,
intersection: Rectangle.intersect(bounds, candidateBounds)
});
if (drawable.skin && drawable._visible) {
// Update the CPU position data
drawable.updateMatrix();
drawable.skin.updateSilhouette();
const candidateBounds = drawable.getFastBounds();
if (bounds.intersects(candidateBounds)) {
result.push({
id,
drawable,
intersection: Rectangle.intersect(bounds, candidateBounds)
});
}
}
}
return result;
}, []);
}
return result;
}
/**
@ -1556,6 +1620,43 @@ class RenderWebGL extends EventEmitter {
// Simplify boundary points using convex hull.
return hull(boundaryPoints, Infinity);
}
/**
* Sample a "final" color from an array of drawables at a given scratch space.
* Will blend any alpha values with the drawables "below" it.
* @param {twgl.v3} vec Scratch Vector Space to sample
* @param {Array<Drawables>} drawables A list of drawables with the "top most"
* drawable at index 0
* @param {Uint8ClampedArray} dst The color3b space to store the answer in.
* @return {Uint8ClampedArray} The dst vector with everything blended down.
*/
static sampleColor3b (vec, drawables, dst) {
dst = dst || new Uint8ClampedArray(3);
dst.fill(0);
let blendAlpha = 1;
for (let index = 0; blendAlpha !== 0 && index < drawables.length; index++) {
/*
if (left > vec[0] || right < vec[0] ||
bottom > vec[1] || top < vec[0]) {
continue;
}
*/
Drawable.sampleColor4b(vec, drawables[index].drawable, __blendColor);
// if we are fully transparent, go to the next one "down"
const sampleAlpha = __blendColor[3] / 255;
// premultiply alpha
dst[0] += __blendColor[0] * blendAlpha * sampleAlpha;
dst[1] += __blendColor[1] * blendAlpha * sampleAlpha;
dst[2] += __blendColor[2] * blendAlpha * sampleAlpha;
blendAlpha *= (1 - sampleAlpha);
}
// Backdrop could be transparent, so we need to go to the "clear color" of the
// draw scene (white) as a fallback if everything was alpha
dst[0] += blendAlpha * 255;
dst[1] += blendAlpha * 255;
dst[2] += blendAlpha * 255;
return dst;
}
}
// :3

View file

@ -27,6 +27,37 @@ const getPoint = ({_width: width, _height: height, _data: data}, x, y) => {
return data[(y * width) + x];
};
/**
* 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 () {
/**
@ -46,6 +77,9 @@ class Silhouette {
* @type {Uint8ClampedArray}
*/
this._data = null;
this._colorData = null;
this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
}
/**
@ -67,12 +101,65 @@ class Silhouette {
const imageData = ctx.getImageData(0, 0, width, height);
this._data = new Uint8ClampedArray(imageData.data.length / 4);
this._colorData = imageData.data;
// delete our custom overriden "uninitalized" color functions
// let the prototype work for itself
delete this.colorAtNearest;
delete this.colorAtLinear;
for (let i = 0; i < imageData.data.length; i += 4) {
this._data[i / 4] = imageData.data[i + 3];
}
}
/**
* 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.
@ -82,8 +169,8 @@ class Silhouette {
if (!this._data) return;
return getPoint(
this,
Math.round(vec[0] * (this._width - 1)),
Math.round(vec[1] * (this._height - 1))
Math.floor(vec[0] * (this._width - 1)),
Math.floor(vec[1] * (this._height - 1))
) > 0;
}

View file

@ -161,7 +161,6 @@ class Skin extends EventEmitter {
* @return {boolean} Did it touch?
*/
isTouchingNearest (vec) {
this.updateSilhouette();
return this._silhouette.isTouchingNearest(vec);
}
@ -172,7 +171,6 @@ class Skin extends EventEmitter {
* @return {boolean} Did it touch?
*/
isTouchingLinear (vec) {
this.updateSilhouette();
return this._silhouette.isTouchingLinear(vec);
}

View file

@ -0,0 +1,86 @@
<body>
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
<script src="../../dist/web/scratch-render.js"></script>
<canvas id="test" width="480" height="360"></canvas>
<canvas id="cpu" width="480" height="360"></canvas>
<br/>
<canvas id="merge" width="480" height="360"></canvas>
<input type="file" id="file" name="file">
<script>
// These variables are going to be available in the "window global" intentionally.
// Allows you easy access to debug with `vm.greenFlag()` etc.
window.devicePixelRatio = 1;
const gpuCanvas = document.getElementById('test');
var render = new ScratchRender(gpuCanvas);
var vm = new VirtualMachine();
var storage = new ScratchStorage();
vm.attachStorage(storage);
vm.attachRenderer(render);
document.getElementById('file').addEventListener('click', e => {
document.body.removeChild(document.getElementById('loaded'));
});
document.getElementById('file').addEventListener('change', e => {
const reader = new FileReader();
const thisFileInput = e.target;
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => {
// we add a `#loaded` div to our document, the integration suite
// waits for that element to show up to assume the vm is ready
// to play!
const div = document.createElement('div');
div.id='loaded';
document.body.appendChild(div);
vm.greenFlag();
setTimeout(() => {
renderCpu();
}, 1000);
});
};
reader.readAsArrayBuffer(thisFileInput.files[0]);
});
const cpuCanvas = document.getElementById('cpu');
const cpuCtx = cpuCanvas.getContext('2d');
const cpuImageData = cpuCtx.getImageData(0, 0, cpuCanvas.width, cpuCanvas.height);
function renderCpu() {
cpuImageData.data.fill(255);
const drawBits = render._drawList.map(id => {
const drawable = render._allDrawables[id];
if (!(drawable._visible && drawable.skin)) {
return;
}
drawable.updateMatrix();
drawable.skin.updateSilhouette();
return { id, drawable };
}).reverse().filter(Boolean);
const color = new Uint8ClampedArray(3);
for (let x = -239; x <= 240; x++) {
for (let y = -180; y< 180; y++) {
render.constructor.sampleColor3b([x, y], drawBits, color);
const offset = (((179-y) * 480) + 239 + x) * 4
cpuImageData.data.set(color, offset);
}
}
cpuCtx.putImageData(cpuImageData, 0, 0);
const merge = document.getElementById('merge');
const ctx = merge.getContext('2d');
ctx.drawImage(gpuCanvas, 0, 0);
const gpuImageData = ctx.getImageData(0, 0, 480, 360);
for (let x=0; x<gpuImageData.data.length; x++) {
gpuImageData.data[x] = 255 - Math.abs(gpuImageData.data[x] - cpuImageData.data[x]);
}
ctx.putImageData(gpuImageData, 0, 0);
}
</script>
</body>

View file

@ -11,6 +11,7 @@
<script>
// These variables are going to be available in the "window global" intentionally.
// Allows you easy access to debug with `vm.greenFlag()` etc.
window.devicePixelRatio = 1;
var canvas = document.getElementById('test');
var render = new ScratchRender(canvas);

View file

@ -26,7 +26,13 @@ const runFile = (file, script) =>
vm.greenFlag();
const sendResults = [];
const idToTargetName = id => vm.runtime.targets.find(target => target.drawableID === id).sprite.name;
const idToTargetName = id => {
const target = vm.runtime.targets.find(tar => tar.drawableID === id);
if (!target) {
return `[Unknown drawableID: ${id}]`;
}
return target.sprite.name;
};
const sprite = vm.runtime.targets.find(target => target.sprite.name === 'Sprite1');
sendResults.push(['center', idToTargetName(render.pick(240, 180))]);