1- const twgl = require ( 'twgl.js' ) ;
2-
31const Skin = require ( './Skin' ) ;
2+ const SVGMIP = require ( './SVGMIP' ) ;
43const SvgRenderer = require ( 'scratch-svg-renderer' ) . SVGRenderer ;
54
65const 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
815class SVGSkin extends Skin {
916 /**
@@ -25,20 +32,28 @@ class SVGSkin extends Skin {
2532 /** @type {WebGLTexture } */
2633 this . _texture = null ;
2734
28- /** @type {number } */
29- this . _textureScale = 1 ;
35+ /** @type {Array.<SVGMIPs> } */
36+ this . _scaledMIPs = [ ] ;
3037
31- /** @type {Number } */
32- this . _maxTextureScale = 0 ;
38+ /**
39+ * Ratio of the size of the SVG and the max size of the WebGL texture
40+ * @type {Number }
41+ */
42+ this . _maxTextureScale = 1 ;
3343 }
3444
3545 /**
3646 * Dispose of this object. Do not use it after calling this method.
3747 */
3848 dispose ( ) {
3949 if ( this . _texture ) {
40- this . _renderer . gl . deleteTexture ( this . _texture ) ;
50+ for ( const mip of this . _scaledMIPs ) {
51+ if ( mip ) {
52+ mip . dispose ( ) ;
53+ }
54+ }
4155 this . _texture = null ;
56+ this . _scaledMIPs . length = 0 ;
4257 }
4358 super . dispose ( ) ;
4459 }
@@ -60,11 +75,34 @@ class SVGSkin extends Skin {
6075 super . setRotationCenter ( x - viewOffset [ 0 ] , y - viewOffset [ 1 ] ) ;
6176 }
6277
78+ /**
79+ * Create a MIP for a given scale and pass it a callback for updating
80+ * state when switching between scales and MIPs.
81+ * @param {number } scale - The relative size of the MIP
82+ * @param {function } resetCallback - this is a callback for doing a hard reset
83+ * of MIPs and a reset of the rotation center. Only passed in if the MIP scale is 1.
84+ * @return {SVGMIP } An object that handles creating and updating SVG textures.
85+ */
86+ createMIP ( scale , resetCallback ) {
87+ const textureCallback = textureData => {
88+ // Check if we have the largest MIP
89+ // eslint-disable-next-line no-use-before-define
90+ if ( ! this . _scaledMIPs . length || this . _scaledMIPs [ this . _scaledMIPs . length - 1 ] . _scale <= scale ) {
91+ // Currently silhouette only gets scaled up
92+ this . _silhouette . update ( textureData ) ;
93+ }
94+
95+ if ( resetCallback ) resetCallback ( ) ;
96+ } ;
97+ const mip = new SVGMIP ( this . _renderer , this . _svgRenderer , scale , textureCallback ) ;
98+
99+ return mip ;
100+ }
101+
63102 /**
64103 * @param {Array<number> } scale - The scaling factors to be used, each in the [0,100] range.
65104 * @return {WebGLTexture } The GL texture representation of this skin when drawing at the given scale.
66105 */
67- // eslint-disable-next-line no-unused-vars
68106 getTexture ( scale ) {
69107 if ( ! this . _svgRenderer . canvas . width || ! this . _svgRenderer . canvas . height ) {
70108 return super . getTexture ( ) ;
@@ -73,80 +111,70 @@ class SVGSkin extends Skin {
73111 // The texture only ever gets uniform scale. Take the larger of the two axes.
74112 const scaleMax = scale ? Math . max ( Math . abs ( scale [ 0 ] ) , Math . abs ( scale [ 1 ] ) ) : 100 ;
75113 const requestedScale = Math . min ( scaleMax / 100 , this . _maxTextureScale ) ;
76- let newScale = this . _textureScale ;
77- while ( ( newScale < this . _maxTextureScale ) && ( requestedScale >= 1.5 * newScale ) ) {
78- newScale *= 2 ;
114+ let newScale = 1 ;
115+ let textureIndex = 0 ;
116+
117+ if ( requestedScale < 1 ) {
118+ while ( ( newScale > MIN_TEXTURE_SCALE ) && ( requestedScale <= newScale * .75 ) ) {
119+ newScale /= 2 ;
120+ textureIndex -= 1 ;
121+ }
122+ } else {
123+ while ( ( newScale < this . _maxTextureScale ) && ( requestedScale >= 1.5 * newScale ) ) {
124+ newScale *= 2 ;
125+ textureIndex += 1 ;
126+ }
79127 }
80- if ( this . _textureScale !== newScale ) {
81- this . _textureScale = newScale ;
82- this . _svgRenderer . _draw ( this . _textureScale , ( ) => {
83- if ( this . _textureScale === newScale ) {
84- const canvas = this . _svgRenderer . canvas ;
85- const context = canvas . getContext ( '2d' ) ;
86- const textureData = context . getImageData ( 0 , 0 , canvas . width , canvas . height ) ;
87-
88- const gl = this . _renderer . gl ;
89- gl . bindTexture ( gl . TEXTURE_2D , this . _texture ) ;
90- gl . texImage2D ( gl . TEXTURE_2D , 0 , gl . RGBA , gl . RGBA , gl . UNSIGNED_BYTE , textureData ) ;
91- this . _silhouette . update ( textureData ) ;
92- }
93- } ) ;
128+
129+ if ( ! this . _scaledMIPs [ textureIndex + INDEX_OFFSET ] ) {
130+ this . _scaledMIPs [ textureIndex + INDEX_OFFSET ] = this . createMIP ( newScale ) ;
94131 }
95132
96- return this . _texture ;
133+ return this . _scaledMIPs [ textureIndex + INDEX_OFFSET ] . getTexture ( ) ;
97134 }
98135
99136 /**
100- * Set the contents of this skin to a snapshot of the provided SVG data.
101- * @param {string } svgData - new SVG to use.
137+ * Do a hard reset of the existing MIPs by calling dispose(), setting a new
138+ * scale 1 MIP in this._scaledMIPs, and finally updating the rotationCenter.
139+ * @param {SVGMIPs } mip - An object that handles creating and updating SVG textures.
102140 * @param {Array<number> } [rotationCenter] - Optional rotation center for the SVG. If not supplied, it will be
103141 * calculated from the bounding box
104- * @fires Skin.event:WasAltered
142+ * @fires Skin.event:WasAltered
105143 */
106- setSVG ( svgData , rotationCenter ) {
107- this . _svgRenderer . fromString ( svgData , 1 , ( ) => {
108- const gl = this . _renderer . gl ;
109- this . _textureScale = this . _maxTextureScale = 1 ;
110-
111- // Pull out the ImageData from the canvas. ImageData speeds up
112- // updating Silhouette and is better handled by more browsers in
113- // regards to memory.
114- const canvas = this . _svgRenderer . canvas ;
115-
116- if ( ! canvas . width || ! canvas . height ) {
117- super . setEmptyImageData ( ) ;
118- return ;
119- }
144+ resetMIPs ( mip , rotationCenter ) {
145+ this . _scaledMIPs . forEach ( oldMIP => oldMIP . dispose ( ) ) ;
146+ this . _scaledMIPs . length = 0 ;
120147
121- const context = canvas . getContext ( '2d' ) ;
122- const textureData = context . getImageData ( 0 , 0 , canvas . width , canvas . height ) ;
148+ // Set new scale 1 MIP after outdated MIPs have been disposed
149+ this . _texture = this . _scaledMIPs [ INDEX_OFFSET ] = mip ;
123150
124- if ( this . _texture ) {
125- gl . bindTexture ( gl . TEXTURE_2D , this . _texture ) ;
126- gl . texImage2D ( gl . TEXTURE_2D , 0 , gl . RGBA , gl . RGBA , gl . UNSIGNED_BYTE , textureData ) ;
127- this . _silhouette . update ( textureData ) ;
128- } else {
129- // TODO: mipmaps?
130- const textureOptions = {
131- auto : true ,
132- wrap : gl . CLAMP_TO_EDGE ,
133- src : textureData
134- } ;
135-
136- this . _texture = twgl . createTexture ( gl , textureOptions ) ;
137- this . _silhouette . update ( textureData ) ;
138- }
151+ if ( typeof rotationCenter === 'undefined' ) rotationCenter = this . calculateRotationCenter ( ) ;
152+ this . setRotationCenter . apply ( this , rotationCenter ) ;
153+ this . emit ( Skin . Events . WasAltered ) ;
154+ }
139155
140- const maxDimension = Math . max ( this . _svgRenderer . canvas . width , this . _svgRenderer . canvas . height ) ;
141- let testScale = 2 ;
142- for ( testScale ; maxDimension * testScale <= MAX_TEXTURE_DIMENSION ; testScale *= 2 ) {
143- this . _maxTextureScale = testScale ;
144- }
156+ /**
157+ * Set the contents of this skin to a snapshot of the provided SVG data.
158+ * @param {string } svgData - new SVG to use.
159+ * @param {Array<number> } [rotationCenter] - Optional rotation center for the SVG.
160+ */
161+ setSVG ( svgData , rotationCenter ) {
162+ this . _svgRenderer . loadString ( svgData ) ;
163+
164+ if ( ! this . _svgRenderer . canvas . width || ! this . _svgRenderer . canvas . height ) {
165+ super . setEmptyImageData ( ) ;
166+ return ;
167+ }
168+
169+ const maxDimension = Math . ceil ( Math . max ( this . size [ 0 ] , this . size [ 1 ] ) ) ;
170+ let testScale = 2 ;
171+ for ( testScale ; maxDimension * testScale <= MAX_TEXTURE_DIMENSION ; testScale *= 2 ) {
172+ this . _maxTextureScale = testScale ;
173+ }
145174
146- if ( typeof rotationCenter === 'undefined' ) rotationCenter = this . calculateRotationCenter ( ) ;
147- this . setRotationCenter . apply ( this , rotationCenter ) ;
148- this . emit ( Skin . Events . WasAltered ) ;
149- } ) ;
175+ // Create the 1.0 scale MIP at INDEX_OFFSET.
176+ const textureScale = 1 ;
177+ const mip = this . createMIP ( textureScale , ( ) => this . resetMIPs ( mip , rotationCenter ) ) ;
150178 }
151179
152180}
0 commit comments