blob: 264cc5f3a2b6223ca71644f3f2f4f7c8726c3a4c [file] [log] [blame]
part of sprites;
/// Options for setting up a [SpriteBox].
///
/// * [nativePoints], use the same points as the parent [Widget].
/// * [letterbox], use the size of the root node for the coordinate system, constrain the aspect ratio and trim off
/// areas that end up outside the screen.
/// * [stretch], use the size of the root node for the coordinate system, scale it to fit the size of the box.
/// * [scaleToFit], similar to the letterbox option, but instead of trimming areas the sprite system will be scaled
/// down to fit the box.
/// * [fixedWidth], uses the width of the root node to set the size of the coordinate system, this option will change
/// the height of the root node to fit the box.
/// * [fixedHeight], uses the height of the root node to set the size of the coordinate system, this option will change
/// the width of the root node to fit the box.
enum SpriteBoxTransformMode {
nativePoints,
letterbox,
stretch,
scaleToFit,
fixedWidth,
fixedHeight,
}
class SpriteBox extends RenderBox {
// Member variables
// Root node for drawing
NodeWithSize _rootNode;
void set rootNode (NodeWithSize value) {
if (value == _rootNode) return;
// Ensure that the root node has a size
assert(value.size.width > 0);
assert(value.size.height > 0);
// Remove sprite box references
if (_rootNode != null) _removeSpriteBoxReference(_rootNode);
// Update the value
_rootNode = value;
// Add new references
_addSpriteBoxReference(_rootNode);
markNeedsLayout();
}
// Tracking of frame rate and updates
double _lastTimeStamp;
double _frameRate = 0.0;
double get frameRate => _frameRate;
// Transformation mode
SpriteBoxTransformMode _transformMode;
void set transformMode (SpriteBoxTransformMode value) {
if (value == _transformMode)
return;
_transformMode = value;
// Invalidate stuff
markNeedsLayout();
}
/// The transform mode used by the [SpriteBox].
SpriteBoxTransformMode get transformMode => _transformMode;
// Cached transformation matrix
Matrix4 _transformMatrix;
List<Node> _eventTargets;
List<ActionController> _actionControllers;
List<Node> _constrainedNodes;
Rect _visibleArea;
Rect get visibleArea {
if (_visibleArea == null)
_calcTransformMatrix();
return _visibleArea;
}
// Setup
/// Creates a new SpriteBox with a node as its content, by default uses letterboxing.
///
/// The [rootNode] provides the content of the node tree, typically it's a custom subclass of [NodeWithSize]. The
/// [mode] provides different ways to scale the content to best fit it to the screen. In most cases it's preferred to
/// use a [SpriteWidget] that automatically wraps the SpriteBox.
///
/// var spriteBox = new SpriteBox(myNode, SpriteBoxTransformMode.fixedHeight);
SpriteBox(NodeWithSize rootNode, [SpriteBoxTransformMode mode = SpriteBoxTransformMode.letterbox]) {
assert(rootNode != null);
assert(rootNode._spriteBox == null);
// Setup root node
this.rootNode = rootNode;
// Setup transform mode
this.transformMode = mode;
}
void _removeSpriteBoxReference(Node node) {
node._spriteBox = null;
for (Node child in node._children) {
_removeSpriteBoxReference(child);
}
}
void _addSpriteBoxReference(Node node) {
node._spriteBox = this;
for (Node child in node._children) {
_addSpriteBoxReference(child);
}
}
void attach() {
super.attach();
_scheduleTick();
}
// Properties
/// The root node of the node tree that is rendered by this box.
///
/// var rootNode = mySpriteBox.rootNode;
NodeWithSize get rootNode => _rootNode;
void performLayout() {
size = constraints.biggest;
_invalidateTransformMatrix();
_callSpriteBoxPerformedLayout(_rootNode);
}
// Adding and removing nodes
_registerNode(Node node) {
_actionControllers = null;
_eventTargets = null;
if (node == null || node.constraints != null) _constrainedNodes = null;
}
_deregisterNode(Node node) {
_actionControllers = null;
_eventTargets = null;
if (node == null || node.constraints != null) _constrainedNodes = null;
}
// Event handling
void _addEventTargets(Node node, List<Node> 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++;
}
}
EventDisposition handleEvent(Event event, _SpriteBoxHitTestEntry entry) {
if (!attached)
return EventDisposition.ignored;
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<Node> 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<Node> 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;
}
}
}
return EventDisposition.ignored;
}
bool hitTest(HitTestResult result, { Point position }) {
result.add(new _SpriteBoxHitTestEntry(this, position));
return true;
}
// Rendering
/// The transformation matrix used to transform the root node to the space of the box.
///
/// It's uncommon to need access to this property.
///
/// var matrix = mySpriteBox.transformMatrix;
Matrix4 get transformMatrix {
// Get cached matrix if available
if (_transformMatrix == null) {
_calcTransformMatrix();
}
return _transformMatrix;
}
void _calcTransformMatrix() {
_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;
}
_visibleArea = new Rect.fromLTRB(-offsetX / scaleX,
-offsetY / scaleY,
systemWidth + offsetX / scaleX,
systemHeight + offsetY / scaleY);
_transformMatrix.translate(offsetX, offsetY);
_transformMatrix.scale(scaleX, scaleY);
}
void _invalidateTransformMatrix() {
_visibleArea = null;
_transformMatrix = null;
_rootNode._invalidateToBoxTransformMatrix();
}
void paint(PaintingContext context, Offset offset) {
final PaintingCanvas canvas = context.canvas;
canvas.save();
// Move to correct coordinate space before drawing
canvas.translate(offset.dx, offset.dy);
canvas.concat(transformMatrix.storage);
// Draw the sprite tree
Matrix4 totalMatrix = new Matrix4.fromFloat32List(canvas.getTotalMatrix());
_rootNode._visit(canvas, totalMatrix);
canvas.restore();
}
// Updates
void _scheduleTick() {
scheduler.requestAnimationFrame(_tick);
}
void _tick(double timeStamp) {
if (!attached)
return;
// Calculate delta and frame rate
if (_lastTimeStamp == null) _lastTimeStamp = timeStamp;
double delta = (timeStamp - _lastTimeStamp) / 1000;
_lastTimeStamp = timeStamp;
_frameRate = 1.0/delta;
_callConstraintsPreUpdate(delta);
_runActions(delta);
_callUpdate(_rootNode, delta);
_callConstraintsConstrain(delta);
// Schedule next update
_scheduleTick();
// Make sure the node graph is redrawn
markNeedsPaint();
}
void _runActions(double dt) {
if (_actionControllers == null) {
_actionControllers = [];
_addActionControllers(_rootNode, _actionControllers);
}
for (ActionController actions in _actionControllers) {
actions.step(dt);
}
}
void _addActionControllers(Node node, List<ActionController> controllers) {
if (node._actions != null) controllers.add(node._actions);
for (int i = node.children.length - 1; i >= 0; i--) {
Node child = node.children[i];
_addActionControllers(child, controllers);
}
}
void _callUpdate(Node node, double dt) {
node.update(dt);
for (int i = node.children.length - 1; i >= 0; i--) {
Node child = node.children[i];
if (!child.paused) {
_callUpdate(child, dt);
}
}
}
void _callConstraintsPreUpdate(double dt) {
if (_constrainedNodes == null) {
_constrainedNodes = [];
_addConstrainedNodes(_rootNode, _constrainedNodes);
}
for (Node node in _constrainedNodes) {
for (Constraint constraint in node.constraints) {
constraint.preUpdate(node, dt);
}
}
}
void _callConstraintsConstrain(double dt) {
if (_constrainedNodes == null) {
_constrainedNodes = [];
_addConstrainedNodes(_rootNode, _constrainedNodes);
}
for (Node node in _constrainedNodes) {
for (Constraint constraint in node.constraints) {
constraint.constrain(node, dt);
}
}
}
void _addConstrainedNodes(Node node, List<Node> nodes) {
if (node._constraints != null && node._constraints.length > 0) {
nodes.add(node);
}
for (Node child in node.children) {
_addConstrainedNodes(child, nodes);
}
}
void _callSpriteBoxPerformedLayout(Node node) {
node.spriteBoxPerformedLayout();
for (Node child in node.children) {
_callSpriteBoxPerformedLayout(child);
}
}
// Hit tests
/// Finds all nodes at a position defined in the box's coordinates.
///
/// Use this method with caution. It searches the complete node tree to locate the nodes, which can be slow if the
/// node tree is large.
///
/// List nodes = mySpriteBox.findNodesAtPosition(new Point(50.0, 50.0));
List<Node> findNodesAtPosition(Point position) {
assert(position != null);
List<Node> nodes = [];
// Traverse the render tree and find objects at the position
_addNodesAtPosition(_rootNode, position, nodes);
return nodes;
}
_addNodesAtPosition(Node node, Point position, List<Node> 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<Node> nodeTargets;
_SpriteBoxHitTestEntry(RenderBox target, Point localPosition) : super(target, localPosition);
}
/// An event that is passed down the node tree when pointer events occur. The SpriteBoxEvent is typically handled in
/// the handleEvent method of [Node].
class SpriteBoxEvent {
/// The position of the event in box coordinates.
///
/// You can use the convertPointToNodeSpace of [Node] to convert the position to local coordinates.
///
/// bool handleEvent(SpriteBoxEvent event) {
/// Point localPosition = convertPointToNodeSpace(event.boxPosition);
/// if (event.type == 'pointerdown') {
/// // Do something!
/// }
/// }
final Point boxPosition;
/// The type of event, there are currently four valid types, 'pointerdown', 'pointermoved', 'pointerup', and
/// 'pointercancel'.
///
/// if (event.type == 'pointerdown') {
/// // Do something!
/// }
final String type;
/// The id of the pointer. Each pointer on the screen will have a unique pointer id.
///
/// if (event.pointer == firstPointerId) {
/// // Do something
/// }
final int pointer;
/// Creates a new SpriteBoxEvent, typically this is done internally inside the SpriteBox.
///
/// var event = new SpriteBoxEvent(new Point(50.0, 50.0), 'pointerdown', 0);
SpriteBoxEvent(this.boxPosition, this.type, this.pointer);
}