1
1
import Matrix from "./Matrix.js" ;
2
2
3
+ import Rectangle from "./Rectangle.js" ;
4
+ import effectTransformPoint from "./effectTransformPoint.js" ;
5
+ import { effectBitmasks } from "./effectInfo.js" ;
6
+
3
7
import { Sprite , Stage } from "../Sprite.js" ;
4
8
9
+ // Returns the determinant of two vectors, the vector from A to B and the vector
10
+ // from A to C. If positive, it means AC is counterclockwise from AB.
11
+ // If negative, AC is clockwise from AB.
12
+ const determinant = ( a , b , c ) => {
13
+ return ( b [ 0 ] - a [ 0 ] ) * ( c [ 1 ] - a [ 1 ] ) - ( b [ 1 ] - a [ 1 ] ) * ( c [ 0 ] - a [ 0 ] ) ;
14
+ } ;
15
+
16
+ // Used to track whether a sprite's transform has changed since we last looked
17
+ // at it.
18
+ // TODO: store renderer-specific data on the sprite and have *it* set a
19
+ // "transform changed" flag.
20
+ class SpriteTransformDiff {
21
+ constructor ( sprite ) {
22
+ this . _sprite = sprite ;
23
+ this . _unset = true ;
24
+ this . update ( ) ;
25
+ }
26
+
27
+ update ( ) {
28
+ this . _lastX = this . _sprite . x ;
29
+ this . _lastY = this . _sprite . y ;
30
+ this . _lastRotation = this . _sprite . direction ;
31
+ this . _lastRotationStyle = this . _sprite . rotationStyle ;
32
+ this . _lastScale = this . _sprite . scale ;
33
+ this . _lastCostume = this . _sprite . costume ;
34
+ this . _lastCostumeLoaded = this . _sprite . costume . img . complete ;
35
+ this . _unset = false ;
36
+ }
37
+
38
+ get changed ( ) {
39
+ return (
40
+ this . _lastX !== this . _sprite . x ||
41
+ this . _lastY !== this . _sprite . y ||
42
+ this . _lastRotation !== this . _sprite . direction ||
43
+ this . _lastRotationStyle !== this . _sprite . rotationStyle ||
44
+ this . _lastScale !== this . _sprite . scale ||
45
+ this . _lastCostume !== this . _sprite . costume ||
46
+ this . _lastCostumeLoaded !== this . _sprite . costume . img . complete ||
47
+ this . _unset
48
+ ) ;
49
+ }
50
+ }
51
+
5
52
// Renderer-specific data for an instance (the original or a clone) of a Sprite
6
53
export default class Drawable {
7
54
constructor ( renderer , sprite ) {
@@ -10,7 +57,230 @@ export default class Drawable {
10
57
11
58
// Transformation matrix for the sprite.
12
59
this . _matrix = Matrix . create ( ) ;
60
+ // Track when the sprite's transform changes so we can recalculate the
61
+ // transform matrix.
62
+ this . _matrixDiff = new SpriteTransformDiff ( sprite ) ;
13
63
this . _calculateSpriteMatrix ( ) ;
64
+
65
+ // Track when the image data used to calculate the convex hull,
66
+ // or distortion effects that affect how it's drawn, change.
67
+ // We also need the image data to know how big the pixels are.
68
+ this . _convexHullImageData = null ;
69
+ this . _convexHullMosaic = 0 ;
70
+ this . _convexHullPixelate = 0 ;
71
+ this . _convexHullWhirl = 0 ;
72
+ this . _convexHullFisheye = 0 ;
73
+ this . _convexHullPoints = null ;
74
+
75
+ this . _aabb = new Rectangle ( ) ;
76
+ this . _tightBoundingBox = new Rectangle ( ) ;
77
+ // Track when the sprite's transform changes so we can recalculate the
78
+ // tight bounding box.
79
+ this . _convexHullMatrixDiff = new SpriteTransformDiff ( sprite ) ;
80
+ }
81
+
82
+ getCurrentSkin ( ) {
83
+ return this . _renderer . _getSkin ( this . _sprite . costume ) ;
84
+ }
85
+
86
+ // Get the rough axis-aligned bounding box for this sprite. Not as tight as
87
+ // getTightBoundingBox, especially when rotated.
88
+ getAABB ( ) {
89
+ return Rectangle . fromMatrix ( this . getMatrix ( ) , this . _aabb ) ;
90
+ }
91
+
92
+ // Get the Scratch-space tight bounding box for this sprite.
93
+ getTightBoundingBox ( ) {
94
+ if ( ! this . _convexHullMatrixDiff . changed ) return this . _tightBoundingBox ;
95
+
96
+ const matrix = this . getMatrix ( ) ;
97
+ const convexHullPoints = this . _calculateConvexHull ( ) ;
98
+ // Maybe the costume isn't loaded yet. Return a 0x0 bounding box around the
99
+ // center of the sprite.
100
+ if ( convexHullPoints === null ) {
101
+ return Rectangle . fromBounds (
102
+ this . _sprite . x ,
103
+ this . _sprite . y ,
104
+ this . _sprite . x ,
105
+ this . _sprite . y ,
106
+ this . _tightBoundingBox
107
+ ) ;
108
+ }
109
+
110
+ let left = Infinity ;
111
+ let right = - Infinity ;
112
+ let top = - Infinity ;
113
+ let bottom = Infinity ;
114
+ const transformedPoint = [ 0 , 0 ] ;
115
+
116
+ // Each convex hull point is the center of a pixel. However, said pixels
117
+ // each have area. We must take into account the size of the pixels when
118
+ // calculating the bounds. The pixel dimensions depend on the scale and
119
+ // rotation (as we're treating pixels as squares, which change dimensions
120
+ // when rotated).
121
+ const xa = matrix [ 0 ] / 2 ;
122
+ const xb = matrix [ 3 ] / 2 ;
123
+ const halfPixelX =
124
+ ( Math . abs ( xa ) + Math . abs ( xb ) ) / this . _convexHullImageData . width ;
125
+ const ya = matrix [ 1 ] / 2 ;
126
+ const yb = matrix [ 4 ] / 2 ;
127
+ const halfPixelY =
128
+ ( Math . abs ( ya ) + Math . abs ( yb ) ) / this . _convexHullImageData . height ;
129
+
130
+ // Transform every point in the convex hull using our transform matrix,
131
+ // and expand the bounds to include that point.
132
+ for ( let i = 0 ; i < convexHullPoints . length ; i ++ ) {
133
+ const point = convexHullPoints [ i ] ;
134
+ transformedPoint [ 0 ] = point [ 0 ] ;
135
+ transformedPoint [ 1 ] = 1 - point [ 1 ] ;
136
+ Matrix . transformPoint ( matrix , transformedPoint , transformedPoint ) ;
137
+
138
+ left = Math . min ( left , transformedPoint [ 0 ] - halfPixelX ) ;
139
+ right = Math . max ( right , transformedPoint [ 0 ] + halfPixelX ) ;
140
+ top = Math . max ( top , transformedPoint [ 1 ] + halfPixelY ) ;
141
+ bottom = Math . min ( bottom , transformedPoint [ 1 ] - halfPixelY ) ;
142
+ }
143
+
144
+ Rectangle . fromBounds ( left , right , bottom , top , this . _tightBoundingBox ) ;
145
+ this . _convexHullMatrixDiff . update ( ) ;
146
+ return this . _tightBoundingBox ;
147
+ }
148
+
149
+ _calculateConvexHull ( ) {
150
+ const sprite = this . _sprite ;
151
+ const skin = this . getCurrentSkin ( ) ;
152
+ const imageData = skin . getImageData (
153
+ "size" in sprite ? sprite . size / 100 : 1
154
+ ) ;
155
+ if ( ! imageData ) return null ;
156
+
157
+ // We only need to recalculate the convex hull points if the image data's
158
+ // changed since we last calculated the convex hull, or if the sprite's
159
+ // effects which distort its shape have changed.
160
+ const { mosaic, pixelate, whirl, fisheye } = sprite . effects ;
161
+ if (
162
+ this . _convexHullImageData === imageData &&
163
+ this . _convexHullMosaic === mosaic &&
164
+ this . _convexHullPixelate === pixelate &&
165
+ this . _convexHullWhirl === whirl &&
166
+ this . _convexHullFisheye === fisheye
167
+ ) {
168
+ return this . _convexHullPoints ;
169
+ }
170
+
171
+ const effectBitmask =
172
+ sprite . effects . _bitmask &
173
+ ( effectBitmasks . mosaic |
174
+ effectBitmasks . pixelate |
175
+ effectBitmasks . whirl |
176
+ effectBitmasks . fisheye ) ;
177
+
178
+ const leftHull = [ ] ;
179
+ const rightHull = [ ] ;
180
+
181
+ const { width, height, data } = imageData ;
182
+
183
+ const pixelPos = [ 0 , 0 ] ;
184
+ const effectPos = [ 0 , 0 ] ;
185
+ let currentPoint ;
186
+ // Not Scratch-space: y increases as we go downwards
187
+ // Loop over all rows of pixels in the costume, starting at the top
188
+ for ( let y = 0 ; y < height ; y ++ ) {
189
+ pixelPos [ 1 ] = ( y + 0.5 ) / height ;
190
+
191
+ // We start at the leftmost point, then go rightwards until we hit an
192
+ // opaque pixel
193
+ let x = 0 ;
194
+ for ( ; x < width ; x ++ ) {
195
+ pixelPos [ 0 ] = ( x + 0.5 ) / width ;
196
+ let pixelX = x ;
197
+ let pixelY = y ;
198
+ if ( effectBitmask !== 0 ) {
199
+ effectTransformPoint ( this , pixelPos , effectPos ) ;
200
+ pixelX = Math . floor ( effectPos [ 0 ] * width ) ;
201
+ pixelY = Math . floor ( effectPos [ 1 ] * height ) ;
202
+ }
203
+ // We hit an opaque pixel
204
+ if ( data [ ( pixelY * width + pixelX ) * 4 + 3 ] > 0 ) {
205
+ currentPoint = [ pixelPos [ 0 ] , pixelPos [ 1 ] ] ;
206
+ break ;
207
+ }
208
+ }
209
+
210
+ // There are no opaque pixels on this row. Go to the next one.
211
+ if ( x >= width ) continue ;
212
+
213
+ // If appending the current point to the left hull makes a
214
+ // counterclockwise turn, we want to append the current point to it.
215
+ // Otherwise, we remove hull points until the current point makes a
216
+ // counterclockwise turn with the last two points.
217
+ while ( leftHull . length >= 2 ) {
218
+ if (
219
+ determinant (
220
+ leftHull [ leftHull . length - 1 ] ,
221
+ leftHull [ leftHull . length - 2 ] ,
222
+ currentPoint
223
+ ) > 0
224
+ ) {
225
+ break ;
226
+ }
227
+
228
+ leftHull . pop ( ) ;
229
+ }
230
+
231
+ leftHull . push ( currentPoint ) ;
232
+
233
+ // Now we repeat the process for the right side, looking leftwards for an
234
+ // opaque pixel.
235
+ for ( x = width - 1 ; x >= 0 ; x -- ) {
236
+ pixelPos [ 0 ] = ( x + 0.5 ) / width ;
237
+ effectTransformPoint ( this , pixelPos , effectPos ) ;
238
+ let pixelX = x ;
239
+ let pixelY = y ;
240
+ if ( effectBitmask !== 0 ) {
241
+ effectTransformPoint ( this , pixelPos , effectPos ) ;
242
+ pixelX = Math . floor ( effectPos [ 0 ] * width ) ;
243
+ pixelY = Math . floor ( effectPos [ 1 ] * height ) ;
244
+ }
245
+ // We hit an opaque pixel
246
+ if ( data [ ( pixelY * width + pixelX ) * 4 + 3 ] > 0 ) {
247
+ currentPoint = [ pixelPos [ 0 ] , pixelPos [ 1 ] ] ;
248
+ break ;
249
+ }
250
+ }
251
+
252
+ // Because we're coming at this from the right, it goes clockwise.
253
+ while ( rightHull . length >= 2 ) {
254
+ if (
255
+ determinant (
256
+ rightHull [ rightHull . length - 1 ] ,
257
+ rightHull [ rightHull . length - 2 ] ,
258
+ currentPoint
259
+ ) < 0
260
+ ) {
261
+ break ;
262
+ }
263
+
264
+ rightHull . pop ( ) ;
265
+ }
266
+
267
+ rightHull . push ( currentPoint ) ;
268
+ }
269
+
270
+ // Add points from the right side in reverse order so all the points are
271
+ // clockwise.
272
+ for ( let i = rightHull . length - 1 ; i >= 0 ; i -- ) {
273
+ leftHull . push ( rightHull [ i ] ) ;
274
+ }
275
+
276
+ this . _convexHullPoints = leftHull ;
277
+ this . _convexHullMosaic = mosaic ;
278
+ this . _convexHullPixelate = pixelate ;
279
+ this . _convexHullWhirl = whirl ;
280
+ this . _convexHullFisheye = fisheye ;
281
+ this . _convexHullImageData = imageData ;
282
+
283
+ return this . _convexHullPoints ;
14
284
}
15
285
16
286
_calculateSpriteMatrix ( ) {
@@ -52,27 +322,13 @@ export default class Drawable {
52
322
53
323
// Store the values we used to compute the matrix so we only recalculate
54
324
// the matrix when we really need to.
55
- this . _matrixX = this . _sprite . x ;
56
- this . _matrixY = this . _sprite . y ;
57
- this . _matrixRotation = this . _sprite . direction ;
58
- this . _matrixRotationStyle = this . _sprite . rotationStyle ;
59
- this . _matrixScale = this . _sprite . scale ;
60
- this . _matrixCostume = this . _sprite . costume ;
61
- this . _matrixCostumeLoaded = this . _sprite . costume . img . complete ;
325
+ this . _matrixDiff . update ( ) ;
62
326
}
63
327
64
328
getMatrix ( ) {
65
329
// If all the values we used to calculate the matrix haven't changed since
66
330
// we last calculated the matrix, we can just return the matrix as-is.
67
- if (
68
- this . _matrixX !== this . _sprite . x ||
69
- this . _matrixY !== this . _sprite . y ||
70
- this . _matrixRotation !== this . _sprite . direction ||
71
- this . _matrixRotationStyle !== this . _sprite . rotationStyle ||
72
- this . _matrixScale !== this . _sprite . scale ||
73
- this . _matrixCostume !== this . _sprite . costume ||
74
- this . _matrixCostumeLoaded !== this . _sprite . costume . img . complete
75
- ) {
331
+ if ( this . _matrixDiff . changed ) {
76
332
this . _calculateSpriteMatrix ( ) ;
77
333
}
78
334
0 commit comments