Compare commits

..

1 Commits

Author SHA1 Message Date
greenkeeper[bot]
8c86ffcf3a chore(package): update uglifyjs-webpack-plugin to version 2.1.0
Closes #345
2018-12-22 16:28:36 +00:00
18 changed files with 244 additions and 700 deletions

View File

@@ -37,23 +37,23 @@
"gh-pages": "^1.0.0",
"jsdoc": "^3.5.5",
"json": "^9.0.4",
"scratch-vm": "0.2.0-prerelease.20190207224121",
"scratch-vm": "0.2.0-prerelease.20181024204838",
"tap": "^11.0.0",
"travis-after-all": "^1.4.4",
"uglifyjs-webpack-plugin": "^1.2.5",
"uglifyjs-webpack-plugin": "^2.1.0",
"webpack": "^4.8.0",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.4"
},
"dependencies": {
"grapheme-breaker": "0.3.2",
"hull.js": "0.2.11",
"hull.js": "0.2.10",
"ify-loader": "1.0.4",
"linebreak": "0.3.0",
"minilog": "3.1.0",
"raw-loader": "^0.5.1",
"scratch-storage": "^1.0.0",
"scratch-svg-renderer": "0.2.0-prerelease.20190419183947",
"scratch-svg-renderer": "0.2.0-prerelease.20181220183040",
"twgl.js": "4.4.0"
}
}

View File

@@ -59,15 +59,6 @@ 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.
@@ -79,31 +70,20 @@ 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, textureData);
this._silhouette.update(textureData);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmapData);
this._silhouette.update(bitmapData);
} else {
// TODO: mipmaps?
const textureOptions = {
auto: true,
wrap: gl.CLAMP_TO_EDGE,
src: textureData
src: bitmapData
};
this._texture = twgl.createTexture(gl, textureOptions);
this._silhouette.update(textureData);
this._silhouette.update(bitmapData);
}
// Do these last in case any of the above throws an exception

View File

@@ -36,10 +36,8 @@ 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 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);
}
// Apply texture effect transform.
EffectTransform.transformPoint(drawable, localPosition, localPosition);
return localPosition;
};
@@ -428,16 +426,16 @@ class Drawable {
* Should the drawable use NEAREST NEIGHBOR or LINEAR INTERPOLATION mode
*/
get useNearest () {
// Raster skins (bitmaps) should always prefer nearest neighbor
if (this.skin.isRaster) {
return true;
}
// We can't use nearest neighbor unless we are a multiple of 90 rotation
if (this._direction % 90 !== 0) {
return false;
}
// Raster skins (bitmaps) should always prefer nearest neighbor
if (this.skin.isRaster) {
return true;
}
// If the scale of the skin is very close to 100 (0.99999 variance is okay I guess)
if (Math.abs(this.scale[0]) > 99 && Math.abs(this.scale[0]) < 101 &&
Math.abs(this.scale[1]) > 99 && Math.abs(this.scale[1]) < 101) {
@@ -538,15 +536,13 @@ class Drawable {
_getTransformedHullPoints () {
const projection = twgl.m4.ortho(-1, 1, -1, 1, -1, 1);
const skinSize = this.skin.size;
const halfXPixel = 1 / skinSize[0] / 2;
const halfYPixel = 1 / skinSize[1] / 2;
const tm = twgl.m4.multiply(this._uniforms.u_modelMatrix, projection);
const transformedHullPoints = [];
for (let i = 0; i < this._convexHullPoints.length; i++) {
const point = this._convexHullPoints[i];
const glPoint = twgl.v3.create(
0.5 + (-point[0] / skinSize[0]) - halfXPixel,
(point[1] / skinSize[1]) - 0.5 + halfYPixel,
0.5 + (-point[0] / skinSize[0]),
(point[1] / skinSize[1]) - 0.5,
0
);
twgl.m4.transformPoint(tm, glPoint, glPoint);

View File

@@ -154,13 +154,6 @@ 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]
*/
@@ -188,9 +181,8 @@ class PenSkin extends Skin {
clear () {
const gl = this._renderer.gl;
twgl.bindFramebufferInfo(gl, this._framebuffer);
/* Reset framebuffer to transparent black */
gl.clearColor(0, 0, 0, 0);
gl.clearColor(1, 1, 1, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
const ctx = this._canvas.getContext('2d');
@@ -606,7 +598,7 @@ class PenSkin extends Skin {
this._silhouetteBuffer = twgl.createFramebufferInfo(gl, [{format: gl.RGBA}], width, height);
}
gl.clearColor(0, 0, 0, 0);
gl.clearColor(1, 1, 1, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
this._silhouetteDirty = true;

View File

@@ -132,9 +132,6 @@ 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 = [];
@@ -246,14 +243,6 @@ 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.
@@ -728,16 +717,9 @@ class RenderWebGL extends EventEmitter {
const bounds = this._candidatesBounds(candidates);
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) {
// 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) {
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b);
}
@@ -746,45 +728,29 @@ class RenderWebGL extends EventEmitter {
const color = __touchingColor;
const hasMask = Boolean(mask3b);
// Scratch Space - +y is top
for (let y = bounds.bottom; y <= bounds.top; y++) {
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) {
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
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 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;
}
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;
}
}
}
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();
@@ -965,11 +931,7 @@ class RenderWebGL extends EventEmitter {
const worldPos = twgl.v3.create();
drawable.updateMatrix();
if (drawable.skin) {
drawable.skin.updateSilhouette();
} else {
log.warn(`Could not find skin for drawable with id: ${drawableID}`);
}
drawable.skin.updateSilhouette();
for (worldPos[1] = bounds.bottom; worldPos[1] <= bounds.top; worldPos[1]++) {
for (worldPos[0] = bounds.left; worldPos[0] <= bounds.right; worldPos[0]++) {
@@ -1001,11 +963,7 @@ class RenderWebGL extends EventEmitter {
// default pick list ignores visible and ghosted sprites.
if (drawable.getVisible() && drawable.getUniforms().u_ghost !== 0) {
drawable.updateMatrix();
if (drawable.skin) {
drawable.skin.updateSilhouette();
} else {
log.warn(`Could not find skin for drawable with id: ${id}`);
}
drawable.skin.updateSilhouette();
return true;
}
return false;
@@ -1082,28 +1040,11 @@ 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;
@@ -1350,20 +1291,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 sx = this._xRight - Math.min(FENCE_WIDTH, inset);
const aabb = drawable.getFastBounds();
const sx = this._xRight - Math.min(FENCE_WIDTH, Math.floor((aabb.right - aabb.left) / 2));
if (aabb.right + dx < -sx) {
x = Math.ceil(drawable._position[0] - (sx + aabb.right));
x = drawable._position[0] - (sx + aabb.right);
} else if (aabb.left + dx > sx) {
x = Math.floor(drawable._position[0] + (sx - aabb.left));
x = drawable._position[0] + (sx - aabb.left);
}
const sy = this._yTop - Math.min(FENCE_WIDTH, inset);
const sy = this._yTop - Math.min(FENCE_WIDTH, Math.floor((aabb.top - aabb.bottom) / 2));
if (aabb.top + dy < -sy) {
y = Math.ceil(drawable._position[1] - (sy + aabb.top));
y = drawable._position[1] - (sy + aabb.top);
} else if (aabb.bottom + dy > sy) {
y = Math.floor(drawable._position[1] + (sy - aabb.bottom));
y = drawable._position[1] + (sy - aabb.bottom);
}
return [x, y];
}
@@ -1620,14 +1561,7 @@ 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);
}
@@ -1810,25 +1744,4 @@ 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,14 +77,9 @@ 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, textureData);
this._silhouette.update(textureData);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas);
}
});
}
@@ -103,28 +98,20 @@ 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, textureData);
this._silhouette.update(textureData);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas);
this._silhouette.update(this._svgRenderer.canvas);
} else {
// TODO: mipmaps?
const textureOptions = {
auto: true,
wrap: gl.CLAMP_TO_EDGE,
src: textureData
src: this._svgRenderer.canvas
};
this._texture = twgl.createTexture(gl, textureOptions);
this._silhouette.update(textureData);
this._silhouette.update(this._svgRenderer.canvas);
}
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, _colorData: data}, x, y) => {
const getPoint = ({_width: width, _height: height, _data: 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) * 4) + 3];
return data[(y * width) + x];
};
/**
@@ -76,6 +76,7 @@ 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);
@@ -87,33 +88,28 @@ class Silhouette {
* rendering can be queried from.
*/
update (bitmapData) {
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');
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;
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(bitmapData, 0, 0, width, height);
imageData = ctx.getImageData(0, 0, width, height);
if (!(width && height)) {
return;
}
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];
}
}
/**
@@ -170,7 +166,7 @@ class Silhouette {
* @return {boolean} If the nearest pixel has an alpha value.
*/
isTouchingNearest (vec) {
if (!this._colorData) return;
if (!this._data) return;
return getPoint(
this,
Math.floor(vec[0] * (this._width - 1)),
@@ -185,7 +181,7 @@ class Silhouette {
* @return {boolean} Any of the pixels have some alpha.
*/
isTouchingLinear (vec) {
if (!this._colorData) return;
if (!this._data) 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,13 +76,6 @@ 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.
*/
@@ -143,15 +136,6 @@ 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,9 +1,7 @@
module.exports = {
root: true,
extends: ['scratch'],
env: {
browser: true
},
rules: {
'no-console': 'off'
}
};

View File

@@ -1,37 +0,0 @@
// 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">
<select id="fudgeproperty" onchange="onFudgePropertyChanged(this.value)">
<option value="posx">Position X</option>
<option value="posy">Position Y</option>
<option value="direction">Direction</option>
@@ -27,12 +27,149 @@
<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">
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any" oninput="onFudgeChanged(this.value)" onchange="onFudgeChanged(this.value)">
</p>
<p>
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number">
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number">
<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)">
</p>
<script src="playground.js"></script>
<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>
</body>
</html>

View File

@@ -1,141 +0,0 @@
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

@@ -1,74 +0,0 @@
<!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

@@ -1,196 +0,0 @@
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,13 +45,13 @@ uniform sampler2D u_skin;
varying vec2 v_texCoord;
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color) || defined(ENABLE_brightness))
// 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 on some mobile devices
// Smaller values can cause problems with "color" and "brightness" effects on some mobile devices
const float epsilon = 1e-3;
// Convert an RGB color to Hue, Saturation, and Value.
@@ -103,7 +103,7 @@ vec3 convertHSV2RGB(vec3 hsv)
float c = hsv.z * hsv.y;
return rgb * c + hsv.z - c;
}
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color) || defined(ENABLE_brightness))
const vec2 kCenter = vec2(0.5, 0.5);
@@ -164,27 +164,31 @@ void main()
gl_FragColor = u_silhouetteColor;
#else // DRAW_MODE_silhouette
#if defined(ENABLE_color)
#if defined(ENABLE_color) || defined(ENABLE_brightness)
{
vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
// 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);
#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 (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
hsv.x = mod(hsv.x + u_color, 1.0);
if (hsv.x < 0.0) hsv.x += 1.0;
hsv.x = mod(hsv.x + u_color, 1.0);
if (hsv.x < 0.0) hsv.x += 1.0;
}
#endif // ENABLE_color
#ifdef ENABLE_brightness
hsv.z = clamp(hsv.z + u_brightness, 0.0, 1.0);
#endif // ENABLE_brightness
gl_FragColor.rgb = convertHSV2RGB(hsv);
}
#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)
#endif // defined(ENABLE_color) || defined(ENABLE_brightness)
#ifdef DRAW_MODE_colorMask
vec3 maskDistance = abs(gl_FragColor.rgb - u_colorMask);
@@ -194,6 +198,12 @@ 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.endsWith('.sb3'));
.filter(uri => uri.endsWith('.sb2') || uri.endsWidth('.sb3'));
for (const file of files) {
await testFile(file);

View File

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