Compare commits

..

82 Commits

Author SHA1 Message Date
greenkeeper[bot]
41378a0d65 fix(package): update twgl.js to version 4.9.2
Closes #352
2019-05-13 22:19:49 +00:00
Chris Willis-Ford
4a55d63ada Merge pull request #424 from ktbee/limit-mosaic-effect
Only check position against effect transform if it falls within the Drawable's space
2019-04-29 17:19:51 -07:00
Chris Willis-Ford
c9c780aa69 Merge pull request #418 from peabrainiac/develop
pen transparency fix
2019-04-29 17:04:15 -07:00
Paul Kaplan
590c2ca084 Merge pull request #440 from LLK/revert-419-coordinates-fixups-2
Revert "Adjust CPU `isTouchingColor` to match GPU results (again)"
2019-04-19 16:14:10 -04:00
Paul Kaplan
757d7e3c96 Revert "Adjust CPU isTouchingColor to match GPU results (again)" 2019-04-19 16:13:47 -04:00
Paul Kaplan
e365a909dc Merge pull request #439 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190419183947
fix(package): update scratch-svg-renderer to version 0.2.0-prerelease…
2019-04-19 16:03:53 -04:00
greenkeeper[bot]
bffe80086e fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190419183947
Closes #430
2019-04-19 18:45:16 +00:00
Chris Willis-Ford
008dc5b15b Merge pull request #419 from cwillisf/coordinates-fixups-2
Adjust CPU `isTouchingColor` to match GPU results (again)
2019-04-10 11:35:25 -07:00
Michael "Z" Goddard
9177705e04 Merge pull request #414 from mzgoddard/image-data-texture
ImageData WebGL Textures
2019-03-26 12:12:57 -04:00
Christopher Willis-Ford
b304ea8fdf Make touching-color test more robust against GPU imprecision
Previously, the `color-touching-tests.sb2` test "touches a color that
doesn't actually exist right now" would use a sprite with ghost 50,
blended against another sprite, to create the color that "doesn't
actually exist" when the query sprite is skipped. Unfortunately the
blend result was near a bit-boundary and, depending on the specific
hardware used, that test could fail on the GPU. When the renderer uses
the CPU path this test works fine, though, so the existing problem went
unnoticed.

To fix the problem I changed the project to use ghost 30 instead, which
results in a color that is less near a bit boundary and is therefore
less likely to fail on specific hardware.

As an example of what was happening: the `touching color` block was
checking for `RGB(127,101,216)` with a mask of `RGB(0xF8,0xF8,0xF0)`. On
the CPU it would find `RGB(120,99,215)`, which is in range, but on some
GPUs the closest color it could find was `RGB(119,98,215)` which
mismatches on all four of the least significant bits -- one of which is
enabled in the mask.
2019-03-20 22:58:36 -07:00
Christopher Willis-Ford
f9428ee096 Run test projects in each GPU usage mode 2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
9526612d79 Add touching-color test to verify stencil use 2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
fb767b7553 Fix exception on first button click 2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
e864018d87 Iterate drawables in the same order on CPU & GPU 2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
e0b420a183 Use alpha test to avoid false touching-color 2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
a24b853af6 Fix (x,y) => point[] conversion comments 2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
73896b6f32 Fix direction for Y iteration on CPU path
For some reason the JavaScript engine insists on running the code
instead of doing what the comment says. I guess they should match.
2019-03-20 11:21:05 -07:00
Christopher Willis-Ford
80630a64da Adjust CPU isTouchingColor to match GPU results 2019-03-20 11:21:05 -07:00
Michael "Z" Goddard
e31934f6a9 update Skin textures with ImageData
When possible pass ImageData to texture creation and updating to help
remove chance of references that keep canvas and underlying data from
being garbage collected.
2019-03-19 17:52:21 -04:00
Katie Broida
8f007c0986 Only check local position against mosaic effect transform if it falls within the drawable's space 2019-03-18 15:25:40 -04:00
Paul Kaplan
3c79a5562e Merge pull request #421 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190304180800
Update scratch-svg-renderer to the latest version 🚀
2019-03-12 08:46:47 -04:00
greenkeeper[bot]
d59d45b6c8 fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190304180800 2019-03-04 18:09:20 +00:00
Michael "Z" Goddard
19ee8e8eaa Merge pull request #415 from mzgoddard/drop-silhouette-alpha-buffer
Replace Silhouette._data with Silhouette._colorData
2019-03-04 12:56:58 -05:00
peabrainiac
fe01fea9d0 Update RenderWebGL.js 2019-03-03 18:49:04 +01:00
peabrainiac
5fb9346036 Update RenderWebGL.js 2019-03-03 18:42:10 +01:00
peabrainiac
3d373571f8 Update PenSkin.js 2019-03-03 18:40:09 +01:00
peabrainiac
152cf028cc Update Skin.js 2019-03-03 18:39:40 +01:00
peabrainiac
147b79d319 Update RenderWebGL.js 2019-03-03 18:12:13 +01:00
peabrainiac
f2a7085492 Update RenderWebGL.js 2019-03-03 17:58:12 +01:00
peabrainiac
996a1d6cf7 Update sprite.frag 2019-03-03 17:55:55 +01:00
peabrainiac
61bf4c84c3 Update RenderWebGL.js 2019-03-02 22:35:42 +01:00
peabrainiac
7628c1e7f9 Update RenderWebGL.js
Modified blend function in `_drawThese` to blend skins with premultiplied alpha correctly
2019-03-02 20:59:00 +01:00
peabrainiac
9f7bd971c9 Update PenSkin.js
changed clearColor on `_setCanvasSize`
2019-03-02 00:29:21 +01:00
peabrainiac
44d2fdeba8 Update PenSkin.js 2019-02-27 08:48:45 +01:00
Michael "Z" Goddard
e022222365 replace Silhouette._data with Silhouette._colorData
_colorData holds the same (and more) data that _data holds. Dropping
the _data array saves a lot of memory for a tiny performance
degradation in regards to touching object.
2019-02-22 17:08:53 -05:00
Michael "Z" Goddard
be5ab2e689 receive ImageData directly in Silhouette.update
Given ImageData we can skip drawing the input and getting image data.
This can help if update's color can also use the ImageData directly.
2019-02-22 17:08:04 -05:00
Chris Willis-Ford
c9f86ef53b Merge pull request #406 from cwillisf/playground-webpack
Add "query playground"
2019-02-14 10:33:31 -08:00
Katie Broida
4bf233ef36 Merge pull request #409 from ktbee/compat-bitmap-position-off-stage
Compatibility fix for bitmap position off stage
2019-02-13 13:29:26 -05:00
Karishma Chadha
253cbd019d Merge pull request #411 from LLK/greenkeeper/scratch-vm-0.2.0-prerelease.20190207224121
chore(package): update scratch-vm to version 0.2.0-prerelease.2019020…
2019-02-13 10:21:44 -05:00
Katie Broida
1f0f89920a Always use getAABB for bitmap skins when determining fenced position 2019-02-11 11:56:59 -05:00
Katie Broida
25df9f1ab7 Merge pull request #408 from ktbee/compat-integer-x-y-off-stage
Use Math.ceil and Math.floor to match Scratch 2 logic
2019-02-08 11:53:07 -05:00
greenkeeper[bot]
7680270f40 chore(package): update scratch-vm to version 0.2.0-prerelease.20190207224121
Closes #361
2019-02-07 22:43:47 +00:00
Chris Willis-Ford
c7b22b58c2 Merge pull request #410 from LLK/revert-407-coordinates-fixups
Revert "Adjust CPU isTouchingColor to match GPU results"
2019-02-07 13:11:57 -08:00
Chris Willis-Ford
f2d457a827 Revert "Adjust CPU isTouchingColor to match GPU results" 2019-02-07 13:00:15 -08:00
Chris Willis-Ford
e3c68e7122 Merge pull request #407 from cwillisf/coordinates-fixups
Adjust CPU isTouchingColor to match GPU results
2019-02-06 13:31:52 -08:00
Christopher Willis-Ford
e64d8727ec Fix (x,y) => point[] conversion comments 2019-02-06 11:08:17 -08:00
Christopher Willis-Ford
c390124df4 Convert 'force GPU' flag into 'useGpuMode' enum 2019-02-06 10:47:49 -08:00
Christopher Willis-Ford
8bd6241160 Fix direction for Y iteration on CPU path
For some reason the JavaScript engine insists on running the code
instead of doing what the comment says. I guess they should match.
2019-02-05 18:19:21 -08:00
Katie Broida
c8b9516219 Use Math.ceil and Math.floor to match Scratch 2 logic 2019-02-04 16:03:33 -05:00
Christopher Willis-Ford
1db67a474e Adjust CPU isTouchingColor to match GPU results 2019-02-04 11:20:59 -08:00
Christopher Willis-Ford
028b4eba3f Adjust cursor coordinates for devicePixelRatio 2019-01-30 16:37:59 -08:00
Christopher Willis-Ford
59cef02fdb Mark correct viewport corners with red dots 2019-01-30 15:58:51 -08:00
Christopher Willis-Ford
99d6e46f7e 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.
2019-01-30 11:00:27 -08:00
Christopher Willis-Ford
992977d6c6 Add debug canvas support to isTouching CPU path 2019-01-30 11:00:14 -08:00
Christopher Willis-Ford
a358c8f916 Lint cleanup 2019-01-28 12:25:58 -08:00
Christopher Willis-Ford
e8d71277e2 Use query playground to compare GPU vs. CPU implementations 2019-01-28 11:43:38 -08:00
Paul Kaplan
b4f9f28417 Merge pull request #405 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190125192231
fix(package): update scratch-svg-renderer to version 0.2.0-prerelease…
2019-01-28 10:40:13 -05:00
Christopher Willis-Ford
fba2d90fda Stub queryPlayground.html 2019-01-25 17:28:51 -08:00
Christopher Willis-Ford
31db3d8596 Build playground using Webpack 2019-01-25 17:28:02 -08:00
greenkeeper[bot]
898d5d7885 fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190125192231
Closes #397
2019-01-25 19:23:50 +00:00
Evelyn Eastmond
9b11ac894d Merge pull request #376 from evhan55/bug/extract-drawable
Fix extraction of drawable to not clip bounds.
2019-01-24 21:33:50 -05:00
Evelyn Eastmond
fc6fcd0543 Removing console log comment. 2019-01-24 10:56:11 -05:00
Evelyn Eastmond
b77f4c663a Removing console log. 2019-01-24 10:54:08 -05:00
Evelyn Eastmond
5e5a423d39 Fixing checkFramebufferstatus check. 2019-01-24 10:54:08 -05:00
Evelyn Eastmond
402cfbf99f Adding a console log for testing. 2019-01-24 10:54:08 -05:00
Evelyn Eastmond
a0dd716c23 Adding some sanity checks and error handling that aren't fully working yet. 2019-01-24 10:54:08 -05:00
Evelyn Eastmond
183919a20a Fixing a comment. 2019-01-24 10:54:08 -05:00
Evelyn Eastmond
3cfafebb2e Fixing extraction of a drawable to not clip bounds. 2019-01-24 10:54:08 -05:00
DD Liu
931ff270dd Merge pull request #404 from LLK/touchingColor2
Update silhouette after getting texture at a new scale
2019-01-22 17:29:43 -05:00
DD Liu
cc448951f9 Update silhouette after getting texture at a new scale 2019-01-22 13:58:59 -05:00
Katie Broida
cfa0194ab8 Merge pull request #402 from ktbee/fix-test-typo
Fix typo for sb3 test files
2019-01-17 15:47:33 -05:00
Katie Broida
2b224eb9da Merge pull request #400 from ktbee/fence-width-compat
Add inset logic that is closer to Scratch 2's inset
2019-01-17 13:25:37 -05:00
Katie Broida
735c7caaae Fix typo for sb3 test files 2019-01-17 12:40:30 -05:00
Katie Broida
ad1b7111c8 Add inset logic that is closer to Scratch 2's inset 2019-01-17 11:59:22 -05:00
Chris Willis-Ford
e54b590d56 Merge pull request #399 from cwillisf/fix-brightness-effect
Change brightness effect to match Scratch 2.0 in 2D mode
2019-01-16 12:22:22 -08:00
Christopher Willis-Ford
355a8c5395 Change brightness effect to match Scratch 2.0 in 2D mode 2019-01-15 19:45:45 -08:00
Paul Kaplan
f1a7aab5a6 Merge pull request #398 from paulkaplan/revert-silhouette
Revert "Merge pull request #394 from paulkaplan/defer-silhouette-upda…
2019-01-15 12:25:37 -05:00
Paul Kaplan
bb84abab87 Revert "Merge pull request #394 from paulkaplan/defer-silhouette-updates"
This reverts commit a5f852fcc2, reversing
changes made to e616ab5d35.
2019-01-14 16:51:26 -05:00
Paul Kaplan
75772989ea Merge pull request #395 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20190109201344
Update scratch-svg-renderer to the latest version 🚀
2019-01-09 15:16:30 -05:00
greenkeeper[bot]
cf5aafc12f fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20190109201344 2019-01-09 20:15:01 +00:00
Paul Kaplan
a5f852fcc2 Merge pull request #394 from paulkaplan/defer-silhouette-updates
Implement updateSilhouette to allow updates to happen when needed
2019-01-09 14:59:56 -05:00
Paul Kaplan
07544595fd Implement updateSilhouette to allow updates to happen when needed 2019-01-09 14:22:08 -05:00
18 changed files with 716 additions and 255 deletions

View File

@@ -37,7 +37,7 @@
"gh-pages": "^1.0.0",
"jsdoc": "^3.5.5",
"json": "^9.0.4",
"scratch-vm": "0.2.0-prerelease.20181024204838",
"scratch-vm": "0.2.0-prerelease.20190207224121",
"tap": "^11.0.0",
"travis-after-all": "^1.4.4",
"uglifyjs-webpack-plugin": "^1.2.5",
@@ -53,7 +53,7 @@
"minilog": "3.1.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^1.0.0",
"scratch-svg-renderer": "0.2.0-prerelease.20181220183040",
"twgl.js": "4.4.0"
"scratch-svg-renderer": "0.2.0-prerelease.20190419183947",
"twgl.js": "4.9.2"
}
}

View File

@@ -59,6 +59,15 @@ class BitmapSkin extends Skin {
return this._texture;
}
/**
* Get the bounds of the drawable for determining its fenced position.
* @param {Array<number>} drawable - The Drawable instance this skin is using.
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB for bitmaps.
*/
getFenceBounds (drawable) {
return drawable.getAABB();
}
/**
* Set the contents of this skin to a snapshot of the provided bitmap data.
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin.
@@ -70,20 +79,31 @@ class BitmapSkin extends Skin {
setBitmap (bitmapData, costumeResolution, rotationCenter) {
const gl = this._renderer.gl;
// Preferably bitmapData is ImageData. ImageData speeds up updating
// Silhouette and is better handled by more browsers in regards to
// memory.
let textureData = bitmapData;
if (bitmapData instanceof HTMLCanvasElement) {
// Given a HTMLCanvasElement get the image data to pass to webgl and
// Silhouette.
const context = bitmapData.getContext('2d');
textureData = context.getImageData(0, 0, bitmapData.width, bitmapData.height);
}
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
this._silhouette.update(textureData);
} else {
// TODO: mipmaps?
const textureOptions = {
auto: true,
wrap: gl.CLAMP_TO_EDGE,
src: bitmapData
src: textureData
};
this._texture = twgl.createTexture(gl, textureOptions);
this._silhouette.update(bitmapData);
this._silhouette.update(textureData);
}
// Do these last in case any of the above throws an exception

View File

@@ -36,8 +36,10 @@ const getLocalPosition = (drawable, vec) => {
// 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);
// Apply texture effect transform if the localPosition is within the drawable's space.
if ((localPosition[0] >= 0 && localPosition[0] < 1) && (localPosition[1] >= 0 && localPosition[1] < 1)) {
EffectTransform.transformPoint(drawable, localPosition, localPosition);
}
return localPosition;
};

View File

@@ -154,6 +154,13 @@ class PenSkin extends Skin {
return true;
}
/**
* @returns {boolean} true if alpha is premultiplied, false otherwise
*/
get hasPremultipliedAlpha () {
return true;
}
/**
* @return {Array<number>} the "native" size, in texels, of this skin. [width, height]
*/
@@ -181,8 +188,9 @@ class PenSkin extends Skin {
clear () {
const gl = this._renderer.gl;
twgl.bindFramebufferInfo(gl, this._framebuffer);
gl.clearColor(1, 1, 1, 0);
/* Reset framebuffer to transparent black */
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
const ctx = this._canvas.getContext('2d');
@@ -598,7 +606,7 @@ class PenSkin extends Skin {
this._silhouetteBuffer = twgl.createFramebufferInfo(gl, [{format: gl.RGBA}], width, height);
}
gl.clearColor(1, 1, 1, 0);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
this._silhouetteDirty = true;

View File

@@ -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 = [];
@@ -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.
@@ -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);
}
@@ -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);
}
// ...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();
@@ -1048,11 +1082,28 @@ class RenderWebGL extends EventEmitter {
const scratchY = this._nativeSize[1] * ((y / this._gl.canvas.clientHeight) - 0.5);
const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
const bounds = drawable.getFastBounds();
bounds.snapToInt();
// Set a reasonable max limit width and height for the bufferInfo bounds
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const clampedWidth = Math.min(2048, bounds.width, maxTextureSize);
const clampedHeight = Math.min(2048, bounds.height, maxTextureSize);
// Make a new bufferInfo since this._queryBufferInfo is limited to 480x360
const attachments = [
{format: gl.RGBA},
{format: gl.DEPTH_STENCIL}
];
const bufferInfo = twgl.createFramebufferInfo(gl, attachments, clampedWidth, clampedHeight);
// If the new bufferInfo is invalid, fall back to using the smaller _queryBufferInfo
twgl.bindFramebufferInfo(gl, bufferInfo);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
}
// Translate to scratch units relative to the drawable
const pickX = scratchX - bounds.left;
const pickY = scratchY + bounds.top;
@@ -1299,20 +1350,20 @@ class RenderWebGL extends EventEmitter {
const dx = x - drawable._position[0];
const dy = y - drawable._position[1];
const aabb = drawable._skin.getFenceBounds(drawable);
const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2);
const aabb = drawable.getFastBounds();
const sx = this._xRight - Math.min(FENCE_WIDTH, Math.floor((aabb.right - aabb.left) / 2));
const sx = this._xRight - Math.min(FENCE_WIDTH, inset);
if (aabb.right + dx < -sx) {
x = drawable._position[0] - (sx + aabb.right);
x = Math.ceil(drawable._position[0] - (sx + aabb.right));
} else if (aabb.left + dx > sx) {
x = drawable._position[0] + (sx - aabb.left);
x = Math.floor(drawable._position[0] + (sx - aabb.left));
}
const sy = this._yTop - Math.min(FENCE_WIDTH, Math.floor((aabb.top - aabb.bottom) / 2));
const sy = this._yTop - Math.min(FENCE_WIDTH, inset);
if (aabb.top + dy < -sy) {
y = drawable._position[1] - (sy + aabb.top);
y = Math.ceil(drawable._position[1] - (sy + aabb.top));
} else if (aabb.bottom + dy > sy) {
y = drawable._position[1] + (sy - aabb.bottom);
y = Math.floor(drawable._position[1] + (sy - aabb.bottom));
}
return [x, y];
}
@@ -1569,7 +1620,14 @@ class RenderWebGL extends EventEmitter {
}
twgl.setUniforms(currentShader, uniforms);
/* adjust blend function for this skin */
if (drawable.skin.hasPremultipliedAlpha){
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
} else {
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
}
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
}
@@ -1752,4 +1810,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;

View File

@@ -77,9 +77,14 @@ class SVGSkin extends Skin {
this._textureScale = newScale;
this._svgRenderer._draw(this._textureScale, () => {
if (this._textureScale === newScale) {
const canvas = this._svgRenderer.canvas;
const context = canvas.getContext('2d');
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
const gl = this._renderer.gl;
gl.bindTexture(gl.TEXTURE_2D, this._texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
this._silhouette.update(textureData);
}
});
}
@@ -98,20 +103,28 @@ class SVGSkin extends Skin {
this._svgRenderer.fromString(svgData, 1, () => {
const gl = this._renderer.gl;
this._textureScale = this._maxTextureScale = 1;
// Pull out the ImageData from the canvas. ImageData speeds up
// updating Silhouette and is better handled by more browsers in
// regards to memory.
const canvas = this._svgRenderer.canvas;
const context = canvas.getContext('2d');
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
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);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
this._silhouette.update(textureData);
} else {
// TODO: mipmaps?
const textureOptions = {
auto: true,
wrap: gl.CLAMP_TO_EDGE,
src: this._svgRenderer.canvas
src: textureData
};
this._texture = twgl.createTexture(gl, textureOptions);
this._silhouette.update(this._svgRenderer.canvas);
this._silhouette.update(textureData);
}
const maxDimension = Math.max(this._svgRenderer.canvas.width, this._svgRenderer.canvas.height);

View File

@@ -19,12 +19,12 @@ let __SilhouetteUpdateCanvas;
* @param {number} y - y
* @return {number} Alpha value for x/y position
*/
const getPoint = ({_width: width, _height: height, _data: data}, x, y) => {
const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => {
// 0 if outside bouds, otherwise read from data.
if (x >= width || y >= height || x < 0 || y < 0) {
return 0;
}
return data[(y * width) + x];
return data[(((y * width) + x) * 4) + 3];
};
/**
@@ -76,7 +76,6 @@ class Silhouette {
* The data representing a skin's silhouette shape.
* @type {Uint8ClampedArray}
*/
this._data = null;
this._colorData = null;
this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
@@ -88,28 +87,33 @@ class Silhouette {
* 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');
let imageData;
if (bitmapData instanceof ImageData) {
// If handed ImageData directly, use it directly.
imageData = bitmapData;
this._width = bitmapData.width;
this._height = bitmapData.height;
} else {
// Draw about anything else to our update canvas and poll image data
// from that.
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');
if (!(width && height)) {
return;
if (!(width && height)) {
return;
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(bitmapData, 0, 0, width, height);
imageData = ctx.getImageData(0, 0, width, height);
}
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);
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];
}
}
/**
@@ -166,7 +170,7 @@ class Silhouette {
* @return {boolean} If the nearest pixel has an alpha value.
*/
isTouchingNearest (vec) {
if (!this._data) return;
if (!this._colorData) return;
return getPoint(
this,
Math.floor(vec[0] * (this._width - 1)),
@@ -181,7 +185,7 @@ class Silhouette {
* @return {boolean} Any of the pixels have some alpha.
*/
isTouchingLinear (vec) {
if (!this._data) return;
if (!this._colorData) return;
const x = Math.floor(vec[0] * (this._width - 1));
const y = Math.floor(vec[1] * (this._height - 1));
return getPoint(this, x, y) > 0 ||

View File

@@ -76,6 +76,13 @@ class Skin extends EventEmitter {
return false;
}
/**
* @returns {boolean} true if alpha is premultiplied, false otherwise
*/
get hasPremultipliedAlpha () {
return false;
}
/**
* @return {int} the unique ID for this Skin.
*/
@@ -136,6 +143,15 @@ class Skin extends EventEmitter {
return null;
}
/**
* Get the bounds of the drawable for determining its fenced position.
* @param {Array<number>} drawable - The Drawable instance this skin is using.
* @return {!Rectangle} The drawable's bounds.
*/
getFenceBounds (drawable) {
return drawable.getFastBounds();
}
/**
* Update and returns the uniforms for this skin.
* @param {Array<number>} scale - The scaling factors to be used.

View File

@@ -1,7 +1,9 @@
module.exports = {
root: true,
extends: ['scratch'],
env: {
browser: true
},
rules: {
'no-console': 'off'
}
};

View File

@@ -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;

View File

@@ -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>
@@ -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>

View File

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

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Scratch WebGL Query Playground</title>
<style>
input[type=range][orient=vertical] {
writing-mode: bt-lr; /* IE */
-webkit-appearance: slider-vertical;
width: 1rem;
padding: 0 0.5rem;
}
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 */
}
</style>
</head>
<body style="background: lightsteelblue">
<div>
<fieldset>
<legend>Query Canvases</legend>
<table>
<tr>
<td>
<fieldset>
<legend>GPU</legend>
<div>Touching color A? <span id="gpuTouchingA">maybe</span></div>
<div>Touching color B? <span id="gpuTouchingB">maybe</span></div>
<canvas id="gpuQueryCanvas" width="480" height="360" style="height: 20rem"></canvas>
</fieldset>
</td>
<td>
<fieldset>
<legend>CPU</legend>
<div>Touching color A? <span id="cpuTouchingA">maybe</span></div>
<div>Touching color B? <span id="cpuTouchingB">maybe</span></div>
<canvas id="cpuQueryCanvas" width="480" height="360" style="height: 20rem"></canvas>
</fieldset>
</td>
</tr>
</table>
</fieldset>
<fieldset>
<legend>Render Canvas</legend>
<div>Cursor Position: <span id="cursorPosition">somewhere</span></div>
<table>
<tr>
<td></td>
<td>
<input id="cursorX" type="range" step="0.25" value="0" />
</td>
</tr>
<tr>
<td>
<input id="cursorY" type="range" orient="vertical" step="0.25" value="0" />
</td>
<td>
<canvas id="renderCanvas" width="480" height="360" style="border:3px dashed black"></canvas>
</td>
</tr>
</table>
</fieldset>
</div>
</body>
<script src="queryPlayground.js"></script>
</html>

View File

@@ -0,0 +1,196 @@
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 renderer = new ScratchRender(renderCanvas);
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 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) {
renderer.draw();
renderer.updateDrawableProperties(drawables.cursor, {
position: [cursorX, cursorY]
});
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.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.setUseGpuMode(ScratchRender.UseGpuModes.Automatic);
}
};
inputCursorX.addEventListener('change', handleCursorPositionChanged);
inputCursorY.addEventListener('change', handleCursorPositionChanged);
inputCursorX.addEventListener('input', handleCursorPositionChanged);
inputCursorY.addEventListener('input', handleCursorPositionChanged);
handleCursorPositionChanged();
let trackingMouse = true;
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);
renderCanvas.addEventListener('click', event => {
trackingMouse = !trackingMouse;
if (trackingMouse) {
handleMouseMove(event);
}
});
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 ? 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 ? patternB : patternA;
context.fillRect(x, y, 2, 2);
}
}
return canvas;
};
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]);
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();
renderer.draw();

View File

@@ -45,33 +45,48 @@ uniform sampler2D u_skin;
varying vec2 v_texCoord;
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color) || defined(ENABLE_brightness))
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
// Branchless color conversions based on code from:
// http://www.chilliant.com/rgb2hsv.html by Ian Taylor
// Based in part on work by Sam Hocevar and Emil Persson
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
// Smaller values can cause problems with "color" and "brightness" effects on some mobile devices
// Smaller values can cause problems on some mobile devices
const float epsilon = 1e-3;
// Convert an RGB color to Hue, Chroma, and Value.
// Convert an RGB color to Hue, Saturation, and Value.
// All components of input and output are expected to be in the [0,1] range.
vec3 convertRGB2HCV(vec3 rgb)
vec3 convertRGB2HSV(vec3 rgb)
{
// Based on work by Sam Hocevar and Emil Persson
vec4 P = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0/3.0) : vec4(rgb.gb, 0.0, -1.0/3.0);
vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx);
float C = Q.x - min(Q.w, Q.y);
float H = abs((Q.w - Q.y) / (6.0 * C + epsilon) + Q.z);
return vec3(H, C, Q.x);
}
// Hue calculation has 3 cases, depending on which RGB component is largest, and one of those cases involves a "mod"
// operation. In order to avoid that "mod" we split the M==R case in two: one for G<B and one for B>G. The B>G case
// will be calculated in the negative and fed through abs() in the hue calculation at the end.
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Hue_and_chroma
const vec4 hueOffsets = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec3 convertRGB2HSL(vec3 rgb)
{
vec3 hcv = convertRGB2HCV(rgb);
float L = hcv.z - hcv.y * 0.5;
float S = hcv.y / (1.0 - abs(L * 2.0 - 1.0) + epsilon);
return vec3(hcv.x, S, L);
// temp1.xy = sort B & G (largest first)
// temp1.z = the hue offset we'll use if it turns out that R is the largest component (M==R)
// temp1.w = the hue offset we'll use if it turns out that R is not the largest component (M==G or M==B)
vec4 temp1 = rgb.b > rgb.g ? vec4(rgb.bg, hueOffsets.wz) : vec4(rgb.gb, hueOffsets.xy);
// temp2.x = the largest component of RGB ("M" / "Max")
// temp2.yw = the smaller components of RGB, ordered for the hue calculation (not necessarily sorted by magnitude!)
// temp2.z = the hue offset we'll use in the hue calculation
vec4 temp2 = rgb.r > temp1.x ? vec4(rgb.r, temp1.yzx) : vec4(temp1.xyw, rgb.r);
// m = the smallest component of RGB ("min")
float m = min(temp2.y, temp2.w);
// Chroma = M - m
float C = temp2.x - m;
// Value = M
float V = temp2.x;
return vec3(
abs(temp2.z + (temp2.w - temp2.y) / (6.0 * C + epsilon)), // Hue
C / (temp2.x + epsilon), // Saturation
V); // Value
}
vec3 convertHue2RGB(float hue)
@@ -82,13 +97,13 @@ vec3 convertHue2RGB(float hue)
return clamp(vec3(r, g, b), 0.0, 1.0);
}
vec3 convertHSL2RGB(vec3 hsl)
vec3 convertHSV2RGB(vec3 hsv)
{
vec3 rgb = convertHue2RGB(hsl.x);
float C = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
return (rgb - 0.5) * C + hsl.z;
vec3 rgb = convertHue2RGB(hsv.x);
float c = hsv.z * hsv.y;
return rgb * c + hsv.z - c;
}
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color) || defined(ENABLE_brightness))
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
const vec2 kCenter = vec2(0.5, 0.5);
@@ -149,31 +164,27 @@ void main()
gl_FragColor = u_silhouetteColor;
#else // DRAW_MODE_silhouette
#if defined(ENABLE_color) || defined(ENABLE_brightness)
#if defined(ENABLE_color)
{
vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
#ifdef ENABLE_color
{
// 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 float minSaturation = 0.09;
if (hsl.z < minLightness) hsl = vec3(0.0, 1.0, minLightness);
else if (hsl.y < minSaturation) hsl = vec3(0.0, minSaturation, hsl.z);
// 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 float minSaturation = 0.09;
if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
hsl.x = mod(hsl.x + u_color, 1.0);
if (hsl.x < 0.0) hsl.x += 1.0;
}
#endif // ENABLE_color
hsv.x = mod(hsv.x + u_color, 1.0);
if (hsv.x < 0.0) hsv.x += 1.0;
#ifdef ENABLE_brightness
hsl.z = clamp(hsl.z + u_brightness, 0.0, 1.0);
#endif // ENABLE_brightness
gl_FragColor.rgb = convertHSL2RGB(hsl);
gl_FragColor.rgb = convertHSV2RGB(hsv);
}
#endif // defined(ENABLE_color) || defined(ENABLE_brightness)
#endif // defined(ENABLE_color)
#if defined(ENABLE_brightness)
gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1));
#endif // defined(ENABLE_brightness)
#ifdef DRAW_MODE_colorMask
vec3 maskDistance = abs(gl_FragColor.rgb - u_colorMask);
@@ -183,12 +194,6 @@ void main()
discard;
}
#endif // DRAW_MODE_colorMask
// WebGL defaults to premultiplied alpha
#ifndef DRAW_MODE_stamp
gl_FragColor.rgb *= gl_FragColor.a;
#endif // DRAW_MODE_stamp
#endif // DRAW_MODE_silhouette
#else // DRAW_MODE_lineSample

View File

@@ -114,7 +114,7 @@ const testBubbles = () => test('bubble snapshot', async t => {
// immediately invoked async function to let us wait for each test to finish before starting the next.
(async () => {
const files = fs.readdirSync(testDir())
.filter(uri => uri.endsWith('.sb2') || uri.endsWidth('.sb3'));
.filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3'));
for (const file of files) {
await testFile(file);

View File

@@ -39,10 +39,10 @@ module.exports = [
Object.assign({}, base, {
target: 'web',
entry: {
'scratch-render': './src/index.js'
playground: './src/playground/playground.js',
queryPlayground: './src/playground/queryPlayground.js'
},
output: {
library: 'ScratchRender',
libraryTarget: 'umd',
path: path.resolve('playground'),
filename: '[name].js'
@@ -50,7 +50,8 @@ module.exports = [
plugins: base.plugins.concat([
new CopyWebpackPlugin([
{
from: 'src/playground'
context: 'src/playground',
from: '*.html'
}
])
])