| 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 |