From 31db3d85969a5e9e700da1a796e44a5f42a7ae6f Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 25 Jan 2019 15:59:17 -0800 Subject: [PATCH 1/9] Build playground using Webpack --- src/playground/.eslintrc.js | 3 + src/playground/getMousePosition.js | 37 ++++++++ src/playground/index.html | 147 +---------------------------- src/playground/playground.js | 141 +++++++++++++++++++++++++++ webpack.config.js | 6 +- 5 files changed, 189 insertions(+), 145 deletions(-) create mode 100644 src/playground/getMousePosition.js create mode 100644 src/playground/playground.js diff --git a/src/playground/.eslintrc.js b/src/playground/.eslintrc.js index 0711afb03..a944b0b16 100644 --- a/src/playground/.eslintrc.js +++ b/src/playground/.eslintrc.js @@ -3,5 +3,8 @@ module.exports = { extends: ['scratch'], env: { browser: true + }, + rules: { + 'no-console': 'off' } }; diff --git a/src/playground/getMousePosition.js b/src/playground/getMousePosition.js new file mode 100644 index 000000000..b8b654acc --- /dev/null +++ b/src/playground/getMousePosition.js @@ -0,0 +1,37 @@ +// Adapted from code by Simon Sarris: http://stackoverflow.com/a/10450761 +const getMousePos = function (event, element) { + const stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(element, null).paddingLeft, 10) || 0; + const stylePaddingTop = parseInt(document.defaultView.getComputedStyle(element, null).paddingTop, 10) || 0; + const styleBorderLeft = parseInt(document.defaultView.getComputedStyle(element, null).borderLeftWidth, 10) || 0; + const styleBorderTop = parseInt(document.defaultView.getComputedStyle(element, null).borderTopWidth, 10) || 0; + + // Some pages have fixed-position bars at the top or left of the page + // They will mess up mouse coordinates and this fixes that + const html = document.body.parentNode; + const htmlTop = html.offsetTop; + const htmlLeft = html.offsetLeft; + + // Compute the total offset. It's possible to cache this if you want + let offsetX = 0; + let offsetY = 0; + if (typeof element.offsetParent !== 'undefined') { + do { + offsetX += element.offsetLeft; + offsetY += element.offsetTop; + } while ((element = element.offsetParent)); + } + + // Add padding and border style widths to offset + // Also add the offsets in case there's a position:fixed bar + // This part is not strictly necessary, it depends on your styling + offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft; + offsetY += stylePaddingTop + styleBorderTop + htmlTop; + + // We return a simple javascript object with x and y defined + return { + x: event.pageX - offsetX, + y: event.pageY - offsetY + }; +}; + +module.exports = getMousePos; diff --git a/src/playground/index.html b/src/playground/index.html index c58ecc94d..14409e458 100644 --- a/src/playground/index.html +++ b/src/playground/index.html @@ -12,7 +12,7 @@

- @@ -27,149 +27,12 @@ - +

- - + +

- - + diff --git a/src/playground/playground.js b/src/playground/playground.js new file mode 100644 index 000000000..0829f4eb1 --- /dev/null +++ b/src/playground/playground.js @@ -0,0 +1,141 @@ +const ScratchRender = require('../RenderWebGL'); +const getMousePosition = require('./getMousePosition'); + +var canvas = document.getElementById('scratch-stage'); +var fudge = 90; +var renderer = new ScratchRender(canvas); +renderer.setLayerGroupOrdering(['group1']); + +var drawableID = renderer.createDrawable('group1'); +renderer.updateDrawableProperties(drawableID, { + position: [0, 0], + scale: [100, 100], + direction: 90 +}); + +var drawableID2 = renderer.createDrawable('group1'); +var wantBitmapSkin = false; + +// Bitmap (squirrel) +var image = new Image(); +image.addEventListener('load', () => { + var bitmapSkinId = renderer.createBitmapSkin(image); + if (wantBitmapSkin) { + renderer.updateDrawableProperties(drawableID2, { + skinId: bitmapSkinId + }); + } +}); +image.crossOrigin = 'anonymous'; +image.src = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/'; + +// SVG (cat 1-a) +var xhr = new XMLHttpRequest(); +xhr.addEventListener('load', function () { + var skinId = renderer.createSVGSkin(xhr.responseText); + if (!wantBitmapSkin) { + renderer.updateDrawableProperties(drawableID2, { + skinId: skinId + }); + } +}); +xhr.open('GET', 'https://cdn.assets.scratch.mit.edu/internalapi/asset/f88bf1935daea28f8ca098462a31dbb0.svg/get/'); +xhr.send(); + +var posX = 0; +var posY = 0; +var scaleX = 100; +var scaleY = 100; +var fudgeProperty = 'posx'; + +const fudgePropertyInput = document.getElementById('fudgeproperty'); +fudgePropertyInput.addEventListener('change', event => { + fudgeProperty = event.target.value; +}); + +const fudgeInput = document.getElementById('fudge'); + +const fudgeMinInput = document.getElementById('fudgeMin'); +fudgeMinInput.addEventListener('change', event => { + fudgeInput.min = event.target.valueAsNumber; +}); + +const fudgeMaxInput = document.getElementById('fudgeMax'); +fudgeMaxInput.addEventListener('change', event => { + fudgeInput.max = event.target.valueAsNumber; +}); + +const handleFudgeChanged = function (event) { + fudge = event.target.valueAsNumber; + var props = {}; + switch (fudgeProperty) { + case 'posx': + props.position = [fudge, posY]; + posX = fudge; + break; + case 'posy': + props.position = [posX, fudge]; + posY = fudge; + break; + case 'direction': + props.direction = fudge; + break; + case 'scalex': + props.scale = [fudge, scaleY]; + scaleX = fudge; + break; + case 'scaley': + props.scale = [scaleX, fudge]; + scaleY = fudge; + break; + case 'color': + props.color = fudge; + break; + case 'whirl': + props.whirl = fudge; + break; + case 'fisheye': + props.fisheye = fudge; + break; + case 'pixelate': + props.pixelate = fudge; + break; + case 'mosaic': + props.mosaic = fudge; + break; + case 'brightness': + props.brightness = fudge; + break; + case 'ghost': + props.ghost = fudge; + break; + } + renderer.updateDrawableProperties(drawableID2, props); +}; +fudgeInput.addEventListener('input', handleFudgeChanged); +fudgeInput.addEventListener('change', handleFudgeChanged); + +canvas.addEventListener('mousemove', event => { + var mousePos = getMousePosition(event, canvas); + renderer.extractColor(mousePos.x, mousePos.y, 30); +}); + +canvas.addEventListener('click', event => { + var mousePos = getMousePosition(event, canvas); + var pickID = renderer.pick(mousePos.x, mousePos.y); + console.log('You clicked on ' + (pickID < 0 ? 'nothing' : 'ID# ' + pickID)); + if (pickID >= 0) { + console.dir(renderer.extractDrawable(pickID, mousePos.x, mousePos.y)); + } +}); + +const drawStep = function () { + renderer.draw(); + // renderer.getBounds(drawableID2); + // renderer.isTouchingColor(drawableID2, [255,255,255]); + requestAnimationFrame(drawStep); +}; +drawStep(); + +var debugCanvas = /** @type {canvas} */ document.getElementById('debug-canvas'); +renderer.setDebugCanvas(debugCanvas); diff --git a/webpack.config.js b/webpack.config.js index f26f63699..f44a6574e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -39,10 +39,9 @@ module.exports = [ Object.assign({}, base, { target: 'web', entry: { - 'scratch-render': './src/index.js' + playground: './src/playground/playground.js' }, output: { - library: 'ScratchRender', libraryTarget: 'umd', path: path.resolve('playground'), filename: '[name].js' @@ -50,7 +49,8 @@ module.exports = [ plugins: base.plugins.concat([ new CopyWebpackPlugin([ { - from: 'src/playground' + context: 'src/playground', + from: '*.html' } ]) ]) From fba2d90fdaedfd9491e1d1142ad3bc2a24dac817 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 25 Jan 2019 17:28:51 -0800 Subject: [PATCH 2/9] Stub queryPlayground.html --- src/playground/queryPlayground.html | 46 ++++++++++++++++++++++++++ src/playground/queryPlayground.js | 50 +++++++++++++++++++++++++++++ webpack.config.js | 3 +- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/playground/queryPlayground.html create mode 100644 src/playground/queryPlayground.js diff --git a/src/playground/queryPlayground.html b/src/playground/queryPlayground.html new file mode 100644 index 000000000..ff953117c --- /dev/null +++ b/src/playground/queryPlayground.html @@ -0,0 +1,46 @@ + + + + + Scratch WebGL Query Playground + + + +
+
+ Query Canvas +
Touching white? maybe
+
Touching black? maybe
+ +
+
+ Render Canvas +
Cursor Position: somewhere
+ + + + + + + + + +
+ +
+ + + +
+
+
+ + + diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js new file mode 100644 index 000000000..91eaba6ad --- /dev/null +++ b/src/playground/queryPlayground.js @@ -0,0 +1,50 @@ +const getMousePosition = require('./getMousePosition'); + +const renderCanvas = document.getElementById('renderCanvas'); +const inputCursorX = document.getElementById('cursorX'); +const inputCursorY = document.getElementById('cursorY'); +const labelCursorPosition = document.getElementById('cursorPosition'); + +const handleResizeRenderCanvas = () => { + const halfWidth = renderCanvas.clientWidth / 2; + const halfHeight = renderCanvas.clientHeight / 2; + + inputCursorX.style.width = `${renderCanvas.clientWidth}px`; + inputCursorY.style.height = `${renderCanvas.clientHeight}px`; + inputCursorX.min = -halfWidth; + inputCursorX.max = halfWidth; + inputCursorY.min = -halfHeight; + inputCursorY.max = halfHeight; +}; +renderCanvas.addEventListener('resize', handleResizeRenderCanvas); +handleResizeRenderCanvas(); + +const handleCursorPositionChanged = () => { + const cursorX = inputCursorX.valueAsNumber; + const cursorY = inputCursorY.valueAsNumber; + const positionHTML = `${cursorX}, ${cursorY}`; + labelCursorPosition.innerHTML = positionHTML; +}; +inputCursorX.addEventListener('change', handleCursorPositionChanged); +inputCursorY.addEventListener('change', handleCursorPositionChanged); +inputCursorX.addEventListener('input', handleCursorPositionChanged); +inputCursorY.addEventListener('input', handleCursorPositionChanged); +handleCursorPositionChanged(); + +let trackingMouse = true; +renderCanvas.addEventListener('click', event => { + trackingMouse = !trackingMouse; + if (trackingMouse) { + handleMouseMove(event); + } +}); + +const handleMouseMove = event => { + if (trackingMouse) { + const mousePosition = getMousePosition(event, renderCanvas); + inputCursorX.value = mousePosition.x - (renderCanvas.clientWidth / 2); + inputCursorY.value = (renderCanvas.clientHeight / 2) - mousePosition.y; + handleCursorPositionChanged(); + } +}; +renderCanvas.addEventListener('mousemove', handleMouseMove); diff --git a/webpack.config.js b/webpack.config.js index f44a6574e..810874b8a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -39,7 +39,8 @@ module.exports = [ Object.assign({}, base, { target: 'web', entry: { - playground: './src/playground/playground.js' + playground: './src/playground/playground.js', + queryPlayground: './src/playground/queryPlayground.js' }, output: { libraryTarget: 'umd', From e8d71277e28ba1dff8799998415bec993a196adb Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 28 Jan 2019 11:43:38 -0800 Subject: [PATCH 3/9] Use query playground to compare GPU vs. CPU implementations --- src/RenderWebGL.js | 26 +++++- src/playground/queryPlayground.html | 28 +++++- src/playground/queryPlayground.js | 134 ++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 8 deletions(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index 24b4696db..f9de5bb24 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -243,6 +243,21 @@ class RenderWebGL extends EventEmitter { this._debugCanvas = canvas; } + /** + * Force "touching color" operations to use (or not use) the GPU. By default the renderer will decide. + * @param {boolean} forceGPU - true to force the renderer to use the GPU, false to force CPU. + */ + setForceGPU (forceGPU) { + this._forceGPU = !!forceGPU; + } + + /** + * Clear any value set by `setForceGPU`, allowing the renderer to decide. + */ + clearForceGPU () { + this._forceGPU = null; + } + /** * Set logical size of the stage in Scratch units. * @param {int} xLeft The left edge's x-coordinate. Scratch 2 uses -240. @@ -717,9 +732,12 @@ class RenderWebGL extends EventEmitter { const bounds = this._candidatesBounds(candidates); - // 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) { + const maxPixelsForCPU = (typeof this._forceGPU === 'boolean') ? + (this._forceGPU ? 0 : Infinity) : + __cpuTouchingColorPixelCount; + + // if there are just too many pixels to CPU render efficiently, we need to let readPixels happen + if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) { this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b); } @@ -729,7 +747,7 @@ class RenderWebGL extends EventEmitter { const hasMask = Boolean(mask3b); for (let y = bounds.bottom; y <= bounds.top; y++) { - if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= __cpuTouchingColorPixelCount) { + if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) { return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom); } // Scratch Space - +y is top diff --git a/src/playground/queryPlayground.html b/src/playground/queryPlayground.html index ff953117c..6d8711786 100644 --- a/src/playground/queryPlayground.html +++ b/src/playground/queryPlayground.html @@ -10,15 +10,35 @@ width: 1rem; padding: 0 0.5rem; } + canvas { + border: 3px dashed black; + }
- Query Canvas -
Touching white? maybe
-
Touching black? maybe
- + Query Canvases + + + + + +
+
+ GPU +
Touching color A? maybe
+
Touching color B? maybe
+ +
+
+
+ CPU +
Touching color A? maybe
+
Touching color B? maybe
+ +
+
Render Canvas diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js index 91eaba6ad..3c6e3aa34 100644 --- a/src/playground/queryPlayground.js +++ b/src/playground/queryPlayground.js @@ -1,9 +1,27 @@ +const ScratchRender = require('../RenderWebGL'); const getMousePosition = require('./getMousePosition'); const renderCanvas = document.getElementById('renderCanvas'); +const gpuQueryCanvas = document.getElementById('gpuQueryCanvas'); +const cpuQueryCanvas = document.getElementById('cpuQueryCanvas'); const inputCursorX = document.getElementById('cursorX'); const inputCursorY = document.getElementById('cursorY'); const labelCursorPosition = document.getElementById('cursorPosition'); +const labelGpuTouchingA = document.getElementById('gpuTouchingA'); +const labelGpuTouchingB = document.getElementById('gpuTouchingB'); +const labelCpuTouchingA = document.getElementById('cpuTouchingA'); +const labelCpuTouchingB = document.getElementById('cpuTouchingB'); + +const drawables = { + testPattern: -1, + cursor: -1 +}; + +const colors = { + cursor: [255, 0, 0], + patternA: [0, 255, 0], + patternB: [0, 0, 255] +}; const handleResizeRenderCanvas = () => { const halfWidth = renderCanvas.clientWidth / 2; @@ -24,6 +42,28 @@ const handleCursorPositionChanged = () => { const cursorY = inputCursorY.valueAsNumber; const positionHTML = `${cursorX}, ${cursorY}`; labelCursorPosition.innerHTML = positionHTML; + if (drawables.cursor >= 0) { + renderer.draw(); + renderer.updateDrawableProperties(drawables.cursor, { + position: [cursorX, cursorY] + }); + + renderer.setForceGPU(true); + renderer.setDebugCanvas(gpuQueryCanvas); + const isGpuTouchingA = renderer.isTouchingColor(drawables.cursor, colors.patternA); + const isGpuTouchingB = renderer.isTouchingColor(drawables.cursor, colors.patternB); + labelGpuTouchingA.innerHTML = isGpuTouchingA ? 'yes' : 'no'; + labelGpuTouchingB.innerHTML = isGpuTouchingB ? 'yes' : 'no'; + + renderer.setForceGPU(false); + renderer.setDebugCanvas(cpuQueryCanvas); + const isCpuTouchingA = renderer.isTouchingColor(drawables.cursor, colors.patternA); + const isCpuTouchingB = renderer.isTouchingColor(drawables.cursor, colors.patternB); + labelCpuTouchingA.innerHTML = isCpuTouchingA ? 'yes' : 'no'; + labelCpuTouchingB.innerHTML = isCpuTouchingB ? 'yes' : 'no'; + + renderer.clearForceGPU(); + } }; inputCursorX.addEventListener('change', handleCursorPositionChanged); inputCursorY.addEventListener('change', handleCursorPositionChanged); @@ -48,3 +88,97 @@ const handleMouseMove = event => { } }; renderCanvas.addEventListener('mousemove', handleMouseMove); + +const rgb2fillStyle = (rgb) => { + return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`; +}; + +const makeCursorImage = () => { + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 1; + + const context = canvas.getContext('2d'); + context.fillStyle = rgb2fillStyle(colors.cursor); + context.fillRect(0, 0, 1, 1); + + return canvas; +}; + +const makeTestPatternImage = () => { + const canvas = document.createElement('canvas'); + canvas.width = 480; + canvas.height = 360; + + const patternA = rgb2fillStyle(colors.patternA); + const patternB = rgb2fillStyle(colors.patternB); + + const context = canvas.getContext('2d'); + context.fillStyle = patternA; + context.fillRect(0, 0, canvas.width, canvas.height); + + context.fillStyle = patternB; + const xSplit1 = Math.floor(canvas.width * 0.25); + const xSplit2 = Math.floor(canvas.width * 0.5); + const xSplit3 = Math.floor(canvas.width * 0.75); + const ySplit = Math.floor(canvas.height * 0.5); + for (let y = 0; y < ySplit; y += 2) { + context.fillRect(0, y, xSplit2, 1); + } + for (let x = xSplit2; x < canvas.width; x += 2) { + context.fillRect(x, 0, 1, ySplit); + } + for (let x = 0; x < xSplit1; x += 2) { + for (let y = ySplit; y < canvas.height; y += 2) { + context.fillRect(x, y, 1, 1); + } + } + for (let x = xSplit1; x < xSplit2; x += 3) { + for (let y = ySplit; y < canvas.height; y += 3) { + context.fillRect(x, y, 2, 2); + } + } + for (let x = xSplit2; x < xSplit3; ++x) { + for (let y = ySplit; y < canvas.height; ++y) { + context.fillStyle = (x + y) % 2 ? patternA : patternB; + context.fillRect(x, y, 1, 1); + } + } + for (let x = xSplit3; x < canvas.width; x += 2) { + for (let y = ySplit; y < canvas.height; y += 2) { + context.fillStyle = (x + y) % 4 ? patternA : patternB; + context.fillRect(x, y, 2, 2); + } + } + + return canvas; +}; + +const makeBitmapDrawable = function (renderer, group, image) { + const skinId = renderer.createBitmapSkin(image, 1); + const drawableId = renderer.createDrawable(group); + renderer.updateDrawableProperties(drawableId, {skinId}); + return drawableId; +}; + +const initRendering = () => { + const renderer = new ScratchRender(renderCanvas); + + const layerGroup = { + testPattern: 'testPattern', + cursor: 'cursor' + }; + renderer.setLayerGroupOrdering([layerGroup.testPattern, layerGroup.cursor]); + + const images = { + testPattern: makeTestPatternImage(), + cursor: makeCursorImage() + }; + + drawables.testPattern = makeBitmapDrawable(renderer, layerGroup.testPattern, images.testPattern); + drawables.cursor = makeBitmapDrawable(renderer, layerGroup.cursor, images.cursor); + + return renderer; +}; + +const renderer = initRendering(); +renderer.draw(); From a358c8f91609c0d2070f597b15e476a10cccd901 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 28 Jan 2019 12:25:58 -0800 Subject: [PATCH 4/9] Lint cleanup --- src/playground/.eslintrc.js | 1 - src/playground/queryPlayground.js | 28 +++++++++++++--------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/playground/.eslintrc.js b/src/playground/.eslintrc.js index a944b0b16..0c483bea7 100644 --- a/src/playground/.eslintrc.js +++ b/src/playground/.eslintrc.js @@ -1,5 +1,4 @@ module.exports = { - root: true, extends: ['scratch'], env: { browser: true diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js index 3c6e3aa34..d96f0ce6c 100644 --- a/src/playground/queryPlayground.js +++ b/src/playground/queryPlayground.js @@ -23,6 +23,8 @@ const colors = { patternB: [0, 0, 255] }; +const renderer = new ScratchRender(renderCanvas); + const handleResizeRenderCanvas = () => { const halfWidth = renderCanvas.clientWidth / 2; const halfHeight = renderCanvas.clientHeight / 2; @@ -72,13 +74,6 @@ inputCursorY.addEventListener('input', handleCursorPositionChanged); handleCursorPositionChanged(); let trackingMouse = true; -renderCanvas.addEventListener('click', event => { - trackingMouse = !trackingMouse; - if (trackingMouse) { - handleMouseMove(event); - } -}); - const handleMouseMove = event => { if (trackingMouse) { const mousePosition = getMousePosition(event, renderCanvas); @@ -89,6 +84,13 @@ const handleMouseMove = event => { }; renderCanvas.addEventListener('mousemove', handleMouseMove); +renderCanvas.addEventListener('click', event => { + trackingMouse = !trackingMouse; + if (trackingMouse) { + handleMouseMove(event); + } +}); + const rgb2fillStyle = (rgb) => { return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`; }; @@ -153,7 +155,7 @@ const makeTestPatternImage = () => { return canvas; }; -const makeBitmapDrawable = function (renderer, group, image) { +const makeBitmapDrawable = function (group, image) { const skinId = renderer.createBitmapSkin(image, 1); const drawableId = renderer.createDrawable(group); renderer.updateDrawableProperties(drawableId, {skinId}); @@ -161,8 +163,6 @@ const makeBitmapDrawable = function (renderer, group, image) { }; const initRendering = () => { - const renderer = new ScratchRender(renderCanvas); - const layerGroup = { testPattern: 'testPattern', cursor: 'cursor' @@ -174,11 +174,9 @@ const initRendering = () => { cursor: makeCursorImage() }; - drawables.testPattern = makeBitmapDrawable(renderer, layerGroup.testPattern, images.testPattern); - drawables.cursor = makeBitmapDrawable(renderer, layerGroup.cursor, images.cursor); - - return renderer; + drawables.testPattern = makeBitmapDrawable(layerGroup.testPattern, images.testPattern); + drawables.cursor = makeBitmapDrawable(layerGroup.cursor, images.cursor); }; -const renderer = initRendering(); +initRendering(); renderer.draw(); From 992977d6c6273eaa646279db63ac5460abcc366b Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 30 Jan 2019 10:58:10 -0800 Subject: [PATCH 5/9] Add debug canvas support to isTouching CPU path --- src/RenderWebGL.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index f9de5bb24..a661b155e 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -736,6 +736,12 @@ class RenderWebGL extends EventEmitter { (this._forceGPU ? 0 : Infinity) : __cpuTouchingColorPixelCount; + const debugCanvasContext = this._debugCanvas && this._debugCanvas.getContext('2d'); + if (debugCanvasContext) { + this._debugCanvas.width = bounds.width; + this._debugCanvas.height = bounds.height; + } + // if there are just too many pixels to CPU render efficiently, we need to let readPixels happen if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) { this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b); @@ -746,23 +752,27 @@ class RenderWebGL extends EventEmitter { const color = __touchingColor; const hasMask = Boolean(mask3b); + // Scratch Space - +y is top for (let y = bounds.bottom; y <= bounds.top; y++) { if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) { 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; + // if we use a mask, check our sample color... + if (hasMask ? + maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) : + drawable.isTouching(point)) { + RenderWebGL.sampleColor3b(point, candidates, color); + if (debugCanvasContext) { + debugCanvasContext.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`; + debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1); + } + // ...and the target color is drawn at this pixel + if (colorMatches(color, color3b, 0)) { + return true; + } } } } From 99d6e46f7eb36e3e31ccceab5ad6b4a8b1518e4a Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 30 Jan 2019 10:58:10 -0800 Subject: [PATCH 6/9] Adjust rendering for crisp pixels - Adjust the rotation center of the cursor so that its single pixel is gets rendered onto a single stage pixel instead of being split across 2-4 stage pixels. - Add canvas CSS to make most browsers scale the canvases without interpolation. --- src/playground/queryPlayground.html | 8 ++++++++ src/playground/queryPlayground.js | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/playground/queryPlayground.html b/src/playground/queryPlayground.html index 6d8711786..56a569d02 100644 --- a/src/playground/queryPlayground.html +++ b/src/playground/queryPlayground.html @@ -12,6 +12,14 @@ } canvas { border: 3px dashed black; + + /* https://stackoverflow.com/a/7665647 */ + image-rendering: optimizeSpeed; /* Older versions of FF */ + image-rendering: -moz-crisp-edges; /* FF 6.0+ */ + image-rendering: -webkit-optimize-contrast; /* Safari */ + image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */ + image-rendering: pixelated; /* Awesome future-browsers */ + -ms-interpolation-mode: nearest-neighbor; /* IE */ } diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js index d96f0ce6c..1db1c369f 100644 --- a/src/playground/queryPlayground.js +++ b/src/playground/queryPlayground.js @@ -155,27 +155,30 @@ const makeTestPatternImage = () => { return canvas; }; -const makeBitmapDrawable = function (group, image) { +const makeTestPatternDrawable = function (group) { + const image = makeTestPatternImage(); const skinId = renderer.createBitmapSkin(image, 1); const drawableId = renderer.createDrawable(group); renderer.updateDrawableProperties(drawableId, {skinId}); return drawableId; }; +const makeCursorDrawable = function (group) { + const image = makeCursorImage(); + const skinId = renderer.createBitmapSkin(image, 1, [0, 0]); + const drawableId = renderer.createDrawable(group); + renderer.updateDrawableProperties(drawableId, {skinId}); + return drawableId; +}; + const initRendering = () => { const layerGroup = { testPattern: 'testPattern', cursor: 'cursor' }; renderer.setLayerGroupOrdering([layerGroup.testPattern, layerGroup.cursor]); - - const images = { - testPattern: makeTestPatternImage(), - cursor: makeCursorImage() - }; - - drawables.testPattern = makeBitmapDrawable(layerGroup.testPattern, images.testPattern); - drawables.cursor = makeBitmapDrawable(layerGroup.cursor, images.cursor); + drawables.testPattern = makeTestPatternDrawable(layerGroup.testPattern); + drawables.cursor = makeCursorDrawable(layerGroup.cursor); }; initRendering(); From 59cef02fdb00bc49b40ab3d2e474b0bd853c08ac Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 30 Jan 2019 15:58:51 -0800 Subject: [PATCH 7/9] Mark correct viewport corners with red dots --- src/playground/queryPlayground.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js index 1db1c369f..eaa947b24 100644 --- a/src/playground/queryPlayground.js +++ b/src/playground/queryPlayground.js @@ -179,6 +179,16 @@ const initRendering = () => { renderer.setLayerGroupOrdering([layerGroup.testPattern, layerGroup.cursor]); drawables.testPattern = makeTestPatternDrawable(layerGroup.testPattern); drawables.cursor = makeCursorDrawable(layerGroup.cursor); + + const corner00 = makeCursorDrawable(layerGroup.cursor); + const corner01 = makeCursorDrawable(layerGroup.cursor); + const corner10 = makeCursorDrawable(layerGroup.cursor); + const corner11 = makeCursorDrawable(layerGroup.cursor); + + renderer.updateDrawableProperties(corner00, {position: [-240, -179]}); + renderer.updateDrawableProperties(corner01, {position: [-240, 180]}); + renderer.updateDrawableProperties(corner10, {position: [239, -179]}); + renderer.updateDrawableProperties(corner11, {position: [239, 180]}); }; initRendering(); From 028b4eba3f024d46a10ea69cdb3a2f047c6ae859 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 30 Jan 2019 16:37:59 -0800 Subject: [PATCH 8/9] Adjust cursor coordinates for devicePixelRatio --- src/playground/queryPlayground.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js index eaa947b24..947bdc924 100644 --- a/src/playground/queryPlayground.js +++ b/src/playground/queryPlayground.js @@ -40,8 +40,9 @@ renderCanvas.addEventListener('resize', handleResizeRenderCanvas); handleResizeRenderCanvas(); const handleCursorPositionChanged = () => { - const cursorX = inputCursorX.valueAsNumber; - const cursorY = inputCursorY.valueAsNumber; + const devicePixelRatio = window.devicePixelRatio || 1; + const cursorX = inputCursorX.valueAsNumber / devicePixelRatio; + const cursorY = inputCursorY.valueAsNumber / devicePixelRatio; const positionHTML = `${cursorX}, ${cursorY}`; labelCursorPosition.innerHTML = positionHTML; if (drawables.cursor >= 0) { @@ -141,13 +142,13 @@ const makeTestPatternImage = () => { } for (let x = xSplit2; x < xSplit3; ++x) { for (let y = ySplit; y < canvas.height; ++y) { - context.fillStyle = (x + y) % 2 ? patternA : patternB; + context.fillStyle = (x + y) % 2 ? patternB : patternA; context.fillRect(x, y, 1, 1); } } for (let x = xSplit3; x < canvas.width; x += 2) { for (let y = ySplit; y < canvas.height; y += 2) { - context.fillStyle = (x + y) % 4 ? patternA : patternB; + context.fillStyle = (x + y) % 4 ? patternB : patternA; context.fillRect(x, y, 2, 2); } } From c390124df4c6842cb4a8f4be81ce906250d4e9f1 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 6 Feb 2019 10:47:49 -0800 Subject: [PATCH 9/9] Convert 'force GPU' flag into 'useGpuMode' enum --- src/RenderWebGL.js | 55 +++++++++++++++++++++++-------- src/playground/queryPlayground.js | 6 ++-- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/RenderWebGL.js b/src/RenderWebGL.js index a661b155e..e4d2c1f26 100644 --- a/src/RenderWebGL.js +++ b/src/RenderWebGL.js @@ -132,6 +132,9 @@ class RenderWebGL extends EventEmitter { throw new Error('Could not get WebGL context: this browser or environment may not support WebGL.'); } + /** @type {RenderWebGL.UseGpuModes} */ + this._useGpuMode = RenderWebGL.UseGpuModes.Automatic; + /** @type {Drawable[]} */ this._allDrawables = []; @@ -244,18 +247,11 @@ class RenderWebGL extends EventEmitter { } /** - * Force "touching color" operations to use (or not use) the GPU. By default the renderer will decide. - * @param {boolean} forceGPU - true to force the renderer to use the GPU, false to force CPU. - */ - setForceGPU (forceGPU) { - this._forceGPU = !!forceGPU; - } - - /** - * Clear any value set by `setForceGPU`, allowing the renderer to decide. + * Control the use of the GPU or CPU paths in `isTouchingColor`. + * @param {RenderWebGL.UseGpuModes} useGpuMode - automatically decide, force CPU, or force GPU. */ - clearForceGPU () { - this._forceGPU = null; + setUseGpuMode (useGpuMode) { + this._useGpuMode = useGpuMode; } /** @@ -732,9 +728,7 @@ class RenderWebGL extends EventEmitter { const bounds = this._candidatesBounds(candidates); - const maxPixelsForCPU = (typeof this._forceGPU === 'boolean') ? - (this._forceGPU ? 0 : Infinity) : - __cpuTouchingColorPixelCount; + const maxPixelsForCPU = this._getMaxPixelsForCPU(); const debugCanvasContext = this._debugCanvas && this._debugCanvas.getContext('2d'); if (debugCanvasContext) { @@ -779,6 +773,18 @@ class RenderWebGL extends EventEmitter { return false; } + _getMaxPixelsForCPU () { + switch (this._useGpuMode) { + case RenderWebGL.UseGpuModes.ForceCPU: + return Infinity; + case RenderWebGL.UseGpuModes.ForceGPU: + return 0; + case RenderWebGL.UseGpuModes.Automatic: + default: + return __cpuTouchingColorPixelCount; + } + } + _isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) { this._doExitDrawRegion(); @@ -1798,4 +1804,25 @@ class RenderWebGL extends EventEmitter { // :3 RenderWebGL.prototype.canHazPixels = RenderWebGL.prototype.extractDrawable; +/** + * Values for setUseGPU() + * @enum {string} + */ +RenderWebGL.UseGpuModes = { + /** + * Heuristically decide whether to use the GPU path, the CPU path, or a dynamic mixture of the two. + */ + Automatic: 'Automatic', + + /** + * Always use the GPU path. + */ + ForceGPU: 'ForceGPU', + + /** + * Always use the CPU path. + */ + ForceCPU: 'ForceCPU' +}; + module.exports = RenderWebGL; diff --git a/src/playground/queryPlayground.js b/src/playground/queryPlayground.js index 947bdc924..7befdf7a1 100644 --- a/src/playground/queryPlayground.js +++ b/src/playground/queryPlayground.js @@ -51,21 +51,21 @@ const handleCursorPositionChanged = () => { position: [cursorX, cursorY] }); - renderer.setForceGPU(true); + renderer.setUseGpuMode(ScratchRender.UseGpuModes.ForceGPU); renderer.setDebugCanvas(gpuQueryCanvas); const isGpuTouchingA = renderer.isTouchingColor(drawables.cursor, colors.patternA); const isGpuTouchingB = renderer.isTouchingColor(drawables.cursor, colors.patternB); labelGpuTouchingA.innerHTML = isGpuTouchingA ? 'yes' : 'no'; labelGpuTouchingB.innerHTML = isGpuTouchingB ? 'yes' : 'no'; - renderer.setForceGPU(false); + renderer.setUseGpuMode(ScratchRender.UseGpuModes.ForceCPU); renderer.setDebugCanvas(cpuQueryCanvas); const isCpuTouchingA = renderer.isTouchingColor(drawables.cursor, colors.patternA); const isCpuTouchingB = renderer.isTouchingColor(drawables.cursor, colors.patternB); labelCpuTouchingA.innerHTML = isCpuTouchingA ? 'yes' : 'no'; labelCpuTouchingB.innerHTML = isCpuTouchingB ? 'yes' : 'no'; - renderer.clearForceGPU(); + renderer.setUseGpuMode(ScratchRender.UseGpuModes.Automatic); } }; inputCursorX.addEventListener('change', handleCursorPositionChanged);