Porytiles
Loading...
Searching...
No Matches
tiles_png_workspace.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4
11
12namespace {
13
14using namespace porytiles;
15
30bool tiles_color_equivalent(
31 const PixelTile<IndexPixel> &expected,
32 const PixelTile<IndexPixel> &actual,
34{
35 constexpr Rgba32 extrinsic{};
36 return canonical_color_tile_from_index_tile(expected, palette, extrinsic) ==
37 canonical_color_tile_from_index_tile(actual, palette, extrinsic);
38}
39
51Image<IndexPixel> export_image_range(
52 const TilesPngWorkspace &workspace, std::size_t start_tile, std::size_t end_tile, ExportFlipMode flip_mode)
53{
54 // Standard tiles.png format: 16 tiles per row (128 pixels wide)
55 constexpr std::size_t tiles_per_row = metatile::metatiles_per_row * metatile::tiles_per_side;
56
57 const std::size_t tile_count = end_tile - start_tile;
58
59 // Calculate number of tile rows needed (ceiling division)
60 const std::size_t tiles_per_col = (tile_count + tiles_per_row - 1) / tiles_per_row;
61
62 // Calculate image dimensions
63 const std::size_t image_width = tiles_per_row * tile::side_length_pix;
64 const std::size_t image_height = tiles_per_col * tile::side_length_pix;
65
66 // Create output image
67 Image<IndexPixel> img{image_width, image_height};
68
69 // Copy each tile's pixels into the image
70 for (std::size_t i = 0; i < tile_count; ++i) {
71 const std::size_t tile_idx = start_tile + i;
72
73 // Calculate tile position in the output grid (relative to start_tile)
74 const std::size_t tile_row = i / tiles_per_row;
75 const std::size_t tile_col = i % tiles_per_row;
76
77 // Calculate pixel offsets for this tile
78 const std::size_t pixel_row_offset = tile_row * tile::side_length_pix;
79 const std::size_t pixel_col_offset = tile_col * tile::side_length_pix;
80
81 // Get the tile at this index
82 const auto &canonical_tile = workspace.tile_at(tile_idx);
83
84 // Determine which tile form to use
85 const PixelTile<IndexPixel> &canonical_base = canonical_tile;
86
87 PixelTile<IndexPixel> tile_to_export;
88 if (flip_mode == ExportFlipMode::original) {
89 tile_to_export = canonical_base.flip(canonical_tile.h_flip(), canonical_tile.v_flip());
90 }
91 else {
92 tile_to_export = canonical_base;
93 }
94
95 // Copy all pixels from the tile to the image
96 for (std::size_t pixel_row = 0; pixel_row < tile::side_length_pix; ++pixel_row) {
97 for (std::size_t pixel_col = 0; pixel_col < tile::side_length_pix; ++pixel_col) {
98 const std::size_t dest_row = pixel_row_offset + pixel_row;
99 const std::size_t dest_col = pixel_col_offset + pixel_col;
100 img.set(dest_row, dest_col, tile_to_export.at(pixel_row, pixel_col));
101 }
102 }
103 }
104
105 return img;
106}
107
114std::size_t find_last_non_transparent(const TilesPngWorkspace &workspace, std::size_t scan_start, std::size_t minimum)
115{
116 for (std::size_t i = scan_start; i > minimum; --i) {
117 const std::size_t idx = i - 1;
118 if (!workspace.tile_at(idx).is_transparent()) {
119 return idx;
120 }
121 }
122 return minimum;
123}
124
125} // namespace
126
127namespace porytiles {
128
129void TilesPngWorkspace::advance_cursor_to_next_transparent()
130{
131 cursor_++;
132 while (cursor_ < capacity_ && !tiles_[cursor_].is_transparent()) {
133 cursor_++;
134 }
135}
136
137TilesPngWorkspace::TilesPngWorkspace(std::size_t capacity) : cursor_{1}, capacity_{capacity}
138{
139 // Initialize tiles_ vector with capacity number of transparent tiles
140 // Default-constructed IndexPixel is IndexPixel(0), which is transparent
141 PixelTile<IndexPixel> transparent_pixel_tile;
142 CanonicalPixelTile transparent_tile{transparent_pixel_tile};
143
144 tiles_.resize(capacity, transparent_tile);
145}
146
148 : cursor_{1}, capacity_{capacity}
149{
150 const PlainTextFormatter formatter{};
151
152 // Precondition: image dimensions are multiples of 8
153 if (img.width() % tile::side_length_pix != 0 || img.height() % tile::side_length_pix != 0) {
154 const auto msg = formatter.format(
155 "Image dimensions must be a multiple of {}, got {}x{}", tile::side_length_pix, img.width(), img.height());
156 panic(msg);
157 }
158
159 // Calculate total tiles in image
160 const std::size_t tiles_per_row = img.width() / tile::side_length_pix;
161 const std::size_t tiles_per_col = img.height() / tile::side_length_pix;
162 const std::size_t total_tiles = tiles_per_row * tiles_per_col;
163
164 // Panic if image contains more tiles than capacity
165 if (total_tiles > capacity) {
166 const auto msg = formatter.format("Image contains {} tiles but capacity is only {}", total_tiles, capacity);
167 panic(msg);
168 }
169
170 // Reserve space for tiles
171 tiles_.reserve(capacity);
172
173 // Extract each 8x8 tile from the image
174 for (std::size_t tile_row = 0; tile_row < tiles_per_col; ++tile_row) {
175 for (std::size_t tile_col = 0; tile_col < tiles_per_row; ++tile_col) {
176 // Create a PixelTile to hold the extracted tile data
177 PixelTile<IndexPixel> pixel_tile;
178
179 // Calculate pixel offsets for this tile
180 const std::size_t pixel_row_offset = tile_row * tile::side_length_pix;
181 const std::size_t pixel_col_offset = tile_col * tile::side_length_pix;
182
183 // Copy pixels from source image to tile
184 for (std::size_t pixel_row = 0; pixel_row < tile::side_length_pix; ++pixel_row) {
185 for (std::size_t pixel_col = 0; pixel_col < tile::side_length_pix; ++pixel_col) {
186 const std::size_t src_row = pixel_row_offset + pixel_row;
187 const std::size_t src_col = pixel_col_offset + pixel_col;
188 pixel_tile.set(pixel_row, pixel_col, img.at(src_row, src_col));
189 }
190 }
191
192 // Create canonical version of the tile
193 CanonicalPixelTile canonical_tile{pixel_tile};
194 tiles_.push_back(canonical_tile);
195
196 // Add non-transparent tiles to canonical_forms_ map
197 if (!canonical_tile.is_transparent()) {
198 const std::size_t tile_index = tiles_.size() - 1;
199 const PixelTile<IndexPixel> &base_tile = canonical_tile;
200 canonical_forms_[base_tile].push_back(tile_index);
201 }
202 }
203 }
204
205 // Pad tiles_ vector to capacity with transparent tiles
206 PixelTile<IndexPixel> transparent_pixel_tile;
207 CanonicalPixelTile transparent_tile{transparent_pixel_tile};
208 tiles_.resize(capacity, transparent_tile);
209
210 // Set cursor to first transparent tile after tile 0
211 // Start at 0 and let advance_cursor_to_next_transparent increment to 1 and scan forward
212 cursor_ = 0;
213 advance_cursor_to_next_transparent();
214}
215
217 const Image<IndexPixel> &primary_tiles_png, std::size_t primary_tile_count, std::size_t total_capacity)
218{
219 const PlainTextFormatter formatter{};
220
221 // Precondition: primary_tile_count must be less than total_capacity
222 if (primary_tile_count >= total_capacity) {
223 const auto msg = formatter.format(
224 "primary_tile_count ({}) must be less than total_capacity ({})", primary_tile_count, total_capacity);
225 panic(msg);
226 }
227
228 // Precondition: image dimensions are multiples of 8
229 if (primary_tiles_png.width() % tile::side_length_pix != 0 ||
230 primary_tiles_png.height() % tile::side_length_pix != 0) {
231 const auto msg = formatter.format(
232 "Primary tiles.png dimensions must be a multiple of {}, got {}x{}",
234 primary_tiles_png.width(),
235 primary_tiles_png.height());
236 panic(msg);
237 }
238
239 // Create empty workspace with total_capacity (all transparent, cursor=1)
240 TilesPngWorkspace workspace{total_capacity};
241
242 // Extract tiles from primary image
243 const std::size_t tiles_per_row = primary_tiles_png.width() / tile::side_length_pix;
244 const std::size_t tiles_per_col = primary_tiles_png.height() / tile::side_length_pix;
245 const std::size_t tiles_in_image = tiles_per_row * tiles_per_col;
246 const std::size_t tiles_to_load = std::min(tiles_in_image, primary_tile_count);
247
248 // Load primary tiles into positions 0..tiles_to_load-1
249 for (std::size_t tile_idx = 0; tile_idx < tiles_to_load; ++tile_idx) {
250 const std::size_t tile_row = tile_idx / tiles_per_row;
251 const std::size_t tile_col = tile_idx % tiles_per_row;
252
253 PixelTile<IndexPixel> pixel_tile;
254 const std::size_t pixel_row_offset = tile_row * tile::side_length_pix;
255 const std::size_t pixel_col_offset = tile_col * tile::side_length_pix;
256
257 for (std::size_t pixel_row = 0; pixel_row < tile::side_length_pix; ++pixel_row) {
258 for (std::size_t pixel_col = 0; pixel_col < tile::side_length_pix; ++pixel_col) {
259 const std::size_t src_row = pixel_row_offset + pixel_row;
260 const std::size_t src_col = pixel_col_offset + pixel_col;
261 // Strip true-color encoding (upper nibble = palette index) to get raw color indices. Primary tiles.png
262 // stores pixels as (palette << 4 | color), but index_tile_from_color_tile() produces raw color indices
263 // (0-15). Using color_index() here ensures workspace tiles match what index_tile_from_color_tile()
264 // produces, enabling first_occurrence_of() deduplication.
265 const IndexPixel true_color_pixel = primary_tiles_png.at(src_row, src_col);
266 pixel_tile.set(pixel_row, pixel_col, IndexPixel{true_color_pixel.color_index()});
267 }
268 }
269
270 CanonicalPixelTile canonical_tile{pixel_tile};
271 workspace.tiles_.at(tile_idx) = canonical_tile;
272
273 // Register non-transparent primary tiles for deduplication
274 if (!canonical_tile.is_transparent()) {
275 const PixelTile<IndexPixel> &base_tile = canonical_tile;
276 workspace.canonical_forms_[base_tile].push_back(tile_idx);
277 }
278 }
279
280 // Position primary_tile_count stays transparent (vanilla secondary tile 0 convention)
281 // Set cursor and anim_start_offset_ to reflect the secondary workspace's logical state
282 workspace.cursor_ = primary_tile_count + 1;
283 workspace.anim_start_offset_ = primary_tile_count + 1;
284 workspace.anim_end_offset_ = primary_tile_count + 1;
285
286 return workspace;
287}
288
290TilesPngWorkspace::for_standalone_secondary(std::size_t primary_tile_count, std::size_t total_capacity)
291{
292 const PlainTextFormatter formatter{};
293
294 // Precondition: primary_tile_count must be less than total_capacity
295 if (primary_tile_count >= total_capacity) {
296 const auto msg = formatter.format(
297 "primary_tile_count ({}) must be less than total_capacity ({})", primary_tile_count, total_capacity);
298 panic(msg);
299 }
300
301 // Create empty workspace with total_capacity (all transparent, cursor=1)
302 TilesPngWorkspace workspace{total_capacity};
303
304 // No primary tiles to load or register in canonical_forms_.
305 // Position primary_tile_count stays transparent (vanilla secondary tile 0 convention).
306 workspace.cursor_ = primary_tile_count + 1;
307 workspace.anim_start_offset_ = primary_tile_count + 1;
308 workspace.anim_end_offset_ = primary_tile_count + 1;
309
310 return workspace;
311}
312
314{
315 // Check if we're at capacity
316 if (cursor_ >= capacity_) {
317 panic("TilesPngWorkspace is at capacity");
318 }
319
320 // No point inserting a transparent tile
321 if (tile.is_transparent()) {
322 return 0;
323 }
324
325 // Insert tile at cursor position
326 tiles_[cursor_] = tile;
327 const std::size_t old_cursor = cursor_;
328
329 // Add to canonical_forms_ map
330 const PixelTile<IndexPixel> &base_tile = tile;
331 canonical_forms_[base_tile].push_back(cursor_);
332
333 // Fast-forward cursor to next transparent tile
334 advance_cursor_to_next_transparent();
335
336 return old_cursor;
337}
338
340{
341 const PixelTile<IndexPixel> &base_tile = tile;
342 auto it = canonical_forms_.find(base_tile);
343 if (it != canonical_forms_.end() && !it->second.empty()) {
344 return it->second.front();
345 }
346 return std::nullopt;
347}
348
351{
352 if (tile.is_transparent()) {
353 return std::nullopt;
354 }
355
356 const PixelTile<IndexPixel> &tile_base = tile;
357
358 for (std::size_t i = 1; i < capacity_; ++i) {
359 if (tiles_[i].is_transparent()) {
360 continue;
361 }
362 const PixelTile<IndexPixel> &workspace_tile_base = tiles_[i];
363 if (tiles_color_equivalent(tile_base, workspace_tile_base, palette)) {
364 return i;
365 }
366 }
367 return std::nullopt;
368}
369
371{
372 if (index >= tiles_.size()) {
373 panic("index " + std::to_string(index) + " >= size " + std::to_string(tiles_.size()));
374 }
375 return tiles_.at(index);
376}
377
379{
380 std::size_t end_tile = capacity_;
382 end_tile = find_last_non_transparent(*this, capacity_, 0) + 1;
383 }
384 return export_image_range(*this, 0, end_tile, flip_mode);
385}
386
388 std::size_t primary_tile_count, ExportFlipMode flip_mode, ExportTrimMode trim_mode) const
389{
390 std::size_t end_tile = capacity_;
392 const std::size_t last_non_transparent = find_last_non_transparent(*this, capacity_, primary_tile_count);
393 // At minimum, export the secondary transparent tile at primary_tile_count
394 end_tile = std::max(last_non_transparent, primary_tile_count) + 1;
395 }
396 return export_image_range(*this, primary_tile_count, end_tile, flip_mode);
397}
398
400{
401 return cursor_ == capacity_;
402}
403
404void TilesPngWorkspace::reserve_anim_slots(std::size_t anim_tile_count, std::size_t start_offset)
405{
406 const PlainTextFormatter formatter{};
407
408 // Precondition: cursor must be at start_offset (no regular tiles inserted past that point)
409 if (cursor_ != start_offset) {
410 const auto msg = formatter.format(
411 "reserve_anim_slots: cursor ({}) must be at start_offset ({}) before reserving animation slots",
412 cursor_,
413 start_offset);
414 panic(msg);
415 }
416
417 // Precondition: animation region must fit in the workspace (with room for at least one regular tile)
418 if (start_offset + anim_tile_count >= capacity_) {
419 const auto msg = formatter.format(
420 "start_offset ({}) + anim_tile_count ({}) must be less than capacity ({}).",
421 start_offset,
422 anim_tile_count,
423 capacity_);
424 panic(msg);
425 }
426
427 // Store the animation region boundaries
428 anim_start_offset_ = start_offset;
429 anim_end_offset_ = start_offset + anim_tile_count;
430
431 // Move cursor past the reserved animation region
432 cursor_ = anim_end_offset_;
433}
434
435void TilesPngWorkspace::place_anim_tile(std::size_t reserved_index, const CanonicalPixelTile<IndexPixel> &tile)
436{
437 const PlainTextFormatter formatter{};
438
439 // Precondition: animation slots must have been reserved
440 if (!has_anim_slots()) {
441 panic("place_animation_tile called but no animation slots were reserved");
442 }
443
444 // reserved_index is 0-based within the reserved region
445 const std::size_t absolute_index = reserved_index + anim_start_offset();
446
447 // Precondition: reserved_index must be within the reserved region
448 if (absolute_index >= anim_end_offset_) {
449 const auto msg = formatter.format(
450 "reserved_index ({}) is out of bounds for animation region (max: {})",
451 reserved_index,
452 anim_end_offset_ - anim_start_offset() - 1);
453 panic(msg);
454 }
455
456 // Place the tile at the absolute index
457 tiles_[absolute_index] = tile;
458
459 // Add to canonical_forms_ map for deduplication support (skip transparent tiles)
460 if (!tile.is_transparent()) {
461 const PixelTile<IndexPixel> &base_tile = tile;
462 canonical_forms_[base_tile].push_back(absolute_index);
463 }
464}
465
466std::optional<std::size_t> TilesPngWorkspace::find_contiguous_transparent_slots(std::size_t count) const
467{
468 // Edge case: count of 0 is trivially satisfied at any position
469 if (count == 0) {
470 return 1; // Return first valid position after tile 0
471 }
472
473 // Scan from index 1 (skip reserved tile 0) looking for contiguous transparent runs
474 std::size_t run_start = 0;
475 std::size_t run_length = 0;
476
477 for (std::size_t i = 1; i < capacity_; ++i) {
478 if (tiles_[i].is_transparent()) {
479 if (run_length == 0) {
480 run_start = i;
481 }
482 run_length++;
483
484 if (run_length >= count) {
485 return run_start;
486 }
487 }
488 else {
489 // Non-transparent tile breaks the run
490 run_length = 0;
491 }
492 }
493
494 // No suitable run found
495 return std::nullopt;
496}
497
499 const std::vector<CanonicalPixelTile<IndexPixel>> &tiles,
500 const std::vector<const Palette<Rgba32, palette::max_size> *> &palettes) const
501{
502 // Edge case: empty sequence is trivially found
503 if (tiles.empty()) {
504 return 1; // Return first valid position after tile 0
505 }
506
507 // Precondition: parallel vectors must have same size
508 if (tiles.size() != palettes.size()) {
509 panic("tiles and palettes vectors must have the same size");
510 }
511
512 // Linear scan through workspace looking for contiguous match
513 // Start at index 1 (skip reserved tile 0)
514 for (std::size_t candidate_start = 1; candidate_start < capacity_; ++candidate_start) {
515 // Check if there's enough room for all tiles from this position
516 if (candidate_start + tiles.size() > capacity_) {
517 break; // No more valid starting positions
518 }
519
520 // Verify all tiles in sequence match using color-equivalence
521 bool all_match = true;
522 for (std::size_t offset = 0; offset < tiles.size(); ++offset) {
523 const std::size_t check_index = candidate_start + offset;
524 const PixelTile<IndexPixel> &expected_base = tiles[offset];
525 const PixelTile<IndexPixel> &actual_base = tiles_[check_index];
526 const auto &palette = *palettes[offset];
527
528 if (!tiles_color_equivalent(expected_base, actual_base, palette)) {
529 all_match = false;
530 break;
531 }
532 }
533
534 if (all_match) {
535 return candidate_start;
536 }
537 }
538
539 // No contiguous sequence found
540 return std::nullopt;
541}
542
544 std::size_t start_index, const std::vector<CanonicalPixelTile<IndexPixel>> &tiles)
545{
546 const PlainTextFormatter formatter{};
547
548 // Precondition: tiles must fit in workspace
549 if (start_index + tiles.size() > capacity_) {
550 const auto msg = formatter.format(
551 "place_tiles_at: start_index ({}) + tiles.size() ({}) exceeds capacity ({})",
552 start_index,
553 tiles.size(),
554 capacity_);
555 panic(msg);
556 }
557
558 // Precondition: all target positions must be transparent
559 for (std::size_t offset = 0; offset < tiles.size(); ++offset) {
560 const std::size_t target_index = start_index + offset;
561 if (!tiles_[target_index].is_transparent()) {
562 const auto msg =
563 formatter.format("place_tiles_at: position {} is not transparent, cannot place tile", target_index);
564 panic(msg);
565 }
566 }
567
568 // Place tiles and update canonical_forms_ map
569 for (std::size_t offset = 0; offset < tiles.size(); ++offset) {
570 const std::size_t target_index = start_index + offset;
571 const auto &tile = tiles[offset];
572
573 tiles_[target_index] = tile;
574
575 // Add to canonical_forms_ map (skip transparent tiles, though they shouldn't be in the input)
576 if (!tile.is_transparent()) {
577 const PixelTile<IndexPixel> &base_tile = tile;
578 canonical_forms_[base_tile].push_back(target_index);
579 }
580 }
581
582 // Advance cursor past any newly-filled positions if needed
583 // The cursor should skip over non-transparent tiles
584 while (cursor_ < capacity_ && !tiles_[cursor_].is_transparent()) {
585 cursor_++;
586 }
587}
588
589} // namespace porytiles
A PixelTile representation that stores the canonical (lexicographically minimal) orientation among al...
A template for two-dimensional images with arbitrarily typed pixel values.
Definition image.hpp:21
std::size_t width() const
Definition image.hpp:102
std::size_t height() const
Definition image.hpp:107
PixelType at(std::size_t i) const
Fetches the pixel value at a given one-dimensional pixel index.
Definition image.hpp:50
Represents an indexed color pixel.
std::size_t color_index() const
Returns the color index within a palette (lower 4 bits).
A generic palette container for colors that support transparency checking.
Definition palette.hpp:45
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
bool is_transparent() const
Checks if this entire PixelTile is transparent (intrinsic transparency only).
PixelTile flip(bool h_flip, bool v_flip) const
Creates a flipped copy of this PixelTile.
PixelType at(std::size_t i) const
void set(std::size_t i, const PixelType &p)
TextFormatter implementation that strips all styling from text.
Represents a 32-bit RGBA color.
Definition rgba32.hpp:21
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > &params) const
Formats a string with styled parameters using fmtlib syntax.
A workspace for managing canonical IndexPixel tiles destined for tiles.png output.
bool at_capacity() const
Checks if the workspace has reached capacity and can no longer accept new tile insertions.
std::optional< std::size_t > find_contiguous_transparent_slots(std::size_t count) const
Finds the first contiguous run of transparent tiles that can accommodate the requested count.
Image< IndexPixel > export_secondary_image(std::size_t primary_tile_count, ExportFlipMode flip_mode=ExportFlipMode::canonical, ExportTrimMode trim_mode=ExportTrimMode::trim_trailing_transparent) const
Exports only the secondary portion of the workspace (tiles from primary_tile_count onward).
TilesPngWorkspace(std::size_t capacity)
Constructs a TilesPngWorkspace with a specified capacity, initializing all slots with transparent til...
std::size_t capacity() const
Returns the maximum number of tiles this workspace can hold.
CanonicalPixelTile< IndexPixel > tile_at(std::size_t index) const
Retrieves the canonical tile at the specified index in the workspace.
std::size_t anim_start_offset() const
Returns the starting absolute index for animation tiles.
static TilesPngWorkspace for_secondary(const Image< IndexPixel > &primary_tiles_png, std::size_t primary_tile_count, std::size_t total_capacity)
Creates a workspace pre-loaded with primary tiles for secondary tileset compilation.
std::optional< std::size_t > first_occurrence_of_by_color(const CanonicalPixelTile< IndexPixel > &tile, const Palette< Rgba32, palette::max_size > &palette) const
Finds the first occurrence of a tile using color-equivalence comparison.
void place_tiles_at(std::size_t start_index, const std::vector< CanonicalPixelTile< IndexPixel > > &tiles)
Places tiles at specific positions for patch mode animation placement.
bool has_anim_slots() const
Returns whether animation slots have been reserved.
std::optional< std::size_t > first_occurrence_of(const CanonicalPixelTile< IndexPixel > &tile) const
Finds the first occurrence index of a given canonical tile in the workspace.
std::optional< std::size_t > find_existing_contiguous_tiles_by_color(const std::vector< CanonicalPixelTile< IndexPixel > > &tiles, const std::vector< const Palette< Rgba32, palette::max_size > * > &palettes) const
Checks if a sequence of tiles already exists contiguously using color-equivalence comparison.
static TilesPngWorkspace for_standalone_secondary(std::size_t primary_tile_count, std::size_t total_capacity)
Creates a workspace for standalone secondary compilation with no paired primary.
void place_anim_tile(std::size_t reserved_index, const CanonicalPixelTile< IndexPixel > &tile)
Places an animation keyframe tile at a specific reserved index.
void reserve_anim_slots(std::size_t anim_tile_count, std::size_t start_offset=1)
Reserves contiguous slots for animation keyframe tiles starting at start_offset.
std::size_t insert_tile(const CanonicalPixelTile< IndexPixel > &tile)
Attempts to insert a non-transparent tile into the workspace at the current cursor position.
Image< IndexPixel > export_image(ExportFlipMode flip_mode=ExportFlipMode::canonical, ExportTrimMode trim_mode=ExportTrimMode::trim_trailing_transparent) const
Exports the workspace tiles to an Image<IndexPixel> in tiles.png format.
constexpr std::size_t tiles_per_side
Definition metatile.hpp:21
constexpr std::size_t metatiles_per_row
Definition metatile.hpp:27
constexpr std::size_t side_length_pix
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
Definition panic.cpp:43
ExportTrimMode
Defines how trailing transparent tiles should be handled during image export.
@ trim_trailing_transparent
Trim trailing transparent tiles from the exported image.
ExportFlipMode
Defines whether flip transformations should be applied during image export.
PixelTile< ColorType > canonical_color_tile_from_index_tile(const PixelTile< IndexPixel > &index_tile, const Palette< ColorType, N > &palette, const ColorType &extrinsic)
Converts a PixelTile<IndexPixel> to its canonical color form using a palette (extrinsic transparency)...