Compare commits
45 Commits
greenkeepe
...
greenkeepe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
097daeec28 | ||
|
|
d73aeb1ac1 | ||
|
|
298200f2ee | ||
|
|
b9411bf51e | ||
|
|
3513cd77ae | ||
|
|
d8c9f339f7 | ||
|
|
aaffc77b23 | ||
|
|
d867e62c74 | ||
|
|
a7bec3a958 | ||
|
|
0ce50b6cd6 | ||
|
|
562e535a13 | ||
|
|
42d3f6c012 | ||
|
|
9c01f364d4 | ||
|
|
5d085f678f | ||
|
|
7df536f492 | ||
|
|
a41bcafac7 | ||
|
|
5566a600ba | ||
|
|
34072d2f53 | ||
|
|
fdd02a6bd6 | ||
|
|
ec141ff76d | ||
|
|
3f68e18b2e | ||
|
|
813792359f | ||
|
|
7349c1967c | ||
|
|
b873b6fec4 | ||
|
|
51c867fa6d | ||
|
|
ddccd0fa79 | ||
|
|
b35adaef88 | ||
|
|
3612461f9c | ||
|
|
7c710c0680 | ||
|
|
fe91133565 | ||
|
|
409df2a74d | ||
|
|
4b03eb29fd | ||
|
|
51bceb68e5 | ||
|
|
70c64f035c | ||
|
|
4b7558f2d2 | ||
|
|
84f878e553 | ||
|
|
b6424a2f80 | ||
|
|
0b47d87f2f | ||
|
|
41e21ed129 | ||
|
|
6d28f42d57 | ||
|
|
5081a71fe9 | ||
|
|
0a214a989d | ||
|
|
3d02a6c769 | ||
|
|
60e7e568d5 | ||
|
|
b448aac1f3 |
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
54
test/helper/page-util.js
Normal 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;
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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.
BIN
test/integration/scratch-tests/fencing-bounds.sb3
Normal file
BIN
test/integration/scratch-tests/fencing-bounds.sb3
Normal file
Binary file not shown.
Reference in New Issue
Block a user