From c54a928f0a2fa0686c5a943671e4fcca09cfd4d9 Mon Sep 17 00:00:00 2001
From: Mx Corey Frang <gnarf37@gmail.com>
Date: Wed, 8 Aug 2018 14:20:35 -0400
Subject: [PATCH] Touching color implementation (#312)

* Touching color draft implementation

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

* Small fix for pick tests
---
 src/Drawable.js                  | 109 ++++++++++++-----
 src/EffectTransform.js           | 172 +++++++++++++++++++++++++--
 src/RenderWebGL.js               | 193 +++++++++++++++++++++++--------
 src/Silhouette.js                |  91 ++++++++++++++-
 src/Skin.js                      |   2 -
 test/integration/cpu-render.html |  86 ++++++++++++++
 test/integration/index.html      |   1 +
 test/integration/pick-tests.js   |   8 +-
 8 files changed, 574 insertions(+), 88 deletions(-)
 create mode 100644 test/integration/cpu-render.html

diff --git a/src/Drawable.js b/src/Drawable.js
index db6bdcc8..ba752bc6 100644
--- a/src/Drawable.js
+++ b/src/Drawable.js
@@ -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;
diff --git a/src/EffectTransform.js b/src/EffectTransform.js
index fad59e65..7b04a27d 100644
--- a/src/EffectTransform.js
+++ b/src/EffectTransform.js
@@ -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;
diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js
index 8b1133ba..dbd55985 100644
--- a/src/RenderWebGL.js
+++ b/src/RenderWebGL.js
@@ -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
diff --git a/src/Silhouette.js b/src/Silhouette.js
index 22642711..413d3b5b 100644
--- a/src/Silhouette.js
+++ b/src/Silhouette.js
@@ -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;
     }
 
diff --git a/src/Skin.js b/src/Skin.js
index 0e0b2ac8..1e180c03 100644
--- a/src/Skin.js
+++ b/src/Skin.js
@@ -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);
     }
 
diff --git a/test/integration/cpu-render.html b/test/integration/cpu-render.html
new file mode 100644
index 00000000..2fecd754
--- /dev/null
+++ b/test/integration/cpu-render.html
@@ -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>
diff --git a/test/integration/index.html b/test/integration/index.html
index 43c603c9..e3d8dd83 100644
--- a/test/integration/index.html
+++ b/test/integration/index.html
@@ -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);
diff --git a/test/integration/pick-tests.js b/test/integration/pick-tests.js
index a34c0b99..97890ac5 100644
--- a/test/integration/pick-tests.js
+++ b/test/integration/pick-tests.js
@@ -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))]);