Skip to content

Add "query playground" #406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 69 additions & 14 deletions src/RenderWebGL.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down Expand Up @@ -243,6 +246,14 @@ class RenderWebGL extends EventEmitter {
this._debugCanvas = canvas;
}

/**
* Control the use of the GPU or CPU paths in `isTouchingColor`.
* @param {RenderWebGL.UseGpuModes} useGpuMode - automatically decide, force CPU, or force GPU.
*/
setUseGpuMode (useGpuMode) {
this._useGpuMode = useGpuMode;
}

/**
* Set logical size of the stage in Scratch units.
* @param {int} xLeft The left edge's x-coordinate. Scratch 2 uses -240.
Expand Down Expand Up @@ -717,9 +728,16 @@ 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 = this._getMaxPixelsForCPU();

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);
}

Expand All @@ -728,29 +746,45 @@ 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) >= __cpuTouchingColorPixelCount) {
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is a weird idea. What if this wasn't drawing but was calling delegate methods. Methods on debugCanvasContext. Then the different playground pages could change what they use about this data if that would be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of delegates. That could also solve another problem I've been having, where using "the" debug canvas is basically impossible in some projects since you only get a brief glimpse of the thing you're interested in before the canvas gets overwritten by the next thing. This could be solved by including some basic context info when calling the delegate method so the delegate could decide which data to handle, and how.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explored this idea a bit and it started getting super complicated, mainly due to the differences between the CPU and GPU paths. It might be better (or at least easier) to have canvas-fetching delegates or something like that... I dunno. Anyway, I still like the idea but I think I'm filing it under "future work"...

}
// ...and the target color is drawn at this pixel
if (colorMatches(color, color3b, 0)) {
return true;
}
}
}
}
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();

Expand Down Expand Up @@ -1770,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;
4 changes: 3 additions & 1 deletion src/playground/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
module.exports = {
root: true,
extends: ['scratch'],
env: {
browser: true
},
rules: {
'no-console': 'off'
}
};
37 changes: 37 additions & 0 deletions src/playground/getMousePosition.js
Original file line number Diff line number Diff line change
@@ -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 <html> 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;
147 changes: 5 additions & 142 deletions src/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<canvas id="debug-canvas" width="10" height="10" style="border:3px dashed red"></canvas>
<p>
<label for="fudgeproperty">Property to tweak:</label>
<select id="fudgeproperty" onchange="onFudgePropertyChanged(this.value)">
<select id="fudgeproperty">
<option value="posx">Position X</option>
<option value="posy">Position Y</option>
<option value="direction">Direction</option>
Expand All @@ -27,149 +27,12 @@
<option value="ghost">Ghost</option>
</select>
<label for="fudge">Property Value:</label>
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any" oninput="onFudgeChanged(this.value)" onchange="onFudgeChanged(this.value)">
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any">
</p>
<p>
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number" onchange="onFudgeMinChanged(this.value)">
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number" onchange="onFudgeMaxChanged(this.value)">
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number">
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number">
</p>
<script src="scratch-render.js"></script>
<script>
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.onload = function () {
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';
function onFudgePropertyChanged(newValue) {
fudgeProperty = newValue;
}
var fudgeInput = document.getElementById('fudge');
function onFudgeMinChanged(newValue) {
fudgeInput.min = newValue;
}
function onFudgeMaxChanged(newValue) {
fudgeInput.max = newValue;
}
function onFudgeChanged(newValue) {
fudge = newValue;
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);
}

// Adapted from code by Simon Sarris: http://stackoverflow.com/a/10450761
function getMousePos(event, element) {
var stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(element, null)['paddingLeft'], 10) || 0;
var stylePaddingTop = parseInt(document.defaultView.getComputedStyle(element, null)['paddingTop'], 10) || 0;
var styleBorderLeft = parseInt(document.defaultView.getComputedStyle(element, null)['borderLeftWidth'], 10) || 0;
var 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
var html = document.body.parentNode;
var htmlTop = html.offsetTop;
var htmlLeft = html.offsetLeft;

// Compute the total offset. It's possible to cache this if you want
var offsetX = 0, offsetY = 0;
if (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 <html> 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
};
}

canvas.onmousemove = function(event) {
var mousePos = getMousePos(event, canvas);
renderer.extractColor(mousePos.x, mousePos.y, 30);
};

canvas.onclick = function(event) {
var mousePos = getMousePos(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));
}
};

function drawStep() {
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);
</script>
<script src="playground.js"></script>
</body>
</html>
Loading