From 1926b49ebc20f4e97e447b3af5e63bbe47c4c41c Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 15 Nov 2017 17:12:10 -0500 Subject: [PATCH 1/5] Add Silhouette class Silhouette emulates silhouette rendering. Performing tests like RenderWebGL.pick can use Silhouette in place of reading a silhouette rendering from the GPU. Silhouette.isTouching(vec) takes a texture coordinate vector and checks if the corresponding location is opaque or not for the underlying information. --- src/Silhouette.js | 83 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Silhouette.js diff --git a/src/Silhouette.js b/src/Silhouette.js new file mode 100644 index 0000000..4bd10e9 --- /dev/null +++ b/src/Silhouette.js @@ -0,0 +1,83 @@ +/** + * @fileoverview + * A representation of a Skin's silhouette that can test if a point on the skin + * renders a pixel where it is drawn. + */ + +/** + * element used to update Silhouette data from skin bitmap data. + * @type {CanvasElement} + */ +let __SilhouetteUpdateCanvas; + +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._data = null; + } + + /** + * 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) { + 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'); + + ctx.clearRect(0, 0, width, height); + ctx.drawImage(bitmapData, 0, 0, width, height); + const imageData = ctx.getImageData(0, 0, width, height); + + this._data = new Uint8ClampedArray(imageData.data.length / 4); + + for (let i = 0; i < imageData.data.length; i += 4) { + this._data[i / 4] = imageData.data[i + 3]; + } + } + + /** + * Does this point touch the silhouette? + * @param {twgl.v3} vec A texture coordinate. + * @return {boolean} Did the point touch? + */ + isTouching (vec) { + const x = Math.floor(vec[0] * this._width); + const y = Math.floor(vec[1] * this._height); + return ( + x < this._width && x >= 0 && + y < this._height && y >= 0 && + this._data[(y * this._width) + x] !== 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; From 448c88e63751d2132630055f552fc47d81d005e3 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 15 Nov 2017 17:29:10 -0500 Subject: [PATCH 2/5] Add EffectTransform EffectTransform transforms a point based on a Drawable's effect uniform values mimicking the texcoord0 transformation in the shader. This way a point in the Drawable space can know if its touching its silhouette when the Drawable has effects active. --- src/EffectTransform.js | 102 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/EffectTransform.js diff --git a/src/EffectTransform.js b/src/EffectTransform.js new file mode 100644 index 0000000..13eaca2 --- /dev/null +++ b/src/EffectTransform.js @@ -0,0 +1,102 @@ +/** + * @fileoverview + * A utility to transform a texture coordinate to another texture coordinate + * representing how the shaders apply effects. + */ + +const twgl = require('twgl.js'); + +const ShaderManager = require('./ShaderManager'); + +/** + * A texture coordinate is between 0 and 1. 0.5 is the center position. + * @const {number} + */ +const CENTER_X = 0.5; + +/** + * A texture coordinate is between 0 and 1. 0.5 is the center position. + * @const {number} + */ +const CENTER_Y = 0.5; + +class EffectTransform { + /** + * Transform a texture coordinate to one that would be select after applying shader effects. + * @param {Drawable} drawable The drawable whose effects to emulate. + * @param {twgl.v3} vec The texture coordinate to transform. + * @param {?twgl.v3} dst A place to store the output coordinate. + * @return {twgl.v3} The coordinate after being transform by effects. + */ + static transformPoint (drawable, vec, dst) { + dst = dst || twgl.v3.create(); + twgl.v3.copy(vec, dst); + + const uniforms = drawable.getUniforms(); + const effects = drawable.getEnabledEffects(); + + if ((effects & ShaderManager.EFFECT_INFO.mosaic.mask) !== 0) { + // texcoord0 = fract(u_mosaic * texcoord0); + dst[0] = uniforms.u_mosaic * dst[0] % 1; + dst[1] = uniforms.u_mosaic * dst[1] % 1; + } + if ((effects & ShaderManager.EFFECT_INFO.pixelate.mask) !== 0) { + const skinUniforms = drawable.skin.getUniforms(); + // vec2 pixelTexelSize = u_skinSize / u_pixelate; + const texelX = skinUniforms.u_skinSize[0] * uniforms.u_pixelate; + const texelY = skinUniforms.u_skinSize[1] * uniforms.u_pixelate; + // texcoord0 = (floor(texcoord0 * pixelTexelSize) + kCenter) / + // pixelTexelSize; + dst[0] = (Math.floor(dst[0] * texelX) + CENTER_X) / texelX; + dst[1] = (Math.floor(dst[1] * texelY) + CENTER_Y) / texelY; + } + if ((effects & ShaderManager.EFFECT_INFO.whirl.mask) !== 0) { + // const float kRadius = 0.5; + const RADIUS = 0.5; + // vec2 offset = texcoord0 - kCenter; + const offsetX = dst[0] - CENTER_X; + const offsetY = dst[1] - CENTER_Y; + // float offsetMagnitude = length(offset); + const offsetMagnitude = twgl.v3.length(dst); + // 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; + const whirlActual = uniforms.u_whirl * whirlFactor * whirlFactor; + // float sinWhirl = sin(whirlActual); + const sinWhirl = Math.sin(whirlActual); + // float cosWhirl = cos(whirlActual); + const cosWhirl = Math.cos(whirlActual); + // mat2 rotationMatrix = mat2( + // cosWhirl, -sinWhirl, + // sinWhirl, cosWhirl + // ); + const rot00 = cosWhirl; + const rot10 = -sinWhirl; + const rot01 = sinWhirl; + const rot11 = cosWhirl; + + // texcoord0 = rotationMatrix * offset + kCenter; + dst[0] = (rot00 * offsetX) + (rot10 * offsetY) + CENTER_X; + dst[1] = (rot01 * offsetX) + (rot11 * offsetY) + CENTER_Y; + } + if ((effects & ShaderManager.EFFECT_INFO.fisheye.mask) !== 0) { + // vec2 vec = (texcoord0 - kCenter) / kCenter; + const vX = (dst[0] - CENTER_X) / CENTER_X; + const vY = (dst[1] - CENTER_Y) / CENTER_Y; + // float vecLength = length(vec); + const vLength = Math.sqrt((vX * vX) + (vY * vY)); + // float r = pow(min(vecLength, 1.0), u_fisheye) * max(1.0, vecLength); + const r = Math.pow(Math.min(vLength, 1), uniforms.u_fisheye) * Math.max(1, vLength); + // vec2 unit = vec / vecLength; + const unitX = vX / vLength; + const unitY = vY / vLength; + // texcoord0 = kCenter + r * unit * kCenter; + dst[0] = CENTER_X + (r * unitX * CENTER_X); + dst[1] = CENTER_Y + (r * unitY * CENTER_Y); + } + + return dst; + } +} + +module.exports = EffectTransform; From a0df7153bca4bb5dcdfc3a36c2d676e85845a810 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 15 Nov 2017 17:29:32 -0500 Subject: [PATCH 3/5] Add Skin.isTouching Use Silhouette to implement Skin.isTouching in subclasses. --- src/BitmapSkin.js | 14 ++++++++++++++ src/PenSkin.js | 27 ++++++++++++++++++++++++++- src/SVGSkin.js | 15 +++++++++++++++ src/Skin.js | 9 +++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/BitmapSkin.js b/src/BitmapSkin.js index df304dd..63cf8fc 100644 --- a/src/BitmapSkin.js +++ b/src/BitmapSkin.js @@ -1,6 +1,7 @@ const twgl = require('twgl.js'); const Skin = require('./Skin'); +const Silhouette = require('./Silhouette'); class BitmapSkin extends Skin { /** @@ -23,6 +24,8 @@ class BitmapSkin extends Skin { /** @type {Array} */ this._textureSize = [0, 0]; + + this._silhouette = new Silhouette(); } /** @@ -66,6 +69,7 @@ class BitmapSkin extends Skin { if (this._texture) { gl.bindTexture(gl.TEXTURE_2D, this._texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmapData); + this._silhouette.update(bitmapData); } else { const textureOptions = { auto: true, @@ -77,6 +81,7 @@ class BitmapSkin extends Skin { }; this._texture = twgl.createTexture(gl, textureOptions); + this._silhouette.update(bitmapData); } // Do these last in case any of the above throws an exception @@ -106,6 +111,15 @@ class BitmapSkin extends Skin { // ImageData or HTMLCanvasElement return [bitmapData.width, bitmapData.height]; } + + /** + * Does this point touch an opaque or translucent point on this skin? + * @param {twgl.v3} vec A texture coordinate. + * @return {boolean} Did it touch? + */ + isTouching (vec) { + return this._silhouette.isTouching(vec); + } } module.exports = BitmapSkin; diff --git a/src/PenSkin.js b/src/PenSkin.js index 9cd474d..2c2a5d8 100644 --- a/src/PenSkin.js +++ b/src/PenSkin.js @@ -2,7 +2,7 @@ const twgl = require('twgl.js'); const RenderConstants = require('./RenderConstants'); const Skin = require('./Skin'); - +const Silhouette = require('./Silhouette'); /** * Attributes to use when drawing with the pen @@ -50,6 +50,12 @@ class PenSkin extends Skin { /** @type {WebGLTexture} */ this._texture = null; + /** @type {Silhouette} */ + this._silhouette = new Silhouette(); + + /** @type {boolean} */ + this._silhouetteDirty = false; + this.onNativeSizeChanged = this.onNativeSizeChanged.bind(this); this._renderer.on(RenderConstants.Events.NativeSizeChanged, this.onNativeSizeChanged); @@ -98,6 +104,7 @@ class PenSkin extends Skin { const ctx = this._canvas.getContext('2d'); ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); this._canvasDirty = true; + this._silhouetteDirty = true; } /** @@ -127,6 +134,7 @@ class PenSkin extends Skin { ctx.lineTo(this._rotationCenter[0] + x1, this._rotationCenter[1] - y1); ctx.stroke(); this._canvasDirty = true; + this._silhouetteDirty = true; } /** @@ -139,6 +147,7 @@ class PenSkin extends Skin { const ctx = this._canvas.getContext('2d'); ctx.drawImage(stampElement, this._rotationCenter[0] + x, this._rotationCenter[1] - y); this._canvasDirty = true; + this._silhouetteDirty = true; } /** @@ -173,6 +182,7 @@ class PenSkin extends Skin { } ); this._canvasDirty = true; + this._silhouetteDirty = true; } /** @@ -195,6 +205,21 @@ class PenSkin extends Skin { context.lineCap = 'round'; context.lineWidth = diameter; } + + /** + * Does this point touch an opaque or translucent point on this skin? + * @param {twgl.v3} vec A texture coordinate. + * @return {boolean} Did it touch? + */ + isTouching (vec) { + if (this._silhouetteDirty) { + if (this._canvasDirty) { + this.getTexture(); + } + this._silhouette.update(this._canvas); + } + return this._silhouette.isTouching(vec); + } } module.exports = PenSkin; diff --git a/src/SVGSkin.js b/src/SVGSkin.js index e849bb1..235dde3 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -3,6 +3,8 @@ const twgl = require('twgl.js'); const Skin = require('./Skin'); const SvgRenderer = require('./svg-quirks-mode/svg-renderer'); +const Silhouette = require('./Silhouette'); + class SVGSkin extends Skin { /** * Create a new SVG skin. @@ -22,6 +24,8 @@ class SVGSkin extends Skin { /** @type {WebGLTexture} */ this._texture = null; + + this._silhouette = new Silhouette(); } /** @@ -75,6 +79,7 @@ class SVGSkin extends Skin { 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); + this._silhouette.update(this._svgRenderer.canvas); } else { const textureOptions = { auto: true, @@ -85,12 +90,22 @@ class SVGSkin extends Skin { }; this._texture = twgl.createTexture(gl, textureOptions); + this._silhouette.update(this._svgRenderer.canvas); } if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter(); this.setRotationCenter.apply(this, rotationCenter); this.emit(Skin.Events.WasAltered); }); } + + /** + * Does this point touch an opaque or translucent point on this skin? + * @param {twgl.v3} vec A texture coordinate. + * @return {boolean} Did it touch? + */ + isTouching (vec) { + return this._silhouette.isTouching(vec); + } } module.exports = SVGSkin; diff --git a/src/Skin.js b/src/Skin.js index f54eddb..2dfaa18 100644 --- a/src/Skin.js +++ b/src/Skin.js @@ -115,6 +115,15 @@ class Skin extends EventEmitter { this._uniforms.u_skinSize = this.size; return this._uniforms; } + + /** + * Does this point touch an opaque or translucent point on this skin? + * @param {twgl.v3} vec A texture coordinate. + * @return {boolean} Did it touch? + */ + isTouching () { + return false; + } } /** From 8bdf56705e0c165a0ceac56ada4a82c62826b550 Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 15 Nov 2017 17:30:14 -0500 Subject: [PATCH 4/5] Add Drawable.isTouching Use EffectTransform and Skin.isTouching to implement Drawable.isTouching. Drawable.isTouching takes a world coordinate. --- src/Drawable.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/Drawable.js b/src/Drawable.js index 544bb1f..830a171 100644 --- a/src/Drawable.js +++ b/src/Drawable.js @@ -4,7 +4,9 @@ const Rectangle = require('./Rectangle'); const RenderConstants = require('./RenderConstants'); const ShaderManager = require('./ShaderManager'); const Skin = require('./Skin'); +const EffectTransform = require('./EffectTransform'); +const __isTouchingPosition = twgl.v3.create(); class Drawable { /** @@ -49,6 +51,8 @@ class Drawable { this._scale = twgl.v3.create(100, 100); this._direction = 90; this._transformDirty = true; + this._inverseMatrix = twgl.m4.create(); + this._inverseTransformDirty = true; this._visible = true; this._effectBits = 0; @@ -73,6 +77,7 @@ class Drawable { */ setTransformDirty () { this._transformDirty = true; + this._inverseTransformDirty = true; } /** @@ -241,6 +246,50 @@ class Drawable { this._convexHullDirty = false; } + /** + * Check if the world position touches the skin. + * @param {twgl.v3} vec World coordinate vector. + * @return {boolean} True if the world position touches the skin. + */ + isTouching (vec) { + if (!this.skin) { + 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); + + return this.skin.isTouching(localPosition); + } + /** * Get the precise bounds for a Drawable. * This function applies the transform matrix to the known convex hull, From 59f649274238f3b72e5e705184363a9b85ac9fbc Mon Sep 17 00:00:00 2001 From: "Michael \"Z\" Goddard" Date: Wed, 15 Nov 2017 17:30:57 -0500 Subject: [PATCH 5/5] Use isTouching methods in RenderWebGL.pick and getConvexHull Use Drawable.isTouching and Skin.isTouching to implement pick and _getConvexHullPointsForDrawable to emulate rendering the drawable's silhouette and determining what is picked or the points for a convex hull. --- src/RenderWebGL.js | 206 ++++++++++++++++++++++++++------------------- 1 file changed, 120 insertions(+), 86 deletions(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 3b42f68..920a94e 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -11,6 +11,7 @@ const RenderConstants = require('./RenderConstants'); const ShaderManager = require('./ShaderManager'); const SVGSkin = require('./SVGSkin'); const SVGTextBubble = require('./util/svg-text-bubble'); +const EffectTransform = require('./EffectTransform'); /** * @callback RenderWebGL#idFilterFunc @@ -606,48 +607,36 @@ class RenderWebGL extends EventEmitter { touchHeight = Math.max(1, Math.min(touchHeight, MAX_TOUCH_SIZE[1])); const pixelLeft = Math.floor(centerX - Math.floor(touchWidth / 2) + 0.5); - const pixelRight = Math.floor(centerX + Math.ceil(touchWidth / 2) + 0.5); const pixelTop = Math.floor(centerY - Math.floor(touchHeight / 2) + 0.5); - const pixelBottom = Math.floor(centerY + Math.ceil(touchHeight / 2) + 0.5); - - twgl.bindFramebufferInfo(gl, this._pickBufferInfo); - gl.viewport(0, 0, touchWidth, touchHeight); - - const noneColor = Drawable.color4fFromID(RenderConstants.ID_NONE); - gl.clearColor.apply(gl, noneColor); - gl.clear(gl.COLOR_BUFFER_BIT); const widthPerPixel = (this._xRight - this._xLeft) / this._gl.canvas.width; const heightPerPixel = (this._yBottom - this._yTop) / this._gl.canvas.height; const pickLeft = this._xLeft + (pixelLeft * widthPerPixel); - const pickRight = this._xLeft + (pixelRight * widthPerPixel); const pickTop = this._yTop + (pixelTop * heightPerPixel); - const pickBottom = this._yTop + (pixelBottom * heightPerPixel); - const projection = twgl.m4.ortho(pickLeft, pickRight, pickTop, pickBottom, -1, 1); + const hits = []; + const worldPos = twgl.v3.create(0, 0, 0); + worldPos[2] = 0; - this._drawThese(candidateIDs, ShaderManager.DRAW_MODE.silhouette, projection); - - const pixels = new Uint8Array(Math.floor(touchWidth * touchHeight * 4)); - gl.readPixels(0, 0, touchWidth, touchHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - - if (this._debugCanvas) { - this._debugCanvas.width = touchWidth; - this._debugCanvas.height = touchHeight; - const context = this._debugCanvas.getContext('2d'); - const imageData = context.getImageData(0, 0, touchWidth, touchHeight); - imageData.data.set(pixels); - context.putImageData(imageData, 0, 0); - } - - const hits = {}; - for (let pixelBase = 0; pixelBase < pixels.length; pixelBase += 4) { - const pixelID = Drawable.color3bToID( - pixels[pixelBase], - pixels[pixelBase + 1], - pixels[pixelBase + 2]); - hits[pixelID] = (hits[pixelID] || 0) + 1; + // Iterate over the canvas pixels and check if any candidate can be + // touched at that point. + for (let x = 0; x < touchWidth; x++) { + worldPos[0] = x + pickLeft; + for (let y = 0; y < touchHeight; y++) { + worldPos[1] = y + pickTop; + // Check candidates in the reverse order they would have been + // drawn. This will determine what candiate's silhouette pixel + // would have been drawn at the point. + for (let d = candidateIDs.length - 1; d >= 0; d--) { + const id = candidateIDs[d]; + const drawable = this._allDrawables[id]; + if (drawable.isTouching(worldPos)) { + hits[id] = (hits[id] || 0) + 1; + break; + } + } + } } // Bias toward selecting anything over nothing @@ -1152,63 +1141,108 @@ class RenderWebGL extends EventEmitter { return []; } - // Only draw to the size of the untransformed drawable. - const gl = this._gl; - twgl.bindFramebufferInfo(gl, this._queryBufferInfo); - gl.viewport(0, 0, width, height); + /** + * Return the determinant of two vectors, the vector from A to B and + * the vector from A to C. + * + * The determinant is useful in this case to know if AC is counter + * clockwise from AB. A positive value means the AC is counter + * clockwise from AC. A negative value menas AC is clockwise from AB. + * + * @param {Float32Array} A A 2d vector in space. + * @param {Float32Array} B A 2d vector in space. + * @param {Float32Array} C A 2d vector in space. + * @return {number} Greater than 0 if counter clockwise, less than if + * clockwise, 0 if all points are on a line. + */ + const CCW = function (A, B, C) { + // AB = B - A + // AC = C - A + // det (AB BC) = AB0 * AC1 - AB1 * AC0 + return (((B[0] - A[0]) * (C[1] - A[1])) - ((B[1] - A[1]) * (C[0] - A[0]))); + }; - // Clear the canvas with RenderConstants.ID_NONE. - const noneColor = Drawable.color4fFromID(RenderConstants.ID_NONE); - gl.clearColor.apply(gl, noneColor); - gl.clear(gl.COLOR_BUFFER_BIT); + // https://github.com/LLK/scratch-flash/blob/dcbeeb59d44c3be911545dfe54d + // 46a32404f8e69/src/scratch/ScratchCostume.as#L369-L413 Following + // RasterHull creation, compare and store left and right values that + // maintain a convex shape until that data can be passed to `hull` for + // further work. + const L = []; + const R = []; + const _pixelPos = twgl.v3.create(); + const _effectPos = twgl.v3.create(); + let ll = -1; + let rr = -1; + let Q; + for (let y = 0; y < height; y++) { + _pixelPos[1] = y / height; + // Scan from left to right, looking for a touchable spot in the + // skin. + let x = 0; + for (; x < width; x++) { + _pixelPos[0] = x / width; + EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); + if (drawable.skin.isTouching(_effectPos)) { + Q = [x, y]; + break; + } + } + // If x is equal to the width there are no touchable points in the + // skin. Nothing we can add to L. And looping for R would find the + // same thing. + if (x === width) { + continue; + } + // Decrement ll until Q is clockwise (CCW returns negative) from the + // last two points in L. + while (ll > 0) { + if (CCW(L[ll - 1], L[ll], Q) < 0) { + break; + } else { + --ll; + } + } + // Increment ll and then set L[ll] to Q. If ll was -1 before this + // line, this will set L[0] to Q. If ll was 0 before this line, this + // will set L[1] to Q. + L[++ll] = Q; - // Overwrite the model matrix to be unrotated, unscaled, untranslated. - const modelMatrix = twgl.m4.identity(); - twgl.m4.rotateZ(modelMatrix, Math.PI, modelMatrix); - twgl.m4.scale(modelMatrix, [width, height], modelMatrix); - - const projection = twgl.m4.ortho(-0.5 * width, 0.5 * width, -0.5 * height, 0.5 * height, -1, 1); - - this._drawThese([drawableID], - ShaderManager.DRAW_MODE.silhouette, - projection, - {extraUniforms: {u_modelMatrix: modelMatrix}} - ); - - const pixels = new Uint8Array(Math.floor(width * height * 4)); - gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + // Scan from right to left, looking for a touchable spot in the + // skin. + for (x = width - 1; x >= 0; x--) { + _pixelPos[0] = x / width; + EffectTransform.transformPoint(drawable, _pixelPos, _effectPos); + if (drawable.skin.isTouching(_effectPos)) { + Q = [x, y]; + break; + } + } + // Decrement rr until Q is counter clockwise (CCW returns positive) + // from the last two points in L. L takes clockwise points and R + // takes counter clockwise points. if y was decremented instead of + // incremented R would take clockwise points. We are going in the + // right direction for L and the wrong direction for R, so we + // compare the opposite value for R from L. + while (rr > 0) { + if (CCW(R[rr - 1], R[rr], Q) > 0) { + break; + } else { + --rr; + } + } + // Increment rr and then set R[rr] to Q. + R[++rr] = Q; + } // Known boundary points on left/right edges of pixels. - const boundaryPoints = []; - - /** - * Helper method to look up a pixel. - * @param {int} x X coordinate of the pixel in `pixels`. - * @param {int} y Y coordinate of the pixel in `pixels`. - * @return {int} Known ID at that pixel, or RenderConstants.ID_NONE. - */ - const _getPixel = (x, y) => { - const pixelBase = Math.round(((width * y) + x) * 4); // Sometimes SVGs don't have int width and height - return Drawable.color3bToID( - pixels[pixelBase], - pixels[pixelBase + 1], - pixels[pixelBase + 2]); - }; - for (let y = 0; y <= height; y++) { - // Scan from left. - for (let x = 0; x < width; x++) { - if (_getPixel(x, y) > RenderConstants.ID_NONE) { - boundaryPoints.push([x, y]); - break; - } - } - // Scan from right. - for (let x = width - 1; x >= 0; x--) { - if (_getPixel(x, y) > RenderConstants.ID_NONE) { - boundaryPoints.push([x, y]); - break; - } - } + const boundaryPoints = L; + // Truncate boundaryPoints to the index of the last added Q to L. L may + // have more entries than the index for the last Q. + boundaryPoints.length = ll + 1; + // Add points in R to boundaryPoints in reverse so all points in + // boundaryPoints are clockwise from each other. + for (let j = rr; j >= 0; --j) { + boundaryPoints.push(R[j]); } // Simplify boundary points using convex hull. return hull(boundaryPoints, Infinity);