Files
flame/lib/components/position_component.dart
2020-12-09 10:22:36 +00:00

162 lines
5.5 KiB
Dart

import 'dart:ui' hide Offset;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../anchor.dart';
import '../extensions/offset.dart';
import '../extensions/vector2.dart';
import 'base_component.dart';
import 'component.dart';
/// A [Component] implementation that represents a component that has a
/// specific, possibly dynamic position on the screen.
///
/// It represents a rectangle of dimension [size], on the [position],
/// rotated around its [anchor] with angle [angle].
///
/// It also uses the [anchor] property to properly position itself.
///
/// A [PositionComponent] can have children. The children are all updated and
/// rendered automatically when this is updated and rendered.
/// They are translated by this component's (x,y). They do not need to fit
/// within this component's (width, height).
abstract class PositionComponent extends BaseComponent {
/// The position of this component on the screen (relative to the anchor).
Vector2 position = Vector2.zero();
/// X position of this component on the screen (relative to the anchor).
double get x => position.x;
set x(double x) => position.x = x;
/// Y position of this component on the screen (relative to the anchor).
double get y => position.y;
set y(double y) => position.y = y;
/// The size that this component is rendered with.
/// This is not necessarily the source size of the asset.
Vector2 size = Vector2.zero();
/// Width (size) that this component is rendered with.
double get width => size.x;
set width(double width) => size.x = width;
/// Height (size) that this component is rendered with.
double get height => size.y;
set height(double height) => size.y = height;
/// Get the top left position regardless of the anchor and angle
Vector2 get topLeftPosition => anchor.translate(position, size);
/// Set the top left position regardless of the anchor
set topLeftPosition(Vector2 position) {
this.position = position + (anchor.toVector2..multiply(size));
}
/// Angle (with respect to the x-axis) this component should be rendered with.
/// It is rotated around its anchor.
double angle = 0.0;
/// Anchor point for this component. This is where flame "grabs it".
/// The [position] is relative to this point inside the component.
/// The [angle] is rotated around this point.
Anchor anchor = Anchor.topLeft;
/// Whether this component should be flipped on the X axis before being rendered.
bool renderFlipX = false;
/// Whether this component should be flipped ofn the Y axis before being rendered.
bool renderFlipY = false;
/// Returns the relative position/size of this component.
/// Relative because it might be translated by their parents (which is not considered here).
Rect toRect() => topLeftPosition.toPositionedRect(size);
/// Mutates position and size using the provided [rect] as basis.
/// This is a relative rect, same definition that [toRect] use (therefore both methods are compatible, i.e. setByRect ∘ toRect = identity).
void setByRect(Rect rect) {
size.setValues(rect.width, rect.height);
topLeftPosition = rect.topLeft.toVector2();
}
@override
bool checkOverlap(Vector2 point) {
final corners = _rotatedCorners();
corners.add(corners.first);
for (int i = 1; i < corners.length; i++) {
final previousCorner = corners[i - 1];
final corner = corners[i];
final isOutside =
(corner.x - previousCorner.x) * (point.y - previousCorner.y) -
(point.x - previousCorner.x) * (corner.y - previousCorner.y) >
0;
if (isOutside) {
// Point is outside of convex polygon (only used for rectangles so far)
return false;
}
}
return true;
}
List<Vector2> _rotatedCorners() {
// Rotates the corner around [position]
Vector2 rotateCorner(Vector2 corner) {
return Vector2(
math.cos(angle) * (corner.x - position.x) -
math.sin(angle) * (corner.y - position.y) +
position.x,
math.sin(angle) * (corner.x - position.x) +
math.cos(angle) * (corner.y - position.y) +
position.y,
);
}
// Counter-clockwise direction
return [
rotateCorner(topLeftPosition), // Top-left
rotateCorner(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left
rotateCorner(topLeftPosition + size), // Bottom-right
rotateCorner(topLeftPosition + Vector2(size.x, 0.0)), // Top-right
];
}
double angleTo(PositionComponent c) => position.angleTo(c.position);
double distance(PositionComponent c) => position.distanceTo(c.position);
@override
void renderDebugMode(Canvas canvas) {
canvas.drawRect(size.toRect(), debugPaint);
debugTextConfig.render(
canvas,
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
Vector2(-50, -15),
);
final Rect rect = toRect();
final dx = rect.right;
final dy = rect.bottom;
debugTextConfig.render(
canvas,
'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}',
Vector2(width - 50, height),
);
}
@override
void prepareCanvas(Canvas canvas) {
canvas.translate(x, y);
canvas.rotate(angle);
final Vector2 delta = -anchor.toVector2
..multiply(size);
canvas.translate(delta.x, delta.y);
// Handle inverted rendering by moving center and flipping.
if (renderFlipX || renderFlipY) {
canvas.translate(width / 2, height / 2);
canvas.scale(renderFlipX ? -1.0 : 1.0, renderFlipY ? -1.0 : 1.0);
canvas.translate(-width / 2, -height / 2);
}
}
}