Compare commits
82 Commits
brightness
...
greenkeepe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41378a0d65 | ||
|
|
4a55d63ada | ||
|
|
c9c780aa69 | ||
|
|
590c2ca084 | ||
|
|
757d7e3c96 | ||
|
|
e365a909dc | ||
|
|
bffe80086e | ||
|
|
008dc5b15b | ||
|
|
9177705e04 | ||
|
|
b304ea8fdf | ||
|
|
f9428ee096 | ||
|
|
9526612d79 | ||
|
|
fb767b7553 | ||
|
|
e864018d87 | ||
|
|
e0b420a183 | ||
|
|
a24b853af6 | ||
|
|
73896b6f32 | ||
|
|
80630a64da | ||
|
|
e31934f6a9 | ||
|
|
8f007c0986 | ||
|
|
3c79a5562e | ||
|
|
d59d45b6c8 | ||
|
|
19ee8e8eaa | ||
|
|
fe01fea9d0 | ||
|
|
5fb9346036 | ||
|
|
3d373571f8 | ||
|
|
152cf028cc | ||
|
|
147b79d319 | ||
|
|
f2a7085492 | ||
|
|
996a1d6cf7 | ||
|
|
61bf4c84c3 | ||
|
|
7628c1e7f9 | ||
|
|
9f7bd971c9 | ||
|
|
44d2fdeba8 | ||
|
|
e022222365 | ||
|
|
be5ab2e689 | ||
|
|
c9f86ef53b | ||
|
|
4bf233ef36 | ||
|
|
253cbd019d | ||
|
|
1f0f89920a | ||
|
|
25df9f1ab7 | ||
|
|
7680270f40 | ||
|
|
c7b22b58c2 | ||
|
|
f2d457a827 | ||
|
|
e3c68e7122 | ||
|
|
e64d8727ec | ||
|
|
c390124df4 | ||
|
|
8bd6241160 | ||
|
|
c8b9516219 | ||
|
|
1db67a474e | ||
|
|
028b4eba3f | ||
|
|
59cef02fdb | ||
|
|
99d6e46f7e | ||
|
|
992977d6c6 | ||
|
|
a358c8f916 | ||
|
|
e8d71277e2 | ||
|
|
b4f9f28417 | ||
|
|
fba2d90fda | ||
|
|
31db3d8596 | ||
|
|
898d5d7885 | ||
|
|
9b11ac894d | ||
|
|
fc6fcd0543 | ||
|
|
b77f4c663a | ||
|
|
5e5a423d39 | ||
|
|
402cfbf99f | ||
|
|
a0dd716c23 | ||
|
|
183919a20a | ||
|
|
3cfafebb2e | ||
|
|
931ff270dd | ||
|
|
cc448951f9 | ||
|
|
cfa0194ab8 | ||
|
|
2b224eb9da | ||
|
|
735c7caaae | ||
|
|
ad1b7111c8 | ||
|
|
e54b590d56 | ||
|
|
355a8c5395 | ||
|
|
f1a7aab5a6 | ||
|
|
bb84abab87 | ||
|
|
75772989ea | ||
|
|
cf5aafc12f | ||
|
|
a5f852fcc2 | ||
|
|
07544595fd |
@@ -37,7 +37,7 @@
|
||||
"gh-pages": "^1.0.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"json": "^9.0.4",
|
||||
"scratch-vm": "0.2.0-prerelease.20181024204838",
|
||||
"scratch-vm": "0.2.0-prerelease.20190207224121",
|
||||
"tap": "^11.0.0",
|
||||
"travis-after-all": "^1.4.4",
|
||||
"uglifyjs-webpack-plugin": "^1.2.5",
|
||||
@@ -53,7 +53,7 @@
|
||||
"minilog": "3.1.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"scratch-storage": "^1.0.0",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20181220183040",
|
||||
"twgl.js": "4.4.0"
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20190419183947",
|
||||
"twgl.js": "4.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,15 @@ class BitmapSkin extends Skin {
|
||||
return this._texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds of the drawable for determining its fenced position.
|
||||
* @param {Array<number>} drawable - The Drawable instance this skin is using.
|
||||
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB for bitmaps.
|
||||
*/
|
||||
getFenceBounds (drawable) {
|
||||
return drawable.getAABB();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of this skin to a snapshot of the provided bitmap data.
|
||||
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} bitmapData - new contents for this skin.
|
||||
@@ -70,20 +79,31 @@ class BitmapSkin extends Skin {
|
||||
setBitmap (bitmapData, costumeResolution, rotationCenter) {
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
// Preferably bitmapData is ImageData. ImageData speeds up updating
|
||||
// Silhouette and is better handled by more browsers in regards to
|
||||
// memory.
|
||||
let textureData = bitmapData;
|
||||
if (bitmapData instanceof HTMLCanvasElement) {
|
||||
// Given a HTMLCanvasElement get the image data to pass to webgl and
|
||||
// Silhouette.
|
||||
const context = bitmapData.getContext('2d');
|
||||
textureData = context.getImageData(0, 0, bitmapData.width, bitmapData.height);
|
||||
}
|
||||
|
||||
if (this._texture) {
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bitmapData);
|
||||
this._silhouette.update(bitmapData);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
|
||||
this._silhouette.update(textureData);
|
||||
} else {
|
||||
// TODO: mipmaps?
|
||||
const textureOptions = {
|
||||
auto: true,
|
||||
wrap: gl.CLAMP_TO_EDGE,
|
||||
src: bitmapData
|
||||
src: textureData
|
||||
};
|
||||
|
||||
this._texture = twgl.createTexture(gl, textureOptions);
|
||||
this._silhouette.update(bitmapData);
|
||||
this._silhouette.update(textureData);
|
||||
}
|
||||
|
||||
// Do these last in case any of the above throws an exception
|
||||
|
||||
@@ -36,8 +36,10 @@ const getLocalPosition = (drawable, vec) => {
|
||||
// localPosition matches that transformation.
|
||||
localPosition[0] = 0.5 - (((v0 * m[0]) + (v1 * m[4]) + m[12]) / d);
|
||||
localPosition[1] = (((v0 * m[1]) + (v1 * m[5]) + m[13]) / d) + 0.5;
|
||||
// Apply texture effect transform.
|
||||
EffectTransform.transformPoint(drawable, localPosition, localPosition);
|
||||
// Apply texture effect transform if the localPosition is within the drawable's space.
|
||||
if ((localPosition[0] >= 0 && localPosition[0] < 1) && (localPosition[1] >= 0 && localPosition[1] < 1)) {
|
||||
EffectTransform.transformPoint(drawable, localPosition, localPosition);
|
||||
}
|
||||
return localPosition;
|
||||
};
|
||||
|
||||
|
||||
@@ -154,6 +154,13 @@ class PenSkin extends Skin {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if alpha is premultiplied, false otherwise
|
||||
*/
|
||||
get hasPremultipliedAlpha () {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Array<number>} the "native" size, in texels, of this skin. [width, height]
|
||||
*/
|
||||
@@ -181,8 +188,9 @@ class PenSkin extends Skin {
|
||||
clear () {
|
||||
const gl = this._renderer.gl;
|
||||
twgl.bindFramebufferInfo(gl, this._framebuffer);
|
||||
|
||||
gl.clearColor(1, 1, 1, 0);
|
||||
|
||||
/* Reset framebuffer to transparent black */
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
const ctx = this._canvas.getContext('2d');
|
||||
@@ -598,7 +606,7 @@ class PenSkin extends Skin {
|
||||
this._silhouetteBuffer = twgl.createFramebufferInfo(gl, [{format: gl.RGBA}], width, height);
|
||||
}
|
||||
|
||||
gl.clearColor(1, 1, 1, 0);
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
this._silhouetteDirty = true;
|
||||
|
||||
@@ -132,6 +132,9 @@ class RenderWebGL extends EventEmitter {
|
||||
throw new Error('Could not get WebGL context: this browser or environment may not support WebGL.');
|
||||
}
|
||||
|
||||
/** @type {RenderWebGL.UseGpuModes} */
|
||||
this._useGpuMode = RenderWebGL.UseGpuModes.Automatic;
|
||||
|
||||
/** @type {Drawable[]} */
|
||||
this._allDrawables = [];
|
||||
|
||||
@@ -243,6 +246,14 @@ class RenderWebGL extends EventEmitter {
|
||||
this._debugCanvas = canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control the use of the GPU or CPU paths in `isTouchingColor`.
|
||||
* @param {RenderWebGL.UseGpuModes} useGpuMode - automatically decide, force CPU, or force GPU.
|
||||
*/
|
||||
setUseGpuMode (useGpuMode) {
|
||||
this._useGpuMode = useGpuMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set logical size of the stage in Scratch units.
|
||||
* @param {int} xLeft The left edge's x-coordinate. Scratch 2 uses -240.
|
||||
@@ -717,9 +728,16 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
const bounds = this._candidatesBounds(candidates);
|
||||
|
||||
// if there are just too many pixels to CPU render efficently, we
|
||||
// need to let readPixels happen
|
||||
if (bounds.width * bounds.height * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
|
||||
const maxPixelsForCPU = this._getMaxPixelsForCPU();
|
||||
|
||||
const debugCanvasContext = this._debugCanvas && this._debugCanvas.getContext('2d');
|
||||
if (debugCanvasContext) {
|
||||
this._debugCanvas.width = bounds.width;
|
||||
this._debugCanvas.height = bounds.height;
|
||||
}
|
||||
|
||||
// if there are just too many pixels to CPU render efficiently, we need to let readPixels happen
|
||||
if (bounds.width * bounds.height * (candidates.length + 1) >= maxPixelsForCPU) {
|
||||
this._isTouchingColorGpuStart(drawableID, candidates.map(({id}) => id).reverse(), bounds, color3b, mask3b);
|
||||
}
|
||||
|
||||
@@ -728,29 +746,45 @@ class RenderWebGL extends EventEmitter {
|
||||
const color = __touchingColor;
|
||||
const hasMask = Boolean(mask3b);
|
||||
|
||||
// Scratch Space - +y is top
|
||||
for (let y = bounds.bottom; y <= bounds.top; y++) {
|
||||
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= __cpuTouchingColorPixelCount) {
|
||||
if (bounds.width * (y - bounds.bottom) * (candidates.length + 1) >= maxPixelsForCPU) {
|
||||
return this._isTouchingColorGpuFin(bounds, color3b, y - bounds.bottom);
|
||||
}
|
||||
// Scratch Space - +y is top
|
||||
for (let x = bounds.left; x <= bounds.right; x++) {
|
||||
point[1] = y;
|
||||
point[0] = x;
|
||||
if (
|
||||
// if we use a mask, check our sample color
|
||||
(hasMask ?
|
||||
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
|
||||
drawable.isTouching(point)) &&
|
||||
// and the target color is drawn at this pixel
|
||||
colorMatches(RenderWebGL.sampleColor3b(point, candidates, color), color3b, 0)
|
||||
) {
|
||||
return true;
|
||||
// if we use a mask, check our sample color...
|
||||
if (hasMask ?
|
||||
maskMatches(Drawable.sampleColor4b(point, drawable, color), mask3b) :
|
||||
drawable.isTouching(point)) {
|
||||
RenderWebGL.sampleColor3b(point, candidates, color);
|
||||
if (debugCanvasContext) {
|
||||
debugCanvasContext.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
|
||||
debugCanvasContext.fillRect(x - bounds.left, bounds.bottom - y, 1, 1);
|
||||
}
|
||||
// ...and the target color is drawn at this pixel
|
||||
if (colorMatches(color, color3b, 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_getMaxPixelsForCPU () {
|
||||
switch (this._useGpuMode) {
|
||||
case RenderWebGL.UseGpuModes.ForceCPU:
|
||||
return Infinity;
|
||||
case RenderWebGL.UseGpuModes.ForceGPU:
|
||||
return 0;
|
||||
case RenderWebGL.UseGpuModes.Automatic:
|
||||
default:
|
||||
return __cpuTouchingColorPixelCount;
|
||||
}
|
||||
}
|
||||
|
||||
_isTouchingColorGpuStart (drawableID, candidateIDs, bounds, color3b, mask3b) {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
@@ -1048,11 +1082,28 @@ class RenderWebGL extends EventEmitter {
|
||||
const scratchY = this._nativeSize[1] * ((y / this._gl.canvas.clientHeight) - 0.5);
|
||||
|
||||
const gl = this._gl;
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
|
||||
const bounds = drawable.getFastBounds();
|
||||
bounds.snapToInt();
|
||||
|
||||
// Set a reasonable max limit width and height for the bufferInfo bounds
|
||||
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
||||
const clampedWidth = Math.min(2048, bounds.width, maxTextureSize);
|
||||
const clampedHeight = Math.min(2048, bounds.height, maxTextureSize);
|
||||
|
||||
// Make a new bufferInfo since this._queryBufferInfo is limited to 480x360
|
||||
const attachments = [
|
||||
{format: gl.RGBA},
|
||||
{format: gl.DEPTH_STENCIL}
|
||||
];
|
||||
const bufferInfo = twgl.createFramebufferInfo(gl, attachments, clampedWidth, clampedHeight);
|
||||
|
||||
// If the new bufferInfo is invalid, fall back to using the smaller _queryBufferInfo
|
||||
twgl.bindFramebufferInfo(gl, bufferInfo);
|
||||
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
|
||||
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
|
||||
}
|
||||
|
||||
// Translate to scratch units relative to the drawable
|
||||
const pickX = scratchX - bounds.left;
|
||||
const pickY = scratchY + bounds.top;
|
||||
@@ -1299,20 +1350,20 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
const dx = x - drawable._position[0];
|
||||
const dy = y - drawable._position[1];
|
||||
const aabb = drawable._skin.getFenceBounds(drawable);
|
||||
const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2);
|
||||
|
||||
const aabb = drawable.getFastBounds();
|
||||
|
||||
const sx = this._xRight - Math.min(FENCE_WIDTH, Math.floor((aabb.right - aabb.left) / 2));
|
||||
const sx = this._xRight - Math.min(FENCE_WIDTH, inset);
|
||||
if (aabb.right + dx < -sx) {
|
||||
x = drawable._position[0] - (sx + aabb.right);
|
||||
x = Math.ceil(drawable._position[0] - (sx + aabb.right));
|
||||
} else if (aabb.left + dx > sx) {
|
||||
x = drawable._position[0] + (sx - aabb.left);
|
||||
x = Math.floor(drawable._position[0] + (sx - aabb.left));
|
||||
}
|
||||
const sy = this._yTop - Math.min(FENCE_WIDTH, Math.floor((aabb.top - aabb.bottom) / 2));
|
||||
const sy = this._yTop - Math.min(FENCE_WIDTH, inset);
|
||||
if (aabb.top + dy < -sy) {
|
||||
y = drawable._position[1] - (sy + aabb.top);
|
||||
y = Math.ceil(drawable._position[1] - (sy + aabb.top));
|
||||
} else if (aabb.bottom + dy > sy) {
|
||||
y = drawable._position[1] + (sy - aabb.bottom);
|
||||
y = Math.floor(drawable._position[1] + (sy - aabb.bottom));
|
||||
}
|
||||
return [x, y];
|
||||
}
|
||||
@@ -1569,7 +1620,14 @@ class RenderWebGL extends EventEmitter {
|
||||
}
|
||||
|
||||
twgl.setUniforms(currentShader, uniforms);
|
||||
|
||||
|
||||
/* adjust blend function for this skin */
|
||||
if (drawable.skin.hasPremultipliedAlpha){
|
||||
gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
} else {
|
||||
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
}
|
||||
|
||||
twgl.drawBufferInfo(gl, this._bufferInfo, gl.TRIANGLES);
|
||||
}
|
||||
|
||||
@@ -1752,4 +1810,25 @@ class RenderWebGL extends EventEmitter {
|
||||
// :3
|
||||
RenderWebGL.prototype.canHazPixels = RenderWebGL.prototype.extractDrawable;
|
||||
|
||||
/**
|
||||
* Values for setUseGPU()
|
||||
* @enum {string}
|
||||
*/
|
||||
RenderWebGL.UseGpuModes = {
|
||||
/**
|
||||
* Heuristically decide whether to use the GPU path, the CPU path, or a dynamic mixture of the two.
|
||||
*/
|
||||
Automatic: 'Automatic',
|
||||
|
||||
/**
|
||||
* Always use the GPU path.
|
||||
*/
|
||||
ForceGPU: 'ForceGPU',
|
||||
|
||||
/**
|
||||
* Always use the CPU path.
|
||||
*/
|
||||
ForceCPU: 'ForceCPU'
|
||||
};
|
||||
|
||||
module.exports = RenderWebGL;
|
||||
|
||||
@@ -77,9 +77,14 @@ class SVGSkin extends Skin {
|
||||
this._textureScale = newScale;
|
||||
this._svgRenderer._draw(this._textureScale, () => {
|
||||
if (this._textureScale === newScale) {
|
||||
const canvas = this._svgRenderer.canvas;
|
||||
const context = canvas.getContext('2d');
|
||||
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const gl = this._renderer.gl;
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
|
||||
this._silhouette.update(textureData);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -98,20 +103,28 @@ class SVGSkin extends Skin {
|
||||
this._svgRenderer.fromString(svgData, 1, () => {
|
||||
const gl = this._renderer.gl;
|
||||
this._textureScale = this._maxTextureScale = 1;
|
||||
|
||||
// Pull out the ImageData from the canvas. ImageData speeds up
|
||||
// updating Silhouette and is better handled by more browsers in
|
||||
// regards to memory.
|
||||
const canvas = this._svgRenderer.canvas;
|
||||
const context = canvas.getContext('2d');
|
||||
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (this._texture) {
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._svgRenderer.canvas);
|
||||
this._silhouette.update(this._svgRenderer.canvas);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureData);
|
||||
this._silhouette.update(textureData);
|
||||
} else {
|
||||
// TODO: mipmaps?
|
||||
const textureOptions = {
|
||||
auto: true,
|
||||
wrap: gl.CLAMP_TO_EDGE,
|
||||
src: this._svgRenderer.canvas
|
||||
src: textureData
|
||||
};
|
||||
|
||||
this._texture = twgl.createTexture(gl, textureOptions);
|
||||
this._silhouette.update(this._svgRenderer.canvas);
|
||||
this._silhouette.update(textureData);
|
||||
}
|
||||
|
||||
const maxDimension = Math.max(this._svgRenderer.canvas.width, this._svgRenderer.canvas.height);
|
||||
|
||||
@@ -19,12 +19,12 @@ let __SilhouetteUpdateCanvas;
|
||||
* @param {number} y - y
|
||||
* @return {number} Alpha value for x/y position
|
||||
*/
|
||||
const getPoint = ({_width: width, _height: height, _data: data}, x, y) => {
|
||||
const getPoint = ({_width: width, _height: height, _colorData: data}, x, y) => {
|
||||
// 0 if outside bouds, otherwise read from data.
|
||||
if (x >= width || y >= height || x < 0 || y < 0) {
|
||||
return 0;
|
||||
}
|
||||
return data[(y * width) + x];
|
||||
return data[(((y * width) + x) * 4) + 3];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -76,7 +76,6 @@ class Silhouette {
|
||||
* The data representing a skin's silhouette shape.
|
||||
* @type {Uint8ClampedArray}
|
||||
*/
|
||||
this._data = null;
|
||||
this._colorData = null;
|
||||
|
||||
this.colorAtNearest = this.colorAtLinear = (_, dst) => dst.fill(0);
|
||||
@@ -88,28 +87,33 @@ class Silhouette {
|
||||
* rendering can be queried from.
|
||||
*/
|
||||
update (bitmapData) {
|
||||
const canvas = Silhouette._updateCanvas();
|
||||
const width = this._width = canvas.width = bitmapData.width;
|
||||
const height = this._height = canvas.height = bitmapData.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let imageData;
|
||||
if (bitmapData instanceof ImageData) {
|
||||
// If handed ImageData directly, use it directly.
|
||||
imageData = bitmapData;
|
||||
this._width = bitmapData.width;
|
||||
this._height = bitmapData.height;
|
||||
} else {
|
||||
// Draw about anything else to our update canvas and poll image data
|
||||
// from that.
|
||||
const canvas = Silhouette._updateCanvas();
|
||||
const width = this._width = canvas.width = bitmapData.width;
|
||||
const height = this._height = canvas.height = bitmapData.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!(width && height)) {
|
||||
return;
|
||||
if (!(width && height)) {
|
||||
return;
|
||||
}
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.drawImage(bitmapData, 0, 0, width, height);
|
||||
imageData = ctx.getImageData(0, 0, width, height);
|
||||
}
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.drawImage(bitmapData, 0, 0, width, height);
|
||||
const imageData = ctx.getImageData(0, 0, width, height);
|
||||
|
||||
this._data = new Uint8ClampedArray(imageData.data.length / 4);
|
||||
this._colorData = imageData.data;
|
||||
// delete our custom overriden "uninitalized" color functions
|
||||
// let the prototype work for itself
|
||||
delete this.colorAtNearest;
|
||||
delete this.colorAtLinear;
|
||||
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
this._data[i / 4] = imageData.data[i + 3];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +170,7 @@ class Silhouette {
|
||||
* @return {boolean} If the nearest pixel has an alpha value.
|
||||
*/
|
||||
isTouchingNearest (vec) {
|
||||
if (!this._data) return;
|
||||
if (!this._colorData) return;
|
||||
return getPoint(
|
||||
this,
|
||||
Math.floor(vec[0] * (this._width - 1)),
|
||||
@@ -181,7 +185,7 @@ class Silhouette {
|
||||
* @return {boolean} Any of the pixels have some alpha.
|
||||
*/
|
||||
isTouchingLinear (vec) {
|
||||
if (!this._data) return;
|
||||
if (!this._colorData) return;
|
||||
const x = Math.floor(vec[0] * (this._width - 1));
|
||||
const y = Math.floor(vec[1] * (this._height - 1));
|
||||
return getPoint(this, x, y) > 0 ||
|
||||
|
||||
16
src/Skin.js
16
src/Skin.js
@@ -76,6 +76,13 @@ class Skin extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if alpha is premultiplied, false otherwise
|
||||
*/
|
||||
get hasPremultipliedAlpha () {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {int} the unique ID for this Skin.
|
||||
*/
|
||||
@@ -136,6 +143,15 @@ class Skin extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds of the drawable for determining its fenced position.
|
||||
* @param {Array<number>} drawable - The Drawable instance this skin is using.
|
||||
* @return {!Rectangle} The drawable's bounds.
|
||||
*/
|
||||
getFenceBounds (drawable) {
|
||||
return drawable.getFastBounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update and returns the uniforms for this skin.
|
||||
* @param {Array<number>} scale - The scaling factors to be used.
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['scratch'],
|
||||
env: {
|
||||
browser: true
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
};
|
||||
|
||||
37
src/playground/getMousePosition.js
Normal file
37
src/playground/getMousePosition.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// Adapted from code by Simon Sarris: http://stackoverflow.com/a/10450761
|
||||
const getMousePos = function (event, element) {
|
||||
const stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(element, null).paddingLeft, 10) || 0;
|
||||
const stylePaddingTop = parseInt(document.defaultView.getComputedStyle(element, null).paddingTop, 10) || 0;
|
||||
const styleBorderLeft = parseInt(document.defaultView.getComputedStyle(element, null).borderLeftWidth, 10) || 0;
|
||||
const styleBorderTop = parseInt(document.defaultView.getComputedStyle(element, null).borderTopWidth, 10) || 0;
|
||||
|
||||
// Some pages have fixed-position bars at the top or left of the page
|
||||
// They will mess up mouse coordinates and this fixes that
|
||||
const html = document.body.parentNode;
|
||||
const htmlTop = html.offsetTop;
|
||||
const htmlLeft = html.offsetLeft;
|
||||
|
||||
// Compute the total offset. It's possible to cache this if you want
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
if (typeof element.offsetParent !== 'undefined') {
|
||||
do {
|
||||
offsetX += element.offsetLeft;
|
||||
offsetY += element.offsetTop;
|
||||
} while ((element = element.offsetParent));
|
||||
}
|
||||
|
||||
// Add padding and border style widths to offset
|
||||
// Also add the <html> offsets in case there's a position:fixed bar
|
||||
// This part is not strictly necessary, it depends on your styling
|
||||
offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft;
|
||||
offsetY += stylePaddingTop + styleBorderTop + htmlTop;
|
||||
|
||||
// We return a simple javascript object with x and y defined
|
||||
return {
|
||||
x: event.pageX - offsetX,
|
||||
y: event.pageY - offsetY
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = getMousePos;
|
||||
@@ -12,7 +12,7 @@
|
||||
<canvas id="debug-canvas" width="10" height="10" style="border:3px dashed red"></canvas>
|
||||
<p>
|
||||
<label for="fudgeproperty">Property to tweak:</label>
|
||||
<select id="fudgeproperty" onchange="onFudgePropertyChanged(this.value)">
|
||||
<select id="fudgeproperty">
|
||||
<option value="posx">Position X</option>
|
||||
<option value="posy">Position Y</option>
|
||||
<option value="direction">Direction</option>
|
||||
@@ -27,149 +27,12 @@
|
||||
<option value="ghost">Ghost</option>
|
||||
</select>
|
||||
<label for="fudge">Property Value:</label>
|
||||
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any" oninput="onFudgeChanged(this.value)" onchange="onFudgeChanged(this.value)">
|
||||
<input type="range" id="fudge" style="width:50%" value="90" min="-90" max="270" step="any">
|
||||
</p>
|
||||
<p>
|
||||
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number" onchange="onFudgeMinChanged(this.value)">
|
||||
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number" onchange="onFudgeMaxChanged(this.value)">
|
||||
<label for="fudgeMin">Min:</label><input id="fudgeMin" type="number">
|
||||
<label for="fudgeMax">Max:</label><input id="fudgeMax" type="number">
|
||||
</p>
|
||||
<script src="scratch-render.js"></script>
|
||||
<script>
|
||||
var canvas = document.getElementById('scratch-stage');
|
||||
var fudge = 90;
|
||||
var renderer = new ScratchRender(canvas);
|
||||
renderer.setLayerGroupOrdering(['group1']);
|
||||
|
||||
var drawableID = renderer.createDrawable('group1');
|
||||
renderer.updateDrawableProperties(drawableID, {
|
||||
position: [0, 0],
|
||||
scale: [100, 100],
|
||||
direction: 90
|
||||
});
|
||||
|
||||
var drawableID2 = renderer.createDrawable('group1');
|
||||
var wantBitmapSkin = false;
|
||||
|
||||
// Bitmap (squirrel)
|
||||
var image = new Image();
|
||||
image.onload = function () {
|
||||
var bitmapSkinId = renderer.createBitmapSkin(image);
|
||||
if (wantBitmapSkin) {
|
||||
renderer.updateDrawableProperties(drawableID2, {
|
||||
skinId: bitmapSkinId
|
||||
});
|
||||
}
|
||||
};
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.src = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/';
|
||||
|
||||
// SVG (cat 1-a)
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener("load", function () {
|
||||
var skinId = renderer.createSVGSkin(xhr.responseText);
|
||||
if (!wantBitmapSkin) {
|
||||
renderer.updateDrawableProperties(drawableID2, {
|
||||
skinId: skinId
|
||||
});
|
||||
}
|
||||
});
|
||||
xhr.open('GET', 'https://cdn.assets.scratch.mit.edu/internalapi/asset/f88bf1935daea28f8ca098462a31dbb0.svg/get/');
|
||||
xhr.send();
|
||||
|
||||
var posX = 0;
|
||||
var posY = 0;
|
||||
var scaleX = 100;
|
||||
var scaleY = 100;
|
||||
var fudgeProperty = 'posx';
|
||||
function onFudgePropertyChanged(newValue) {
|
||||
fudgeProperty = newValue;
|
||||
}
|
||||
var fudgeInput = document.getElementById('fudge');
|
||||
function onFudgeMinChanged(newValue) {
|
||||
fudgeInput.min = newValue;
|
||||
}
|
||||
function onFudgeMaxChanged(newValue) {
|
||||
fudgeInput.max = newValue;
|
||||
}
|
||||
function onFudgeChanged(newValue) {
|
||||
fudge = newValue;
|
||||
var props = {};
|
||||
switch (fudgeProperty) {
|
||||
case 'posx': props.position = [fudge, posY]; posX = fudge; break;
|
||||
case 'posy': props.position = [posX, fudge]; posY = fudge; break;
|
||||
case 'direction': props.direction = fudge; break;
|
||||
case 'scalex': props.scale = [fudge, scaleY]; scaleX = fudge; break;
|
||||
case 'scaley': props.scale = [scaleX, fudge]; scaleY = fudge; break;
|
||||
case 'color': props.color = fudge; break;
|
||||
case 'whirl': props.whirl = fudge; break;
|
||||
case 'fisheye': props.fisheye = fudge; break;
|
||||
case 'pixelate': props.pixelate = fudge; break;
|
||||
case 'mosaic': props.mosaic = fudge; break;
|
||||
case 'brightness': props.brightness = fudge; break;
|
||||
case 'ghost': props.ghost = fudge; break;
|
||||
}
|
||||
renderer.updateDrawableProperties(drawableID2, props);
|
||||
}
|
||||
|
||||
// Adapted from code by Simon Sarris: http://stackoverflow.com/a/10450761
|
||||
function getMousePos(event, element) {
|
||||
var stylePaddingLeft = parseInt(document.defaultView.getComputedStyle(element, null)['paddingLeft'], 10) || 0;
|
||||
var stylePaddingTop = parseInt(document.defaultView.getComputedStyle(element, null)['paddingTop'], 10) || 0;
|
||||
var styleBorderLeft = parseInt(document.defaultView.getComputedStyle(element, null)['borderLeftWidth'], 10) || 0;
|
||||
var styleBorderTop = parseInt(document.defaultView.getComputedStyle(element, null)['borderTopWidth'], 10) || 0;
|
||||
|
||||
// Some pages have fixed-position bars at the top or left of the page
|
||||
// They will mess up mouse coordinates and this fixes that
|
||||
var html = document.body.parentNode;
|
||||
var htmlTop = html.offsetTop;
|
||||
var htmlLeft = html.offsetLeft;
|
||||
|
||||
// Compute the total offset. It's possible to cache this if you want
|
||||
var offsetX = 0, offsetY = 0;
|
||||
if (element.offsetParent !== undefined) {
|
||||
do {
|
||||
offsetX += element.offsetLeft;
|
||||
offsetY += element.offsetTop;
|
||||
} while ((element = element.offsetParent));
|
||||
}
|
||||
|
||||
// Add padding and border style widths to offset
|
||||
// Also add the <html> offsets in case there's a position:fixed bar
|
||||
// This part is not strictly necessary, it depends on your styling
|
||||
offsetX += stylePaddingLeft + styleBorderLeft + htmlLeft;
|
||||
offsetY += stylePaddingTop + styleBorderTop + htmlTop;
|
||||
|
||||
// We return a simple javascript object with x and y defined
|
||||
return {
|
||||
x: event.pageX - offsetX,
|
||||
y: event.pageY - offsetY
|
||||
};
|
||||
}
|
||||
|
||||
canvas.onmousemove = function(event) {
|
||||
var mousePos = getMousePos(event, canvas);
|
||||
renderer.extractColor(mousePos.x, mousePos.y, 30);
|
||||
};
|
||||
|
||||
canvas.onclick = function(event) {
|
||||
var mousePos = getMousePos(event, canvas);
|
||||
var pickID = renderer.pick(mousePos.x, mousePos.y);
|
||||
console.log('You clicked on ' + (pickID < 0 ? 'nothing' : 'ID# ' + pickID));
|
||||
if (pickID >= 0) {
|
||||
console.dir(renderer.extractDrawable(pickID, mousePos.x, mousePos.y));
|
||||
}
|
||||
};
|
||||
|
||||
function drawStep() {
|
||||
renderer.draw();
|
||||
// renderer.getBounds(drawableID2);
|
||||
// renderer.isTouchingColor(drawableID2, [255,255,255]);
|
||||
requestAnimationFrame(drawStep);
|
||||
}
|
||||
drawStep();
|
||||
|
||||
var debugCanvas = /** @type {canvas} */ document.getElementById('debug-canvas');
|
||||
renderer.setDebugCanvas(debugCanvas);
|
||||
</script>
|
||||
<script src="playground.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
141
src/playground/playground.js
Normal file
141
src/playground/playground.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const ScratchRender = require('../RenderWebGL');
|
||||
const getMousePosition = require('./getMousePosition');
|
||||
|
||||
var canvas = document.getElementById('scratch-stage');
|
||||
var fudge = 90;
|
||||
var renderer = new ScratchRender(canvas);
|
||||
renderer.setLayerGroupOrdering(['group1']);
|
||||
|
||||
var drawableID = renderer.createDrawable('group1');
|
||||
renderer.updateDrawableProperties(drawableID, {
|
||||
position: [0, 0],
|
||||
scale: [100, 100],
|
||||
direction: 90
|
||||
});
|
||||
|
||||
var drawableID2 = renderer.createDrawable('group1');
|
||||
var wantBitmapSkin = false;
|
||||
|
||||
// Bitmap (squirrel)
|
||||
var image = new Image();
|
||||
image.addEventListener('load', () => {
|
||||
var bitmapSkinId = renderer.createBitmapSkin(image);
|
||||
if (wantBitmapSkin) {
|
||||
renderer.updateDrawableProperties(drawableID2, {
|
||||
skinId: bitmapSkinId
|
||||
});
|
||||
}
|
||||
});
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.src = 'https://cdn.assets.scratch.mit.edu/internalapi/asset/7e24c99c1b853e52f8e7f9004416fa34.png/get/';
|
||||
|
||||
// SVG (cat 1-a)
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', function () {
|
||||
var skinId = renderer.createSVGSkin(xhr.responseText);
|
||||
if (!wantBitmapSkin) {
|
||||
renderer.updateDrawableProperties(drawableID2, {
|
||||
skinId: skinId
|
||||
});
|
||||
}
|
||||
});
|
||||
xhr.open('GET', 'https://cdn.assets.scratch.mit.edu/internalapi/asset/f88bf1935daea28f8ca098462a31dbb0.svg/get/');
|
||||
xhr.send();
|
||||
|
||||
var posX = 0;
|
||||
var posY = 0;
|
||||
var scaleX = 100;
|
||||
var scaleY = 100;
|
||||
var fudgeProperty = 'posx';
|
||||
|
||||
const fudgePropertyInput = document.getElementById('fudgeproperty');
|
||||
fudgePropertyInput.addEventListener('change', event => {
|
||||
fudgeProperty = event.target.value;
|
||||
});
|
||||
|
||||
const fudgeInput = document.getElementById('fudge');
|
||||
|
||||
const fudgeMinInput = document.getElementById('fudgeMin');
|
||||
fudgeMinInput.addEventListener('change', event => {
|
||||
fudgeInput.min = event.target.valueAsNumber;
|
||||
});
|
||||
|
||||
const fudgeMaxInput = document.getElementById('fudgeMax');
|
||||
fudgeMaxInput.addEventListener('change', event => {
|
||||
fudgeInput.max = event.target.valueAsNumber;
|
||||
});
|
||||
|
||||
const handleFudgeChanged = function (event) {
|
||||
fudge = event.target.valueAsNumber;
|
||||
var props = {};
|
||||
switch (fudgeProperty) {
|
||||
case 'posx':
|
||||
props.position = [fudge, posY];
|
||||
posX = fudge;
|
||||
break;
|
||||
case 'posy':
|
||||
props.position = [posX, fudge];
|
||||
posY = fudge;
|
||||
break;
|
||||
case 'direction':
|
||||
props.direction = fudge;
|
||||
break;
|
||||
case 'scalex':
|
||||
props.scale = [fudge, scaleY];
|
||||
scaleX = fudge;
|
||||
break;
|
||||
case 'scaley':
|
||||
props.scale = [scaleX, fudge];
|
||||
scaleY = fudge;
|
||||
break;
|
||||
case 'color':
|
||||
props.color = fudge;
|
||||
break;
|
||||
case 'whirl':
|
||||
props.whirl = fudge;
|
||||
break;
|
||||
case 'fisheye':
|
||||
props.fisheye = fudge;
|
||||
break;
|
||||
case 'pixelate':
|
||||
props.pixelate = fudge;
|
||||
break;
|
||||
case 'mosaic':
|
||||
props.mosaic = fudge;
|
||||
break;
|
||||
case 'brightness':
|
||||
props.brightness = fudge;
|
||||
break;
|
||||
case 'ghost':
|
||||
props.ghost = fudge;
|
||||
break;
|
||||
}
|
||||
renderer.updateDrawableProperties(drawableID2, props);
|
||||
};
|
||||
fudgeInput.addEventListener('input', handleFudgeChanged);
|
||||
fudgeInput.addEventListener('change', handleFudgeChanged);
|
||||
|
||||
canvas.addEventListener('mousemove', event => {
|
||||
var mousePos = getMousePosition(event, canvas);
|
||||
renderer.extractColor(mousePos.x, mousePos.y, 30);
|
||||
});
|
||||
|
||||
canvas.addEventListener('click', event => {
|
||||
var mousePos = getMousePosition(event, canvas);
|
||||
var pickID = renderer.pick(mousePos.x, mousePos.y);
|
||||
console.log('You clicked on ' + (pickID < 0 ? 'nothing' : 'ID# ' + pickID));
|
||||
if (pickID >= 0) {
|
||||
console.dir(renderer.extractDrawable(pickID, mousePos.x, mousePos.y));
|
||||
}
|
||||
});
|
||||
|
||||
const drawStep = function () {
|
||||
renderer.draw();
|
||||
// renderer.getBounds(drawableID2);
|
||||
// renderer.isTouchingColor(drawableID2, [255,255,255]);
|
||||
requestAnimationFrame(drawStep);
|
||||
};
|
||||
drawStep();
|
||||
|
||||
var debugCanvas = /** @type {canvas} */ document.getElementById('debug-canvas');
|
||||
renderer.setDebugCanvas(debugCanvas);
|
||||
74
src/playground/queryPlayground.html
Normal file
74
src/playground/queryPlayground.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Scratch WebGL Query Playground</title>
|
||||
<style>
|
||||
input[type=range][orient=vertical] {
|
||||
writing-mode: bt-lr; /* IE */
|
||||
-webkit-appearance: slider-vertical;
|
||||
width: 1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
canvas {
|
||||
border: 3px dashed black;
|
||||
|
||||
/* https://stackoverflow.com/a/7665647 */
|
||||
image-rendering: optimizeSpeed; /* Older versions of FF */
|
||||
image-rendering: -moz-crisp-edges; /* FF 6.0+ */
|
||||
image-rendering: -webkit-optimize-contrast; /* Safari */
|
||||
image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
|
||||
image-rendering: pixelated; /* Awesome future-browsers */
|
||||
-ms-interpolation-mode: nearest-neighbor; /* IE */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background: lightsteelblue">
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Query Canvases</legend>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<fieldset>
|
||||
<legend>GPU</legend>
|
||||
<div>Touching color A? <span id="gpuTouchingA">maybe</span></div>
|
||||
<div>Touching color B? <span id="gpuTouchingB">maybe</span></div>
|
||||
<canvas id="gpuQueryCanvas" width="480" height="360" style="height: 20rem"></canvas>
|
||||
</fieldset>
|
||||
</td>
|
||||
<td>
|
||||
<fieldset>
|
||||
<legend>CPU</legend>
|
||||
<div>Touching color A? <span id="cpuTouchingA">maybe</span></div>
|
||||
<div>Touching color B? <span id="cpuTouchingB">maybe</span></div>
|
||||
<canvas id="cpuQueryCanvas" width="480" height="360" style="height: 20rem"></canvas>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Render Canvas</legend>
|
||||
<div>Cursor Position: <span id="cursorPosition">somewhere</span></div>
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input id="cursorX" type="range" step="0.25" value="0" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input id="cursorY" type="range" orient="vertical" step="0.25" value="0" />
|
||||
</td>
|
||||
<td>
|
||||
<canvas id="renderCanvas" width="480" height="360" style="border:3px dashed black"></canvas>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
</div>
|
||||
</body>
|
||||
<script src="queryPlayground.js"></script>
|
||||
</html>
|
||||
196
src/playground/queryPlayground.js
Normal file
196
src/playground/queryPlayground.js
Normal file
@@ -0,0 +1,196 @@
|
||||
const ScratchRender = require('../RenderWebGL');
|
||||
const getMousePosition = require('./getMousePosition');
|
||||
|
||||
const renderCanvas = document.getElementById('renderCanvas');
|
||||
const gpuQueryCanvas = document.getElementById('gpuQueryCanvas');
|
||||
const cpuQueryCanvas = document.getElementById('cpuQueryCanvas');
|
||||
const inputCursorX = document.getElementById('cursorX');
|
||||
const inputCursorY = document.getElementById('cursorY');
|
||||
const labelCursorPosition = document.getElementById('cursorPosition');
|
||||
const labelGpuTouchingA = document.getElementById('gpuTouchingA');
|
||||
const labelGpuTouchingB = document.getElementById('gpuTouchingB');
|
||||
const labelCpuTouchingA = document.getElementById('cpuTouchingA');
|
||||
const labelCpuTouchingB = document.getElementById('cpuTouchingB');
|
||||
|
||||
const drawables = {
|
||||
testPattern: -1,
|
||||
cursor: -1
|
||||
};
|
||||
|
||||
const colors = {
|
||||
cursor: [255, 0, 0],
|
||||
patternA: [0, 255, 0],
|
||||
patternB: [0, 0, 255]
|
||||
};
|
||||
|
||||
const renderer = new ScratchRender(renderCanvas);
|
||||
|
||||
const handleResizeRenderCanvas = () => {
|
||||
const halfWidth = renderCanvas.clientWidth / 2;
|
||||
const halfHeight = renderCanvas.clientHeight / 2;
|
||||
|
||||
inputCursorX.style.width = `${renderCanvas.clientWidth}px`;
|
||||
inputCursorY.style.height = `${renderCanvas.clientHeight}px`;
|
||||
inputCursorX.min = -halfWidth;
|
||||
inputCursorX.max = halfWidth;
|
||||
inputCursorY.min = -halfHeight;
|
||||
inputCursorY.max = halfHeight;
|
||||
};
|
||||
renderCanvas.addEventListener('resize', handleResizeRenderCanvas);
|
||||
handleResizeRenderCanvas();
|
||||
|
||||
const handleCursorPositionChanged = () => {
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
const cursorX = inputCursorX.valueAsNumber / devicePixelRatio;
|
||||
const cursorY = inputCursorY.valueAsNumber / devicePixelRatio;
|
||||
const positionHTML = `${cursorX}, ${cursorY}`;
|
||||
labelCursorPosition.innerHTML = positionHTML;
|
||||
if (drawables.cursor >= 0) {
|
||||
renderer.draw();
|
||||
renderer.updateDrawableProperties(drawables.cursor, {
|
||||
position: [cursorX, cursorY]
|
||||
});
|
||||
|
||||
renderer.setUseGpuMode(ScratchRender.UseGpuModes.ForceGPU);
|
||||
renderer.setDebugCanvas(gpuQueryCanvas);
|
||||
const isGpuTouchingA = renderer.isTouchingColor(drawables.cursor, colors.patternA);
|
||||
const isGpuTouchingB = renderer.isTouchingColor(drawables.cursor, colors.patternB);
|
||||
labelGpuTouchingA.innerHTML = isGpuTouchingA ? 'yes' : 'no';
|
||||
labelGpuTouchingB.innerHTML = isGpuTouchingB ? 'yes' : 'no';
|
||||
|
||||
renderer.setUseGpuMode(ScratchRender.UseGpuModes.ForceCPU);
|
||||
renderer.setDebugCanvas(cpuQueryCanvas);
|
||||
const isCpuTouchingA = renderer.isTouchingColor(drawables.cursor, colors.patternA);
|
||||
const isCpuTouchingB = renderer.isTouchingColor(drawables.cursor, colors.patternB);
|
||||
labelCpuTouchingA.innerHTML = isCpuTouchingA ? 'yes' : 'no';
|
||||
labelCpuTouchingB.innerHTML = isCpuTouchingB ? 'yes' : 'no';
|
||||
|
||||
renderer.setUseGpuMode(ScratchRender.UseGpuModes.Automatic);
|
||||
}
|
||||
};
|
||||
inputCursorX.addEventListener('change', handleCursorPositionChanged);
|
||||
inputCursorY.addEventListener('change', handleCursorPositionChanged);
|
||||
inputCursorX.addEventListener('input', handleCursorPositionChanged);
|
||||
inputCursorY.addEventListener('input', handleCursorPositionChanged);
|
||||
handleCursorPositionChanged();
|
||||
|
||||
let trackingMouse = true;
|
||||
const handleMouseMove = event => {
|
||||
if (trackingMouse) {
|
||||
const mousePosition = getMousePosition(event, renderCanvas);
|
||||
inputCursorX.value = mousePosition.x - (renderCanvas.clientWidth / 2);
|
||||
inputCursorY.value = (renderCanvas.clientHeight / 2) - mousePosition.y;
|
||||
handleCursorPositionChanged();
|
||||
}
|
||||
};
|
||||
renderCanvas.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
renderCanvas.addEventListener('click', event => {
|
||||
trackingMouse = !trackingMouse;
|
||||
if (trackingMouse) {
|
||||
handleMouseMove(event);
|
||||
}
|
||||
});
|
||||
|
||||
const rgb2fillStyle = (rgb) => {
|
||||
return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;
|
||||
};
|
||||
|
||||
const makeCursorImage = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvas.height = 1;
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = rgb2fillStyle(colors.cursor);
|
||||
context.fillRect(0, 0, 1, 1);
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const makeTestPatternImage = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 480;
|
||||
canvas.height = 360;
|
||||
|
||||
const patternA = rgb2fillStyle(colors.patternA);
|
||||
const patternB = rgb2fillStyle(colors.patternB);
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
context.fillStyle = patternA;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
context.fillStyle = patternB;
|
||||
const xSplit1 = Math.floor(canvas.width * 0.25);
|
||||
const xSplit2 = Math.floor(canvas.width * 0.5);
|
||||
const xSplit3 = Math.floor(canvas.width * 0.75);
|
||||
const ySplit = Math.floor(canvas.height * 0.5);
|
||||
for (let y = 0; y < ySplit; y += 2) {
|
||||
context.fillRect(0, y, xSplit2, 1);
|
||||
}
|
||||
for (let x = xSplit2; x < canvas.width; x += 2) {
|
||||
context.fillRect(x, 0, 1, ySplit);
|
||||
}
|
||||
for (let x = 0; x < xSplit1; x += 2) {
|
||||
for (let y = ySplit; y < canvas.height; y += 2) {
|
||||
context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
for (let x = xSplit1; x < xSplit2; x += 3) {
|
||||
for (let y = ySplit; y < canvas.height; y += 3) {
|
||||
context.fillRect(x, y, 2, 2);
|
||||
}
|
||||
}
|
||||
for (let x = xSplit2; x < xSplit3; ++x) {
|
||||
for (let y = ySplit; y < canvas.height; ++y) {
|
||||
context.fillStyle = (x + y) % 2 ? patternB : patternA;
|
||||
context.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
for (let x = xSplit3; x < canvas.width; x += 2) {
|
||||
for (let y = ySplit; y < canvas.height; y += 2) {
|
||||
context.fillStyle = (x + y) % 4 ? patternB : patternA;
|
||||
context.fillRect(x, y, 2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const makeTestPatternDrawable = function (group) {
|
||||
const image = makeTestPatternImage();
|
||||
const skinId = renderer.createBitmapSkin(image, 1);
|
||||
const drawableId = renderer.createDrawable(group);
|
||||
renderer.updateDrawableProperties(drawableId, {skinId});
|
||||
return drawableId;
|
||||
};
|
||||
|
||||
const makeCursorDrawable = function (group) {
|
||||
const image = makeCursorImage();
|
||||
const skinId = renderer.createBitmapSkin(image, 1, [0, 0]);
|
||||
const drawableId = renderer.createDrawable(group);
|
||||
renderer.updateDrawableProperties(drawableId, {skinId});
|
||||
return drawableId;
|
||||
};
|
||||
|
||||
const initRendering = () => {
|
||||
const layerGroup = {
|
||||
testPattern: 'testPattern',
|
||||
cursor: 'cursor'
|
||||
};
|
||||
renderer.setLayerGroupOrdering([layerGroup.testPattern, layerGroup.cursor]);
|
||||
drawables.testPattern = makeTestPatternDrawable(layerGroup.testPattern);
|
||||
drawables.cursor = makeCursorDrawable(layerGroup.cursor);
|
||||
|
||||
const corner00 = makeCursorDrawable(layerGroup.cursor);
|
||||
const corner01 = makeCursorDrawable(layerGroup.cursor);
|
||||
const corner10 = makeCursorDrawable(layerGroup.cursor);
|
||||
const corner11 = makeCursorDrawable(layerGroup.cursor);
|
||||
|
||||
renderer.updateDrawableProperties(corner00, {position: [-240, -179]});
|
||||
renderer.updateDrawableProperties(corner01, {position: [-240, 180]});
|
||||
renderer.updateDrawableProperties(corner10, {position: [239, -179]});
|
||||
renderer.updateDrawableProperties(corner11, {position: [239, 180]});
|
||||
};
|
||||
|
||||
initRendering();
|
||||
renderer.draw();
|
||||
@@ -45,33 +45,48 @@ uniform sampler2D u_skin;
|
||||
|
||||
varying vec2 v_texCoord;
|
||||
|
||||
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color) || defined(ENABLE_brightness))
|
||||
#if !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
|
||||
// Branchless color conversions based on code from:
|
||||
// http://www.chilliant.com/rgb2hsv.html by Ian Taylor
|
||||
// Based in part on work by Sam Hocevar and Emil Persson
|
||||
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
|
||||
|
||||
// Smaller values can cause problems with "color" and "brightness" effects on some mobile devices
|
||||
// Smaller values can cause problems on some mobile devices
|
||||
const float epsilon = 1e-3;
|
||||
|
||||
// Convert an RGB color to Hue, Chroma, and Value.
|
||||
// Convert an RGB color to Hue, Saturation, and Value.
|
||||
// All components of input and output are expected to be in the [0,1] range.
|
||||
vec3 convertRGB2HCV(vec3 rgb)
|
||||
vec3 convertRGB2HSV(vec3 rgb)
|
||||
{
|
||||
// Based on work by Sam Hocevar and Emil Persson
|
||||
vec4 P = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0/3.0) : vec4(rgb.gb, 0.0, -1.0/3.0);
|
||||
vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx);
|
||||
float C = Q.x - min(Q.w, Q.y);
|
||||
float H = abs((Q.w - Q.y) / (6.0 * C + epsilon) + Q.z);
|
||||
return vec3(H, C, Q.x);
|
||||
}
|
||||
// Hue calculation has 3 cases, depending on which RGB component is largest, and one of those cases involves a "mod"
|
||||
// operation. In order to avoid that "mod" we split the M==R case in two: one for G<B and one for B>G. The B>G case
|
||||
// will be calculated in the negative and fed through abs() in the hue calculation at the end.
|
||||
// See also: https://en.wikipedia.org/wiki/HSL_and_HSV#Hue_and_chroma
|
||||
const vec4 hueOffsets = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
|
||||
|
||||
vec3 convertRGB2HSL(vec3 rgb)
|
||||
{
|
||||
vec3 hcv = convertRGB2HCV(rgb);
|
||||
float L = hcv.z - hcv.y * 0.5;
|
||||
float S = hcv.y / (1.0 - abs(L * 2.0 - 1.0) + epsilon);
|
||||
return vec3(hcv.x, S, L);
|
||||
// temp1.xy = sort B & G (largest first)
|
||||
// temp1.z = the hue offset we'll use if it turns out that R is the largest component (M==R)
|
||||
// temp1.w = the hue offset we'll use if it turns out that R is not the largest component (M==G or M==B)
|
||||
vec4 temp1 = rgb.b > rgb.g ? vec4(rgb.bg, hueOffsets.wz) : vec4(rgb.gb, hueOffsets.xy);
|
||||
|
||||
// temp2.x = the largest component of RGB ("M" / "Max")
|
||||
// temp2.yw = the smaller components of RGB, ordered for the hue calculation (not necessarily sorted by magnitude!)
|
||||
// temp2.z = the hue offset we'll use in the hue calculation
|
||||
vec4 temp2 = rgb.r > temp1.x ? vec4(rgb.r, temp1.yzx) : vec4(temp1.xyw, rgb.r);
|
||||
|
||||
// m = the smallest component of RGB ("min")
|
||||
float m = min(temp2.y, temp2.w);
|
||||
|
||||
// Chroma = M - m
|
||||
float C = temp2.x - m;
|
||||
|
||||
// Value = M
|
||||
float V = temp2.x;
|
||||
|
||||
return vec3(
|
||||
abs(temp2.z + (temp2.w - temp2.y) / (6.0 * C + epsilon)), // Hue
|
||||
C / (temp2.x + epsilon), // Saturation
|
||||
V); // Value
|
||||
}
|
||||
|
||||
vec3 convertHue2RGB(float hue)
|
||||
@@ -82,13 +97,13 @@ vec3 convertHue2RGB(float hue)
|
||||
return clamp(vec3(r, g, b), 0.0, 1.0);
|
||||
}
|
||||
|
||||
vec3 convertHSL2RGB(vec3 hsl)
|
||||
vec3 convertHSV2RGB(vec3 hsv)
|
||||
{
|
||||
vec3 rgb = convertHue2RGB(hsl.x);
|
||||
float C = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
|
||||
return (rgb - 0.5) * C + hsl.z;
|
||||
vec3 rgb = convertHue2RGB(hsv.x);
|
||||
float c = hsv.z * hsv.y;
|
||||
return rgb * c + hsv.z - c;
|
||||
}
|
||||
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color) || defined(ENABLE_brightness))
|
||||
#endif // !defined(DRAW_MODE_silhouette) && (defined(ENABLE_color))
|
||||
|
||||
const vec2 kCenter = vec2(0.5, 0.5);
|
||||
|
||||
@@ -149,31 +164,27 @@ void main()
|
||||
gl_FragColor = u_silhouetteColor;
|
||||
#else // DRAW_MODE_silhouette
|
||||
|
||||
#if defined(ENABLE_color) || defined(ENABLE_brightness)
|
||||
#if defined(ENABLE_color)
|
||||
{
|
||||
vec3 hsl = convertRGB2HSL(gl_FragColor.xyz);
|
||||
vec3 hsv = convertRGB2HSV(gl_FragColor.xyz);
|
||||
|
||||
#ifdef ENABLE_color
|
||||
{
|
||||
// this code forces grayscale values to be slightly saturated
|
||||
// so that some slight change of hue will be visible
|
||||
const float minLightness = 0.11 / 2.0;
|
||||
const float minSaturation = 0.09;
|
||||
if (hsl.z < minLightness) hsl = vec3(0.0, 1.0, minLightness);
|
||||
else if (hsl.y < minSaturation) hsl = vec3(0.0, minSaturation, hsl.z);
|
||||
// this code forces grayscale values to be slightly saturated
|
||||
// so that some slight change of hue will be visible
|
||||
const float minLightness = 0.11 / 2.0;
|
||||
const float minSaturation = 0.09;
|
||||
if (hsv.z < minLightness) hsv = vec3(0.0, 1.0, minLightness);
|
||||
else if (hsv.y < minSaturation) hsv = vec3(0.0, minSaturation, hsv.z);
|
||||
|
||||
hsl.x = mod(hsl.x + u_color, 1.0);
|
||||
if (hsl.x < 0.0) hsl.x += 1.0;
|
||||
}
|
||||
#endif // ENABLE_color
|
||||
hsv.x = mod(hsv.x + u_color, 1.0);
|
||||
if (hsv.x < 0.0) hsv.x += 1.0;
|
||||
|
||||
#ifdef ENABLE_brightness
|
||||
hsl.z = clamp(hsl.z + u_brightness, 0.0, 1.0);
|
||||
#endif // ENABLE_brightness
|
||||
|
||||
gl_FragColor.rgb = convertHSL2RGB(hsl);
|
||||
gl_FragColor.rgb = convertHSV2RGB(hsv);
|
||||
}
|
||||
#endif // defined(ENABLE_color) || defined(ENABLE_brightness)
|
||||
#endif // defined(ENABLE_color)
|
||||
|
||||
#if defined(ENABLE_brightness)
|
||||
gl_FragColor.rgb = clamp(gl_FragColor.rgb + vec3(u_brightness), vec3(0), vec3(1));
|
||||
#endif // defined(ENABLE_brightness)
|
||||
|
||||
#ifdef DRAW_MODE_colorMask
|
||||
vec3 maskDistance = abs(gl_FragColor.rgb - u_colorMask);
|
||||
@@ -183,12 +194,6 @@ void main()
|
||||
discard;
|
||||
}
|
||||
#endif // DRAW_MODE_colorMask
|
||||
|
||||
// WebGL defaults to premultiplied alpha
|
||||
#ifndef DRAW_MODE_stamp
|
||||
gl_FragColor.rgb *= gl_FragColor.a;
|
||||
#endif // DRAW_MODE_stamp
|
||||
|
||||
#endif // DRAW_MODE_silhouette
|
||||
|
||||
#else // DRAW_MODE_lineSample
|
||||
|
||||
@@ -114,7 +114,7 @@ const testBubbles = () => test('bubble snapshot', async t => {
|
||||
// immediately invoked async function to let us wait for each test to finish before starting the next.
|
||||
(async () => {
|
||||
const files = fs.readdirSync(testDir())
|
||||
.filter(uri => uri.endsWith('.sb2') || uri.endsWidth('.sb3'));
|
||||
.filter(uri => uri.endsWith('.sb2') || uri.endsWith('.sb3'));
|
||||
|
||||
for (const file of files) {
|
||||
await testFile(file);
|
||||
|
||||
BIN
test/integration/scratch-tests/sprite-goes-off-stage.sb2
Normal file
BIN
test/integration/scratch-tests/sprite-goes-off-stage.sb2
Normal file
Binary file not shown.
@@ -39,10 +39,10 @@ module.exports = [
|
||||
Object.assign({}, base, {
|
||||
target: 'web',
|
||||
entry: {
|
||||
'scratch-render': './src/index.js'
|
||||
playground: './src/playground/playground.js',
|
||||
queryPlayground: './src/playground/queryPlayground.js'
|
||||
},
|
||||
output: {
|
||||
library: 'ScratchRender',
|
||||
libraryTarget: 'umd',
|
||||
path: path.resolve('playground'),
|
||||
filename: '[name].js'
|
||||
@@ -50,7 +50,8 @@ module.exports = [
|
||||
plugins: base.plugins.concat([
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: 'src/playground'
|
||||
context: 'src/playground',
|
||||
from: '*.html'
|
||||
}
|
||||
])
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user