Compare commits

...

45 Commits

Author SHA1 Message Date
greenkeeper[bot]
097daeec28 chore(package): update scratch-vm to version 0.2.0-prerelease.20200519195348
Closes #560
2020-05-19 20:10:34 +00:00
adroitwhiz
d73aeb1ac1 Merge pull request #598 from adroitwhiz/cpu-gpu-integration-tests
Run integration tests on both the CPU and GPU (again)
2020-05-12 16:24:10 -04:00
Christopher Willis-Ford
298200f2ee 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.
2020-05-12 15:52:51 -04:00
adroitwhiz
b9411bf51e Skip "doesn't touch say bubble" test
There are some compatibility concerns to be worked out before the
corresponding bug can be fixed.
2020-05-12 15:52:51 -04:00
adroitwhiz
3513cd77ae Adapt f9428ee to Playwright 2020-05-12 15:52:51 -04:00
adroitwhiz
d8c9f339f7 Merge pull request #576 from adroitwhiz/fix-ghost-touching-color
Don't apply ghost effect to "color is touching color" drawable
2020-05-11 16:29:17 -04:00
adroitwhiz
aaffc77b23 Clarify isTouchingColor ghost comment 2020-05-11 16:07:05 -04:00
adroitwhiz
d867e62c74 Don't apply ghost to "touching color" drawables 2020-05-11 16:07:05 -04:00
adroitwhiz
a7bec3a958 Merge pull request #604 from adroitwhiz/playwright
Playwright: the re-wright
2020-05-11 15:50:57 -04:00
adroitwhiz
0ce50b6cd6 log error.message 2020-05-11 15:38:11 -04:00
adroitwhiz
562e535a13 recache node_modules 2020-05-11 15:02:01 -04:00
adroitwhiz
42d3f6c012 uncache node_modules 2020-05-11 14:58:47 -04:00
adroitwhiz
9c01f364d4 Catch promise errors and exit with code 1 2020-05-11 14:43:54 -04:00
adroitwhiz
5d085f678f Add cached Playwright directory to Travis cache 2020-05-11 14:43:54 -04:00
adroitwhiz
7df536f492 Bump Node to 10 2020-05-11 14:43:54 -04:00
adroitwhiz
a41bcafac7 Use Playwright instead of Puppeteer 2020-05-11 14:43:54 -04:00
adroitwhiz
5566a600ba reference "wait for SVG skins to load" issue 2020-05-11 14:43:54 -04:00
adroitwhiz
34072d2f53 Fix file input in tests 2020-05-11 14:43:54 -04:00
adroitwhiz
fdd02a6bd6 download Chromium only when tests are run 2020-05-11 14:43:54 -04:00
adroitwhiz
ec141ff76d Replace Chromeless with Puppeteer 2020-05-11 14:43:54 -04:00
adroitwhiz
3f68e18b2e Merge pull request #603 from LLK/revert-597-playwright
Revert "Switch from Chromeless to Playwright for tests"
2020-05-08 16:40:47 -04:00
adroitwhiz
813792359f Revert "Switch from Chromeless to Playwright for tests" 2020-05-08 16:32:38 -04:00
adroitwhiz
7349c1967c Merge pull request #597 from adroitwhiz/playwright
Switch from Chromeless to Playwright for tests
2020-05-08 15:47:48 -04:00
adroitwhiz
b873b6fec4 Bump Node to 10 2020-05-07 16:37:52 -04:00
adroitwhiz
51c867fa6d Use Playwright instead of Puppeteer 2020-05-07 16:21:37 -04:00
adroitwhiz
ddccd0fa79 reference "wait for SVG skins to load" issue 2020-05-07 16:08:28 -04:00
DD Liu
b35adaef88 Merge pull request #596 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20200507183648
Update scratch-svg-renderer to the latest version 🚀
2020-05-07 15:45:34 -04:00
adroitwhiz
3612461f9c Merge pull request #478 from adroitwhiz/fix-fence-bounds
Always use getAABB for fence bounds
2020-05-07 15:43:28 -04:00
adroitwhiz
7c710c0680 Merge pull request #474 from adroitwhiz/deobfuscate-convex-hull
Ergonomics: Deobfuscate _getConvexHullPointsForDrawable
2020-05-07 15:28:18 -04:00
adroitwhiz
fe91133565 Re-add link to 2.0's convex hull algorithm 2020-05-07 15:21:16 -04:00
greenkeeper[bot]
409df2a74d fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20200507183648 2020-05-07 19:13:23 +00:00
Chris Willis-Ford
4b03eb29fd Merge pull request #590 from adroitwhiz/i-hate-safari
Tweak pen shader to avoid float precision issues
2020-05-06 13:38:32 -07:00
adroitwhiz
51bceb68e5 Address review feedback 2020-05-06 16:21:18 -04:00
adroitwhiz
70c64f035c Deobfuscate _getConvexHullPointsForDrawable 2020-05-05 16:25:38 -04:00
adroitwhiz
4b7558f2d2 Fix another annoying precision bug.
Who knew fragment shaders were so difficult?
2020-05-01 14:04:03 -04:00
DD Liu
84f878e553 Merge pull request #591 from LLK/master
Merge master into develop
2020-04-27 13:47:31 -04:00
DD Liu
b6424a2f80 Merge pull request #588 from adroitwhiz/fix-resize
Call draw() after resizing canvas
2020-04-27 13:38:25 -04:00
adroitwhiz
0b47d87f2f Tweak pen shader to avoid float precision issues 2020-04-25 19:43:20 -04:00
adroitwhiz
41e21ed129 Only resize canvas when necessary 2020-04-24 18:51:48 -04:00
adroitwhiz
6d28f42d57 Call draw() after resizing canvas 2020-04-24 16:20:11 -04:00
adroitwhiz
5081a71fe9 Fix file input in tests 2020-03-08 08:18:46 -04:00
adroitwhiz
0a214a989d download Chromium only when tests are run 2020-03-08 08:18:46 -04:00
adroitwhiz
3d02a6c769 Replace Chromeless with Puppeteer 2020-03-08 08:18:28 -04:00
adroitwhiz
60e7e568d5 Add fencing test 2019-07-09 10:38:19 -04:00
adroitwhiz
b448aac1f3 Always use AABB for fence bounds 2019-07-09 10:25:12 -04:00
17 changed files with 287 additions and 215 deletions

View File

@@ -1,14 +1,10 @@
language: node_js
dist: trusty
addons:
chrome: stable
node_js:
- 8
- 10
- node
env:
- NODE_ENV=production
before_install:
- google-chrome-stable --headless --no-sandbox --remote-debugging-port=9222 &
install:
- npm --production=false install
- npm --production=false update
@@ -16,6 +12,7 @@ sudo: false
cache:
directories:
- node_modules
- ~/.cache/ms-playwright
jobs:
include:
- stage: test
@@ -24,7 +21,7 @@ jobs:
- npm run docs
- npm run tap
- stage: deploy
node_js: 8
node_js: 10
script: npm run build
before_deploy:
- VPKG=$($(npm bin)/json -f package.json version)

View File

@@ -29,7 +29,6 @@
"babel-loader": "^7.1.4",
"babel-polyfill": "^6.22.0",
"babel-preset-env": "^1.6.1",
"chromeless": "^1.5.1",
"copy-webpack-plugin": "^4.5.1",
"docdash": "^0.4.0",
"eslint": "^4.6.1",
@@ -37,7 +36,8 @@
"gh-pages": "^1.0.0",
"jsdoc": "^3.5.5",
"json": "^9.0.4",
"scratch-vm": "0.2.0-prerelease.20191227164934",
"playwright-chromium": "^1.0.1",
"scratch-vm": "0.2.0-prerelease.20200519195348",
"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.20200205003400",
"scratch-svg-renderer": "0.2.0-prerelease.20200507183648",
"twgl.js": "4.4.0"
}
}

View File

@@ -56,16 +56,6 @@ class BitmapSkin extends Skin {
return this._texture || super.getTexture();
}
/**
* Get the bounds of the drawable for determining its fenced position.
* @param {Array<number>} drawable - The Drawable instance this skin is using.
* @param {?Rectangle} result - Optional destination for bounds calculation.
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB for bitmaps.
*/
getFenceBounds (drawable, result) {
return drawable.getAABB(result);
}
/**
* 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.

View File

@@ -679,9 +679,10 @@ class Drawable {
* @param {twgl.v3} vec The scratch space [x,y] vector
* @param {Drawable} drawable The drawable to sample the texture from
* @param {Uint8ClampedArray} dst The "color4b" representation of the texture at point.
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
* @returns {Uint8ClampedArray} The dst object filled with the color4b
*/
static sampleColor4b (vec, drawable, dst) {
static sampleColor4b (vec, drawable, dst, effectMask) {
const localPosition = getLocalPosition(drawable, vec);
if (localPosition[0] < 0 || localPosition[1] < 0 ||
localPosition[0] > 1 || localPosition[1] > 1) {
@@ -698,7 +699,7 @@ class Drawable {
// : drawable.skin._silhouette.colorAtLinear(localPosition, dst);
if (drawable.enabledEffects === 0) return textColor;
return EffectTransform.transformColor(drawable, textColor);
return EffectTransform.transformColor(drawable, textColor, effectMask);
}
}

View File

@@ -118,16 +118,17 @@ class EffectTransform {
* Ghost and Color and Brightness effects.
* @param {Drawable} drawable The drawable to get uniforms from.
* @param {Uint8ClampedArray} inOutColor The color to transform.
* @param {number} [effectMask] A bitmask for which effects to use. Optional.
* @returns {Uint8ClampedArray} dst filled with the transformed color
*/
static transformColor (drawable, inOutColor) {
static transformColor (drawable, inOutColor, effectMask) {
// If the color is fully transparent, don't bother attempting any transformations.
if (inOutColor[3] === 0) {
return inOutColor;
}
const effects = drawable.enabledEffects;
let effects = drawable.enabledEffects;
if (typeof effectMask === 'number') effects &= effectMask;
const uniforms = drawable.getUniforms();
const enableColor = (effects & ShaderManager.EFFECT_INFO.color.mask) !== 0;

View File

@@ -220,9 +220,20 @@ class RenderWebGL extends EventEmitter {
* @param {int} pixelsTall The desired height in device-independent pixels.
*/
resize (pixelsWide, pixelsTall) {
const {canvas} = this._gl;
const pixelRatio = window.devicePixelRatio || 1;
this._gl.canvas.width = pixelsWide * pixelRatio;
this._gl.canvas.height = pixelsTall * pixelRatio;
const newWidth = pixelsWide * pixelRatio;
const newHeight = pixelsTall * pixelRatio;
// Certain operations, such as moving the color picker, call `resize` once per frame, even though the canvas
// size doesn't change. To avoid unnecessary canvas updates, check that we *really* need to resize the canvas.
if (canvas.width !== newWidth || canvas.height !== newHeight) {
canvas.width = newWidth;
canvas.height = newHeight;
// Resizing the canvas causes it to be cleared, so redraw it.
this.draw();
}
}
/**
@@ -752,6 +763,9 @@ class RenderWebGL extends EventEmitter {
const color = __touchingColor;
const hasMask = Boolean(mask3b);
// Masked drawable ignores ghost effect
const effectMask = ~ShaderManager.EFFECT_INFO.ghost.mask;
// Scratch Space - +y is top
for (let y = bounds.bottom; y <= bounds.top; y++) {
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) {
@@ -762,7 +776,7 @@ class RenderWebGL extends EventEmitter {
point[0] = x;
// if we use a mask, check our sample color...
if (hasMask ?
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
maskMatches(Drawable.sampleColor4b(point, drawable, color, effectMask), mask3b) :
drawable.isTouching(point)) {
RenderWebGL.sampleColor3b(point, candidates, color);
if (debugCanvasContext) {
@@ -1754,9 +1768,7 @@ class RenderWebGL extends EventEmitter {
/**
* Get the convex hull points for a particular Drawable.
* To do this, draw the Drawable unrotated, unscaled, and untranslated.
* Read back the pixels and find all boundary points.
* Finally, apply a convex hull algorithm to simplify the set.
* To do this, calculate it based on the drawable's Silhouette.
* @param {int} drawableID The Drawable IDs calculate convex hull for.
* @return {Array<Array<number>>} points Convex hull points, as [[x, y], ...]
*/
@@ -1773,110 +1785,120 @@ class RenderWebGL extends EventEmitter {
}
/**
* Return the determinant of two vectors, the vector from A to B and
* the vector from A to C.
* Return the determinant of two vectors, the vector from A to B and the vector from A to C.
*
* The determinant is useful in this case to know if AC is counter
* clockwise from AB. A positive value means the AC is counter
* clockwise from AC. A negative value menas AC is clockwise from AB.
* The determinant is useful in this case to know if AC is counter-clockwise from AB.
* A positive value means that AC is counter-clockwise from AB. A negative value means AC is clockwise from AB.
*
* @param {Float32Array} A A 2d vector in space.
* @param {Float32Array} B A 2d vector in space.
* @param {Float32Array} C A 2d vector in space.
* @return {number} Greater than 0 if counter clockwise, less than if
* clockwise, 0 if all points are on a line.
* @return {number} Greater than 0 if counter clockwise, less than if clockwise, 0 if all points are on a line.
*/
const CCW = function (A, B, C) {
const determinant = function (A, B, C) {
// AB = B - A
// AC = C - A
// det (AB BC) = AB0 * AC1 - AB1 * AC0
return (((B[0] - A[0]) * (C[1] - A[1])) - ((B[1] - A[1]) * (C[0] - A[0])));
};
// https://github.com/LLK/scratch-flash/blob/dcbeeb59d44c3be911545dfe54d
// 46a32404f8e69/src/scratch/ScratchCostume.as#L369-L413 Following
// RasterHull creation, compare and store left and right values that
// maintain a convex shape until that data can be passed to `hull` for
// further work.
const L = [];
const R = [];
// This algorithm for calculating the convex hull somewhat resembles the monotone chain algorithm.
// The main difference is that instead of sorting the points by x-coordinate, and y-coordinate in case of ties,
// it goes through them by y-coordinate in the outer loop and x-coordinate in the inner loop.
// This gives us "left" and "right" hulls, whereas the monotone chain algorithm gives "top" and "bottom" hulls.
// Adapted from https://github.com/LLK/scratch-flash/blob/dcbeeb59d44c3be911545dfe54d46a32404f8e69/src/scratch/ScratchCostume.as#L369-L413
const leftHull = [];
const rightHull = [];
// While convex hull algorithms usually push and pop values from the list of hull points,
// here, we keep indices for the "last" point in each array. Any points past these indices are ignored.
// This is functionally equivalent to pushing and popping from a "stack" of hull points.
let leftEndPointIndex = -1;
let rightEndPointIndex = -1;
const _pixelPos = twgl.v3.create();
const _effectPos = twgl.v3.create();
let ll = -1;
let rr = -1;
let Q;
let currentPoint;
// *Not* Scratch Space-- +y is bottom
// Loop over all rows of pixels, starting at the top
for (let y = 0; y < height; y++) {
_pixelPos[1] = y / height;
// Scan from left to right, looking for a touchable spot in the
// skin.
// We start at the leftmost point, then go rightwards until we hit an opaque pixel
let x = 0;
for (; x < width; x++) {
_pixelPos[0] = x / width;
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
if (drawable.skin.isTouchingLinear(_effectPos)) {
Q = [x, y];
currentPoint = [x, y];
break;
}
}
// If x is equal to the width there are no touchable points in the
// skin. Nothing we can add to L. And looping for R would find the
// same thing.
// If we managed to loop all the way through, there are no opaque pixels on this row. Go to the next one
if (x >= width) {
continue;
}
// Decrement ll until Q is clockwise (CCW returns negative) from the
// last two points in L.
while (ll > 0) {
if (CCW(L[ll - 1], L[ll], Q) < 0) {
// Because leftEndPointIndex is initialized to -1, this is skipped for the first two rows.
// It runs only when there are enough points in the left hull to make at least one line.
// If appending the current point to the left hull makes a counter-clockwise turn,
// we want to append the current point. Otherwise, we decrement the index of the "last" hull point until the
// current point makes a counter-clockwise turn.
// This decrementing has the same effect as popping from the point list, but is hopefully faster.
while (leftEndPointIndex > 0) {
if (determinant(leftHull[leftEndPointIndex], leftHull[leftEndPointIndex - 1], currentPoint) > 0) {
break;
} else {
--ll;
// leftHull.pop();
--leftEndPointIndex;
}
}
// Increment ll and then set L[ll] to Q. If ll was -1 before this
// line, this will set L[0] to Q. If ll was 0 before this line, this
// will set L[1] to Q.
L[++ll] = Q;
// Scan from right to left, looking for a touchable spot in the
// skin.
// This has the same effect as pushing to the point list.
// This "list head pointer" coding style leaves excess points dangling at the end of the list,
// but that doesn't matter; we simply won't copy them over to the final hull.
// leftHull.push(currentPoint);
leftHull[++leftEndPointIndex] = currentPoint;
// Now we repeat the process for the right side, looking leftwards for a pixel.
for (x = width - 1; x >= 0; x--) {
_pixelPos[0] = x / width;
EffectTransform.transformPoint(drawable, _pixelPos, _effectPos);
if (drawable.skin.isTouchingLinear(_effectPos)) {
Q = [x, y];
currentPoint = [x, y];
break;
}
}
// Decrement rr until Q is counter clockwise (CCW returns positive)
// from the last two points in L. L takes clockwise points and R
// takes counter clockwise points. if y was decremented instead of
// incremented R would take clockwise points. We are going in the
// right direction for L and the wrong direction for R, so we
// compare the opposite value for R from L.
while (rr > 0) {
if (CCW(R[rr - 1], R[rr], Q) > 0) {
// Because we're coming at this from the right, it goes clockwise this time.
while (rightEndPointIndex > 0) {
if (determinant(rightHull[rightEndPointIndex], rightHull[rightEndPointIndex - 1], currentPoint) < 0) {
break;
} else {
--rr;
--rightEndPointIndex;
}
}
// Increment rr and then set R[rr] to Q.
R[++rr] = Q;
rightHull[++rightEndPointIndex] = currentPoint;
}
// Known boundary points on left/right edges of pixels.
const boundaryPoints = L;
// Truncate boundaryPoints to the index of the last added Q to L. L may
// have more entries than the index for the last Q.
boundaryPoints.length = ll + 1;
// Add points in R to boundaryPoints in reverse so all points in
// boundaryPoints are clockwise from each other.
for (let j = rr; j >= 0; --j) {
boundaryPoints.push(R[j]);
// Start off "hullPoints" with the left hull points.
const hullPoints = leftHull;
// This is where we get rid of those dangling extra points.
hullPoints.length = leftEndPointIndex + 1;
// Add points from the right side in reverse order so all points are ordered clockwise.
for (let j = rightEndPointIndex; j >= 0; --j) {
hullPoints.push(rightHull[j]);
}
// Simplify boundary points using convex hull.
return hull(boundaryPoints, Infinity);
// Simplify boundary points using hull.js.
// TODO: Remove this; this algorithm already generates convex hulls.
return hull(hullPoints, Infinity);
}
/**

View File

@@ -143,10 +143,10 @@ class Skin extends EventEmitter {
* Get the bounds of the drawable for determining its fenced position.
* @param {Array<number>} drawable - The Drawable instance this skin is using.
* @param {?Rectangle} result - Optional destination for bounds calculation.
* @return {!Rectangle} The drawable's bounds.
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB.
*/
getFenceBounds (drawable, result) {
return drawable.getFastBounds(result);
return drawable.getAABB(result);
}
/**

View File

@@ -214,20 +214,34 @@ void main()
// Un-premultiply alpha.
gl_FragColor.rgb /= gl_FragColor.a + epsilon;
#endif
#else // DRAW_MODE_line
// Maaaaagic antialiased-line-with-round-caps shader.
// Adapted from Inigo Quilez' 2D distance function cheat sheet
// https://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm
// On (some?) devices with 16-bit float precision, sufficiently long lines will overflow the float's range.
// Avoid this by scaling these problematic values down to fit within (-1, 1) then scaling them back up later.
// TODO: Is this a problem on all drivers with 16-bit mediump floats, or just Mali?
vec2 pointDiff = abs(u_penPoints.zw - u_penPoints.xy);
float FLOAT_SCALING_INVERSE = max(1.0, max(pointDiff.x, pointDiff.y));
float FLOAT_SCALING = 1.0 / FLOAT_SCALING_INVERSE;
// The xy component of u_penPoints is the first point; the zw is the second point.
// This is done to minimize the number of gl.uniform calls, which can add up.
vec2 pa = v_texCoord - u_penPoints.xy, ba = u_penPoints.zw - u_penPoints.xy;
// vec2 pa = v_texCoord - u_penPoints.xy, ba = u_penPoints.zw - u_penPoints.xy;
vec2 pa = (v_texCoord - u_penPoints.xy) * FLOAT_SCALING, ba = (u_penPoints.zw - u_penPoints.xy) * FLOAT_SCALING;
// Avoid division by zero
float baDot = dot(ba, ba);
// the dot product of a vector and itself is always positive
baDot = max(baDot, epsilon);
// Magnitude of vector projection of this fragment onto the line (both relative to the line's start point).
// This results in a "linear gradient" which goes from 0.0 at the start point to 1.0 at the end point.
float projMagnitude = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
float projMagnitude = clamp(dot(pa, ba) / baDot, 0.0, 1.0);
float lineDistance = length(pa - (ba * projMagnitude));
float lineDistance = length(pa - (ba * projMagnitude)) * FLOAT_SCALING_INVERSE;
// The distance to the line allows us to create lines of any thickness.
// Instead of checking whether this fragment's distance < the line thickness,

View File

@@ -40,7 +40,13 @@ void main() {
position -= expandedRadius;
// Rotate quad to line angle
vec2 normalized = (u_penPoints.zw - u_penPoints.xy + epsilon) / (lineLength + epsilon);
vec2 pointDiff = u_penPoints.zw - u_penPoints.xy;
// Ensure line has a nonzero length so it's rendered properly
// As long as either component is nonzero, the line length will be nonzero
pointDiff.x = abs(pointDiff.x) < epsilon ? epsilon : pointDiff.x;
// The `normalized` vector holds rotational values equivalent to sine/cosine
// We're applying the standard rotation matrix formula to the position to rotate the quad to the line angle
vec2 normalized = pointDiff / max(lineLength, epsilon);
position = mat2(normalized.x, normalized.y, -normalized.y, normalized.x) * position;
// Translate quad
position += u_penPoints.xy;

54
test/helper/page-util.js Normal file
View File

@@ -0,0 +1,54 @@
/* global window, VirtualMachine, ScratchStorage, ScratchSVGRenderer */
/* eslint-env browser */
// Wait for all SVG skins to be loaded.
// TODO: this is extremely janky and should be removed once vm.loadProject waits for SVG skins to load
// https://github.com/LLK/scratch-render/issues/563
window.waitForSVGSkinLoad = renderer => new Promise(resolve => {
// eslint-disable-next-line prefer-const
let interval;
const waitInner = () => {
let numSVGSkins = 0;
let numLoadedSVGSkins = 0;
for (const skin of renderer._allSkins) {
if (skin.constructor.name !== 'SVGSkin') continue;
numSVGSkins++;
if (skin._svgRenderer.loaded) numLoadedSVGSkins++;
}
if (numSVGSkins === numLoadedSVGSkins) {
clearInterval(interval);
resolve();
}
};
interval = setInterval(waitInner, 1);
});
window.loadFileInputIntoVM = (fileInput, vm, render) => {
const reader = new FileReader();
return new Promise(resolve => {
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => window.waitForSVGSkinLoad(render))
.then(() => {
resolve();
});
};
reader.readAsArrayBuffer(fileInput.files[0]);
});
};
window.initVM = render => {
const vm = new VirtualMachine();
const storage = new ScratchStorage();
vm.attachStorage(storage);
vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
return vm;
};

View File

@@ -2,6 +2,7 @@
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
<script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script>
<script src="../helper/page-util.js"></script>
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
<script src="../../dist/web/scratch-render.js"></script>
@@ -17,38 +18,18 @@
window.devicePixelRatio = 1;
const gpuCanvas = document.getElementById('test');
var render = new ScratchRender(gpuCanvas);
var vm = new VirtualMachine();
var storage = new ScratchStorage();
var vm = initVM(render);
vm.attachStorage(storage);
vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
document.getElementById('file').addEventListener('click', e => {
document.body.removeChild(document.getElementById('loaded'));
});
document.getElementById('file').addEventListener('change', e => {
const reader = new FileReader();
const thisFileInput = e.target;
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => {
// we add a `#loaded` div to our document, the integration suite
// waits for that element to show up to assume the vm is ready
// to play!
const div = document.createElement('div');
div.id='loaded';
document.body.appendChild(div);
vm.greenFlag();
setTimeout(() => {
renderCpu();
}, 1000);
});
};
reader.readAsArrayBuffer(thisFileInput.files[0]);
const fileInput = document.getElementById('file');
const loadFile = loadFileInputIntoVM.bind(null, fileInput, vm, render);
fileInput.addEventListener('change', e => {
loadFile()
.then(() => {
vm.greenFlag();
setTimeout(() => {
renderCpu();
}, 1000);
});
});
const cpuCanvas = document.getElementById('cpu');

View File

@@ -2,6 +2,7 @@
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
<script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script>
<script src="../helper/page-util.js"></script>
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
<script src="../../dist/web/scratch-render.js"></script>
@@ -15,39 +16,13 @@
var canvas = document.getElementById('test');
var render = new ScratchRender(canvas);
var vm = new VirtualMachine();
var storage = new ScratchStorage();
var vm = initVM(render);
var mockMouse = data => vm.runtime.postIOData('mouse', {
canvasWidth: canvas.width,
canvasHeight: canvas.height,
...data,
});
vm.attachStorage(storage);
vm.attachRenderer(render);
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
document.getElementById('file').addEventListener('click', e => {
document.body.removeChild(document.getElementById('loaded'));
});
document.getElementById('file').addEventListener('change', e => {
const reader = new FileReader();
const thisFileInput = e.target;
reader.onload = () => {
vm.start();
vm.loadProject(reader.result)
.then(() => {
// we add a `#loaded` div to our document, the integration suite
// waits for that element to show up to assume the vm is ready
// to play!
const div = document.createElement('div');
div.id='loaded';
document.body.appendChild(div);
});
};
reader.readAsArrayBuffer(thisFileInput.files[0]);
});
const loadFile = loadFileInputIntoVM.bind(null, document.getElementById('file'), vm, render);
</script>
</body>

View File

@@ -1,29 +1,34 @@
/* global vm, render, Promise */
const {Chromeless} = require('chromeless');
const {chromium} = require('playwright-chromium');
const test = require('tap').test;
const path = require('path');
const chromeless = new Chromeless();
const indexHTML = path.resolve(__dirname, 'index.html');
const testDir = (...args) => path.resolve(__dirname, 'pick-tests', ...args);
const runFile = (file, action, script) =>
const runFile = async (file, action, page, script) => {
// start each test by going to the index.html, and loading the scratch file
chromeless.goto(`file://${indexHTML}`)
.setFileInput('#file', testDir(file))
// the index.html handler for file input will add a #loaded element when it
// finishes.
.wait('#loaded')
.evaluate(`function () {return (${script})(${action});}`)
;
await page.goto(`file://${indexHTML}`);
const fileInput = await page.$('#file');
await fileInput.setInputFiles(testDir(file));
await page.evaluate(() =>
// `loadFile` is defined on the page itself.
// eslint-disable-next-line no-undef
loadFile()
);
return page.evaluate(`(function () {return (${script})(${action});})()`);
};
// immediately invoked async function to let us wait for each test to finish before starting the next.
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const testOperation = async function (name, action, expect) {
await test(name, async t => {
const results = await runFile('test-mouse-touch.sb2', action, boundAction => {
const results = await runFile('test-mouse-touch.sb2', action, page, boundAction => {
vm.greenFlag();
const sendResults = [];
@@ -97,5 +102,10 @@ const runFile = (file, action, script) =>
}
// close the browser window we used
await chromeless.end();
})();
await browser.close();
})().catch(err => {
// Handle promise rejections by exiting with a nonzero code to ensure that tests don't erroneously pass
// eslint-disable-next-line no-console
console.error(err.message);
process.exit(1);
});

View File

@@ -1,55 +1,15 @@
/* global vm, Promise */
const {Chromeless} = require('chromeless');
const {chromium} = require('playwright-chromium');
const test = require('tap').test;
const path = require('path');
const fs = require('fs');
const chromeless = new Chromeless();
const allGpuModes = ['ForceCPU', 'ForceGPU', 'Automatic'];
const indexHTML = path.resolve(__dirname, 'index.html');
const testDir = (...args) => path.resolve(__dirname, 'scratch-tests', ...args);
const testFile = file => test(file, async t => {
// start each test by going to the index.html, and loading the scratch file
const says = await chromeless.goto(`file://${indexHTML}`)
.setFileInput('#file', testDir(file))
// the index.html handler for file input will add a #loaded element when it
// finishes.
.wait('#loaded')
.evaluate(() => {
// This function is run INSIDE the integration chrome browser via some
// injection and .toString() magic. We can return some "simple data"
// back across as a promise, so we will just log all the says that happen
// for parsing after.
// this becomes the `says` in the outer scope
const messages = [];
const TIMEOUT = 5000;
vm.runtime.on('SAY', (_, __, message) => {
messages.push(message);
});
vm.greenFlag();
const startTime = Date.now();
return Promise.resolve()
.then(async () => {
// waiting for all threads to complete, then we return
while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) {
if ((Date.now() - startTime) >= TIMEOUT) {
// if we push the message after end, the failure from tap is not very useful:
// "not ok test after end() was called"
messages.unshift(`fail Threads still running after ${TIMEOUT}ms`);
break;
}
await new Promise(resolve => setTimeout(resolve, 50));
}
return messages;
});
});
const checkOneGpuMode = (t, says) => {
// Map string messages to tap reporting methods. This will be used
// with events from scratch's runtime emitted on block instructions.
let didPlan = false;
@@ -99,17 +59,78 @@ const testFile = file => test(file, async t => {
t.fail('did not say "end"');
t.end();
}
});
};
const testFile = async (file, page) => {
// start each test by going to the index.html, and loading the scratch file
await page.goto(`file://${indexHTML}`);
const fileInput = await page.$('#file');
await fileInput.setInputFiles(testDir(file));
await page.evaluate(() =>
// `loadFile` is defined on the page itself.
// eslint-disable-next-line no-undef
loadFile()
);
const says = await page.evaluate(async useGpuModes => {
// This function is run INSIDE the integration chrome browser via some
// injection and .toString() magic. We can return some "simple data"
// back across as a promise, so we will just log all the says that happen
// for parsing after.
// this becomes the `says` in the outer scope
const allMessages = {};
const TIMEOUT = 5000;
vm.runtime.on('SAY', (_, __, message) => {
const messages = allMessages[vm.renderer._useGpuMode];
messages.push(message);
});
for (const useGpuMode of useGpuModes) {
const messages = allMessages[useGpuMode] = [];
vm.renderer.setUseGpuMode(useGpuMode);
vm.greenFlag();
const startTime = Date.now();
// wait for all threads to complete before moving on to the next mode
while (vm.runtime.threads.some(thread => vm.runtime.isActiveThread(thread))) {
if ((Date.now() - startTime) >= TIMEOUT) {
// if we push the message after end, the failure from tap is not very useful:
// "not ok test after end() was called"
messages.unshift(`fail Threads still running after ${TIMEOUT}ms`);
break;
}
await new Promise(resolve => setTimeout(resolve, 50));
}
}
return allMessages;
}, allGpuModes);
for (const gpuMode of allGpuModes) {
test(`File: ${file}, GPU Mode: ${gpuMode}`, t => checkOneGpuMode(t, says[gpuMode]));
}
};
// immediately invoked async function to let us wait for each test to finish before starting the next.
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
const files = fs.readdirSync(testDir())
.filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3'));
for (const file of files) {
await testFile(file);
await testFile(file, page);
}
// close the browser window we used
await chromeless.end();
})();
await browser.close();
})().catch(err => {
// Handle promise rejections by exiting with a nonzero code to ensure that tests don't erroneously pass
// eslint-disable-next-line no-console
console.error(err.message);
process.exit(1);
});

Binary file not shown.