diff options
| author | Xander Swan <[hidden email]> | 2026-01-07 23:12:22 -0500 |
|---|---|---|
| committer | Xander Swan <[hidden email]> | 2026-01-07 23:12:22 -0500 |
| commit | 0988ab832bfc7a1b1c851125b6172cf68c6d9cb9 (patch) | |
| tree | 460bc2d9f0bce463af273d6b2b2c20faa880ac29 /tools/compile_assets/aseprite/utils/sprite_sheet.odin | |
| parent | ade0dc4d257d053b7064184f193f8168c496e308 (diff) | |
doesn't compile but i'm commiting anywya
Diffstat (limited to 'tools/compile_assets/aseprite/utils/sprite_sheet.odin')
| -rw-r--r-- | tools/compile_assets/aseprite/utils/sprite_sheet.odin | 382 |
1 files changed, 382 insertions, 0 deletions
diff --git a/tools/compile_assets/aseprite/utils/sprite_sheet.odin b/tools/compile_assets/aseprite/utils/sprite_sheet.odin new file mode 100644 index 0000000..ceb4b76 --- /dev/null +++ b/tools/compile_assets/aseprite/utils/sprite_sheet.odin @@ -0,0 +1,382 @@ +package aseprite_file_handler_utility + +import "base:runtime" +import "core:slice" + +@require import "core:fmt" +@require import "core:log" + +import ase ".." + + +create_sprite_sheet :: proc { + create_sprite_sheet_from_doc, + create_sprite_sheet_from_info, +} + + +/* +Creates internal allocation on `context.temp_allocator`, will attempt to clean up after itself. +*/ +create_sprite_sheet_from_doc :: proc ( + doc: ^ase.Document, s_info: Sprite_Info, + write_rules := DEFAULT_SPRITE_WRITE_RULES, alloc := context.allocator +) -> (res: Sprite_Sheet, err: Errors) { + + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(context.allocator == context.temp_allocator) + info: Info + get_info(doc, &info, context.temp_allocator) or_return + + return create_sprite_sheet_from_info(info, s_info, write_rules, alloc) +} + + +/* +Creates internal allocation on `context.temp_allocator`, will attempt to clean up after itself. +*/ +create_sprite_sheet_from_info :: proc ( + info: Info, s_info: Sprite_Info, + write_rules := DEFAULT_SPRITE_WRITE_RULES, alloc := context.allocator +) -> (res: Sprite_Sheet, err: Errors) { + + switch { + case write_rules.align < min(Sprite_Alignment) || max(Sprite_Alignment) < write_rules.align: + err = .Invalid_Alignment + return + + case s_info.size.x < write_rules.offset.x || s_info.size.y < write_rules.offset.y: + err = .Invalid_Offset + return + + case s_info.spacing.x < 0 || s_info.spacing.y < 0: + err = .Invalid_Spacing + return + + case s_info.boarder.x < 0 || s_info.boarder.y < 0: + err = .Invalid_Boarder + return + + case s_info.count <= 0: + err = .Invalid_Count + return + + case (s_info.size.x * s_info.size.y) < (info.md.width * info.md.height): + if !write_rules.ingore_sprite_size { + err = .Sprite_Size_to_Small + return + } + + // If `shrink_to_pixels` is set the new bounds may fit just fine. + // However it's not reasonable to check them all, instead we just assume they will be. + if !write_rules.shrink_to_pixels { + fast_log(.Warning, "Sprite smaller than Frame. Ingoring & continuing.") + } + } + + // Note(blob): + // Gets the clostest multiple of `s_info.count` that's `>=` to `len(info.frames)`. + // Allows for `len(info.frames)` to not be a multiple of `s_info.count`; + // and still make a valid grid. + frame_count := len(info.frames) + (s_info.count - ((len(info.frames) - 1) %% s_info.count + 1)) + + y_count := max( 1, frame_count / s_info.count ) + width := ( s_info.count * s_info.size.x ) + ( (s_info.count - 1) * s_info.spacing.x ) + height := ( y_count * s_info.size.y ) + ( (y_count - 1) * s_info.spacing.y ) + + + img_width := width + (s_info.boarder.x * 2) + img_height := height + (s_info.boarder.y * 2) + img_size := img_width * img_height * 4 + + res.info = s_info + res.img = { + width = img_width, + height = img_height, + bpp = .RGBA, + data = make([]u8, img_size, alloc) or_return, + } + + defer { + if err != nil { + delete(res.img.data, alloc) + } + } + + + if write_rules.use_index_bg_colour && info.md.bpp == .Indexed && !info.layers[0].is_background { + img_p := slice.reinterpret([]Pixel, res.img.data) + c := info.palette[info.md.trans_idx].color + c.a = 0 + slice.fill(img_p, c) + + } else { + fill_colour(res.img.data, write_rules.background_colour) + } + + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(context.allocator == context.temp_allocator) + tileset_alloc := context.temp_allocator + + sprite_pos: [2]int + sw, sh := s_info.size.x, s_info.size.y + + for frame in info.frames { + defer { + sprite_pos.x += s_info.size.x + s_info.spacing.x + if width <= sprite_pos.x { + sprite_pos.x = 0 + sprite_pos.y += s_info.size.y + s_info.spacing.y + } + + if height + s_info.spacing.y < sprite_pos.y { + panic("Sprite Y Pos is OOB. This shouldn't happen... send help.") + } + } + + if len(frame.cels) == 0 { + continue + } + + if !slice.is_sorted_by(frame.cels, cel_less) { + slice.sort_by(frame.cels, cel_less) + } + + fw, fh := info.md.width, info.md.height + fp: [2]int = 0 + + if write_rules.shrink_to_pixels { + + min_pos := [2]int{fw, fh} + max_pos: [2]int + + for cel in frame.cels { + layer := info.layers[cel.layer] + if !layer.visiable || layer.is_background { continue } + + size := [2]int{cel.width, cel.height} + if cel.tilemap.tiles != nil { + ts := info.tilesets[layer.tileset] + size = {ts.width, ts.height} + } + + min_pos = { + min(min_pos.x, cel.pos.x), + min(min_pos.y, cel.pos.y), + } + max_pos = { + max(max_pos.x, cel.pos.x + size.x), + max(max_pos.y, cel.pos.y + size.y), + } + } + + frame_size := max_pos - min_pos + + fw, fh = frame_size.x, frame_size.y + fp = min_pos + } + + cel_offset := write_rules.offset + s_info.boarder + sprite_pos + + alignment: [2]int + // Sprite Sheet Aligment https://www.desmos.com/geometry/miqzk9ijus + switch write_rules.align { + case .Top_Left: // Default Alignment + case .Top_Center: alignment.x = (sw - fw) / 2 + case .Top_Right: alignment.x = (sw - fw) + + case .Mid_Left: alignment.y = (sh - fh) / 2 + case .Mid_Center: alignment = { (sw - fw) / 2, (sh - fh) / 2 } + case .Mid_Right: alignment = { (sw - fw), (sh - fh) / 2 } + + case .Bot_Left: alignment.y = sh - fh + case .Bot_Center: alignment = { (sw - fw) / 2, sh - fh } + case .Bot_Right: alignment = { (sw - fw), sh - fh } + + case: + err = .Invalid_Alignment + return + } + + + for cel in frame.cels { + layer := info.layers[cel.layer] + skip := (!layer.visiable) || (write_rules.ingore_bg_layers && layer.is_background) + if skip { continue } + + s_cel := cel + if cel.tilemap.tiles != nil { + ts := info.tilesets[layer.tileset] + s_cel = cel_from_tileset(cel, ts, info.md.bpp, tileset_alloc) or_return + } + + if layer.is_background { + // NOTE(blob): + // This isn't truly right & only for really works for solid colours. + // `write_cel` would need to rewrite support both arbitrary reads & writes. + s_cel.pos += cel_offset + s_cel.width = s_info.size.x + s_cel.height = s_info.size.y + write_cel(res.img.data, s_cel, layer, res.img.md, info.palette) or_return + continue + } + + s_cel.pos += alignment + cel_offset - fp + // Make sure we don't pass a negitive position. + s_cel.pos = { max(0, s_cel.pos.x), max(0, s_cel.pos.y) } + + write_cel(res.img.data, s_cel, layer, res.img.md, info.palette) or_return + } + } + + return +} + + +// Finds the smallest Sprite size need to fit all visable pixels. +// Ingores Background Layers +find_min_sprite_size :: proc(info: Info, make_square := true) -> (res: [2]int) { + + for frame in info.frames { + for cel in frame.cels { + layer := info.layers[cel.layer] + if !layer.visiable || layer.is_background { continue } + + size := [2]int{ cel.width, cel.height } + if cel.tilemap.tiles != nil { + ts := info.tilesets[layer.tileset] + size = {ts.width, ts.height} + } + + res.x = max(res.x, size.x) + res.y = max(res.y, size.y) + } + } + + if make_square { + res = max(res.x, res.y) + } + + return +} + + +draw_sheet_grid :: proc(sheet: ^Sprite_Sheet, colour: [4]u8) { + draw_sheet_spacing(sheet, colour, true) +} + + +draw_sheet_spacing :: proc(sheet: ^Sprite_Sheet, colour: [4]u8, always_draw: bool) { + img := sheet.img + info := sheet.info + assert(img.bpp == .RGBA) + + raw := slice.reinterpret([][4]u8, img.data) + + row_count := (img.height - info.size.y - (info.boarder.y * 2)) / ( info.size.y + info.spacing.y ) + if 0 < row_count { + row_block := img.width * info.size.y + row_space := img.width * info.spacing.y + row_step := row_block + row_space + row_fill := always_draw ? max(img.width, row_space) : row_space + + row_offset := row_block + img.width * info.boarder.x + base := raw[row_offset:][:row_fill] + + slice.fill(base, colour) + + for row in 1..<row_count { + start := row_step * row + row_offset + copy(raw[start:], base) + } + } + + col_count := info.count - 1 + if 0 < col_count { + col_block := info.size.x + info.spacing.x + col_fill := always_draw ? max(1, info.spacing.y) : info.spacing.y + col_offset := info.size.x + info.boarder.x + + base := raw[col_offset:][:col_fill] + slice.fill(base, colour) + + for col in 1..<col_count { + pos := col_block * col + col_offset + copy(raw[pos:], base) + } + + for y in 1..<img.height { + start := img.width * y + col_offset + for col in 0..<col_count { + pos := col_block * col + start + copy(raw[pos:], base) + } + } + } + + return +} + + +draw_sheet_boarder :: proc(sheet: ^Sprite_Sheet, colour: [4]u8) { + img := sheet.img + info := sheet.info + assert(img.bpp == .RGBA) + + raw := slice.reinterpret([][4]u8, img.data) + + if 0 < info.boarder.y { + base := raw[:img.width * info.boarder.y] + slice.fill(base, colour) + copy(raw[(img.height - info.boarder.y) * img.width:], base) + } + + if 0 < info.boarder.x { + base_start := img.width * info.boarder.y + base := raw[base_start:][:info.boarder.x] + count := img.height - (info.boarder.y * 2) + + slice.fill(base, colour) + copy(raw[base_start + img.width - info.boarder.x:], base) + + for pos in 0..<count { + start := base_start + (img.width * pos) + copy(raw[start:], base) + copy(raw[start + img.width - info.boarder.x:], base) + } + } + + return +} + + +find_pixel_bounds :: proc(img: Image, bg_colour: [4]u8 = 0, check_trans := true) -> (bounds: Bounds) { + assert(img.md.bpp == .RGBA) + raw := slice.reinterpret([][4]u8, img.data) + + min_pos, max_pos := find_pixel_bounds_min_max(raw, img.width, img.height, bg_colour, check_trans) + + return { pos = min_pos, width = max_pos.x - min_pos.x, height = max_pos.y - min_pos.y } +} + + +find_pixel_bounds_min_max :: proc(img: [][4]u8, width, height: int, bg_colour: [4]u8, check_trans: bool) -> (min_pos, max_pos: [2]int) { + + min_pos = { width, height } + max_pos = { 0, 0 } + + for y in 0..<height { + for x in 0..<width { + pix := img[y * width + x] + if (pix.a == 0 && check_trans) || pix == bg_colour { + continue + } + + min_pos = { min(x, min_pos.x), min(y, min_pos.y) } + max_pos = { max(x, max_pos.x), max(y, max_pos.y) } + } + } + + max_pos += 1 + + return +} + |
