Compare commits
71 Commits
greenkeepe
...
greenkeepe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6af4e5a9e5 | ||
|
|
b1274d5820 | ||
|
|
58aa05c0a7 | ||
|
|
c6b5824694 | ||
|
|
066b243f04 | ||
|
|
45482fecbd | ||
|
|
058fefbb12 | ||
|
|
c7669b00cf | ||
|
|
89294a4583 | ||
|
|
e46c0ec3de | ||
|
|
471c88b850 | ||
|
|
eccfb44a96 | ||
|
|
f17252624e | ||
|
|
861b9429c0 | ||
|
|
bc893f86dd | ||
|
|
74a7a8c0b6 | ||
|
|
99af15a974 | ||
|
|
8dbd71470d | ||
|
|
c25d1f37b8 | ||
|
|
13b9ed7e16 | ||
|
|
e7a7c14032 | ||
|
|
503c3f7b0b | ||
|
|
857b541e84 | ||
|
|
8e836a7a11 | ||
|
|
b7a3d32cde | ||
|
|
11665299bc | ||
|
|
303464ddd4 | ||
|
|
9da15b1326 | ||
|
|
110371b029 | ||
|
|
2a1d215e50 | ||
|
|
832b0274be | ||
|
|
1cd9e54834 | ||
|
|
928cd60dd5 | ||
|
|
30ef2b1e51 | ||
|
|
ad62542a52 | ||
|
|
2d4419c929 | ||
|
|
ad6ddd9f99 | ||
|
|
c3ede9c3d5 | ||
|
|
ab517fff51 | ||
|
|
1371b6f685 | ||
|
|
5c6997131b | ||
|
|
b4c7dbf0ea | ||
|
|
c0e5115bfc | ||
|
|
3819b66683 | ||
|
|
b9732c222c | ||
|
|
afaa758615 | ||
|
|
a3a526d6a3 | ||
|
|
d77afaa6c4 | ||
|
|
3bfd4c65fb | ||
|
|
131f5372db | ||
|
|
ef13d3bb08 | ||
|
|
94257a4214 | ||
|
|
7c4393787b | ||
|
|
a340b8a04b | ||
|
|
06def05119 | ||
|
|
dce90a3f56 | ||
|
|
471d4b91a4 | ||
|
|
ae23bb5e56 | ||
|
|
99d14f0e16 | ||
|
|
96aa930895 | ||
|
|
89e6de035d | ||
|
|
1b51f1f393 | ||
|
|
d0ed283c72 | ||
|
|
34f265ab22 | ||
|
|
6646041ba4 | ||
|
|
f9309b1ace | ||
|
|
7599a82406 | ||
|
|
6e755ea015 | ||
|
|
750f40ddf2 | ||
|
|
603fd87782 | ||
|
|
13f7a1038d |
@@ -15,6 +15,7 @@
|
||||
"private": true,
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"template": "node_modules/docdash"
|
||||
"template": "node_modules/docdash",
|
||||
"tutorials": "docs"
|
||||
}
|
||||
}
|
||||
|
||||
192
docs/Rectangle-AABB-Matrix.md
Normal file
192
docs/Rectangle-AABB-Matrix.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Rectangle AABB Matrix
|
||||
|
||||
Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed by a model matrix.
|
||||
|
||||
-----
|
||||
|
||||
Every drawable is a 1 x 1 unit square that is rotated by its direction, scaled by its skin size and scale, and offset by its rotation center and position. The square representation is made up of 4 points that are transformed by the drawable properties. Often we want a shape that simplifies those 4 points into a non-rotated shape, a axis aligned bounding box.
|
||||
|
||||
One approach is to compare the x and y components of each transformed vector and find the minimum and maximum x component and the minimum and maximum y component.
|
||||
|
||||
We can start from this approach and determine an alternative one that prodcues the same output with less work.
|
||||
|
||||
Starting with transforming one point, here is a 3D point, `v`, transformation by a matrix, `m`.
|
||||
|
||||
```js
|
||||
const v0 = v[0];
|
||||
const v1 = v[1];
|
||||
const v2 = v[2];
|
||||
|
||||
const d = v0 * m[(0 * 4) + 3] + v1 * m[(1 * 4) + 3] + v2 * m[(2 * 4) + 3] + m[(3 * 4) + 3];
|
||||
dst[0] = (v0 * m[(0 * 4) + 0] + v1 * m[(1 * 4) + 0] + v2 * m[(2 * 4) + 0] + m[(3 * 4) + 0]) / d;
|
||||
dst[1] = (v0 * m[(0 * 4) + 1] + v1 * m[(1 * 4) + 1] + v2 * m[(2 * 4) + 1] + m[(3 * 4) + 1]) / d;
|
||||
dst[2] = (v0 * m[(0 * 4) + 2] + v1 * m[(1 * 4) + 2] + v2 * m[(2 * 4) + 2] + m[(3 * 4) + 2]) / d;
|
||||
```
|
||||
|
||||
As this is a 2D rectangle we can cancel out the third dimension, and the determinant, 'd'.
|
||||
|
||||
```js
|
||||
const v0 = v[0];
|
||||
const v1 = v[1];
|
||||
|
||||
dst = [
|
||||
v0 * m[(0 * 4) + 0] + v1 * m[(1 * 4) + 0] + m[(3 * 4) + 0,
|
||||
v0 * m[(0 * 4) + 1] + v1 * m[(1 * 4) + 1] + m[(3 * 4) + 1
|
||||
];
|
||||
```
|
||||
|
||||
Let's set the matrix points to shorter names for convenience.
|
||||
|
||||
```js
|
||||
const m00 = m[(0 * 4) + 0];
|
||||
const m01 = m[(0 * 4) + 1];
|
||||
const m10 = m[(1 * 4) + 0];
|
||||
const m11 = m[(1 * 4) + 1];
|
||||
const m30 = m[(3 * 4) + 0];
|
||||
const m31 = m[(3 * 4) + 1];
|
||||
```
|
||||
|
||||
We need 4 points with positive and negative 0.5 values so the square has sides of length 1.
|
||||
|
||||
```js
|
||||
let p = [0.5, 0.5];
|
||||
let q = [-0.5, 0.5];
|
||||
let r = [-0.5, -0.5];
|
||||
let s = [0.5, -0.5];
|
||||
```
|
||||
|
||||
Transform the points by the matrix.
|
||||
|
||||
```js
|
||||
p = [
|
||||
0.5 * m00 + 0.5 * m10 + m30,
|
||||
0.5 * m01 + 0.5 * m11 + m31
|
||||
];
|
||||
q = [
|
||||
-0.5 * m00 + -0.5 * m10 + m30,
|
||||
0.5 * m01 + 0.5 * m11 + m31
|
||||
];
|
||||
r = [
|
||||
-0.5 * m00 + -0.5 * m10 + m30,
|
||||
-0.5 * m01 + -0.5 * m11 + m31
|
||||
];
|
||||
s = [
|
||||
0.5 * m00 + 0.5 * m10 + m30,
|
||||
-0.5 * m01 + -0.5 * m11 + m31
|
||||
];
|
||||
```
|
||||
|
||||
With 4 transformed points we can build the left, right, top, and bottom values for the Rectangle. Each will use the minimum or the maximum of one of the components of all points.
|
||||
|
||||
```js
|
||||
const left = Math.min(p[0], q[0], r[0], s[0]);
|
||||
const right = Math.max(p[0], q[0], r[0], s[0]);
|
||||
const top = Math.max(p[1], q[1], r[1], s[1]);
|
||||
const bottom = Math.min(p[1], q[1], r[1], s[1]);
|
||||
```
|
||||
|
||||
Fill those calls with the vector expressions.
|
||||
|
||||
```js
|
||||
const left = Math.min(
|
||||
0.5 * m00 + 0.5 * m10 + m30,
|
||||
-0.5 * m00 + 0.5 * m10 + m30,
|
||||
-0.5 * m00 + -0.5 * m10 + m30,
|
||||
0.5 * m00 + -0.5 * m10 + m30
|
||||
);
|
||||
const right = Math.max(
|
||||
0.5 * m00 + 0.5 * m10 + m30,
|
||||
-0.5 * m00 + 0.5 * m10 + m30,
|
||||
-0.5 * m00 + -0.5 * m10 + m30,
|
||||
0.5 * m00 + -0.5 * m10 + m30
|
||||
);
|
||||
const top = Math.max(
|
||||
0.5 * m01 + 0.5 * m11 + m31,
|
||||
-0.5 * m01 + 0.5 * m11 + m31,
|
||||
-0.5 * m01 + -0.5 * m11 + m31,
|
||||
0.5 * m01 + -0.5 * m11 + m31
|
||||
);
|
||||
const bottom = Math.min(
|
||||
0.5 * m01 + 0.5 * m11 + m31,
|
||||
-0.5 * m01 + 0.5 * m11 + m31,
|
||||
-0.5 * m01 + -0.5 * m11 + m31,
|
||||
0.5 * m01 + -0.5 * m11 + m31
|
||||
);
|
||||
```
|
||||
|
||||
Pull out the `0.5 * m??` patterns.
|
||||
|
||||
```js
|
||||
const x0 = 0.5 * m00;
|
||||
const x1 = 0.5 * m10;
|
||||
const y0 = 0.5 * m01;
|
||||
const y1 = 0.5 * m11;
|
||||
|
||||
const left = Math.min(x0 + x1 + m30, -x0 + x1 + m30, -x0 + -x1 + m30, x0 + -x1 + m30);
|
||||
const right = Math.max(x0 + x1 + m30, -x0 + x1 + m30, -x0 + -x1 + m30, x0 + -x1 + m30);
|
||||
const top = Math.max(y0 + y1 + m31, -y0 + y1 + m31, -y0 + -y1 + m31, y0 + -y1 + m31);
|
||||
const bottom = Math.min(y0 + y1 + m31, -y0 + y1 + m31, -y0 + -y1 + m31, y0 + -y1 + m31);
|
||||
```
|
||||
|
||||
Now each argument for the min and max calls take an expression like `(a * x0 + b * x1 + m3?)`. As each expression has the x0, x1, and m3? variables we can split the min and max calls on the addition operators. Each new call has all the coefficients of that variable.
|
||||
|
||||
```js
|
||||
const left = Math.min(x0, -x0) + Math.min(x1, -x1) + Math.min(m30, m30);
|
||||
const right = Math.max(x0, -x0) + Math.max(x1, -x1) + Math.max(m30, m30);
|
||||
const top = Math.max(y0, -y0) + Math.max(y1, -y1) + Math.max(m31, m31);
|
||||
const bottom = Math.min(y0, -y0) + Math.min(y1, -y1) + Math.min(m31, m31);
|
||||
```
|
||||
|
||||
The min or max of two copies of the same value will just be that value.
|
||||
|
||||
```js
|
||||
const left = Math.min(x0, -x0) + Math.min(x1, -x1) + m30;
|
||||
const right = Math.max(x0, -x0) + Math.max(x1, -x1) + m30;
|
||||
const top = Math.max(y0, -y0) + Math.max(y1, -y1) + m31;
|
||||
const bottom = Math.min(y0, -y0) + Math.min(y1, -y1) + m31;
|
||||
```
|
||||
|
||||
The max of a negative and positive variable will be the absolute value of that variable. The min of a negative and positive variable will the negated absolute value of that variable.
|
||||
|
||||
```js
|
||||
const left = -Math.abs(x0) + -Math.abs(x1) + m30;
|
||||
const right = Math.abs(x0) + Math.abs(x1) + m30;
|
||||
const top = Math.abs(y0) + Math.abs(y1) + m31;
|
||||
const bottom = -Math.abs(y0) + -Math.abs(y1) + m31;
|
||||
```
|
||||
|
||||
Pulling out the negations of the absolute values, left and right as well as top and bottom are the positive or negative sum of the absolute value of the saled and rotated unit value.
|
||||
|
||||
```js
|
||||
const left = -(Math.abs(x0) + Math.abs(x1)) + m30;
|
||||
const right = Math.abs(x0) + Math.abs(x1) + m30;
|
||||
const top = Math.abs(y0) + Math.abs(y1) + m31;
|
||||
const bottom = -(Math.abs(y0) + Math.abs(y1)) + m31;
|
||||
```
|
||||
|
||||
We call pull out those sums and use them twice.
|
||||
|
||||
```js
|
||||
const x = Math.abs(x0) + Math.abs(x1);
|
||||
const y = Math.abs(y0) + Math.abs(y1);
|
||||
|
||||
const left = -x + m30;
|
||||
const right = x + m30;
|
||||
const top = y + m31;
|
||||
const bottom = -y + m31;
|
||||
```
|
||||
|
||||
This lets us arrive at our goal. Inlining some of our variables we get this block that will initialize a Rectangle to a unit square transformed by a matrix.
|
||||
|
||||
```js
|
||||
const m30 = m[(3 * 4) + 0];
|
||||
const m31 = m[(3 * 4) + 1];
|
||||
|
||||
const x = Math.abs(0.5 * m[(0 * 4) + 0]) + Math.abs(0.5 * m[(1 * 4) + 0]);
|
||||
const y = Math.abs(0.5 * m[(0 * 4) + 1]) + Math.abs(0.5 * m[(1 * 4) + 1]);
|
||||
|
||||
const left = -x + m30;
|
||||
const right = x + m30;
|
||||
const top = y + m31;
|
||||
const bottom = -y + m31;
|
||||
```
|
||||
@@ -32,12 +32,12 @@
|
||||
"chromeless": "^1.5.1",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"docdash": "^0.4.0",
|
||||
"eslint": "^6.2.0",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-config-scratch": "^5.0.0",
|
||||
"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.20190213162739",
|
||||
"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.20190715153806",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20191104164753",
|
||||
"twgl.js": "4.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,16 +56,17 @@ class BitmapSkin extends Skin {
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getTexture (scale) {
|
||||
return this._texture;
|
||||
return this._texture || super.getTexture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds of the drawable for determining its fenced position.
|
||||
* @param {Array<number>} drawable - The Drawable instance this skin is using.
|
||||
* @param {?Rectangle} result - Optional destination for bounds calculation.
|
||||
* @return {!Rectangle} The drawable's bounds. For compatibility with Scratch 2, we always use getAABB for bitmaps.
|
||||
*/
|
||||
getFenceBounds (drawable) {
|
||||
return drawable.getAABB();
|
||||
getFenceBounds (drawable, result) {
|
||||
return drawable.getAABB(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,6 +78,10 @@ class BitmapSkin extends Skin {
|
||||
* @fires Skin.event:WasAltered
|
||||
*/
|
||||
setBitmap (bitmapData, costumeResolution, rotationCenter) {
|
||||
if (!bitmapData.width || !bitmapData.height) {
|
||||
super.setEmptyImageData();
|
||||
return;
|
||||
}
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
// Preferably bitmapData is ImageData. ImageData speeds up updating
|
||||
|
||||
179
src/Drawable.js
179
src/Drawable.js
@@ -183,56 +183,99 @@ class Drawable {
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position if it is different. Marks the transform as dirty.
|
||||
* @param {Array.<number>} position A new position.
|
||||
*/
|
||||
updatePosition (position) {
|
||||
if (this._position[0] !== position[0] ||
|
||||
this._position[1] !== position[1]) {
|
||||
this._position[0] = Math.round(position[0]);
|
||||
this._position[1] = Math.round(position[1]);
|
||||
this.setTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the direction if it is different. Marks the transform as dirty.
|
||||
* @param {number} direction A new direction.
|
||||
*/
|
||||
updateDirection (direction) {
|
||||
if (this._direction !== direction) {
|
||||
this._direction = direction;
|
||||
this._rotationTransformDirty = true;
|
||||
this.setTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the scale if it is different. Marks the transform as dirty.
|
||||
* @param {Array.<number>} scale A new scale.
|
||||
*/
|
||||
updateScale (scale) {
|
||||
if (this._scale[0] !== scale[0] ||
|
||||
this._scale[1] !== scale[1]) {
|
||||
this._scale[0] = scale[0];
|
||||
this._scale[1] = scale[1];
|
||||
this._rotationCenterDirty = true;
|
||||
this._skinScaleDirty = true;
|
||||
this.setTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update visibility if it is different. Marks the convex hull as dirty.
|
||||
* @param {boolean} visible A new visibility state.
|
||||
*/
|
||||
updateVisible (visible) {
|
||||
if (this._visible !== visible) {
|
||||
this._visible = visible;
|
||||
this.setConvexHullDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an effect. Marks the convex hull as dirty if the effect changes shape.
|
||||
* @param {string} effectName The name of the effect.
|
||||
* @param {number} rawValue A new effect value.
|
||||
*/
|
||||
updateEffect (effectName, rawValue) {
|
||||
const effectInfo = ShaderManager.EFFECT_INFO[effectName];
|
||||
if (rawValue) {
|
||||
this._effectBits |= effectInfo.mask;
|
||||
} else {
|
||||
this._effectBits &= ~effectInfo.mask;
|
||||
}
|
||||
const converter = effectInfo.converter;
|
||||
this._uniforms[effectInfo.uniformName] = converter(rawValue);
|
||||
if (effectInfo.shapeChanges) {
|
||||
this.setConvexHullDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position, direction, scale, or effect properties of this Drawable.
|
||||
* @deprecated Use specific update* methods instead.
|
||||
* @param {object.<string,*>} properties The new property values to set.
|
||||
*/
|
||||
updateProperties (properties) {
|
||||
let dirty = false;
|
||||
if ('position' in properties && (
|
||||
this._position[0] !== properties.position[0] ||
|
||||
this._position[1] !== properties.position[1])) {
|
||||
this._position[0] = Math.round(properties.position[0]);
|
||||
this._position[1] = Math.round(properties.position[1]);
|
||||
dirty = true;
|
||||
if ('position' in properties) {
|
||||
this.updatePosition(properties.position);
|
||||
}
|
||||
if ('direction' in properties && this._direction !== properties.direction) {
|
||||
this._direction = properties.direction;
|
||||
this._rotationTransformDirty = true;
|
||||
dirty = true;
|
||||
if ('direction' in properties) {
|
||||
this.updateDirection(properties.direction);
|
||||
}
|
||||
if ('scale' in properties && (
|
||||
this._scale[0] !== properties.scale[0] ||
|
||||
this._scale[1] !== properties.scale[1])) {
|
||||
this._scale[0] = properties.scale[0];
|
||||
this._scale[1] = properties.scale[1];
|
||||
this._rotationCenterDirty = true;
|
||||
this._skinScaleDirty = true;
|
||||
dirty = true;
|
||||
if ('scale' in properties) {
|
||||
this.updateScale(properties.scale);
|
||||
}
|
||||
if ('visible' in properties) {
|
||||
this._visible = properties.visible;
|
||||
this.setConvexHullDirty();
|
||||
}
|
||||
if (dirty) {
|
||||
this.setTransformDirty();
|
||||
this.updateVisible(properties.visible);
|
||||
}
|
||||
const numEffects = ShaderManager.EFFECTS.length;
|
||||
for (let index = 0; index < numEffects; ++index) {
|
||||
const effectName = ShaderManager.EFFECTS[index];
|
||||
if (effectName in properties) {
|
||||
const rawValue = properties[effectName];
|
||||
const effectInfo = ShaderManager.EFFECT_INFO[effectName];
|
||||
if (rawValue) {
|
||||
this._effectBits |= effectInfo.mask;
|
||||
} else {
|
||||
this._effectBits &= ~effectInfo.mask;
|
||||
}
|
||||
const converter = effectInfo.converter;
|
||||
this._uniforms[effectInfo.uniformName] = converter(rawValue);
|
||||
if (effectInfo.shapeChanges) {
|
||||
this.setConvexHullDirty();
|
||||
}
|
||||
this.updateEffect(effectName, properties[effectName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +461,9 @@ class Drawable {
|
||||
|
||||
const localPosition = getLocalPosition(this, vec);
|
||||
|
||||
if (this.useNearest) {
|
||||
// We're not passing in a scale to useNearest, but that's okay because "touching" queries
|
||||
// happen at the "native" size anyway.
|
||||
if (this.useNearest()) {
|
||||
return this.skin.isTouchingNearest(localPosition);
|
||||
}
|
||||
return this.skin.isTouchingLinear(localPosition);
|
||||
@@ -426,21 +471,33 @@ class Drawable {
|
||||
|
||||
/**
|
||||
* Should the drawable use NEAREST NEIGHBOR or LINEAR INTERPOLATION mode
|
||||
* @param {?Array<Number>} scale Optionally, the screen-space scale of the drawable.
|
||||
* @return {boolean} True if the drawable should use nearest-neighbor interpolation.
|
||||
*/
|
||||
get useNearest () {
|
||||
useNearest (scale = this.scale) {
|
||||
// Raster skins (bitmaps) should always prefer nearest neighbor
|
||||
if (this.skin.isRaster) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the effect bits for mosaic, pixelate, whirl, or fisheye are set, use linear
|
||||
if ((this._effectBits & (
|
||||
ShaderManager.EFFECT_INFO.fisheye.mask |
|
||||
ShaderManager.EFFECT_INFO.whirl.mask |
|
||||
ShaderManager.EFFECT_INFO.pixelate.mask |
|
||||
ShaderManager.EFFECT_INFO.mosaic.mask
|
||||
)) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We can't use nearest neighbor unless we are a multiple of 90 rotation
|
||||
if (this._direction % 90 !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (Math.abs(scale[0]) > 99 && Math.abs(scale[0]) < 101 &&
|
||||
Math.abs(scale[1]) > 99 && Math.abs(scale[1]) < 101) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -451,9 +508,10 @@ class Drawable {
|
||||
* This function applies the transform matrix to the known convex hull,
|
||||
* and then finds the minimum box along the axes.
|
||||
* Before calling this, ensure the renderer has updated convex hull points.
|
||||
* @param {?Rectangle} result optional destination for bounds calculation
|
||||
* @return {!Rectangle} Bounds for a tight box around the Drawable.
|
||||
*/
|
||||
getBounds () {
|
||||
getBounds (result) {
|
||||
if (this.needsConvexHullPoints()) {
|
||||
throw new Error('Needs updated convex hull points before bounds calculation.');
|
||||
}
|
||||
@@ -462,18 +520,19 @@ class Drawable {
|
||||
}
|
||||
const transformedHullPoints = this._getTransformedHullPoints();
|
||||
// Search through transformed points to generate box on axes.
|
||||
const bounds = new Rectangle();
|
||||
bounds.initFromPointsAABB(transformedHullPoints);
|
||||
return bounds;
|
||||
result = result || new Rectangle();
|
||||
result.initFromPointsAABB(transformedHullPoints);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the precise bounds for the upper 8px slice of the Drawable.
|
||||
* Used for calculating where to position a text bubble.
|
||||
* Before calling this, ensure the renderer has updated convex hull points.
|
||||
* @param {?Rectangle} result optional destination for bounds calculation
|
||||
* @return {!Rectangle} Bounds for a tight box around a slice of the Drawable.
|
||||
*/
|
||||
getBoundsForBubble () {
|
||||
getBoundsForBubble (result) {
|
||||
if (this.needsConvexHullPoints()) {
|
||||
throw new Error('Needs updated convex hull points before bubble bounds calculation.');
|
||||
}
|
||||
@@ -485,9 +544,9 @@ class Drawable {
|
||||
const maxY = Math.max.apply(null, transformedHullPoints.map(p => p[1]));
|
||||
const filteredHullPoints = transformedHullPoints.filter(p => p[1] > maxY - slice);
|
||||
// Search through filtered points to generate box on axes.
|
||||
const bounds = new Rectangle();
|
||||
bounds.initFromPointsAABB(filteredHullPoints);
|
||||
return bounds;
|
||||
result = result || new Rectangle();
|
||||
result.initFromPointsAABB(filteredHullPoints);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,35 +556,31 @@ class Drawable {
|
||||
* which is tightly snapped to account for a Drawable's transparent regions.
|
||||
* `getAABB` returns a much less accurate bounding box, but will be much
|
||||
* faster to calculate so may be desired for quick checks/optimizations.
|
||||
* @param {?Rectangle} result optional destination for bounds calculation
|
||||
* @return {!Rectangle} Rough axis-aligned bounding box for Drawable.
|
||||
*/
|
||||
getAABB () {
|
||||
getAABB (result) {
|
||||
if (this._transformDirty) {
|
||||
this._calculateTransform();
|
||||
}
|
||||
const tm = this._uniforms.u_modelMatrix;
|
||||
const bounds = new Rectangle();
|
||||
bounds.initFromPointsAABB([
|
||||
twgl.m4.transformPoint(tm, [-0.5, -0.5, 0]),
|
||||
twgl.m4.transformPoint(tm, [0.5, -0.5, 0]),
|
||||
twgl.m4.transformPoint(tm, [-0.5, 0.5, 0]),
|
||||
twgl.m4.transformPoint(tm, [0.5, 0.5, 0])
|
||||
]);
|
||||
return bounds;
|
||||
result = result || new Rectangle();
|
||||
result.initFromModelMatrix(tm);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the best Drawable bounds possible without performing graphics queries.
|
||||
* I.e., returns the tight bounding box when the convex hull points are already
|
||||
* known, but otherwise return the rough AABB of the Drawable.
|
||||
* @param {?Rectangle} result optional destination for bounds calculation
|
||||
* @return {!Rectangle} Bounds for the Drawable.
|
||||
*/
|
||||
getFastBounds () {
|
||||
this.updateMatrix();
|
||||
getFastBounds (result) {
|
||||
if (!this.needsConvexHullPoints()) {
|
||||
return this.getBounds();
|
||||
return this.getBounds(result);
|
||||
}
|
||||
return this.getAABB();
|
||||
return this.getAABB(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -634,7 +689,7 @@ class Drawable {
|
||||
}
|
||||
const textColor =
|
||||
// commenting out to only use nearest for now
|
||||
// drawable.useNearest ?
|
||||
// drawable.useNearest() ?
|
||||
drawable.skin._silhouette.colorAtNearest(localPosition, dst);
|
||||
// : drawable.skin._silhouette.colorAtLinear(localPosition, dst);
|
||||
return EffectTransform.transformColor(drawable, textColor, textColor);
|
||||
|
||||
@@ -332,8 +332,7 @@ class PenSkin extends Skin {
|
||||
|
||||
const uniforms = {
|
||||
u_skin: this._texture,
|
||||
u_projectionMatrix: projection,
|
||||
u_fudge: 0
|
||||
u_projectionMatrix: projection
|
||||
};
|
||||
|
||||
twgl.setUniforms(currentShader, uniforms);
|
||||
@@ -474,8 +473,7 @@ class PenSkin extends Skin {
|
||||
0
|
||||
), __modelScalingMatrix),
|
||||
__modelMatrix
|
||||
),
|
||||
u_fudge: 0
|
||||
)
|
||||
};
|
||||
|
||||
twgl.setTextureParameters(gl, texture, {minMag: gl.NEAREST});
|
||||
|
||||
@@ -54,6 +54,31 @@ class Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a Rectangle to a 1 unit square centered at 0 x 0 transformed
|
||||
* by a model matrix.
|
||||
* @param {Array.<number>} m A 4x4 matrix to transform the rectangle by.
|
||||
* @tutorial Rectangle-AABB-Matrix
|
||||
*/
|
||||
initFromModelMatrix (m) {
|
||||
// In 2D space, we will soon use the 2x2 "top left" scale and rotation
|
||||
// submatrix, while we store and the 1x2 "top right" that position
|
||||
// vector.
|
||||
const m30 = m[(3 * 4) + 0];
|
||||
const m31 = m[(3 * 4) + 1];
|
||||
|
||||
// "Transform" a (0.5, 0.5) vector by the scale and rotation matrix but
|
||||
// sum the absolute of each component instead of use the signed values.
|
||||
const x = Math.abs(0.5 * m[(0 * 4) + 0]) + Math.abs(0.5 * m[(1 * 4) + 0]);
|
||||
const y = Math.abs(0.5 * m[(0 * 4) + 1]) + Math.abs(0.5 * m[(1 * 4) + 1]);
|
||||
|
||||
// And adding them to the position components initializes our Rectangle.
|
||||
this.left = -x + m30;
|
||||
this.right = x + m30;
|
||||
this.top = y + m31;
|
||||
this.bottom = -y + m31;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this Rectangle intersects some other.
|
||||
* Note that this is a comparison assuming the Rectangle was
|
||||
@@ -98,11 +123,11 @@ class Rectangle {
|
||||
this.right = Math.min(this.right, right);
|
||||
this.bottom = Math.max(this.bottom, bottom);
|
||||
this.top = Math.min(this.top, top);
|
||||
// Ensure rectangle coordinates in order.
|
||||
this.left = Math.min(this.left, this.right);
|
||||
this.right = Math.max(this.right, this.left);
|
||||
this.bottom = Math.min(this.bottom, this.top);
|
||||
this.top = Math.max(this.top, this.bottom);
|
||||
|
||||
this.left = Math.min(this.left, right);
|
||||
this.right = Math.max(this.right, left);
|
||||
this.bottom = Math.min(this.bottom, top);
|
||||
this.top = Math.max(this.top, bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ const log = require('./util/log');
|
||||
|
||||
const __isTouchingDrawablesPoint = twgl.v3.create();
|
||||
const __candidatesBounds = new Rectangle();
|
||||
const __fenceBounds = new Rectangle();
|
||||
const __touchingColor = new Uint8ClampedArray(4);
|
||||
const __blendColor = new Uint8ClampedArray(4);
|
||||
|
||||
@@ -104,7 +105,7 @@ class RenderWebGL extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
static _getContext (canvas) {
|
||||
return twgl.getWebGLContext(canvas, {alpha: false, stencil: true});
|
||||
return twgl.getWebGLContext(canvas, {alpha: false, stencil: true, antialias: false});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -393,8 +394,6 @@ class RenderWebGL extends EventEmitter {
|
||||
for (const drawable of this._allDrawables) {
|
||||
if (drawable && drawable.skin === oldSkin) {
|
||||
drawable.skin = newSkin;
|
||||
drawable.setConvexHullDirty();
|
||||
drawable.setTransformDirty();
|
||||
}
|
||||
}
|
||||
oldSkin.dispose();
|
||||
@@ -1313,9 +1312,122 @@ class RenderWebGL extends EventEmitter {
|
||||
}, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's skin.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {number} skinId The skin to update to.
|
||||
*/
|
||||
updateDrawableSkinId (drawableID, skinId) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.skin = this._allSkins[skinId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's skin rotation center.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {Array.<number>} rotationCenter The rotation center for the skin.
|
||||
*/
|
||||
updateDrawableRotationCenter (drawableID, rotationCenter) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.skin.setRotationCenter(rotationCenter[0], rotationCenter[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's skin and rotation center together.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {number} skinId The skin to update to.
|
||||
* @param {Array.<number>} rotationCenter The rotation center for the skin.
|
||||
*/
|
||||
updateDrawableSkinIdRotationCenter (drawableID, skinId, rotationCenter) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.skin = this._allSkins[skinId];
|
||||
drawable.skin.setRotationCenter(rotationCenter[0], rotationCenter[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's position.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {Array.<number>} position The new position.
|
||||
*/
|
||||
updateDrawablePosition (drawableID, position) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.updatePosition(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's direction.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {number} direction A new direction.
|
||||
*/
|
||||
updateDrawableDirection (drawableID, direction) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.updateDirection(direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's scale.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {Array.<number>} scale A new scale.
|
||||
*/
|
||||
updateDrawableScale (drawableID, scale) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.updateScale(scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's direction and scale together.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {number} direction A new direction.
|
||||
* @param {Array.<number>} scale A new scale.
|
||||
*/
|
||||
updateDrawableDirectionScale (drawableID, direction, scale) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.updateDirection(direction);
|
||||
drawable.updateScale(scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's visibility.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {boolean} visible Will the drawable be visible?
|
||||
*/
|
||||
updateDrawableVisible (drawableID, visible) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.updateVisible(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a drawable's visual effect.
|
||||
* @param {number} drawableID The drawable's id.
|
||||
* @param {string} effectName The effect to change.
|
||||
* @param {number} value A new effect value.
|
||||
*/
|
||||
updateDrawableEffect (drawableID, effectName, value) {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
// TODO: https://github.com/LLK/scratch-vm/issues/2288
|
||||
if (!drawable) return;
|
||||
drawable.updateEffect(effectName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the position, direction, scale, or effect properties of this Drawable.
|
||||
* @deprecated Use specific updateDrawable* methods instead.
|
||||
* @param {int} drawableID The ID of the Drawable to update.
|
||||
* @param {object.<string,*>} properties The new property values to set.
|
||||
*/
|
||||
@@ -1323,17 +1435,16 @@ class RenderWebGL extends EventEmitter {
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
if (!drawable) {
|
||||
/**
|
||||
* @todo fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
||||
* @todo(https://github.com/LLK/scratch-vm/issues/2288) fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
||||
* Right now this happens so much on some projects that a warning or exception here can hang the browser.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
if ('skinId' in properties) {
|
||||
drawable.skin = this._allSkins[properties.skinId];
|
||||
this.updateDrawableSkinId(drawableID, properties.skinId);
|
||||
}
|
||||
if ('rotationCenter' in properties) {
|
||||
const newRotationCenter = properties.rotationCenter;
|
||||
drawable.skin.setRotationCenter(newRotationCenter[0], newRotationCenter[1]);
|
||||
this.updateDrawableRotationCenter(drawableID, properties.rotationCenter);
|
||||
}
|
||||
drawable.updateProperties(properties);
|
||||
}
|
||||
@@ -1350,14 +1461,14 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
const drawable = this._allDrawables[drawableID];
|
||||
if (!drawable) {
|
||||
// TODO: fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
||||
// @todo(https://github.com/LLK/scratch-vm/issues/2288) fix whatever's wrong in the VM which causes this, then add a warning or throw here.
|
||||
// Right now this happens so much on some projects that a warning or exception here can hang the browser.
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
const dx = x - drawable._position[0];
|
||||
const dy = y - drawable._position[1];
|
||||
const aabb = drawable._skin.getFenceBounds(drawable);
|
||||
const aabb = drawable._skin.getFenceBounds(drawable, __fenceBounds);
|
||||
const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2);
|
||||
|
||||
const sx = this._xRight - Math.min(FENCE_WIDTH, inset);
|
||||
@@ -1416,8 +1527,6 @@ class RenderWebGL extends EventEmitter {
|
||||
* @param {int} stampID - the unique ID of the Drawable to use as the stamp.
|
||||
*/
|
||||
penStamp (penSkinID, stampID) {
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const stampDrawable = this._allDrawables[stampID];
|
||||
if (!stampDrawable) {
|
||||
return;
|
||||
@@ -1428,6 +1537,8 @@ class RenderWebGL extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this._doExitDrawRegion();
|
||||
|
||||
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
|
||||
|
||||
const gl = this._gl;
|
||||
@@ -1548,6 +1659,7 @@ class RenderWebGL extends EventEmitter {
|
||||
this._exitRegion();
|
||||
}
|
||||
this._exitRegion = null;
|
||||
this._regionId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1606,8 +1718,7 @@ class RenderWebGL extends EventEmitter {
|
||||
gl.useProgram(currentShader.program);
|
||||
twgl.setBuffersAndAttributes(gl, currentShader, this._bufferInfo);
|
||||
Object.assign(uniforms, {
|
||||
u_projectionMatrix: projection,
|
||||
u_fudge: window.fudge || 0
|
||||
u_projectionMatrix: projection
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1622,19 +1733,19 @@ class RenderWebGL extends EventEmitter {
|
||||
|
||||
if (uniforms.u_skin) {
|
||||
twgl.setTextureParameters(
|
||||
gl, uniforms.u_skin, {minMag: drawable.useNearest ? gl.NEAREST : gl.LINEAR}
|
||||
gl, uniforms.u_skin, {minMag: drawable.useNearest(drawableScale) ? gl.NEAREST : gl.LINEAR}
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
56
src/SVGMIP.js
Normal file
56
src/SVGMIP.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const twgl = require('twgl.js');
|
||||
|
||||
class SVGMIP {
|
||||
/**
|
||||
* Create a new SVG MIP for a given scale.
|
||||
* @param {RenderWebGL} renderer - The renderer which this MIP's skin uses.
|
||||
* @param {SvgRenderer} svgRenderer - The svg renderer which this MIP's skin uses.
|
||||
* @param {number} scale - The relative size of the MIP
|
||||
* @param {function} callback - A callback that should always fire after draw()
|
||||
* @constructor
|
||||
*/
|
||||
constructor (renderer, svgRenderer, scale, callback) {
|
||||
this._renderer = renderer;
|
||||
this._svgRenderer = svgRenderer;
|
||||
this._scale = scale;
|
||||
this._texture = null;
|
||||
this._callback = callback;
|
||||
|
||||
this.draw();
|
||||
}
|
||||
|
||||
draw () {
|
||||
this._svgRenderer._draw(this._scale, () => {
|
||||
const textureData = this._getTextureData();
|
||||
const textureOptions = {
|
||||
auto: false,
|
||||
wrap: this._renderer.gl.CLAMP_TO_EDGE,
|
||||
src: textureData
|
||||
};
|
||||
|
||||
this._texture = twgl.createTexture(this._renderer.gl, textureOptions);
|
||||
this._callback(textureData);
|
||||
});
|
||||
}
|
||||
|
||||
dispose () {
|
||||
this._renderer.gl.deleteTexture(this.getTexture());
|
||||
}
|
||||
|
||||
getTexture () {
|
||||
return this._texture;
|
||||
}
|
||||
|
||||
_getTextureData () {
|
||||
// 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);
|
||||
|
||||
return textureData;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SVGMIP;
|
||||
163
src/SVGSkin.js
163
src/SVGSkin.js
@@ -1,9 +1,16 @@
|
||||
const twgl = require('twgl.js');
|
||||
|
||||
const Skin = require('./Skin');
|
||||
const SVGMIP = require('./SVGMIP');
|
||||
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
|
||||
|
||||
const MAX_TEXTURE_DIMENSION = 2048;
|
||||
const MIN_TEXTURE_SCALE = 1 / 256;
|
||||
/**
|
||||
* All scaled renderings of the SVG are stored in an array. The 1.0 scale of
|
||||
* the SVG is stored at the 8th index. The smallest possible 1 / 256 scale
|
||||
* rendering is stored at the 0th index.
|
||||
* @const {number}
|
||||
*/
|
||||
const INDEX_OFFSET = 8;
|
||||
|
||||
class SVGSkin extends Skin {
|
||||
/**
|
||||
@@ -25,11 +32,14 @@ class SVGSkin extends Skin {
|
||||
/** @type {WebGLTexture} */
|
||||
this._texture = null;
|
||||
|
||||
/** @type {number} */
|
||||
this._textureScale = 1;
|
||||
/** @type {Array.<SVGMIPs>} */
|
||||
this._scaledMIPs = [];
|
||||
|
||||
/** @type {Number} */
|
||||
this._maxTextureScale = 0;
|
||||
/**
|
||||
* Ratio of the size of the SVG and the max size of the WebGL texture
|
||||
* @type {Number}
|
||||
*/
|
||||
this._maxTextureScale = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,8 +47,13 @@ class SVGSkin extends Skin {
|
||||
*/
|
||||
dispose () {
|
||||
if (this._texture) {
|
||||
this._renderer.gl.deleteTexture(this._texture);
|
||||
for (const mip of this._scaledMIPs) {
|
||||
if (mip) {
|
||||
mip.dispose();
|
||||
}
|
||||
}
|
||||
this._texture = null;
|
||||
this._scaledMIPs.length = 0;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@@ -60,83 +75,105 @@ class SVGSkin extends Skin {
|
||||
super.setRotationCenter(x - viewOffset[0], y - viewOffset[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a MIP for a given scale and pass it a callback for updating
|
||||
* state when switching between scales and MIPs.
|
||||
* @param {number} scale - The relative size of the MIP
|
||||
* @param {function} resetCallback - this is a callback for doing a hard reset
|
||||
* of MIPs and a reset of the rotation center. Only passed in if the MIP scale is 1.
|
||||
* @return {SVGMIP} An object that handles creating and updating SVG textures.
|
||||
*/
|
||||
createMIP (scale, resetCallback) {
|
||||
const textureCallback = textureData => {
|
||||
if (resetCallback) resetCallback();
|
||||
// Check if we have the largest MIP
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
if (!this._scaledMIPs.length || this._scaledMIPs[this._scaledMIPs.length - 1]._scale <= scale) {
|
||||
// Currently silhouette only gets scaled up
|
||||
this._silhouette.update(textureData);
|
||||
}
|
||||
};
|
||||
const mip = new SVGMIP(this._renderer, this._svgRenderer, scale, textureCallback);
|
||||
|
||||
return mip;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
|
||||
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getTexture (scale) {
|
||||
if (!this._svgRenderer.canvas.width || !this._svgRenderer.canvas.height) {
|
||||
return super.getTexture();
|
||||
}
|
||||
|
||||
// The texture only ever gets uniform scale. Take the larger of the two axes.
|
||||
const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
|
||||
const requestedScale = Math.min(scaleMax / 100, this._maxTextureScale);
|
||||
let newScale = this._textureScale;
|
||||
while ((newScale < this._maxTextureScale) && (requestedScale >= 1.5 * newScale)) {
|
||||
newScale *= 2;
|
||||
}
|
||||
if (this._textureScale !== newScale) {
|
||||
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);
|
||||
let newScale = 1;
|
||||
let textureIndex = 0;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (requestedScale < 1) {
|
||||
while ((newScale > MIN_TEXTURE_SCALE) && (requestedScale <= newScale * .75)) {
|
||||
newScale /= 2;
|
||||
textureIndex -= 1;
|
||||
}
|
||||
} else {
|
||||
while ((newScale < this._maxTextureScale) && (requestedScale >= 1.5 * newScale)) {
|
||||
newScale *= 2;
|
||||
textureIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return this._texture;
|
||||
if (!this._scaledMIPs[textureIndex + INDEX_OFFSET]) {
|
||||
this._scaledMIPs[textureIndex + INDEX_OFFSET] = this.createMIP(newScale);
|
||||
}
|
||||
|
||||
return this._scaledMIPs[textureIndex + INDEX_OFFSET].getTexture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do a hard reset of the existing MIPs by calling dispose(), setting a new
|
||||
* scale 1 MIP in this._scaledMIPs, and finally updating the rotationCenter.
|
||||
* @param {SVGMIPs} mip - An object that handles creating and updating SVG textures.
|
||||
* @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
|
||||
* calculated from the bounding box
|
||||
* @fires Skin.event:WasAltered
|
||||
*/
|
||||
resetMIPs (mip, rotationCenter) {
|
||||
this._scaledMIPs.forEach(oldMIP => oldMIP.dispose());
|
||||
this._scaledMIPs.length = 0;
|
||||
|
||||
// Set new scale 1 MIP after outdated MIPs have been disposed
|
||||
this._texture = this._scaledMIPs[INDEX_OFFSET] = mip;
|
||||
|
||||
if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
|
||||
this.setRotationCenter.apply(this, rotationCenter);
|
||||
this.emit(Skin.Events.WasAltered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of this skin to a snapshot of the provided SVG data.
|
||||
* @param {string} svgData - new SVG to use.
|
||||
* @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
|
||||
* calculated from the bounding box
|
||||
* @fires Skin.event:WasAltered
|
||||
* @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG.
|
||||
*/
|
||||
setSVG (svgData, rotationCenter) {
|
||||
this._svgRenderer.fromString(svgData, 1, () => {
|
||||
const gl = this._renderer.gl;
|
||||
this._textureScale = this._maxTextureScale = 1;
|
||||
this._svgRenderer.loadString(svgData);
|
||||
|
||||
// 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._svgRenderer.canvas.width || !this._svgRenderer.canvas.height) {
|
||||
super.setEmptyImageData();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
// TODO: mipmaps?
|
||||
const textureOptions = {
|
||||
auto: true,
|
||||
wrap: gl.CLAMP_TO_EDGE,
|
||||
src: textureData
|
||||
};
|
||||
const maxDimension = Math.ceil(Math.max(this.size[0], this.size[1]));
|
||||
let testScale = 2;
|
||||
for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
|
||||
this._maxTextureScale = testScale;
|
||||
}
|
||||
|
||||
this._texture = twgl.createTexture(gl, textureOptions);
|
||||
this._silhouette.update(textureData);
|
||||
}
|
||||
|
||||
const maxDimension = Math.max(this._svgRenderer.canvas.width, this._svgRenderer.canvas.height);
|
||||
let testScale = 2;
|
||||
for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
|
||||
this._maxTextureScale = testScale;
|
||||
}
|
||||
|
||||
if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
|
||||
this.setRotationCenter.apply(this, rotationCenter);
|
||||
this.emit(Skin.Events.WasAltered);
|
||||
});
|
||||
// Create the 1.0 scale MIP at INDEX_OFFSET.
|
||||
const textureScale = 1;
|
||||
const mip = this.createMIP(textureScale, () => this.resetMIPs(mip, rotationCenter));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
39
src/Skin.js
39
src/Skin.js
@@ -140,16 +140,17 @@ class Skin extends EventEmitter {
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getTexture (scale) {
|
||||
return null;
|
||||
return this._emptyImageTexture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bounds of the drawable for determining its fenced position.
|
||||
* @param {Array<number>} drawable - The Drawable instance this skin is using.
|
||||
* @param {?Rectangle} result - Optional destination for bounds calculation.
|
||||
* @return {!Rectangle} The drawable's bounds.
|
||||
*/
|
||||
getFenceBounds (drawable) {
|
||||
return drawable.getFastBounds();
|
||||
getFenceBounds (drawable, result) {
|
||||
return drawable.getFastBounds(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,6 +171,38 @@ class Skin extends EventEmitter {
|
||||
*/
|
||||
updateSilhouette () {}
|
||||
|
||||
/**
|
||||
* Set the contents of this skin to an empty skin.
|
||||
* @fires Skin.event:WasAltered
|
||||
*/
|
||||
setEmptyImageData () {
|
||||
// Free up the current reference to the _texture
|
||||
this._texture = null;
|
||||
|
||||
if (!this._emptyImageData) {
|
||||
// Create a transparent pixel
|
||||
this._emptyImageData = new ImageData(1, 1);
|
||||
|
||||
// Create a new texture and update the silhouette
|
||||
const gl = this._renderer.gl;
|
||||
|
||||
const textureOptions = {
|
||||
auto: true,
|
||||
wrap: gl.CLAMP_TO_EDGE,
|
||||
src: this._emptyImageData
|
||||
};
|
||||
|
||||
// Note: we're using _emptyImageTexture here instead of _texture
|
||||
// so that we can cache this empty texture for later use as needed.
|
||||
// this._texture can get modified by other skins (e.g. BitmapSkin
|
||||
// and SVGSkin, so we can't use that same field for caching)
|
||||
this._emptyImageTexture = twgl.createTexture(gl, textureOptions);
|
||||
}
|
||||
|
||||
this._silhouette.update(this._emptyImageData);
|
||||
this.emit(Skin.Events.WasAltered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this point touch an opaque or translucent point on this skin?
|
||||
* Nearest Neighbor version
|
||||
|
||||
@@ -54,7 +54,7 @@ class TextBubbleSkin extends Skin {
|
||||
/** @type {Array<string>} */
|
||||
this._lines = [];
|
||||
|
||||
this._textSize = {width: 0, height: 0};
|
||||
/** @type {object} */
|
||||
this._textAreaSize = {width: 0, height: 0};
|
||||
|
||||
/** @type {string} */
|
||||
@@ -127,17 +127,14 @@ class TextBubbleSkin extends Skin {
|
||||
this._lines = this.textWrapper.wrapText(BubbleStyle.MAX_LINE_WIDTH, this._text);
|
||||
|
||||
// Measure width of longest line to avoid extra-wide bubbles
|
||||
let longestLine = 0;
|
||||
let longestLineWidth = 0;
|
||||
for (const line of this._lines) {
|
||||
longestLine = Math.max(longestLine, this.measurementProvider.measureText(line));
|
||||
longestLineWidth = Math.max(longestLineWidth, this.measurementProvider.measureText(line));
|
||||
}
|
||||
|
||||
this._textSize.width = longestLine;
|
||||
this._textSize.height = BubbleStyle.LINE_HEIGHT * this._lines.length;
|
||||
|
||||
// Calculate the canvas-space sizes of the padded text area and full text bubble
|
||||
const paddedWidth = Math.max(this._textSize.width, BubbleStyle.MIN_WIDTH) + (BubbleStyle.PADDING * 2);
|
||||
const paddedHeight = this._textSize.height + (BubbleStyle.PADDING * 2);
|
||||
const paddedWidth = Math.max(longestLineWidth, BubbleStyle.MIN_WIDTH) + (BubbleStyle.PADDING * 2);
|
||||
const paddedHeight = (BubbleStyle.LINE_HEIGHT * this._lines.length) + (BubbleStyle.PADDING * 2);
|
||||
|
||||
this._textAreaSize.width = paddedWidth;
|
||||
this._textAreaSize.height = paddedHeight;
|
||||
@@ -183,6 +180,7 @@ class TextBubbleSkin extends Skin {
|
||||
}
|
||||
|
||||
// Draw the bubble's rounded borders
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(BubbleStyle.CORNER_RADIUS, paddedHeight);
|
||||
ctx.arcTo(0, paddedHeight, 0, paddedHeight - BubbleStyle.CORNER_RADIUS, BubbleStyle.CORNER_RADIUS);
|
||||
ctx.arcTo(0, 0, paddedWidth, 0, BubbleStyle.CORNER_RADIUS);
|
||||
@@ -267,14 +265,13 @@ class TextBubbleSkin extends Skin {
|
||||
|
||||
if (this._texture === null) {
|
||||
const textureOptions = {
|
||||
auto: true,
|
||||
wrap: gl.CLAMP_TO_EDGE,
|
||||
src: textureData
|
||||
auto: false,
|
||||
wrap: gl.CLAMP_TO_EDGE
|
||||
};
|
||||
|
||||
this._texture = twgl.createTexture(gl, textureOptions);
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
precision mediump float;
|
||||
|
||||
uniform float u_fudge;
|
||||
|
||||
#ifdef DRAW_MODE_silhouette
|
||||
uniform vec4 u_silhouetteColor;
|
||||
#else // DRAW_MODE_silhouette
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/* IMPORTANT
|
||||
* This snapshot file is auto-generated, but designed for humans.
|
||||
* It should be checked into source control and tracked carefully.
|
||||
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
|
||||
* Make sure to inspect the output below. Do not ignore changes!
|
||||
*/
|
||||
'use strict'
|
||||
exports[`test/integration/scratch-tests.js TAP bubble snapshot > bubble-text-snapshot 1`] = `
|
||||
<text xmlns="http://www.w3.org/2000/svg" alignment-baseline="text-before-edge" font-size="14" fill="#575E75" font-family="Helvetica"><tspan x="0" dy="1.2em"><e*&%$&^$></!abc'></tspan></text>
|
||||
`
|
||||
@@ -1,6 +1,7 @@
|
||||
<body>
|
||||
<script src="../../node_modules/scratch-vm/dist/web/scratch-vm.js"></script>
|
||||
<script src="../../node_modules/scratch-storage/dist/web/scratch-storage.js"></script>
|
||||
<script src="../../node_modules/scratch-svg-renderer/dist/web/scratch-svg-renderer.js"></script>
|
||||
<!-- note: this uses the BUILT version of scratch-render! make sure to npm run build -->
|
||||
<script src="../../dist/web/scratch-render.js"></script>
|
||||
|
||||
@@ -21,6 +22,8 @@
|
||||
|
||||
vm.attachStorage(storage);
|
||||
vm.attachRenderer(render);
|
||||
vm.attachV2SVGAdapter(new ScratchSVGRenderer.SVGRenderer());
|
||||
vm.attachV2BitmapAdapter(new ScratchSVGRenderer.BitmapAdapter());
|
||||
|
||||
document.getElementById('file').addEventListener('click', e => {
|
||||
document.body.removeChild(document.getElementById('loaded'));
|
||||
|
||||
BIN
test/integration/scratch-tests/disappearing-pen.sb3
Normal file
BIN
test/integration/scratch-tests/disappearing-pen.sb3
Normal file
Binary file not shown.
Reference in New Issue
Block a user