-
-
Notifications
You must be signed in to change notification settings - Fork 872
/
viewfinder.dart
229 lines (210 loc) · 7.7 KB
/
viewfinder.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import 'dart:math';
import 'dart:ui';
import 'package:flame/src/anchor.dart';
import 'package:flame/src/camera/camera_component.dart';
import 'package:flame/src/components/core/component.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:flame/src/game/transform2d.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
/// [Viewfinder] is a part of a [CameraComponent] system that controls which
/// part of the game world is currently visible through a viewport.
///
/// The viewfinder contains the game point that is currently at the
/// "cross-hairs" of the viewport ([position]), the [zoom] level, and the
/// [angle] of rotation of the camera.
///
/// If you add children to the [Viewfinder] they will appear like HUDs i.e.
/// statically in front of the world.
class Viewfinder extends Component
implements AnchorProvider, AngleProvider, PositionProvider, ScaleProvider {
/// Internal transform matrix used by the viewfinder.
final Transform2D _transform = Transform2D();
@internal
Transform2D get transform => _transform;
/// The game coordinates of a point that is to be positioned at the center
/// of the viewport.
@override
Vector2 get position => -_transform.offset;
@override
set position(Vector2 value) {
_transform.offset = -value;
_visibleRect = null;
}
/// Zoom level of the game.
///
/// The default zoom value of 1 means that the world coordinates are in 1:1
/// correspondence with the pixels on the screen. Zoom levels higher than 1
/// make the world appear closer: each unit of game coordinate systems maps
/// to [zoom] pixels on the screen. Conversely, when [zoom] is less than 1,
/// the game world will appear further away and smaller in size.
///
/// See also: [visibleGameSize] for setting the zoom level dynamically.
double get zoom => _transform.scale.x;
set zoom(double value) {
assert(value > 0, 'zoom level must be positive: $value');
_transform.scale = Vector2.all(value);
_visibleRect = null;
}
/// Rotation angle of the game world, in radians.
///
/// The rotation is around the axis that is perpendicular to the screen.
@override
double get angle => -_transform.angle;
@override
set angle(double value) {
_transform.angle = -value;
_visibleRect = null;
}
/// The point within the viewport that is considered the "logical center" of
/// the camera.
///
/// This anchor is relative to the viewport's bounding rect, and by default
/// is at the center of the viewport.
///
/// The "logical center" of the camera means the point within the viewport
/// where the viewfinder's focus is located at. It is at this point within
/// the viewport that the world's point [position] will be displayed.
@override
Anchor get anchor => _anchor;
Anchor _anchor = Anchor.center;
@override
set anchor(Anchor value) {
_anchor = value;
onViewportResize();
}
/// Reference to the parent camera.
CameraComponent get camera => parent! as CameraComponent;
/// Convert a point from the global coordinate system to the viewfinder's
/// coordinate system.
///
/// Use [output] to send in a Vector2 object that will be used to avoid
/// creating a new Vector2 object in this method.
///
/// Opposite of [localToGlobal].
Vector2 globalToLocal(Vector2 point, {Vector2? output}) {
return _transform.globalToLocal(point, output: output);
}
/// Convert a point from the viewfinder's coordinate system to the global
/// coordinate system.
///
/// Use [output] to send in a Vector2 object that will be used to avoid
/// creating a new Vector2 object in this method.
///
/// Opposite of [globalToLocal].
Vector2 localToGlobal(Vector2 point, {Vector2? output}) {
return _transform.localToGlobal(point, output: output);
}
/// How much of a game world ought to be visible through the viewport.
///
/// When this property is non-null, the viewfinder will automatically select
/// the maximum zoom level such that a rectangle of size [visibleGameSize]
/// (in game coordinates) is visible through the viewport. If you want a
/// certain dimension to be unconstrained, set it to zero.
///
/// For example, if `visibleGameSize` is set to `[100.0, 0.0]`, the zoom level
/// will be chosen such that 100 game units will be visible across the width
/// of the viewport. Likewise, setting `visibleGameSize` to `[5.0, 10.0]`
/// will ensure that 5 or more game units are visible across the width of the
/// viewport, and 10 or more game units across the height.
///
/// This property is an alternative way to set the [zoom] level for the
/// viewfinder. It is persistent too: if the game size changes, the zoom
/// will be recalculated to fit the constraint.
Vector2? get visibleGameSize => _visibleGameSize;
Vector2? _visibleGameSize;
set visibleGameSize(Vector2? value) {
if (value == null || (value.x == 0 && value.y == 0)) {
_visibleGameSize = null;
} else {
assert(
value.x >= 0 && value.y >= 0,
'visibleGameSize cannot be negative: $value',
);
_visibleGameSize = value;
_initZoom();
}
}
/// See [CameraComponent.visibleWorldRect].
@internal
Rect get visibleWorldRect => _visibleRect ??= _computeVisibleRect();
Rect? _visibleRect;
Rect _computeVisibleRect() {
final viewportSize = camera.viewport.size;
final topLeft = _transform.globalToLocal(Vector2.zero());
final bottomRight = _transform.globalToLocal(viewportSize);
var minX = min(topLeft.x, bottomRight.x);
var minY = min(topLeft.y, bottomRight.y);
var maxX = max(topLeft.x, bottomRight.x);
var maxY = max(topLeft.y, bottomRight.y);
if (angle != 0) {
final topRight = _transform.globalToLocal(Vector2(viewportSize.x, 0));
final bottomLeft = _transform.globalToLocal(Vector2(0, viewportSize.y));
minX = min(minX, min(topRight.x, bottomLeft.x));
minY = min(minY, min(topRight.y, bottomLeft.y));
maxX = max(maxX, max(topRight.x, bottomLeft.x));
maxY = max(maxY, max(topRight.y, bottomLeft.y));
}
return Rect.fromLTRB(minX, minY, maxX, maxY);
}
/// Set [zoom] level based on the [_visibleGameSize].
void _initZoom() {
if (parent != null && _visibleGameSize != null) {
final viewportSize = camera.viewport.size;
final zoomX = viewportSize.x / _visibleGameSize!.x;
final zoomY = viewportSize.y / _visibleGameSize!.y;
zoom = min(zoomX, zoomY);
}
}
@override
void onGameResize(Vector2 size) {
_initZoom();
super.onGameResize(size);
}
/// Called by the viewport when its size changes.
@internal
void onViewportResize() {
if (parent != null) {
final viewportSize = camera.viewport.size;
_transform.position.x = viewportSize.x * _anchor.x;
_transform.position.y = viewportSize.y * _anchor.y;
_visibleRect = null;
}
}
@mustCallSuper
@override
void onLoad() {
// This has to be done here and on onMount so that it is available for
// the CameraComponent.visibleWorldRect calculation in onLoad of the game.
updateTransform();
}
@mustCallSuper
@override
void onMount() {
updateTransform();
}
@internal
void updateTransform() {
assert(
parent! is CameraComponent,
'Viewfinder can only be mounted to a CameraComponent',
);
_initZoom();
onViewportResize();
}
/// [ScaleProvider]'s API.
@internal
@override
Vector2 get scale => _transform.scale;
@internal
@override
set scale(Vector2 value) {
assert(
value.x == value.y,
'Non-uniform scale cannot be applied to a Viewfinder: $value',
);
assert(value.x > 0, 'Zoom must be positive: ${value.x}');
_transform.scale = value;
_visibleRect = null;
}
}