Skip to content

Commit 2b8ba22

Browse files
committed
Merge branch 'develop' into premultiply-the-sequel
2 parents 4f4716b + b1274d5 commit 2b8ba22

File tree

2 files changed

+151
-60
lines changed

2 files changed

+151
-60
lines changed

src/SVGMIP.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const twgl = require('twgl.js');
2+
3+
class SVGMIP {
4+
/**
5+
* Create a new SVG MIP for a given scale.
6+
* @param {RenderWebGL} renderer - The renderer which this MIP's skin uses.
7+
* @param {SvgRenderer} svgRenderer - The svg renderer which this MIP's skin uses.
8+
* @param {number} scale - The relative size of the MIP
9+
* @param {function} callback - A callback that should always fire after draw()
10+
* @constructor
11+
*/
12+
constructor (renderer, svgRenderer, scale, callback) {
13+
this._renderer = renderer;
14+
this._svgRenderer = svgRenderer;
15+
this._scale = scale;
16+
this._texture = null;
17+
this._callback = callback;
18+
19+
this.draw();
20+
}
21+
22+
draw () {
23+
this._svgRenderer._draw(this._scale, () => {
24+
const textureData = this._getTextureData();
25+
const textureOptions = {
26+
auto: false,
27+
wrap: this._renderer.gl.CLAMP_TO_EDGE,
28+
src: textureData,
29+
premultiplyAlpha: true
30+
};
31+
32+
this._texture = twgl.createTexture(this._renderer.gl, textureOptions);
33+
this._callback(textureData);
34+
});
35+
}
36+
37+
dispose () {
38+
this._renderer.gl.deleteTexture(this.getTexture());
39+
}
40+
41+
getTexture () {
42+
return this._texture;
43+
}
44+
45+
_getTextureData () {
46+
// Pull out the ImageData from the canvas. ImageData speeds up
47+
// updating Silhouette and is better handled by more browsers in
48+
// regards to memory.
49+
const canvas = this._svgRenderer.canvas;
50+
const context = canvas.getContext('2d');
51+
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
52+
53+
return textureData;
54+
}
55+
}
56+
57+
module.exports = SVGMIP;

src/SVGSkin.js

Lines changed: 94 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
const twgl = require('twgl.js');
2-
31
const Skin = require('./Skin');
2+
const SVGMIP = require('./SVGMIP');
43
const SvgRenderer = require('scratch-svg-renderer').SVGRenderer;
54

65
const MAX_TEXTURE_DIMENSION = 2048;
6+
const MIN_TEXTURE_SCALE = 1 / 256;
7+
/**
8+
* All scaled renderings of the SVG are stored in an array. The 1.0 scale of
9+
* the SVG is stored at the 8th index. The smallest possible 1 / 256 scale
10+
* rendering is stored at the 0th index.
11+
* @const {number}
12+
*/
13+
const INDEX_OFFSET = 8;
714

815
class SVGSkin extends Skin {
916
/**
@@ -22,20 +29,28 @@ class SVGSkin extends Skin {
2229
/** @type {SvgRenderer} */
2330
this._svgRenderer = new SvgRenderer();
2431

25-
/** @type {number} */
26-
this._textureScale = 1;
32+
/** @type {Array.<SVGMIPs>} */
33+
this._scaledMIPs = [];
2734

28-
/** @type {Number} */
29-
this._maxTextureScale = 0;
35+
/**
36+
* Ratio of the size of the SVG and the max size of the WebGL texture
37+
* @type {Number}
38+
*/
39+
this._maxTextureScale = 1;
3040
}
3141

3242
/**
3343
* Dispose of this object. Do not use it after calling this method.
3444
*/
3545
dispose () {
3646
if (this._texture) {
37-
this._renderer.gl.deleteTexture(this._texture);
47+
for (const mip of this._scaledMIPs) {
48+
if (mip) {
49+
mip.dispose();
50+
}
51+
}
3852
this._texture = null;
53+
this._scaledMIPs.length = 0;
3954
}
4055
super.dispose();
4156
}
@@ -57,11 +72,33 @@ class SVGSkin extends Skin {
5772
super.setRotationCenter(x - viewOffset[0], y - viewOffset[1]);
5873
}
5974

75+
/**
76+
* Create a MIP for a given scale and pass it a callback for updating
77+
* state when switching between scales and MIPs.
78+
* @param {number} scale - The relative size of the MIP
79+
* @param {function} resetCallback - this is a callback for doing a hard reset
80+
* of MIPs and a reset of the rotation center. Only passed in if the MIP scale is 1.
81+
* @return {SVGMIP} An object that handles creating and updating SVG textures.
82+
*/
83+
createMIP (scale, resetCallback) {
84+
const textureCallback = textureData => {
85+
if (resetCallback) resetCallback();
86+
// Check if we have the largest MIP
87+
// eslint-disable-next-line no-use-before-define
88+
if (!this._scaledMIPs.length || this._scaledMIPs[this._scaledMIPs.length - 1]._scale <= scale) {
89+
// Currently silhouette only gets scaled up
90+
this._silhouette.update(textureData);
91+
}
92+
};
93+
const mip = new SVGMIP(this._renderer, this._svgRenderer, scale, textureCallback);
94+
95+
return mip;
96+
}
97+
6098
/**
6199
* @param {Array<number>} scale - The scaling factors to be used, each in the [0,100] range.
62100
* @return {WebGLTexture} The GL texture representation of this skin when drawing at the given scale.
63101
*/
64-
// eslint-disable-next-line no-unused-vars
65102
getTexture (scale) {
66103
if (!this._svgRenderer.canvas.width || !this._svgRenderer.canvas.height) {
67104
return super.getTexture();
@@ -70,73 +107,70 @@ class SVGSkin extends Skin {
70107
// The texture only ever gets uniform scale. Take the larger of the two axes.
71108
const scaleMax = scale ? Math.max(Math.abs(scale[0]), Math.abs(scale[1])) : 100;
72109
const requestedScale = Math.min(scaleMax / 100, this._maxTextureScale);
73-
let newScale = this._textureScale;
74-
while ((newScale < this._maxTextureScale) && (requestedScale >= 1.5 * newScale)) {
75-
newScale *= 2;
110+
let newScale = 1;
111+
let textureIndex = 0;
112+
113+
if (requestedScale < 1) {
114+
while ((newScale > MIN_TEXTURE_SCALE) && (requestedScale <= newScale * .75)) {
115+
newScale /= 2;
116+
textureIndex -= 1;
117+
}
118+
} else {
119+
while ((newScale < this._maxTextureScale) && (requestedScale >= 1.5 * newScale)) {
120+
newScale *= 2;
121+
textureIndex += 1;
122+
}
76123
}
77-
if (this._textureScale !== newScale) {
78-
this._textureScale = newScale;
79-
this._svgRenderer._draw(this._textureScale, () => {
80-
if (this._textureScale === newScale) {
81-
const canvas = this._svgRenderer.canvas;
82-
const context = canvas.getContext('2d');
83-
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
84-
85-
this._setTexture(textureData);
86-
}
87-
});
124+
125+
if (!this._scaledMIPs[textureIndex + INDEX_OFFSET]) {
126+
this._scaledMIPs[textureIndex + INDEX_OFFSET] = this.createMIP(newScale);
88127
}
89128

90-
return this._texture;
129+
return this._scaledMIPs[textureIndex + INDEX_OFFSET].getTexture();
91130
}
92131

93132
/**
94-
* Set the contents of this skin to a snapshot of the provided SVG data.
95-
* @param {string} svgData - new SVG to use.
133+
* Do a hard reset of the existing MIPs by calling dispose(), setting a new
134+
* scale 1 MIP in this._scaledMIPs, and finally updating the rotationCenter.
135+
* @param {SVGMIPs} mip - An object that handles creating and updating SVG textures.
96136
* @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
97137
* calculated from the bounding box
98-
* @fires Skin.event:WasAltered
138+
* @fires Skin.event:WasAltered
99139
*/
100-
setSVG (svgData, rotationCenter) {
101-
this._svgRenderer.fromString(svgData, 1, () => {
102-
const gl = this._renderer.gl;
103-
this._textureScale = this._maxTextureScale = 1;
104-
105-
// Pull out the ImageData from the canvas. ImageData speeds up
106-
// updating Silhouette and is better handled by more browsers in
107-
// regards to memory.
108-
const canvas = this._svgRenderer.canvas;
109-
110-
if (!canvas.width || !canvas.height) {
111-
super.setEmptyImageData();
112-
return;
113-
}
140+
resetMIPs (mip, rotationCenter) {
141+
this._scaledMIPs.forEach(oldMIP => oldMIP.dispose());
142+
this._scaledMIPs.length = 0;
114143

115-
const context = canvas.getContext('2d');
116-
const textureData = context.getImageData(0, 0, canvas.width, canvas.height);
144+
// Set new scale 1 MIP after outdated MIPs have been disposed
145+
this._texture = this._scaledMIPs[INDEX_OFFSET] = mip;
117146

118-
if (this._texture === null) {
119-
// TODO: mipmaps?
120-
const textureOptions = {
121-
auto: false,
122-
wrap: gl.CLAMP_TO_EDGE
123-
};
147+
if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
148+
this.setRotationCenter.apply(this, rotationCenter);
149+
this.emit(Skin.Events.WasAltered);
150+
}
124151

125-
this._texture = twgl.createTexture(gl, textureOptions);
126-
}
152+
/**
153+
* Set the contents of this skin to a snapshot of the provided SVG data.
154+
* @param {string} svgData - new SVG to use.
155+
* @param {Array<number>} [rotationCenter] - Optional rotation center for the SVG.
156+
*/
157+
setSVG (svgData, rotationCenter) {
158+
this._svgRenderer.loadString(svgData);
127159

128-
this._setTexture(textureData);
160+
if (!this._svgRenderer.canvas.width || !this._svgRenderer.canvas.height) {
161+
super.setEmptyImageData();
162+
return;
163+
}
129164

130-
const maxDimension = Math.max(this._svgRenderer.canvas.width, this._svgRenderer.canvas.height);
131-
let testScale = 2;
132-
for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
133-
this._maxTextureScale = testScale;
134-
}
165+
const maxDimension = Math.ceil(Math.max(this.size[0], this.size[1]));
166+
let testScale = 2;
167+
for (testScale; maxDimension * testScale <= MAX_TEXTURE_DIMENSION; testScale *= 2) {
168+
this._maxTextureScale = testScale;
169+
}
135170

136-
if (typeof rotationCenter === 'undefined') rotationCenter = this.calculateRotationCenter();
137-
this.setRotationCenter.apply(this, rotationCenter);
138-
this.emit(Skin.Events.WasAltered);
139-
});
171+
// Create the 1.0 scale MIP at INDEX_OFFSET.
172+
const textureScale = 1;
173+
const mip = this.createMIP(textureScale, () => this.resetMIPs(mip, rotationCenter));
140174
}
141175

142176
}

0 commit comments

Comments
 (0)