blob: 25685a6b99118e7932eb3462a07198fda5bf6ab3 [file] [log] [blame]
part of flutter_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(_transformMode == SpriteBoxTransformMode.nativePoints
|| value.size.width > 0);
assert(_transformMode == SpriteBoxTransformMode.nativePoints
|| value.size.height > 0);
// Remove sprite box references
if (_rootNode != null)
_removeSpriteBoxReference(_rootNode);
// Update the value
_rootNode = value;
_actionControllers = null;
// Add new references
_addSpriteBoxReference(_rootNode);
markNeedsLayout();
}
// Tracking of frame rate and updates
Duration _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;
List<PhysicsWorld> _physicsNodes;
Rect _visibleArea;
Rect get visibleArea {
if (_visibleArea == null)
_calcTransformMatrix();
return _visibleArea;
}
bool _initialized = false;
// 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 transform mode
this.transformMode = mode;
// Setup root node
this.rootNode = rootNode;
}
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);
_initialized = true;
}
// Adding and removing nodes
void _registerNode(Node node) {
_actionControllers = null;
_eventTargets = null;
_physicsNodes = null;
if (node == null || node.constraints != null) _constrainedNodes = null;
}
void _deregisterNode(Node node) {
_actionControllers = null;
_eventTargets = null;
_physicsNodes = 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++;
}
}
void handleEvent(PointerEvent event, _SpriteBoxHitTestEntry entry) {
if (!attached)
return;
if (event is PointerDownEvent) {
// Build list of event targets
if (_eventTargets == null) {
_eventTargets = <Node>[];
_addEventTargets(_rootNode, _eventTargets);
}
// Find the once that are hit by the pointer
List<Node> nodeTargets = <Node>[];
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(event.position, event.runtimeType, 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 is PointerUpEvent || event is PointerCancelEvent)
node._handlingPointer = null;
}
}
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:
systemWidth = size.width;
systemHeight = size.height;
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 Canvas canvas = context.canvas;
canvas.save();
// Move to correct coordinate space before drawing
canvas.translate(offset.dx, offset.dy);
canvas.transform(transformMatrix.storage);
// Draw the sprite tree
Matrix4 totalMatrix = new Matrix4.fromFloat64List(canvas.getTotalMatrix());
_rootNode._visit(canvas, totalMatrix);
// Draw physics debug
if (_physicsNodes == null)
_rebuildActionControllersAndPhysicsNodes();
for (PhysicsWorld world in _physicsNodes) {
if (world.drawDebug) {
canvas.setMatrix(world._debugDrawTransform.storage);
world.paintDebug(canvas);
}
}
canvas.restore();
}
// Updates
void _scheduleTick() {
Scheduler.instance.scheduleFrameCallback(_tick);
}
void _tick(Duration timeStamp) {
if (!attached)
return;
// Calculate delta and frame rate
if (_lastTimeStamp == null)
_lastTimeStamp = timeStamp;
double delta = (timeStamp - _lastTimeStamp).inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
_lastTimeStamp = timeStamp;
_frameRate = 1.0 / delta;
if (_initialized) {
_callConstraintsPreUpdate(delta);
_runActions(delta);
_callUpdate(_rootNode, delta);
_callStepPhysics(delta);
_callConstraintsConstrain(delta);
}
// Schedule next update
_scheduleTick();
// Make sure the node graph is redrawn
markNeedsPaint();
}
void _runActions(double dt) {
if (_actionControllers == null) {
_rebuildActionControllersAndPhysicsNodes();
}
for (ActionController actions in _actionControllers) {
actions.step(dt);
}
}
void _rebuildActionControllersAndPhysicsNodes() {
_actionControllers = <ActionController>[];
_physicsNodes = <PhysicsWorld>[];
_addActionControllersAndPhysicsNodes(_rootNode);
}
void _addActionControllersAndPhysicsNodes(Node node) {
if (node._actions != null) _actionControllers.add(node._actions);
if (node is PhysicsWorld) _physicsNodes.add(node);
for (int i = node.children.length - 1; i >= 0; i--) {
Node child = node.children[i];
_addActionControllersAndPhysicsNodes(child);
}
}
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 _callStepPhysics(double dt) {
if (_physicsNodes == null)
_rebuildActionControllersAndPhysicsNodes();
for (PhysicsWorld physicsNode in _physicsNodes) {
physicsNode._stepPhysics(dt);
}
}
void _callConstraintsPreUpdate(double dt) {
if (_constrainedNodes == null) {
_constrainedNodes = <Node>[];
_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 = <Node>[];
_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 = <Node>[];
// Traverse the render tree and find objects at the position
_addNodesAtPosition(_rootNode, position, nodes);
return nodes;
}
void _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, PointerDownEvent, PointerMoveEvent, PointerUpEvent, and
/// PointerCancelEvent.
///
/// if (event.type == PointerDownEvent) {
/// // Do something!
/// }
final Type 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);
}