Line data Source code
1 : import 'dart:collection';
2 : import 'dart:ui';
3 :
4 : import 'package:flutter/foundation.dart';
5 :
6 : import 'assets/images.dart';
7 : import 'extensions/image.dart';
8 : import 'extensions/vector2.dart';
9 : import 'flame.dart';
10 : import 'game/game.dart';
11 :
12 : extension SpriteBatchExtension on Game {
13 : /// Utility method to load and cache the image for a [SpriteBatch] based on its options
14 0 : Future<SpriteBatch> loadSpriteBatch(
15 : String path, {
16 : Color defaultColor = const Color(0x00000000),
17 : BlendMode defaultBlendMode = BlendMode.srcOver,
18 : RSTransform? defaultTransform,
19 : }) {
20 0 : return SpriteBatch.load(
21 : path,
22 : defaultColor: defaultColor,
23 : defaultBlendMode: defaultBlendMode,
24 : defaultTransform: defaultTransform,
25 0 : images: images,
26 : );
27 : }
28 : }
29 :
30 : /// This is the scale value used in [BatchItem.matrix], we can't determine this from the [BatchItem.transform],
31 : /// but we also don't need to do so because it is already calculated inside the transform values.
32 : const _defaultScale = 0.0;
33 :
34 : /// A single item in a SpriteBatch.
35 : ///
36 : /// Holds all the important information of a batch item,
37 : ///
38 : /// Web currently does not support `Canvas.drawAtlas`, so a BatchItem will
39 : /// automatically calculate a transform matrix based on the [transform] value, to be
40 : /// used when rendering on the web. It will initialize a [destination] object
41 : /// and a [paint] object.
42 : class BatchItem {
43 : /// The source rectangle on the [SpriteBatch.atlas].
44 : final Rect source;
45 :
46 : /// The destination rectangle for the Canvas.
47 : ///
48 : /// It will be transformed by [matrix].
49 : final Rect destination;
50 :
51 : /// The transform values for this batch item.
52 : final RSTransform transform;
53 :
54 : /// The background color for this batch item.
55 : final Color color;
56 :
57 : /// Fallback matrix for the web.
58 : ///
59 : /// Because `Canvas.drawAtlas` is not supported on the web we also
60 : /// build a `Matrix4` based on the [transform] values.
61 : final Matrix4 matrix;
62 :
63 : /// Paint object used for the web.
64 : final Paint paint;
65 :
66 0 : BatchItem({
67 : required this.source,
68 : required this.transform,
69 : required this.color,
70 0 : }) : matrix = Matrix4(
71 0 : transform.scos, transform.ssin, 0, 0, //
72 0 : -transform.ssin, transform.scos, 0, 0, //
73 : 0, 0, _defaultScale, 0, //
74 0 : transform.tx, transform.ty, 0, 1, //
75 : ),
76 0 : paint = Paint()..color = color,
77 0 : destination = Offset.zero & source.size;
78 : }
79 :
80 : /// The SpriteBatch API allows for rendering multiple items at once.
81 : ///
82 : /// This class allows for optimization when you want to draw many parts of an
83 : /// image onto the canvas. It is more efficient than using multiple calls to [Canvas.drawImageRect]
84 : /// and provides more functionality by allowing each [BatchItem] to have their own transform
85 : /// rotation and color.
86 : ///
87 : /// By collecting all the necessary transforms on a single image and sending those transforms
88 : /// in a single batch to the GPU, we can render multiple parts of a single image at once.
89 : ///
90 : /// **Note**: Currently web does not support `Canvas.drawAtlas`, which SpriteBatch uses under
91 : /// the hood, instead it will render each [BatchItem] using `Canvas.drawImageRect`, so there
92 : /// might be a performance hit on web when working with many batch items.
93 : class SpriteBatch {
94 : /// List of all the existing batch items.
95 : final _batchItems = <BatchItem>[];
96 :
97 : /// The sources to use on the [atlas].
98 : final _sources = <Rect>[];
99 :
100 : /// The sources list shouldn't be modified directly, that is why an
101 : /// [UnmodifiableListView] is used. If you want to add sources use the
102 : /// [add] or [addTransform] method.
103 0 : UnmodifiableListView<Rect> get sources {
104 0 : return UnmodifiableListView<Rect>(_sources);
105 : }
106 :
107 : /// The transforms that should be applied on the [_sources].
108 : final _transforms = <RSTransform>[];
109 :
110 : /// The transforms list shouldn't be modified directly, that is why an
111 : /// [UnmodifiableListView] is used. If you want to add transforms use the
112 : /// [add] or [addTransform] method.
113 0 : UnmodifiableListView<RSTransform> get transforms {
114 0 : return UnmodifiableListView<RSTransform>(_transforms);
115 : }
116 :
117 : /// The background color for the [_sources].
118 : final _colors = <Color>[];
119 :
120 : /// The colors list shouldn't be modified directly, that is why an
121 : /// [UnmodifiableListView] is used. If you want to add colors use the
122 : /// [add] or [addTransform] method.
123 0 : UnmodifiableListView<Color> get colors {
124 0 : return UnmodifiableListView<Color>(_colors);
125 : }
126 :
127 : /// The atlas used by the [SpriteBatch].
128 : final Image atlas;
129 :
130 : /// The default color, used as a background color for a [BatchItem].
131 : final Color defaultColor;
132 :
133 : /// The default transform, used when a transform was not supplied for a [BatchItem].
134 : final RSTransform? defaultTransform;
135 :
136 : /// The default blend mode, used for blending a batch item.
137 : final BlendMode defaultBlendMode;
138 :
139 : /// The width of the [atlas].
140 0 : int get width => atlas.width;
141 :
142 : /// The height of the [atlas].
143 0 : int get height => atlas.height;
144 :
145 : /// The size of the [atlas].
146 0 : Vector2 get size => atlas.size;
147 :
148 0 : SpriteBatch(
149 : this.atlas, {
150 : this.defaultColor = const Color(0x00000000),
151 : this.defaultBlendMode = BlendMode.srcOver,
152 : this.defaultTransform,
153 : });
154 :
155 : /// Takes a path of an image, and optional arguments for the SpriteBatch.
156 : ///
157 : /// When the [images] is omitted, the global [Flame.images] is used.
158 0 : static Future<SpriteBatch> load(
159 : String path, {
160 : Color defaultColor = const Color(0x00000000),
161 : BlendMode defaultBlendMode = BlendMode.srcOver,
162 : RSTransform? defaultTransform,
163 : Images? images,
164 : }) async {
165 0 : final _images = images ?? Flame.images;
166 0 : return SpriteBatch(
167 0 : await _images.load(path),
168 : defaultColor: defaultColor,
169 0 : defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0),
170 : defaultBlendMode: defaultBlendMode,
171 : );
172 : }
173 :
174 : /// Add a new batch item using a RSTransform.
175 : ///
176 : /// The [source] parameter is the source location on the [atlas].
177 : ///
178 : /// You can position, rotate and scale it on the canvas using the [transform] parameter.
179 : ///
180 : /// The [color] parameter allows you to render a color behind the batch item, as a background color.
181 : ///
182 : /// The [add] method may be a simpler way to add a batch item to the batch. However,
183 : /// if there is a way to factor out the computations of the sine and cosine of the
184 : /// rotation so that they can be reused over multiple calls to this constructor,
185 : /// it may be more efficient to directly use this method instead.
186 0 : void addTransform({
187 : required Rect source,
188 : RSTransform? transform,
189 : Color? color,
190 : }) {
191 0 : final batchItem = BatchItem(
192 : source: source,
193 0 : transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),
194 0 : color: color ?? defaultColor,
195 : );
196 :
197 0 : _batchItems.add(batchItem);
198 :
199 0 : _sources.add(batchItem.source);
200 0 : _transforms.add(batchItem.transform);
201 0 : _colors.add(batchItem.color);
202 : }
203 :
204 : /// Add a new batch item.
205 : ///
206 : /// The [source] parameter is the source location on the [atlas]. You can position it
207 : /// on the canvas using the [offset] parameter.
208 : ///
209 : /// You can transform the sprite from its [offset] using [scale], [rotation] and [anchor].
210 : ///
211 : /// The [color] paramater allows you to render a color behind the batch item, as a background color.
212 : ///
213 : /// This method creates a new [RSTransform] based on the given transform arguments. If many [RSTransform] objects are being
214 : /// created and there is a way to factor out the computations of the sine and cosine of the rotation
215 : /// (which are computed each time this method is called) and reuse them over multiple [RSTransform] objects,
216 : /// it may be more efficient to directly use the more direct [addTransform] method instead.
217 0 : void add({
218 : required Rect source,
219 : double scale = 1.0,
220 : Vector2? anchor,
221 : double rotation = 0,
222 : Vector2? offset,
223 : Color? color,
224 : }) {
225 0 : anchor ??= Vector2.zero();
226 0 : offset ??= Vector2.zero();
227 : RSTransform? transform;
228 :
229 : // If any of the transform arguments is different from the defaults,
230 : // then we create one. This is to prevent unnecessary computations
231 : // of the sine and cosine of the rotation.
232 0 : if (scale != 1.0 ||
233 0 : anchor != Vector2.zero() ||
234 0 : rotation != 0 ||
235 0 : offset != Vector2.zero()) {
236 0 : transform = RSTransform.fromComponents(
237 : scale: scale,
238 0 : anchorX: anchor.x,
239 0 : anchorY: anchor.y,
240 : rotation: rotation,
241 0 : translateX: offset.x,
242 0 : translateY: offset.y,
243 : );
244 : }
245 :
246 0 : addTransform(source: source, transform: transform, color: color);
247 : }
248 :
249 : /// Clear the SpriteBatch so it can be reused.
250 0 : void clear() {
251 0 : _sources.clear();
252 0 : _transforms.clear();
253 0 : _colors.clear();
254 0 : _batchItems.clear();
255 : }
256 :
257 0 : void render(
258 : Canvas canvas, {
259 : BlendMode? blendMode,
260 : Rect? cullRect,
261 : Paint? paint,
262 : }) {
263 0 : paint ??= Paint();
264 :
265 : if (kIsWeb) {
266 0 : for (final batchItem in _batchItems) {
267 0 : paint..blendMode = blendMode ?? paint.blendMode;
268 :
269 : canvas
270 0 : ..save()
271 0 : ..transform(batchItem.matrix.storage)
272 0 : ..drawRect(batchItem.destination, batchItem.paint)
273 0 : ..drawImageRect(
274 0 : atlas,
275 0 : batchItem.source,
276 0 : batchItem.destination,
277 : paint,
278 : )
279 0 : ..restore();
280 : }
281 : } else {
282 0 : canvas.drawAtlas(
283 0 : atlas,
284 0 : _transforms,
285 0 : _sources,
286 0 : _colors,
287 0 : blendMode ?? defaultBlendMode,
288 : cullRect,
289 : paint,
290 : );
291 : }
292 : }
293 : }
|