Compare commits

...

42 Commits

Author SHA1 Message Date
greenkeeper[bot]
6abe232487 chore(package): update eslint to version 6.8.0
Closes #307
2019-12-20 22:37:46 +00:00
picklesrus
932846f484 Merge pull request #531 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20191217211338
Update scratch-svg-renderer to the latest version 🚀
2019-12-17 16:21:56 -05:00
greenkeeper[bot]
db1c7b3b2d fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20191217211338 2019-12-17 21:14:57 +00:00
DD Liu
e12646871e Merge pull request #487 from adroitwhiz/pen-stamp-to-framebuffer
penStamp() directly to the PenSkin's framebuffer
2019-12-11 15:30:28 -05:00
Chris Willis-Ford
b1274d5820 Merge pull request #431 from ktbee/javascript-scaled-textures
Add JavaScript MIPs for scaling textures larger and smaller
2019-11-26 12:57:51 -08:00
DD Liu
58aa05c0a7 Merge pull request #480 from adroitwhiz/usenearest-fix-2
Fix useNearest() to take renderer scale into account, take two
2019-11-06 15:43:06 -05:00
DD Liu
c6b5824694 Merge pull request #514 from adroitwhiz/simplify-reskin
Remove setConvexHullDirty and setTransformDirty calls from _reskin
2019-11-06 11:36:38 -05:00
DD Liu
066b243f04 Merge pull request #521 from adroitwhiz/text-bubble-edge-fix
Call beginPath on TextBubbleSkin canvas after clearing it with clearRect
2019-11-06 11:29:58 -05:00
DD Liu
45482fecbd Merge pull request #517 from adroitwhiz/text-bubble-cleanups
Clean up TextBubbleSkin code
2019-11-06 11:22:47 -05:00
DD Liu
058fefbb12 Merge pull request #523 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20191104164753
Update scratch-svg-renderer to the latest version 🚀
brings in https://github.com/LLK/scratch-svg-renderer/pull/99
2019-11-04 16:36:27 -05:00
greenkeeper[bot]
c7669b00cf fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20191104164753 2019-11-04 16:49:13 +00:00
DD Liu
89294a4583 Merge pull request #519 from LLK/greenkeeper/scratch-svg-renderer-0.2.0-prerelease.20191031221353
Update scratch-svg-renderer to the latest version 🚀
brings in https://github.com/LLK/scratch-svg-renderer/pull/84
2019-11-01 15:26:48 -04:00
adroitwhiz
e46c0ec3de Call beginPath after clearing text bubble canvas 2019-11-01 10:16:24 -04:00
greenkeeper[bot]
471c88b850 fix(package): update scratch-svg-renderer to version 0.2.0-prerelease.20191031221353 2019-10-31 22:15:21 +00:00
DD Liu
eccfb44a96 Merge pull request #412 from LLK/greenkeeper/scratch-vm-0.2.0-prerelease.20190213162739
Update scratch-vm to the latest version 🚀
2019-10-29 15:54:35 -04:00
Katie Broida
f17252624e Add SVGMIP as a wrapper for WebGL textures to handle creating textures at a different scale and updating them
Check scale for largest MIP
2019-10-28 13:53:10 -04:00
adroitwhiz
861b9429c0 Clean up text bubble code 2019-10-26 10:49:07 -04:00
Karishma Chadha
bc893f86dd Merge pull request #512 from kchadha/skin-empty-image-data
Add Support for Empty Skins
2019-10-21 14:00:04 -04:00
Karishma Chadha
74a7a8c0b6 Code cleanup and remove todo comment. 2019-10-21 13:53:01 -04:00
Karishma Chadha
99af15a974 Update jsdoc comment for new setEmptyImageData function. 2019-10-17 14:35:15 -04:00
adroitwhiz
8dbd71470d Remove unnecessary "set dirty" calls from _reskin 2019-10-12 10:16:40 -04:00
Karishma Chadha
c25d1f37b8 Add/update methods to Skin class to handle an empty skin. 2019-10-07 13:11:58 -04:00
Chris Willis-Ford
13b9ed7e16 Merge pull request #491 from adroitwhiz/dont-fudge-up-the-renderer
Remove u_fudge
2019-10-01 13:02:15 -07:00
DD Liu
e7a7c14032 Merge pull request #468 from mzgoddard/rm-update-fast-bounds
remove updateMatrix call from getFastBounds
2019-10-01 16:01:23 -04:00
DD Liu
503c3f7b0b Merge branch 'develop' into rm-update-fast-bounds 2019-10-01 15:53:19 -04:00
DD Liu
857b541e84 Merge pull request #469 from mzgoddard/update-drawable
Add update drawable methods for each property
2019-10-01 15:39:50 -04:00
DD Liu
8e836a7a11 Add a bug to the todos for missing drawable 2019-10-01 15:36:13 -04:00
Karishma Chadha
b7a3d32cde Merge pull request #508 from LLK/revert-506-revert-505-revert-496-revert-493-revert-470-skin-alter-push
Revert "Put Skin Alter Push Back In"
2019-09-26 09:57:26 -05:00
Karishma Chadha
11665299bc Revert "Put Skin Alter Push Back In" 2019-09-26 09:39:05 -05:00
Chris Willis-Ford
303464ddd4 Merge pull request #506 from LLK/revert-505-revert-496-revert-493-revert-470-skin-alter-push
Put Skin Alter Push Back In
2019-09-25 13:16:09 -07:00
Chris Willis-Ford
9da15b1326 Merge pull request #507 from mzgoddard/sync-svg-renderer-skin-props
synchronously store SVGRenderer props used in SVGSkin
2019-09-25 11:45:21 -07:00
Michael "Z" Goddard
110371b029 synchronously store SVGRenderer props used in SVGSkin
Some APIs need SVGSkin to update some of its values immediately when changing
the SVG. Namely size needs to be synchronously set. However rotation center
doesn't need to be set at that time so we can leave it to update where it
currently is.
2019-09-16 17:22:27 -04:00
Karishma Chadha
2a1d215e50 Revert "Revert "Revert "Revert "Skin alter push"""" 2019-09-12 13:43:32 -04:00
adroitwhiz
06def05119 Remove u_fudge 2019-08-08 12:15:27 -04:00
adroitwhiz
b1419d0468 set silhouette dirty 2019-08-04 06:43:59 -04:00
adroitwhiz
a8320b9ebc remove magic numbers 2019-08-01 22:28:49 -04:00
adroitwhiz
9f15b86672 penStamp directly to PenSkin's framebuffer 2019-07-25 07:59:52 -04:00
adroitwhiz
ae23bb5e56 add jsdoc for useNearest 2019-07-11 10:23:15 -04:00
adroitwhiz
99d14f0e16 Take screen-space scale into account w/ useNearest 2019-07-11 10:07:33 -04:00
Michael "Z" Goddard
96aa930895 update drawable calls 2019-06-07 18:32:04 -04:00
Michael "Z" Goddard
89e6de035d remove updateMatrix call from getFastBounds 2019-06-07 11:17:54 -04:00
greenkeeper[bot]
13f7a1038d chore(package): update scratch-vm to version 0.2.0-prerelease.20190213162739 2019-02-13 16:42:45 +00:00
11 changed files with 425 additions and 161 deletions

View File

@@ -32,12 +32,12 @@
"chromeless": "^1.5.1",
"copy-webpack-plugin": "^4.5.1",
"docdash": "^0.4.0",
"eslint": "^4.6.1",
"eslint": "^6.8.0",
"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.20190822202608",
"scratch-svg-renderer": "0.2.0-prerelease.20191217211338",
"twgl.js": "4.4.0"
}
}

View File

@@ -56,7 +56,7 @@ class BitmapSkin extends Skin {
*/
// eslint-disable-next-line no-unused-vars
getTexture (scale) {
return this._texture;
return this._texture || super.getTexture();
}
/**
@@ -78,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

View File

@@ -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,8 +471,10 @@ 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;
@@ -449,8 +496,8 @@ class Drawable {
}
// 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;
@@ -530,7 +577,6 @@ class Drawable {
* @return {!Rectangle} Bounds for the Drawable.
*/
getFastBounds (result) {
this.updateMatrix();
if (!this.needsConvexHullPoints()) {
return this.getBounds(result);
}
@@ -643,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);

View File

@@ -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});

View File

@@ -394,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();
@@ -1314,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.
*/
@@ -1324,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);
}
@@ -1351,7 +1461,7 @@ 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];
}
@@ -1432,23 +1542,20 @@ class RenderWebGL extends EventEmitter {
const skin = /** @type {PenSkin} */ this._allSkins[penSkinID];
const gl = this._gl;
twgl.bindFramebufferInfo(gl, this._queryBufferInfo);
twgl.bindFramebufferInfo(gl, skin._framebuffer);
// Limit size of viewport to the bounds around the stamp Drawable and create the projection matrix for the draw.
gl.viewport(0, 0, bounds.width, bounds.height);
gl.viewport(
(this._nativeSize[0] * 0.5) + bounds.left,
(this._nativeSize[1] * 0.5) - bounds.top,
bounds.width,
bounds.height
);
const projection = twgl.m4.ortho(bounds.left, bounds.right, bounds.top, bounds.bottom, -1, 1);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
try {
gl.disable(gl.BLEND);
this._drawThese([stampID], ShaderManager.DRAW_MODE.stamp, projection, {ignoreVisibility: true});
} finally {
gl.enable(gl.BLEND);
}
skin._drawToBuffer(this._queryBufferInfo.attachments[0], bounds.left, bounds.top);
// Draw the stamped sprite onto the PenSkin's framebuffer.
this._drawThese([stampID], ShaderManager.DRAW_MODE.stamp, projection, {ignoreVisibility: true});
skin._silhouetteDirty = true;
}
/* ******
@@ -1608,8 +1715,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
});
}
@@ -1624,7 +1730,7 @@ 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}
);
}

56
src/SVGMIP.js Normal file
View 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;

View File

@@ -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));
}
}

View File

@@ -140,7 +140,7 @@ class Skin extends EventEmitter {
*/
// eslint-disable-next-line no-unused-vars
getTexture (scale) {
return null;
return this._emptyImageTexture;
}
/**
@@ -171,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

View File

@@ -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);

View File

@@ -1,7 +1,5 @@
precision mediump float;
uniform float u_fudge;
#ifdef DRAW_MODE_silhouette
uniform vec4 u_silhouetteColor;
#else // DRAW_MODE_silhouette

View File

@@ -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">&lt;e*&amp;%$&amp;^$&gt;&lt;/!abc'&gt;</tspan></text>
`