Line data Source code
1 : import 'dart:ui' hide Offset;
2 :
3 : import '../anchor.dart';
4 : import '../extensions/offset.dart';
5 : import '../extensions/rect.dart';
6 : import '../extensions/vector2.dart';
7 : import '../geometry/rectangle.dart';
8 : import 'base_component.dart';
9 : import 'cache/value_cache.dart';
10 : import 'component.dart';
11 : import 'mixins/hitbox.dart';
12 :
13 : /// A [Component] implementation that represents a component that has a
14 : /// specific, possibly dynamic position on the screen.
15 : ///
16 : /// It represents a rectangle of dimension [size], on the [position],
17 : /// rotated around its [anchor] with angle [angle].
18 : ///
19 : /// It also uses the [anchor] property to properly position itself.
20 : ///
21 : /// A [PositionComponent] can have children. The children are all updated and
22 : /// rendered automatically when this is updated and rendered.
23 : /// They are translated by this component's (x,y). They do not need to fit
24 : /// within this component's (width, height).
25 : abstract class PositionComponent extends BaseComponent {
26 : /// The position of this component on the screen (relative to the anchor).
27 30 : Vector2 get position => _position;
28 15 : set position(Vector2 position) => _position.setFrom(position);
29 : final Vector2 _position;
30 :
31 : /// X position of this component on the screen (relative to the anchor).
32 12 : double get x => _position.x;
33 3 : set x(double x) => _position.x = x;
34 :
35 : /// Y position of this component on the screen (relative to the anchor).
36 12 : double get y => _position.y;
37 3 : set y(double y) => _position.y = y;
38 :
39 : /// The size that this component is rendered with before [scale] is applied.
40 : /// This is not necessarily the source size of the asset.
41 38 : Vector2 get size => _size;
42 6 : set size(Vector2 size) => _size.setFrom(size);
43 : final Vector2 _size;
44 :
45 : /// Width (size) that this component is rendered with.
46 3 : double get width => scaledSize.x;
47 6 : set width(double width) => size.x = width / scale.x;
48 :
49 : /// Height (size) that this component is rendered with.
50 3 : double get height => scaledSize.y;
51 6 : set height(double height) => size.y = height / scale.y;
52 :
53 : /// The scale factor of this component
54 32 : Vector2 get scale => _scale;
55 6 : set scale(Vector2 scale) => _scale.setFrom(scale);
56 : final Vector2 _scale;
57 :
58 : /// Cache to store the calculated scaled size
59 : final ValueCache<Vector2> _scaledSizeCache = ValueCache();
60 :
61 : /// The size that this component is rendered with after [scale] is applied.
62 8 : Vector2 get scaledSize {
63 40 : if (!_scaledSizeCache.isCacheValid([scale, size])) {
64 16 : _scaledSizeCache.updateCache(
65 32 : size.clone()..multiply(scale),
66 40 : [scale.clone(), size.clone()],
67 : );
68 : }
69 16 : return _scaledSizeCache.value!;
70 : }
71 :
72 : /// Get the absolute position, with the anchor taken into consideration
73 24 : Vector2 get absolutePosition => absoluteParentPosition + position;
74 :
75 : /// Get the relative top left position regardless of the anchor and angle
76 5 : Vector2 get topLeftPosition {
77 10 : return anchor.toOtherAnchorPosition(
78 5 : position,
79 : Anchor.topLeft,
80 5 : scaledSize,
81 : );
82 : }
83 :
84 : /// Set the top left position regardless of the anchor
85 1 : set topLeftPosition(Vector2 position) {
86 6 : this.position = position + (anchor.toVector2()..multiply(scaledSize));
87 : }
88 :
89 : /// Get the absolute top left position regardless of whether it is a child or not
90 4 : Vector2 get absoluteTopLeftPosition {
91 4 : final p = parent;
92 4 : if (p is PositionComponent) {
93 6 : return p.absoluteTopLeftPosition + topLeftPosition;
94 : } else {
95 4 : return topLeftPosition;
96 : }
97 : }
98 :
99 : /// Get the position that everything in this component is positioned in relation to
100 : /// If this component has no parent the absolute parent position is the origin,
101 : /// otherwise it's the parents absolute top left position
102 6 : Vector2 get absoluteParentPosition {
103 6 : final p = parent;
104 6 : if (p is PositionComponent) {
105 2 : return p.absoluteTopLeftPosition;
106 : } else {
107 6 : return Vector2.zero();
108 : }
109 : }
110 :
111 : /// Get the position of the center of the component's bounding rectangle
112 7 : Vector2 get center {
113 14 : if (anchor == Anchor.center) {
114 2 : return position;
115 : } else {
116 24 : return anchor.toOtherAnchorPosition(position, Anchor.center, scaledSize)
117 18 : ..rotate(angle, center: absolutePosition);
118 : }
119 : }
120 :
121 : /// Get the absolute center of the component
122 24 : Vector2 get absoluteCenter => absoluteParentPosition + center;
123 :
124 : /// Angle (with respect to the x-axis) this component should be rendered with.
125 : /// It is rotated around its anchor.
126 : double angle;
127 :
128 : /// Anchor point for this component. This is where flame "grabs it".
129 : /// The [position] is relative to this point inside the component.
130 : /// The [angle] is rotated around this point.
131 : Anchor anchor;
132 :
133 : /// Whether this component should be flipped on the X axis before being rendered.
134 : bool renderFlipX = false;
135 :
136 : /// Whether this component should be flipped ofn the Y axis before being rendered.
137 : bool renderFlipY = false;
138 :
139 27 : PositionComponent({
140 : Vector2? position,
141 : Vector2? size,
142 : Vector2? scale,
143 : this.angle = 0.0,
144 : this.anchor = Anchor.topLeft,
145 : this.renderFlipX = false,
146 : this.renderFlipY = false,
147 : int? priority,
148 21 : }) : _position = position ?? Vector2.zero(),
149 16 : _size = size ?? Vector2.zero(),
150 26 : _scale = scale ?? Vector2.all(1.0),
151 27 : super(priority: priority);
152 :
153 4 : @override
154 : bool containsPoint(Vector2 point) {
155 12 : final rectangle = Rectangle.fromRect(toAbsoluteRect(), angle: angle)
156 8 : ..position = absoluteCenter;
157 4 : return rectangle.containsPoint(point);
158 : }
159 :
160 0 : double angleTo(PositionComponent c) => position.angleTo(c.position);
161 :
162 0 : double distance(PositionComponent c) => position.distanceTo(c.position);
163 :
164 0 : @override
165 : void renderDebugMode(Canvas canvas) {
166 0 : if (this is Hitbox) {
167 0 : (this as Hitbox).renderHitboxes(canvas);
168 : }
169 0 : canvas.drawRect(scaledSize.toRect(), debugPaint);
170 0 : debugTextPaint.render(
171 : canvas,
172 0 : 'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
173 0 : Vector2(-50, -15),
174 : );
175 :
176 0 : final rect = toRect();
177 0 : final dx = rect.right;
178 0 : final dy = rect.bottom;
179 0 : debugTextPaint.render(
180 : canvas,
181 0 : 'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}',
182 0 : Vector2(width - 50, height),
183 : );
184 : }
185 :
186 : final Matrix4 _preRenderMatrix = Matrix4.identity();
187 :
188 3 : @override
189 : void preRender(Canvas canvas) {
190 : // Move canvas to the components anchor position.
191 12 : _preRenderMatrix.translate(x, y);
192 : // Rotate canvas around anchor
193 9 : _preRenderMatrix.rotateZ(angle);
194 : // Scale canvas if it should be scaled
195 18 : if (scale.x != 1.0 || scale.y != 1.0) {
196 0 : _preRenderMatrix.scale(scale.x, scale.y);
197 : }
198 9 : canvas.transform(_preRenderMatrix.storage);
199 6 : _preRenderMatrix.setIdentity();
200 :
201 12 : final delta = anchor.toVector2()..multiply(size);
202 15 : canvas.translate(-delta.x, -delta.y);
203 :
204 : // Handle inverted rendering by moving center and flipping.
205 6 : if (renderFlipX || renderFlipY) {
206 0 : final size = scaledSize;
207 0 : _preRenderMatrix.translate(size.x / 2, size.y / 2);
208 0 : _preRenderMatrix.scale(
209 0 : renderFlipX ? -1.0 : 1.0,
210 0 : renderFlipY ? -1.0 : 1.0,
211 : );
212 0 : canvas.transform(_preRenderMatrix.storage);
213 0 : canvas.translate(-size.x / 2, -size.y / 2);
214 0 : _preRenderMatrix.setIdentity();
215 : }
216 : }
217 :
218 : /// Returns the relative position/size of this component.
219 : /// Relative because it might be translated by their parents (which is not considered here).
220 4 : Rect toRect() => topLeftPosition.toPositionedRect(scaledSize);
221 :
222 : /// Returns the absolute position/size of this component.
223 : /// Absolute because it takes any possible parent position into consideration.
224 16 : Rect toAbsoluteRect() => absoluteTopLeftPosition.toPositionedRect(scaledSize);
225 :
226 : /// Mutates position and size using the provided [rect] as basis.
227 : /// This is a relative rect, same definition that [toRect] use
228 : /// (therefore both methods are compatible, i.e. setByRect ∘ toRect = identity).
229 1 : void setByRect(Rect rect) {
230 4 : size.setValues(rect.width, rect.height);
231 3 : topLeftPosition = rect.topLeft.toVector2();
232 : }
233 : }
|