OLD | NEW |
| (Empty) |
1 part of sprites; | |
2 | |
3 /// Options for setting up a [SpriteBox]. | |
4 /// | |
5 /// * [nativePoints], use the same points as the parent [Widget]. | |
6 /// * [letterbox], use the size of the root node for the coordinate system, con
strain the aspect ratio and trim off | |
7 /// areas that end up outside the screen. | |
8 /// * [stretch], use the size of the root node for the coordinate system, scale
it to fit the size of the box. | |
9 /// * [scaleToFit], similar to the letterbox option, but instead of trimming ar
eas the sprite system will be scaled | |
10 /// down to fit the box. | |
11 /// * [fixedWidth], uses the width of the root node to set the size of the coor
dinate system, this option will change | |
12 /// the height of the root node to fit the box. | |
13 /// * [fixedHeight], uses the height of the root node to set the size of the co
ordinate system, this option will change | |
14 /// the width of the root node to fit the box. | |
15 enum SpriteBoxTransformMode { | |
16 nativePoints, | |
17 letterbox, | |
18 stretch, | |
19 scaleToFit, | |
20 fixedWidth, | |
21 fixedHeight, | |
22 } | |
23 | |
24 class SpriteBox extends RenderBox { | |
25 | |
26 // Member variables | |
27 | |
28 // Root node for drawing | |
29 NodeWithSize _rootNode; | |
30 | |
31 // Tracking of frame rate and updates | |
32 double _lastTimeStamp; | |
33 int _numFrames = 0; | |
34 double _frameRate = 0.0; | |
35 | |
36 // Transformation mode | |
37 SpriteBoxTransformMode _transformMode; | |
38 | |
39 /// The transform mode used by the [SpriteBox]. | |
40 SpriteBoxTransformMode get transformMode => _transformMode; | |
41 | |
42 // Cached transformation matrix | |
43 Matrix4 _transformMatrix; | |
44 | |
45 List<Node> _eventTargets; | |
46 | |
47 // Setup | |
48 | |
49 /// Creates a new SpriteBox with a node as its content, by default uses letter
boxing. | |
50 /// | |
51 /// The [rootNode] provides the content of the node tree, typically it's a cus
tom subclass of [NodeWithSize]. The | |
52 /// [mode] provides different ways to scale the content to best fit it to the
screen. In most cases it's preferred to | |
53 /// use a [SpriteWidget] that automatically wraps the SpriteBox. | |
54 /// | |
55 /// var spriteBox = new SpriteBox(myNode, SpriteBoxTransformMode.fixedHeig
ht); | |
56 SpriteBox(NodeWithSize rootNode, [SpriteBoxTransformMode mode = SpriteBoxTrans
formMode.letterbox]) { | |
57 assert(rootNode != null); | |
58 assert(rootNode._spriteBox == null); | |
59 | |
60 // Setup root node | |
61 _rootNode = rootNode; | |
62 | |
63 // Assign SpriteBox reference to all the nodes | |
64 _addSpriteBoxReference(_rootNode); | |
65 | |
66 // Setup transform mode | |
67 _transformMode = mode; | |
68 | |
69 _scheduleTick(); | |
70 } | |
71 | |
72 void _addSpriteBoxReference(Node node) { | |
73 node._spriteBox = this; | |
74 for (Node child in node._children) { | |
75 _addSpriteBoxReference(child); | |
76 } | |
77 } | |
78 | |
79 // Properties | |
80 | |
81 /// The root node of the node tree that is rendered by this box. | |
82 /// | |
83 /// var rootNode = mySpriteBox.rootNode; | |
84 NodeWithSize get rootNode => _rootNode; | |
85 | |
86 void performLayout() { | |
87 size = constraints.constrain(Size.infinite); | |
88 _invalidateTransformMatrix(); | |
89 _callSpriteBoxPerformedLayout(_rootNode); | |
90 } | |
91 | |
92 // Event handling | |
93 | |
94 void _addEventTargets(Node node, List<Node> eventTargets) { | |
95 List children = node.children; | |
96 int i = 0; | |
97 | |
98 // Add childrens that are behind this node | |
99 while (i < children.length) { | |
100 Node child = children[i]; | |
101 if (child.zPosition >= 0.0) break; | |
102 _addEventTargets(child, eventTargets); | |
103 i++; | |
104 } | |
105 | |
106 // Add this node | |
107 if (node.userInteractionEnabled) { | |
108 eventTargets.add(node); | |
109 } | |
110 | |
111 // Add children in front of this node | |
112 while (i < children.length) { | |
113 Node child = children[i]; | |
114 _addEventTargets(child, eventTargets); | |
115 i++; | |
116 } | |
117 } | |
118 | |
119 void handleEvent(Event event, _SpriteBoxHitTestEntry entry) { | |
120 if (event is PointerEvent) { | |
121 | |
122 if (event.type == 'pointerdown') { | |
123 // Build list of event targets | |
124 if (_eventTargets == null) { | |
125 _eventTargets = []; | |
126 _addEventTargets(_rootNode, _eventTargets); | |
127 } | |
128 | |
129 // Find the once that are hit by the pointer | |
130 List<Node> nodeTargets = []; | |
131 for (int i = _eventTargets.length - 1; i >= 0; i--) { | |
132 Node node = _eventTargets[i]; | |
133 | |
134 // Check if the node is ready to handle a pointer | |
135 if (node.handleMultiplePointers || node._handlingPointer == null) { | |
136 // Do the hit test | |
137 Point posInNodeSpace = node.convertPointToNodeSpace(entry.localPosit
ion); | |
138 if (node.isPointInside(posInNodeSpace)) { | |
139 nodeTargets.add(node); | |
140 node._handlingPointer = event.pointer; | |
141 } | |
142 } | |
143 } | |
144 | |
145 entry.nodeTargets = nodeTargets; | |
146 } | |
147 | |
148 // Pass the event down to nodes that were hit by the pointerdown | |
149 List<Node> targets = entry.nodeTargets; | |
150 for (Node node in targets) { | |
151 // Check if this event should be dispatched | |
152 if (node.handleMultiplePointers || event.pointer == node._handlingPointe
r) { | |
153 // Dispatch event | |
154 bool consumedEvent = node.handleEvent(new SpriteBoxEvent(new Point(eve
nt.x, event.y), event.type, event.pointer)); | |
155 if (consumedEvent == null || consumedEvent) break; | |
156 } | |
157 } | |
158 | |
159 // De-register pointer for nodes that doesn't handle multiple pointers | |
160 for (Node node in targets) { | |
161 if (event.type == 'pointerup' || event.type == 'pointercancel') { | |
162 node._handlingPointer = null; | |
163 } | |
164 } | |
165 } | |
166 } | |
167 | |
168 bool hitTest(HitTestResult result, { Point position }) { | |
169 result.add(new _SpriteBoxHitTestEntry(this, position)); | |
170 return true; | |
171 } | |
172 | |
173 // Rendering | |
174 | |
175 /// The transformation matrix used to transform the root node to the space of
the box. | |
176 /// | |
177 /// It's uncommon to need access to this property. | |
178 /// | |
179 /// var matrix = mySpriteBox.transformMatrix; | |
180 Matrix4 get transformMatrix { | |
181 // Get cached matrix if available | |
182 if (_transformMatrix != null) { | |
183 return _transformMatrix; | |
184 } | |
185 | |
186 _transformMatrix = new Matrix4.identity(); | |
187 | |
188 // Calculate matrix | |
189 double scaleX = 1.0; | |
190 double scaleY = 1.0; | |
191 double offsetX = 0.0; | |
192 double offsetY = 0.0; | |
193 | |
194 double systemWidth = rootNode.size.width; | |
195 double systemHeight = rootNode.size.height; | |
196 | |
197 switch(_transformMode) { | |
198 case SpriteBoxTransformMode.stretch: | |
199 scaleX = size.width/systemWidth; | |
200 scaleY = size.height/systemHeight; | |
201 break; | |
202 case SpriteBoxTransformMode.letterbox: | |
203 scaleX = size.width/systemWidth; | |
204 scaleY = size.height/systemHeight; | |
205 if (scaleX > scaleY) { | |
206 scaleY = scaleX; | |
207 offsetY = (size.height - scaleY * systemHeight)/2.0; | |
208 } else { | |
209 scaleX = scaleY; | |
210 offsetX = (size.width - scaleX * systemWidth)/2.0; | |
211 } | |
212 break; | |
213 case SpriteBoxTransformMode.scaleToFit: | |
214 scaleX = size.width/systemWidth; | |
215 scaleY = size.height/systemHeight; | |
216 if (scaleX < scaleY) { | |
217 scaleY = scaleX; | |
218 offsetY = (size.height - scaleY * systemHeight)/2.0; | |
219 } else { | |
220 scaleX = scaleY; | |
221 offsetX = (size.width - scaleX * systemWidth)/2.0; | |
222 } | |
223 break; | |
224 case SpriteBoxTransformMode.fixedWidth: | |
225 scaleX = size.width/systemWidth; | |
226 scaleY = scaleX; | |
227 systemHeight = size.height/scaleX; | |
228 rootNode.size = new Size(systemWidth, systemHeight); | |
229 break; | |
230 case SpriteBoxTransformMode.fixedHeight: | |
231 scaleY = size.height/systemHeight; | |
232 scaleX = scaleY; | |
233 systemWidth = size.width/scaleY; | |
234 rootNode.size = new Size(systemWidth, systemHeight); | |
235 break; | |
236 case SpriteBoxTransformMode.nativePoints: | |
237 break; | |
238 default: | |
239 assert(false); | |
240 break; | |
241 } | |
242 | |
243 _transformMatrix.translate(offsetX, offsetY); | |
244 _transformMatrix.scale(scaleX, scaleY); | |
245 | |
246 return _transformMatrix; | |
247 } | |
248 | |
249 void _invalidateTransformMatrix() { | |
250 _transformMatrix = null; | |
251 _rootNode._invalidateToBoxTransformMatrix(); | |
252 } | |
253 | |
254 void paint(RenderCanvas canvas) { | |
255 canvas.save(); | |
256 | |
257 // Move to correct coordinate space before drawing | |
258 canvas.concat(transformMatrix.storage); | |
259 | |
260 // Draw the sprite tree | |
261 _rootNode._visit(canvas); | |
262 | |
263 canvas.restore(); | |
264 } | |
265 | |
266 // Updates | |
267 | |
268 int _animationId = 0; | |
269 | |
270 void _scheduleTick() { | |
271 _animationId = scheduler.requestAnimationFrame(_tick); | |
272 } | |
273 | |
274 void _tick(double timeStamp) { | |
275 | |
276 // Calculate the time between frames in seconds | |
277 if (_lastTimeStamp == null) _lastTimeStamp = timeStamp; | |
278 double delta = (timeStamp - _lastTimeStamp) / 1000; | |
279 _lastTimeStamp = timeStamp; | |
280 | |
281 // Count the number of frames we've been running | |
282 _numFrames += 1; | |
283 | |
284 _frameRate = 1.0/delta; | |
285 | |
286 // Print frame rate | |
287 if (_numFrames % 60 == 0) print("delta: $delta fps: $_frameRate"); | |
288 | |
289 _callUpdate(_rootNode, delta); | |
290 _scheduleTick(); | |
291 } | |
292 | |
293 void _callUpdate(Node node, double dt) { | |
294 node.update(dt); | |
295 for (Node child in node.children) { | |
296 if (!child.paused) { | |
297 _callUpdate(child, dt); | |
298 } | |
299 } | |
300 } | |
301 | |
302 void _callSpriteBoxPerformedLayout(Node node) { | |
303 node.spriteBoxPerformedLayout(); | |
304 for (Node child in node.children) { | |
305 _callSpriteBoxPerformedLayout(child); | |
306 } | |
307 } | |
308 | |
309 // Hit tests | |
310 | |
311 /// Finds all nodes at a position defined in the box's coordinates. | |
312 /// | |
313 /// Use this method with caution. It searches the complete node tree to locate
the nodes, which can be slow if the | |
314 /// node tree is large. | |
315 /// | |
316 /// List nodes = mySpriteBox.findNodesAtPosition(new Point(50.0, 50.0)); | |
317 List<Node> findNodesAtPosition(Point position) { | |
318 assert(position != null); | |
319 | |
320 List<Node> nodes = []; | |
321 | |
322 // Traverse the render tree and find objects at the position | |
323 _addNodesAtPosition(_rootNode, position, nodes); | |
324 | |
325 return nodes; | |
326 } | |
327 | |
328 _addNodesAtPosition(Node node, Point position, List<Node> list) { | |
329 // Visit children first | |
330 for (Node child in node.children) { | |
331 _addNodesAtPosition(child, position, list); | |
332 } | |
333 // Do the hit test | |
334 Point posInNodeSpace = node.convertPointToNodeSpace(position); | |
335 if (node.isPointInside(posInNodeSpace)) { | |
336 list.add(node); | |
337 } | |
338 } | |
339 } | |
340 | |
341 class _SpriteBoxHitTestEntry extends BoxHitTestEntry { | |
342 List<Node> nodeTargets; | |
343 _SpriteBoxHitTestEntry(RenderBox target, Point localPosition) : super(target,
localPosition); | |
344 } | |
345 | |
346 /// An event that is passed down the node tree when pointer events occur. The Sp
riteBoxEvent is typically handled in | |
347 /// the handleEvent method of [Node]. | |
348 class SpriteBoxEvent { | |
349 | |
350 /// The position of the event in box coordinates. | |
351 /// | |
352 /// You can use the convertPointToNodeSpace of [Node] to convert the position
to local coordinates. | |
353 /// | |
354 /// bool handleEvent(SpriteBoxEvent event) { | |
355 /// Point localPosition = convertPointToNodeSpace(event.boxPosition); | |
356 /// if (event.type == 'pointerdown') { | |
357 /// // Do something! | |
358 /// } | |
359 /// } | |
360 final Point boxPosition; | |
361 | |
362 /// The type of event, there are currently four valid types, 'pointerdown', 'p
ointermoved', 'pointerup', and | |
363 /// 'pointercancel'. | |
364 /// | |
365 /// if (event.type == 'pointerdown') { | |
366 /// // Do something! | |
367 /// } | |
368 final String type; | |
369 | |
370 /// The id of the pointer. Each pointer on the screen will have a unique point
er id. | |
371 /// | |
372 /// if (event.pointer == firstPointerId) { | |
373 /// // Do something | |
374 /// } | |
375 final int pointer; | |
376 | |
377 /// Creates a new SpriteBoxEvent, typically this is done internally inside the
SpriteBox. | |
378 /// | |
379 /// var event = new SpriteBoxEvent(new Point(50.0, 50.0), 'pointerdown', 0
); | |
380 SpriteBoxEvent(this.boxPosition, this.type, this.pointer); | |
381 } | |
OLD | NEW |