part of sprites; enum SpriteBoxTransformMode { nativePoints, letterbox, stretch, scaleToFit, fixedWidth, fixedHeight, } class SpriteBox extends RenderBox { // Member variables // Root node for drawing NodeWithSize _rootNode; // Tracking of frame rate and updates double _lastTimeStamp; int _numFrames = 0; double _frameRate = 0.0; // Transformation mode SpriteBoxTransformMode transformMode; // double _systemWidth; // double _systemHeight; // Cached transformation matrix Matrix4 _transformMatrix; List _eventTargets; // Setup SpriteBox(NodeWithSize rootNode, [SpriteBoxTransformMode mode = SpriteBoxTransformMode.nativePoints]) { assert(rootNode != null); assert(rootNode._spriteBox == null); // Setup root node _rootNode = rootNode; // Assign SpriteBox reference to all the nodes _addSpriteBoxReference(_rootNode); // Setup transform mode transformMode = mode; // _systemWidth = rootNode.size.width; // _systemHeight = rootNode.size.height; _scheduleTick(); } void _addSpriteBoxReference(Node node) { node._spriteBox = this; for (Node child in node._children) { _addSpriteBoxReference(child); } } // Properties double get systemWidth => rootNode.size.width; double get systemHeight => rootNode.size.height; NodeWithSize get rootNode => _rootNode; void performLayout() { size = constraints.constrain(Size.infinite); _invalidateTransformMatrix(); _callSpriteBoxPerformedLayout(_rootNode); } // Event handling void _addEventTargets(Node node, List eventTargets) { List children = node.children; int i = 0; // Add childrens that are behind this node while (i < children.length) { Node child = children[i]; if (child.zPosition >= 0.0) break; _addEventTargets(child, eventTargets); i++; } // Add this node if (node.userInteractionEnabled) { eventTargets.add(node); } // Add children in front of this node while (i < children.length) { Node child = children[i]; _addEventTargets(child, eventTargets); i++; } } void handleEvent(Event event, SpriteBoxHitTestEntry entry) { if (event is PointerEvent) { if (event.type == 'pointerdown') { // Build list of event targets if (_eventTargets == null) { _eventTargets = []; _addEventTargets(_rootNode, _eventTargets); } // Find the once that are hit by the pointer List nodeTargets = []; for (int i = _eventTargets.length - 1; i >= 0; i--) { Node node = _eventTargets[i]; // Check if the node is ready to handle a pointer if (node.handleMultiplePointers || node._handlingPointer == null) { // Do the hit test Point posInNodeSpace = node.convertPointToNodeSpace(entry.localPosition); if (node.isPointInside(posInNodeSpace)) { nodeTargets.add(node); node._handlingPointer = event.pointer; } } } entry.nodeTargets = nodeTargets; } // Pass the event down to nodes that were hit by the pointerdown List targets = entry.nodeTargets; for (Node node in targets) { // Check if this event should be dispatched if (node.handleMultiplePointers || event.pointer == node._handlingPointer) { // Dispatch event bool consumedEvent = node.handleEvent(new SpriteBoxEvent(new Point(event.x, event.y), event.type, event.pointer)); if (consumedEvent == null || consumedEvent) break; } } // De-register pointer for nodes that doesn't handle multiple pointers for (Node node in targets) { if (event.type == 'pointerup' || event.type == 'pointercancel') { node._handlingPointer = null; } } } } bool hitTest(HitTestResult result, { Point position }) { result.add(new SpriteBoxHitTestEntry(this, position)); return true; } // Rendering Matrix4 get transformMatrix { // Get cached matrix if available if (_transformMatrix != null) { return _transformMatrix; } _transformMatrix = new Matrix4.identity(); // Calculate matrix double scaleX = 1.0; double scaleY = 1.0; double offsetX = 0.0; double offsetY = 0.0; double systemWidth = rootNode.size.width; double systemHeight = rootNode.size.height; switch(transformMode) { case SpriteBoxTransformMode.stretch: scaleX = size.width/systemWidth; scaleY = size.height/systemHeight; break; case SpriteBoxTransformMode.letterbox: scaleX = size.width/systemWidth; scaleY = size.height/systemHeight; if (scaleX > scaleY) { scaleY = scaleX; offsetY = (size.height - scaleY * systemHeight)/2.0; } else { scaleX = scaleY; offsetX = (size.width - scaleX * systemWidth)/2.0; } break; case SpriteBoxTransformMode.scaleToFit: scaleX = size.width/systemWidth; scaleY = size.height/systemHeight; if (scaleX < scaleY) { scaleY = scaleX; offsetY = (size.height - scaleY * systemHeight)/2.0; } else { scaleX = scaleY; offsetX = (size.width - scaleX * systemWidth)/2.0; } break; case SpriteBoxTransformMode.fixedWidth: scaleX = size.width/systemWidth; scaleY = scaleX; systemHeight = size.height/scaleX; rootNode.size = new Size(systemWidth, systemHeight); break; case SpriteBoxTransformMode.fixedHeight: scaleY = size.height/systemHeight; scaleX = scaleY; systemWidth = size.width/scaleY; rootNode.size = new Size(systemWidth, systemHeight); break; case SpriteBoxTransformMode.nativePoints: break; default: assert(false); break; } _transformMatrix.translate(offsetX, offsetY); _transformMatrix.scale(scaleX, scaleY); return _transformMatrix; } void _invalidateTransformMatrix() { _transformMatrix = null; _rootNode._invalidateToBoxTransformMatrix(); } void paint(RenderObjectDisplayList canvas) { canvas.save(); // Move to correct coordinate space before drawing canvas.concat(transformMatrix.storage); // Draw the sprite tree _rootNode._visit(canvas); canvas.restore(); } // Updates int _animationId = 0; void _scheduleTick() { _animationId = scheduler.requestAnimationFrame(_tick); } void _tick(double timeStamp) { // Calculate the time between frames in seconds if (_lastTimeStamp == null) _lastTimeStamp = timeStamp; double delta = (timeStamp - _lastTimeStamp) / 1000; _lastTimeStamp = timeStamp; // Count the number of frames we've been running _numFrames += 1; _frameRate = 1.0/delta; // Print frame rate if (_numFrames % 60 == 0) print("delta: $delta fps: $_frameRate"); _callUpdate(_rootNode, delta); _scheduleTick(); } void _callUpdate(Node node, double dt) { node.update(dt); for (Node child in node.children) { if (!child.paused) { _callUpdate(child, dt); } } } void _callSpriteBoxPerformedLayout(Node node) { node.spriteBoxPerformedLayout(); for (Node child in node.children) { _callSpriteBoxPerformedLayout(child); } } // Hit tests List findNodesAtPosition(Point position) { assert(position != null); List nodes = []; // Traverse the render tree and find objects at the position _addNodesAtPosition(_rootNode, position, nodes); return nodes; } _addNodesAtPosition(Node node, Point position, List list) { // Visit children first for (Node child in node.children) { _addNodesAtPosition(child, position, list); } // Do the hit test Point posInNodeSpace = node.convertPointToNodeSpace(position); if (node.isPointInside(posInNodeSpace)) { list.add(node); } } } class SpriteBoxHitTestEntry extends BoxHitTestEntry { List nodeTargets; SpriteBoxHitTestEntry(RenderBox target, Point localPosition) : super(target, localPosition); } class SpriteBoxEvent { Point boxPosition; String type; int pointer; SpriteBoxEvent(this.boxPosition, this.type, this.pointer); }