diff options
Diffstat (limited to 'tools/compile_assets/aseprite/utils')
| -rw-r--r-- | tools/compile_assets/aseprite/utils/animation.odin | 181 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/blend.odin | 727 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/common.odin | 303 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/extract.odin | 613 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/image.odin | 443 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/sprite_sheet.odin | 382 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/types.odin | 306 | ||||
| -rw-r--r-- | tools/compile_assets/aseprite/utils/utils_internal.odin | 110 |
8 files changed, 3065 insertions, 0 deletions
diff --git a/tools/compile_assets/aseprite/utils/animation.odin b/tools/compile_assets/aseprite/utils/animation.odin new file mode 100644 index 0000000..7fe5059 --- /dev/null +++ b/tools/compile_assets/aseprite/utils/animation.odin @@ -0,0 +1,181 @@ +package aseprite_file_handler_utility + + +import "core:time" +import "core:slice" + +@(require) import "core:fmt" +@(require) import "core:log" + +import ase ".." + +get_animation_from_doc :: proc( + doc: ^ase.Document, anim: ^Animation, + use_tag := "", alloc := context.allocator +) -> (err: Errors) { + info: Info + get_info(doc, &info, alloc) or_return + defer destroy(&info) + return get_animation_from_info(info, anim, use_tag) +} + + +get_animation_from_info :: proc ( + info: Info, anim: ^Animation, use_tag := "", +) -> (err: Errors) { + context.allocator = info.allocator + + s, f := 0, len(info.frames) + tag: Tag + + if use_tag != "" { + for t in info.tags { + if t.name == use_tag { + if len(info.frames) < t.to || len(info.frames) < t.from { + return Animation_Error.Tag_Index_Out_Of_Bounds + } + tag = t + s = t.from + f = t.to+1 + break + } + } + + if tag == {} { + return Animation_Error.Tag_Not_Found + } + } + + if anim.fps == 0 { + anim.fps = 30 + } + + anim.md = info.md + anim_frames := make([dynamic][]byte) or_return + defer if err != nil { delete(anim_frames) } + + pos: int + + for frame in info.frames[s:f] { + img := get_image_bytes_from_frame(frame, info) or_return + defer delete(img) + + to_add := f64(frame.duration) * f64(anim.fps) / 1000 + for _ in 0..<to_add { + append_elem(&anim_frames, slice.clone(img) or_return) or_return + pos += 1 + } + } + + if tag.direction == .Reverse || tag.direction == .Ping_Pong_Reverse { + slice.reverse(anim_frames[:]) + } + + if tag.direction == .Ping_Pong || tag.direction == .Ping_Pong_Reverse { + rev := slice.clone(anim_frames[:]) + defer delete(rev) + + slice.reverse(rev) + append_elems(&anim_frames, ..rev) or_return + } + + anim.frames = anim_frames[:] + anim.length = time.Second * time.Duration(len(anim_frames) / anim.fps) + return +} + +// Assumes 30 FPS & 100ms Frame Duration +get_animation_from_images :: proc(imgs: []Image, md: Metadata, anim: ^Animation, alloc := context.allocator) -> (err: Errors) { + context.allocator = alloc + anim.fps = 30 + anim.md = md + anim.length = time.Second * time.Duration(len(imgs) / 10) + + frames := make([dynamic][]byte, 0, 3 * len(imgs)) or_return + defer if err != nil { delete(frames) } + + for img in imgs { + frame := slice.clone(img.data) or_return + append_elem(&frames, frame) or_return + + frame = slice.clone(img.data) or_return + append_elem(&frames, frame) or_return + + frame = slice.clone(img.data) or_return + append_elem(&frames, frame) or_return + } + + anim.frames = frames[:] + return +} + +// Assumes 30 FPS & 100ms Frame Duration +get_animation_from_bytes :: proc(imgs: [][]byte, md: Metadata, anim: ^Animation, alloc := context.allocator) -> (err: Errors) { + context.allocator = alloc + anim.fps = 30 + anim.md = md + anim.length = time.Second * time.Duration(len(imgs) / 10) + + frames := make([dynamic][]byte, 0, 3 * len(imgs)) or_return + defer if err != nil { delete(frames) } + + for img in imgs { + append_elem(&frames, slice.clone(img) or_return) or_return + append_elem(&frames, slice.clone(img) or_return) or_return + append_elem(&frames, slice.clone(img) or_return) or_return + } + + anim.frames = frames[:] + return +} + + +get_animation :: proc { + get_animation_from_doc, + get_animation_from_info, + get_animation_from_images, + get_animation_from_bytes, +} + + +// Returns a new image set with Red/Blue Tint or Merge Onion Skin +apply_onion_skin :: proc( + imgs: []Image, opacity := 69, merge := false, + alloc := context.allocator +) -> (frames: []Image, err: Errors) { + context.allocator = alloc + + frames = make([]Image, len(imgs)) or_return + defer if err != nil { delete(frames) } + + blank := make([]byte, len(imgs[0].data)) or_return + defer delete(blank) + + cur, next: []byte + last := blank + defer if cur != nil { delete(cur) } + + for img, pos in imgs { + if pos < len(imgs)-1 { + next = imgs[pos+1].data + } else { + next = blank + } + + cur = slice.clone(img.data) or_return + if merge { + blend_bytes(last, cur, opacity, .Merge) or_return + blend_bytes(next, cur, opacity, .Merge) or_return + } else { + blend_bytes(last, cur, opacity, .Red_Tint) or_return + blend_bytes(next, cur, opacity, .Blue_Tint) or_return + } + + + frames[pos] = img + frames[pos].data = cur + last = cur + } + + return +} diff --git a/tools/compile_assets/aseprite/utils/blend.odin b/tools/compile_assets/aseprite/utils/blend.odin new file mode 100644 index 0000000..9bf0f7e --- /dev/null +++ b/tools/compile_assets/aseprite/utils/blend.odin @@ -0,0 +1,727 @@ +package aseprite_file_handler_utility + +import "core:math" + +@(require) import "core:fmt" +@(require) import "core:log" + + +ASE_USE_BUGGED_SAT :: #config(ASE_USE_BUGGED_SAT, false) + + +// https://printtechnologies.org/standards/files/pdf-reference-1.6-addendum-blend-modes.pdf + +@(private) +slow_alpha :: proc(a: int, b: ..int) -> (res: int) { + // α = α1 * α2 *..αn / 255^(n-1) + if len(b) == 0 { return a } + if len(b) == 1 { return a * b[0] / 255} + res = a + d := 255 + for i in b { + res *= i + d *= 255 + } + return res / d +} + + +// Modifies current Image (`cur`) +blend_images :: proc(last, cur: Image, opacity: int, mode: Blend_Mode) -> (err: Blend_Error) { + if len(last.data) != len(cur.data) { + return Blend_Error.Unequal_Image_Sizes + } + return blend_bytes(last.data, cur.data, opacity, mode) +} + +// Modifies current Image (`cur`) +blend_bytes :: proc(last, cur: []byte, opacity: int, mode: Blend_Mode) -> (err: Blend_Error) { + if len(last) != len(cur) { + return Blend_Error.Unequal_Image_Sizes + } + + for pix in 0..<len(cur)/4 { + pos := pix * 4 + l_pix, c_pix: [4]u8 + + copy(l_pix[:], last[pos:pos+4]) + copy(c_pix[:], cur[pos:pos+4]) + + r_pix := blend(l_pix, c_pix, i32(opacity), mode) or_return + + copy(cur[pos:pos+4], r_pix[:]) + } + return +} + + +alpha :: mul +mul :: proc{mul_u8, mul_i32, mul_int, mul_vec4, mul_vec3} + +mul_vec4 :: proc(a, b: [4]i32) -> [4]i32 { + t := a * b + 128 + return { + ((t.r >> 8) + t.r) >> 8, + ((t.g >> 8) + t.g) >> 8, + ((t.b >> 8) + t.b) >> 8, + ((t.a >> 8) + t.a) >> 8, + } +} + +mul_vec3 :: proc(a, b: [3]i32) -> [3]i32 { + t := a * b + 128 + return { + ((t.r >> 8) + t.r) >> 8, + ((t.g >> 8) + t.g) >> 8, + ((t.b >> 8) + t.b) >> 8, + } +} + + +mul_i32 :: proc(a, b: i32) -> i32 { + // License `.\3rd party licenses\libpixman license` + // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67 + + t := a * b + 128 + return ((t >> 8 ) + t ) >> 8 +} + +mul_int :: proc(a, b: int) -> i32 { + // License `.\3rd party licenses\libpixman license` + // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67 + + t := a * b + 128 + return i32(((t >> 8 ) + t ) >> 8) +} + +mul_u8 :: proc(a, b: byte) -> i32 { + // License `.\3rd party licenses\libpixman license` + // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67 + + t := a * b + 128 + return i32(((t >> 8 ) + t ) >> 8) +} + + +div :: proc( #any_int a, b: u16) -> u16 { + // License `.\3rd party licenses\libpixman license` + // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L70 + return (a * 255 + (b / 2)) / b +} + +blend :: proc(last, cur: Pixel, opacity: i32, mode: Blend_Mode) -> (res: Pixel, err: Blend_Error) { + // https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp + if last.a == 0 { + res.rgb = cur.rgb + res.a = byte(mul(i32(cur.a), opacity)) + return + } + + back: [4]i32 = {i32(last.r), i32(last.g), i32(last.b), i32(last.a)} + pix: [4]i32 = {i32(cur.r), i32(cur.g), i32(cur.b), i32(cur.a)} + + blen: [4]i32 + switch mode { + case .Src: blen = blend_src(back, pix, opacity) + case .Merge: blen = blend_merge(back, pix, opacity) + case .Neg_BW: blen = blend_neg_bw(back, pix, opacity) + case .Red_Tint: blen = blend_red_tint(back, pix, opacity) + case .Blue_Tint: blen = blend_blue_tint(back, pix, opacity) + case .Dst_Over: blen = blend_normal_dst_over(back, pix, opacity) + case .Addition: blen = blend_addition(back, pix, opacity) + case .Color: blen = blend_color(back, pix, opacity) + case .Color_Burn: blen = blend_color_burn(back, pix, opacity) + case .Color_Dodge: blen = blend_color_dodge(back, pix, opacity) + case .Darken: blen = blend_darken(back, pix, opacity) + case .Difference: blen = blend_difference(back, pix, opacity) + case .Divide: blen = blend_divide(back, pix, opacity) + case .Exclusion: blen = blend_exclusion(back, pix, opacity) + case .Hard_Light: blen = blend_hard_light(back, pix, opacity) + case .Hue: blen = blend_hue(back, pix, opacity) + case .Lighten: blen = blend_lighten(back, pix, opacity) + case .Luminosity: blen = blend_luminosity(back, pix, opacity) + case .Multiply: blen = blend_multiply(back, pix, opacity) + case .Normal: blen = blend_normal(back, pix, opacity) + case .Overlay: blen = blend_overlay(back, pix, opacity) + case .Saturation: blen = blend_saturation(back, pix, opacity) + case .Screen: blen = blend_screen(back, pix, opacity) + case .Soft_Light: blen = blend_soft_light(back, pix, opacity) + case .Subtract: blen = blend_subtract(back, pix, opacity) + case .Unspecified: fallthrough + case: + log.error("Invalid Bland Mode provided:", mode) + return last, .Invalid_Mode + } + + normal := blend_normal(back, pix, opacity) + norm_merge := blend_merge(normal, blen, back.a) + blen = blend_merge(norm_merge, blen, alpha(back.a, alpha(pix.a, opacity))) + + return {u8(blen.r), u8(blen.g), u8(blen.b), u8(blen.a)}, nil +} + + +/* ------------------------------------------------------------------- */ +// RGB Blenders +blend_normal :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + if last.a == 0 { + res = cur + res.a = alpha(cur.a, opacity) + } else if cur.a == 0 { + return last + } + + cur := cur + cur.a = alpha(cur.a, opacity) + + res.a = cur.a + last.a - alpha(last.a, cur.a) + res.rgb = last.rgb + (cur.rgb - last.rgb) * cur.a / res.a + + return res +} + +blend_src :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + return last +} + +blend_merge :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + + if last.a == 0 { + res.rgb = cur.rgb + + } else if cur.a == 0 { + res.rgb = last.rgb + + } else { + op: [4]i32 = opacity + res = last + mul((cur - last), op) + } + + res.a = last.a + mul((cur.a - last.a), opacity) + if res.a == 0 { + res.rgb = 0 + } + + return +} + +blend_neg_bw :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + if rgba_luma(last) < 128{ + return { 255, 255, 255, 255 } + } + return { 0, 0, 0, 255 } +} + +blend_red_tint :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + cur := cur + luma := rgba_luma(cur) + cur = { (255 + luma) >> 1, luma >> 1, luma >> 1, last.a } + return blend_normal(last, cur, opacity) +} + +blend_blue_tint :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + luma := rgba_luma(cur) + res = { luma >> 1, luma >> 1, (255 + luma) >> 1, last.a } + return blend_normal(last, res, opacity) +} + +blend_normal_dst_over :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res.a = alpha(cur.a, opacity) + return blend_normal(last, res, opacity) +} + + +blend_multiply :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res = mul(last, cur) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_screen :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res = last + cur - mul(last, cur) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_overlay :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + hl :: proc(b, s: i32) -> i32 { + if s < 128 { + return mul(b, s<<1) + } + return b + ((s<<1)-255) - mul(b, (s<<1)-255) + } + + res.r = hl(cur.r, last.r) + res.b = hl(cur.b, last.b) + res.g = hl(cur.g, last.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_darken :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res.r = min(last.r, cur.r) + res.b = min(last.b, cur.b) + res.g = min(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_lighten :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res.r = max(last.r, cur.r) + res.b = max(last.b, cur.b) + res.g = max(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_color_dodge :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + cd :: proc( #any_int b, s: u32) -> i32 { + if b == 0 { + return 0 + } + s1 := 255 - s + if b >= s1 { + return 255 + } + return i32(div(b, s1)) + } + + res.r = cd(last.r, cur.r) + res.b = cd(last.b, cur.b) + res.g = cd(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_color_burn :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + cb :: proc( #any_int b, s: u32) -> i32 { + if b == 255 { + return 255 + } + b1 := 255 - b + if b1 >= s { + return 0 + } + return i32(255 - div(b1, s)) + } + + res.r = cb(last.r, cur.r) + res.b = cb(last.b, cur.b) + res.g = cb(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_hard_light :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + hl :: proc(b, s: i32) -> i32 { + if s < 128 { + return mul(b, s<<1) + } + return b + ((s<<1)-255) - mul(b, (s<<1)-255) + } + + res.r = hl(last.r, cur.r) + res.b = hl(last.b, cur.b) + res.g = hl(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_soft_light :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + sl :: proc(b, s: i32) -> i32 { + b := f64(b) / 255 + s := f64(s) / 255 + r, d: f64 + + if b <= 0.25 { + d = ((16 * b - 12) * b + 4) * b + } else { + d = math.sqrt(b) + } + + if s <= 0.5 { + r = b - (1 - 2 * s) * b * (1 - b) + } else { + r = b + (2 * s - 1) * (d - b) + } + return i32(r * 255 + 0.5) + } + + res.r = sl(last.r, cur.r) + res.b = sl(last.b, cur.b) + res.g = sl(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_difference :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res.r = abs(last.r - cur.r) + res.b = abs(last.b - cur.b) + res.g = abs(last.g - cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_exclusion :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + ex :: proc(b, s: i32) -> i32 { + return b + s - (mul(b, s) << 1) + } + + res.r = ex(last.r, cur.r) + res.b = ex(last.b, cur.b) + res.g = ex(last.g, cur.g) + res.a = cur.a + return blend_normal(last, res, opacity) +} + + + +/* ------------------------------------------------------------------- */ +// HSV Blenders +blend_hue :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + /* + https://github.com/alpine-alpaca/asefile/blob/main/src/blend.rs#L392 + https://drafts.fxtf.org/compositing-1/#blendinghue + https://printtechnologies.org/standards/files/pdf-reference-1.6-addendum-blend-modes.pdf + https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp#L425 + https://gitlab.freedesktop.org/pixman/pixman/-/blob/master/pixman/pixman-combine-float.c?ref_type=heads#L908 + */ + + lpix := [3]f64{f64(last.r), f64(last.g), f64(last.b)} / 255 + sat := hsv_sat(lpix) + lum := hsv_luma(lpix) + + cpix := [3]f64{f64(cur.r), f64(cur.g), f64(cur.b)} / 255 + cpix = set_sat(cpix, sat) + cpix = set_luma(cpix, lum) + cpix *= 255 + + return blend_normal(last, {i32(cpix.r), i32(cpix.g), i32(cpix.b), cur.a}, opacity) +} + + +blend_saturation :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + pix := [3]f64{ f64(cur.r), f64(cur.g), f64(cur.b) } + pix /= 255 + s := hsv_sat(pix) + + pix = { f64(last.r), f64(last.g), f64(last.b) } + pix /= 255 + l := hsv_luma(pix) + + pix = set_luma(set_sat(pix, s), l) + + pix *= 255 + res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a } + + return blend_normal(last, res, opacity) +} + + +blend_color :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + pix := [3]f64{ f64(last.r), f64(last.g), f64(last.b) } + pix /= 255 + l := hsv_luma(pix) + + pix = { f64(cur.r), f64(cur.g), f64(cur.b)} + pix /= 255 + pix = set_luma(pix, l) + + pix *= 255 + res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a } + + return blend_normal(last, res, opacity) +} + + +blend_luminosity :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + pix := [3]f64{ f64(cur.r), f64(cur.g), f64(cur.b) } + pix /= 255 + l := hsv_luma(pix) + + pix = { f64(last.r), f64(last.g), f64(last.b) } + pix /= 255 + pix = set_luma(pix, l) + + pix *= 255 + res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a } + + return blend_normal(last, res, opacity) +} + + +blend_addition :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res.r = min(last.r + cur.r, 255) + res.g = min(last.g + cur.g, 255) + res.b = min(last.b + cur.b, 255) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_subtract :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + res.r = max(last.r - cur.r, 0) + res.g = max(last.g - cur.g, 0) + res.b = max(last.b - cur.b, 0) + res.a = cur.a + return blend_normal(last, res, opacity) +} + +blend_divide :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) { + bd :: proc( #any_int b, s: u32) -> i32 { + if b == 0 { + return 0 + } else if b >= s { + return 255 + } + return i32(div(b, s)) + } + + res.r = bd( last.r, cur.r ) + res.g = bd( last.g, cur.g ) + res.b = bd( last.b, cur.b ) + res.a = cur.a + return blend_normal(last, res, opacity) +} + + + +/* ------------------------------------------------------------------- */ +// RGB Helpers + +rgba_luma :: proc(pix: B_Pixel) -> i32 { + return rgb_luma(pix.r, pix.b, pix.g) +} + +rgb_luma :: proc(#any_int r, b, g: int) -> i32 { + return i32(( r*2126 + g*7152 + b*722 ) / 10000 ) +} + +/* ------------------------------------------------------------------- */ +// HSV Helpers + +hsv_luma :: proc(p: [3]f64) -> f64 { + return 0.3*p.r + 0.59*p.g + 0.11*p.b +} + +hsv_sat :: proc(p: [3]f64) -> f64 { + return max(p.r, p.g, p.b) - min(p.r, p.g, p.b) +} + +clip_color :: proc(pix: [3]f64) -> [3]f64 { + p := pix + lum := hsv_luma(p) + n := min(p.r, p.g, p.b) + x := max(p.r, p.g, p.b) + + if n < 0 { + p = lum + (((p - lum) * lum) / (lum - n)) + } + + if x > 1 { + p = lum + (((p - lum) * (1 - lum)) / (x - lum)) + } + + return p +} + +set_luma :: proc(p: [3]f64, l: f64) -> [3]f64 { + d := l - hsv_luma(p) + return clip_color(p + d) +} + + +set_sat :: proc(p: [3]f64, s: f64) -> (res: [3]f64) { + // https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp#L400 + // The chosen solution is a mix a asefile & pixman + // https://github.com/alpine-alpaca/asefile/blob/main/src/blend.rs#L522 + // https://gitlab.freedesktop.org/pixman/pixman/-/blob/master/pixman/pixman-combine-float.c#L831 + + + when ASE_USE_BUGGED_SAT == true { + res = p + MIN :: proc(x, y: ^f64) -> ^f64 { + return (x^ < y^) ? x : y + } + MAX :: proc(x, y: ^f64) -> ^f64 { + return (x^ > y^) ? x : y + } + + r, g, b := &res.r, &res.g, &res.b + min := MIN(r, MIN(g, b)) + max := MAX(r, MAX(g, b)) + mid := r > g ? (g > b ? g : (r > b ? b : r)) : (g > b ? (b > r ? b : r): g) + + if max > min { + mid^ = ((mid^ - min^) * s) / (max^ - min^) + max^ = s + } else { + mid^ = 0 + max^ = 0 + } + min^ = 0 + + } else { + val: [3]struct{v: f64, p: int} = { + {p.r, 0}, {p.g, 1}, {p.b, 2}, + } + val.rg = val.r.v < val.g.v ? val.rg : val.gr + val.rb = val.r.v < val.b.v ? val.rb : val.br + val.gb = val.g.v < val.b.v ? val.gb : val.bg + min, mid, max := val.r.p, val.g.p, val.b.p + + if (p[max] - p[min]) != 0 { + res[mid] = ( (p[mid] - p[min]) * s ) / ( p[max] - p[min] ) + res[max] = s + } + } + + return +} + + +/* + The following is for mine (blob) record keeping: + + The way aseprite does set_set is very much bugged. + First the sorting doesn't work when if r == g and g < b + + MIN :: proc(x, y: ^f64) -> ^f64 { + return (x^ < y^) ? x : y + } + MAX :: proc(x, y: ^f64) -> ^f64 { + return (y^ < x^) ? x : y + } + + r, g, b := &pix.r, &pix.g, &pix.b + min := MIN(r, MIN(g, b)) + max := MAX(r, MAX(g, b)) + mid := r > g ? (g > b ? g : (r > b ? b : r)) : (g > b ? (b > r ? b : r): g) + + Second the way it's value calculations is also wrong & only leaves the blue channel + if max > min { + mid^ = ((mid^ - min^) * s) / (max^ - min^) + max^ = s + } else { + mid^ = 0 + max^ = 0 + } + + The sullotion griven by asefile + // r --*--*----- min + // | | + // g --*--|--*-- mid + // | | + // b -----*--*-- max + + res = p + val := [3][2]f64{{p.r, 0}, {p.g, 1}, {p.b, 2}} + + val.rg = val.r[0] < val.g[0] ? val.rg : val.gr + val.rb = val.r[0] < val.b[0] ? val.rb : val.br + val.gb = val.g[0] < val.b[0] ? val.gb : val.bg + min, mid, max := int(val.r[1]), int(val.g[1]), int(val.b[1]) + + + if max > min { + res[mid] = ((res[mid] - res[min]) * s) / (res[max] - res[min]) + res[max] = s + } else { + res[mid], res[max] = 0, 0 + } + res[min] = 0 + + Pointer based version og working sullotion from pixman + res = p + min, mid, max: ^f64 + if p.r > p.g { + if p.r > p.b { + max = &res.r + if p.g > p.b { + mid = &res.g + min = &res.b + + } else { + mid = &res.b + min = &res.g + } + + } else { + max = &res.b + mid = &res.r + min = &res.g + } + + } else { + if p.r > p.b { + max = &res.g + mid = &res.r + min = &res.b + + } else { + min = &res.r + if p.g > p.b { + max = &res.g + mid = &res.b + + } else { + max = &res.b + mid = &res.g + } + } + } + + + if (max^ - min^) == 0 { + mid^ = 0 + max^ = 0 + } else { + mid^ = ((mid^ - min^) * s) / (max^ - min^) + max^ = s + } + min^ = 0 + + + Pointerless pixman + min, mid, max: int + if p.r > p.g { + if p.r > p.b { + max = 0 + if p.g > p.b { + mid = 1 + min = 2 + + } else { + mid = 2 + min = 1 + } + + } else { + max = 2 + mid = 0 + min = 1 + } + + } else { + if p.r > p.b { + max = 1 + mid = 0 + min = 2 + + } else { + min = 0 + if p.g > p.b { + max = 1 + mid = 2 + + } else { + max = 2 + mid = 1 + } + } + } + + if (p[max] - p[min]) != 0 { + res[mid] = ((p[mid] - p[min]) * s) / (p[max] - p[min]) + res[max] = s + } +*/ diff --git a/tools/compile_assets/aseprite/utils/common.odin b/tools/compile_assets/aseprite/utils/common.odin new file mode 100644 index 0000000..69d7c71 --- /dev/null +++ b/tools/compile_assets/aseprite/utils/common.odin @@ -0,0 +1,303 @@ +package aseprite_file_handler_utility + +import "base:runtime" +import "core:mem" +import "core:slice" +import "core:image" + +@(require) import "core:fmt" +@(require) import "core:log" + +import ase ".." + + +UTILS_DEBUG_MODE :: #config(ASE_UTILS_DEBUG, ODIN_DEBUG) + + +/* ================================== Destruction procs ================================== */ +destroy_frame :: proc(frame: Frame, alloc := context.allocator) -> runtime.Allocator_Error { + for &cel in frame.cels { + destroy(&cel, alloc) or_return + } + return delete(frame.cels, alloc) +} + +destroy_frames :: proc(frames: []Frame, alloc := context.allocator) -> runtime.Allocator_Error { + for frame in frames { + for &cel in frame.cels { + destroy(&cel, alloc) or_return + } + delete(frame.cels, alloc) or_return + } + return delete(frames, alloc) +} + +destroy_image :: proc(img: Image, alloc := context.allocator) -> runtime.Allocator_Error { + return delete(img.data, alloc) +} + +destroy_images :: proc(imgs: []Image, alloc := context.allocator) -> runtime.Allocator_Error { + for img in imgs { + delete(img.data, alloc) or_return + } + return delete(imgs, alloc) +} + +destroy_image_bytes :: proc(imgs: [][]byte, alloc := context.allocator) -> runtime.Allocator_Error { + for img in imgs { + delete(img, alloc) or_return + } + return delete(imgs, alloc) +} + +destroy_animation :: proc(anim: ^Animation, alloc := context.allocator) -> runtime.Allocator_Error { + for frame in anim.frames { + delete(frame, alloc) or_return + } + return delete(anim.frames, alloc) +} + +destroy_info :: proc(info: ^Info) -> runtime.Allocator_Error { + context.allocator = info.allocator + destroy_frames(info.frames) or_return + delete(info.layers) or_return + delete(info.palette) or_return + delete(info.tags) or_return + delete(info.tilesets) or_return + destroy_slices(info.slices) or_return + return nil +} + +destroy_cel :: proc(cel: ^Cel, alloc := context.allocator) -> runtime.Allocator_Error { + return delete(cel.tilemap.tiles, alloc) +} + +destroy_layers :: proc(lay: []Layer, alloc := context.allocator) -> runtime.Allocator_Error { + return delete(lay, alloc) +} + +destroy_palette :: proc(pal: Palette, alloc := context.allocator) -> runtime.Allocator_Error { + return delete(pal) +} + +destroy_slices :: proc(sls: []Slice, alloc := context.allocator) -> runtime.Allocator_Error { + for sl in sls { + destroy_slice(sl, alloc) or_return + } + return delete(sls) +} + +destroy_slice :: proc(sl: Slice, alloc := context.allocator) -> runtime.Allocator_Error { + return delete(sl.keys) +} + +destroy_sheet :: proc(s: Sprite_Sheet, alloc := context.allocator) -> runtime.Allocator_Error { + return destroy_image(s.img, alloc) +} + +destroy :: proc { + destroy_frames, + destroy_image, + destroy_animation, + destroy_image_bytes, + destroy_images, + destroy_info, + destroy_cel, + destroy_frame, + destroy_layers, + destroy_palette, + destroy_slices, + destroy_slice, + destroy_sheet, +} +/* ======================================================================================= */ + + +get_metadata :: proc(header: ase.File_Header) -> (md: Metadata) { + return { + int(header.width), + int(header.height), + Pixel_Depth(header.color_depth), + u8(header.transparent_index), + } +} + + +// Use with slice.sort_by, .sort_by_with_indices, .stable_sort_by, .is_sorted_by & .reverse_sort_by +cel_less :: proc(i, j: Cel) -> bool { + // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5 + ior := i.layer + i.z_index + jor := j.layer + j.z_index + return ior < jor || (ior == jor && i.z_index < j.z_index ) +} + +// Use with slice.sort_by_cmp, .stable_sort_by_cmp, .is_sorted_cmp & .reverse_sort_by_cmp +cel_cmp :: proc(i, j: Cel) -> slice.Ordering { + // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5 + iz := i.z_index + jz := j.z_index + ior := i.layer + iz + jor := j.layer + jz + + if ior < jor { return .Less } + else if ior > jor { return .Greater } + else if iz < jz { return .Less } + else if iz > jz { return .Greater } + return .Equal +} + + +has_new_palette :: proc(doc: ^ase.Document) -> bool { + for f in doc.frames { + for c in f.chunks { + _ = c.(ase.Palette_Chunk) or_continue + return true + } + } + return false +} + + +has_tileset :: proc(doc: ^ase.Document) -> bool { + for f in doc.frames { + for c in f.chunks { + _ = c.(ase.Tileset_Chunk) or_continue + return true + } + } + return false +} + + +// Uses Nearest-neighbor Upscaling +upscale_image_from_bytes :: proc(img: []byte, md: Metadata, factor := 10, alloc := context.allocator) -> (res: []byte, res_md: Metadata, err: Errors) { + ch := int(md.bpp) >> 3 + if len(img) != (md.width * md.height * ch) { + fast_log(.Error, "image size doesn't match metadata") + err = .Buffer_Size_Not_Match_Metadata + return + } + + res = make([]byte, len(img) * factor * factor, alloc) or_return + res_md = {md.width*factor, md.height*factor, md.bpp, md.trans_idx} + + for h in 0..<md.height { + for w in 0..<md.width { + start := (h*md.width*factor + w) * factor * ch + first := res[start:start + factor * ch] + + copy(first[:ch], img[(h*md.width + w) * ch:]) + + for x in 1..<factor { + copy(res[start + x * ch:][:ch], first) + } + + for y in 1..<factor { + copy(res[start + (y*md.width*factor*ch):], first) + } + } + } + + return +} + +// Uses Nearest-neighbor Upscaling +upscale_image_from_img :: proc(img: Image, factor := 10, alloc := context.allocator) -> (res: Image, err: Errors) { + res.data, res.md = upscale_image_from_bytes(img.data, img.md, factor, alloc) or_return + return +} + +// Uses Nearest-neighbor Upscaling +upscale_image :: proc { upscale_image_from_img, upscale_image_from_bytes } + + +upscale_all_from_imgs :: proc(imgs: []Image, factor := 10, alloc := context.allocator) -> (res: []Image, err: Errors) { + res = make([]Image, len(imgs), alloc) or_return + for img, pos in imgs { + res[pos] = upscale_image_from_img(img, factor, alloc) or_return + } + return +} + +upscale_all_from_byte:: proc(imgs: [][]byte, md: Metadata, factor := 10, alloc := context.allocator) -> (res: [][]byte, res_md: Metadata, err: Errors) { + res = make([][]byte, len(imgs), alloc) or_return + if len(imgs) == 0 { return } + res[0], res_md = upscale_image_from_bytes(imgs[0], md, factor, alloc) or_return + + for img, pos in imgs[1:] { + res[pos], _ = upscale_image_from_bytes(img, md, factor, alloc) or_return + } + return +} + +upscale_all :: proc{ upscale_all_from_imgs, upscale_all_from_byte } + + +compute_alpha :: proc(img: []u8, alloc := context.allocator) -> (res: []u8, err: Errors) { + if len(img) % 4 != 0 { + fast_log(.Error, "Given buffer isn't RGBA") + return nil, .Buffer_Not_RGBA + } + + img_buf := mem.slice_data_cast([][4]u8, img) + buf := make([][3]u8, len(img_buf), alloc) or_return + + for p, pos in img_buf { + bp := [4]i32{ i32(p.r), i32(p.g), i32(p.b), i32(p.a) } + bp.rgb = bp.rgb * bp.a / (255 + bp.a - alpha(bp.a, 255)) + buf[pos] = { u8(bp.r), u8(bp.g), u8(bp.b) } + } + + return mem.slice_data_cast([]u8, buf), nil +} + + +remove_alpha :: proc(img: []u8, alloc := context.allocator) -> (res: []u8, err: Errors) { + if len(img) % 4 != 0 { + fast_log(.Error, "Given buffer isn't RGBA") + return nil, .Buffer_Not_RGBA + } + + img_buf := mem.slice_data_cast([][4]u8, img) + buf := make([][3]u8, len(img_buf), alloc) or_return + + for p, pos in img_buf { + buf[pos] = p.rgb + } + + return mem.slice_data_cast([]u8, buf), nil +} + + +// Converts `utils.Image` to a `core:image.Image` with allocation +to_core_image :: proc(buf: []byte, md: Metadata, alloc := context.allocator) -> (img: image.Image, err: runtime.Allocator_Error) { + img.width = md.width + img.height = md.height + img.depth = 8 + img.channels = 4 + img.pixels.buf = make([dynamic]byte, len(buf), alloc) or_return + + copy(img.pixels.buf[:], buf) + return +} + +// Converts `utils.Image` to a `core:image.Image` with no allocation +to_core_image_non_alloc :: proc(buf: []byte, md: Metadata) -> (img: image.Image) { + img.width = md.width + img.height = md.height + img.depth = 8 + img.channels = 4 + + raw := runtime.Raw_Dynamic_Array { + data = raw_data(buf), + len = len(buf), + cap = len(buf), + allocator = runtime.nil_allocator(), + } + + img.pixels.buf = transmute([dynamic]byte)raw + return +} + + + diff --git a/tools/compile_assets/aseprite/utils/extract.odin b/tools/compile_assets/aseprite/utils/extract.odin new file mode 100644 index 0000000..f1b009d --- /dev/null +++ b/tools/compile_assets/aseprite/utils/extract.odin @@ -0,0 +1,613 @@ +package aseprite_file_handler_utility + +import "base:runtime" +import "core:math/fixed" + +@(require) import "core:fmt" +@(require) import "core:log" + +import ase ".." + + +cels_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Cel, err: runtime.Allocator_Error) { + cels := make([dynamic]Cel, alloc) or_return + defer if err != nil { delete(cels) } + + for frame in doc.frames { + f_cels := get_cels(frame, alloc) or_return + for &c in f_cels { + if c.raw == nil && c.tilemap.tiles == nil { + for l in cels[c.link:] { + if l.layer == c.layer { + c.height = l.height + c.width = l.width + c.raw = l.raw + } + } + } + } + + append(&cels, ..f_cels) or_return + delete(f_cels, alloc) or_return + } + + return cels[:], nil +} + +cels_from_doc_frame :: proc(frame: ase.Frame, alloc := context.allocator) -> (res: []Cel, err: runtime.Allocator_Error) { + cels := make([dynamic]Cel, alloc) or_return + defer if err != nil { delete(cels) } + + for chunk in frame.chunks { + #partial switch c in chunk { + case ase.Cel_Chunk: + cel := Cel { + pos = { int(c.x), int(c.y) }, + opacity = int(c.opacity_level), + z_index = int(c.z_index), + layer = int(c.layer_index), + } + + switch v in c.cel { + case ase.Com_Image_Cel: + cel.width = int(v.width) + cel.height = int(v.height) + cel.raw = v.pixels + + case ase.Raw_Cel: + cel.width = int(v.width) + cel.height = int(v.height) + cel.raw = v.pixels + + case ase.Linked_Cel: + cel.link = int(v) + + case ase.Com_Tilemap_Cel: + + cel.tilemap = Tilemap { + width = int(v.width), + height = int(v.height), + x_flip = uint(v.bitmask_x), // Bitmask for X flip + y_flip = uint(v.bitmask_y), // Bitmask for Y flip + diag_flip = uint(v.bitmask_diagonal), // Bitmask for diagonal flip (swap X/Y axis) + tiles = make([]int, len(v.tiles), alloc) or_return, + } + + for &n, p in cel.tilemap.tiles { + switch t in v.tiles[p] { + case ase.BYTE: n = int(t) + case ase.WORD: n = int(t) + case ase.DWORD: n = int(t) + } + } + + } + append(&cels, cel) or_return + + case ase.Cel_Extra_Chunk: + if ase.Cel_Extra_Flag.Precise in c.flags { + extra := Precise_Bounds { + fixed.to_f64(c.x), fixed.to_f64(c.y), + fixed.to_f64(c.width), fixed.to_f64(c.height), + } + cels[len(cels)-1].extra = extra + } + } + } + + return cels[:], nil +} + +get_cels :: proc{ cels_from_doc_frame, cels_from_doc } + + +layers_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Layer, err: runtime.Allocator_Error) { + layers := make([dynamic]Layer, alloc) or_return + defer if err != nil { delete(layers) } + + for frame in doc.frames { + f_lays := get_layers(frame, .Layer_Opacity in doc.header.flags) or_return + append(&layers, ..f_lays) or_return + delete(f_lays, alloc) or_return + } + + return layers[:], nil +} + +layers_from_doc_frame :: proc(frame: ase.Frame, layer_valid_opacity := false, alloc := context.allocator) -> (res: []Layer, err: runtime.Allocator_Error) { + layers := make([dynamic]Layer, alloc) or_return + defer if err != nil { delete(layers) } + + all_lays := make([dynamic]^ase.Layer_Chunk) or_return + defer delete(all_lays) + + for chunk in frame.chunks { + #partial switch &v in chunk { + case ase.Layer_Chunk: + lay := Layer { + name = v.name, + opacity = int(v.opacity) if layer_valid_opacity else 255, + index = len(layers), + blend_mode = Blend_Mode(v.blend_mode), + visiable = .Visiable in v.flags, + tileset = int(v.tileset_index), + is_background = .Background in v.flags, + } + + #reverse for l in all_lays { + if l.type == .Group { + if .Visiable not_in l.flags { + lay.visiable = false + break + } + if l.child_level == 0 { + break + } + } + } + + append(&all_lays, &v) or_return + append(&layers, lay) or_return + } + } + + return layers[:], nil +} + +get_layers :: proc{ layers_from_doc_frame, layers_from_doc } + + +tags_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Tag, err: runtime.Allocator_Error) { + tags := make([dynamic]Tag, alloc) + defer if err != nil { delete(tags) } + + for frame in doc.frames { + f_tags := get_tags(frame, alloc) or_return + append(&tags, ..f_tags) or_return + delete(f_tags, alloc) or_return + } + + return tags[:], nil +} + +tags_from_doc_frame :: proc(frame: ase.Frame, alloc := context.allocator) -> (res: []Tag, err: runtime.Allocator_Error) { + tags := make([dynamic]Tag, alloc) or_return + defer if err != nil { delete(tags) } + + for chunk in frame.chunks { + #partial switch v in chunk { + case ase.Tags_Chunk: + for t in v { + tag := Tag { + from = int(t.from_frame), + to = int(t.to_frame), + direction = t.loop_direction, + name = t.name, + } + append(&tags, tag) or_return + } + } + } + + return tags[:], nil +} + +get_tags :: proc{ tags_from_doc_frame, tags_from_doc } + + +frames_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (frames: []Frame, err: runtime.Allocator_Error) { + return get_frames(doc.frames, alloc) +} + +frames_from_doc_frames :: proc(data: []ase.Frame, alloc := context.allocator) -> (frames: []Frame, err: runtime.Allocator_Error) { + res := make([dynamic]Frame, alloc) or_return + defer if err != nil { delete(res) } + + for frame in data { + append(&res, get_frame(frame) or_return) or_return + } + return +} + +get_frames :: proc { + frames_from_doc, + frames_from_doc_frames, +} + +get_frame :: proc(data: ase.Frame, alloc := context.allocator) -> (frame: Frame, err: runtime.Allocator_Error) { + frame.duration = i64(data.header.duration) + frame.cels = get_cels(data, alloc) or_return + return +} + + +palette_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (palette: Palette, err: Errors) { + pal := make([dynamic]Color, alloc) or_return + defer if err != nil { delete(pal) } + + for frame in doc.frames { + get_palette(frame, &pal, has_new_palette(doc)) or_return + } + + return pal[:], nil +} + +palette_from_doc_frame:: proc(frame: ase.Frame, pal: ^[dynamic]Color, has_new: bool) -> (err: Errors) { + for chunk in frame.chunks { + #partial switch c in chunk { + case ase.Palette_Chunk: + if int(c.last_index) >= len(pal) { + resize(pal, int(c.last_index)+1) or_return + } + + for i in c.first_index..=c.last_index { + if int(i) > len(pal) { + return Palette_Error.Color_Index_Out_of_Bounds + } + + if n, ok := c.entries[i].name.(string); ok { + pal[i].name = n + } + pal[i].color = c.entries[i].color + + } + + case ase.Old_Palette_256_Chunk: + if has_new { continue } + for p in c { + first := len(pal) + int(p.entries_to_skip) + last := first + len(p.colors) + if last >= len(pal) { + resize(pal, last) or_return + } + + for i in first..<last { + if i >= len(pal) { + return Palette_Error.Color_Index_Out_of_Bounds + } + pal[i].color.rgb = p.colors[i] + if p.colors[i] != 0 { + pal[i].color.a = 255 + } + + } + } + + case ase.Old_Palette_64_Chunk: + if has_new { continue } + for p in c { + first := len(pal) + int(p.entries_to_skip) + last := first + len(p.colors) + if last >= len(pal) { + resize(pal, last) or_return + } + + for i in first..<last { + if i >= len(pal) { + return Palette_Error.Color_Index_Out_of_Bounds + } + + pal[i].color.rgb = p.colors[i] + if p.colors[i] != 0 { + pal[i].color.a = 255 + } + } + } + } + } + + return +} + +get_palette :: proc{ palette_from_doc, palette_from_doc_frame } + + +tileset_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (ts: []Tileset, err: runtime.Allocator_Error) { + buf := make([dynamic]Tileset, alloc) or_return + for frame in doc.frames { + err = get_tileset(frame, &buf, alloc) + if err != nil { + return buf[:], err + } + } + if len(buf) > 0 { + log.warn("Tilemaps & Tilesets currently only work for RGBA colour space.") + } + return buf[:], nil +} + +tileset_from_doc_frame :: proc(frame: ase.Frame, buf: ^[dynamic]Tileset, alloc := context.allocator) -> (err: runtime.Allocator_Error) { + for chunk in frame.chunks { + #partial switch v in chunk { + case ase.Tileset_Chunk: + ts: Tileset = { + int(v.id), + int(v.width), + int(v.height), + int(v.num_of_tiles), + v.name, + nil, + } + + if t, ok := v.compressed.(ase.Tileset_Compressed); ok { + ts.tiles = (Pixels)(t) + } + + append(buf, ts) or_return + + case ase.User_Data_Chunk: + } + } + + return +} + +get_tileset :: proc{ tileset_from_doc, tileset_from_doc_frame } + + +get_info :: proc(doc: ^ase.Document, info: ^Info, alloc := context.allocator) -> (err: Errors) { + context.allocator = alloc + info.allocator = alloc + + layer_valid_opacity := .Layer_Opacity in doc.header.flags + has_new := has_new_palette(doc) + + frames := make([dynamic]Frame) or_return + lays := make([dynamic]Layer) or_return + tags := make([dynamic]Tag) or_return + all_ts := make([dynamic]Tileset) or_return + pal := make([dynamic]Color) or_return + sls := make([dynamic]Slice) or_return + md := get_metadata(doc.header) + + all_lays := make([dynamic]^ase.Layer_Chunk) or_return + defer delete(all_lays) + + hue_sat_warn: bool + + // TODO: Make big assumption that only Cel Chunks appear after first frame. + + for doc_frame in doc.frames { + frame: Frame + frame.duration = i64(doc_frame.header.duration) + cels := make([dynamic]Cel) or_return + + for &chunk in doc_frame.chunks { + + #partial switch &c in chunk { + case ase.Cel_Chunk: + cel := Cel { + pos = {int(c.x), int(c.y)}, + opacity = int(c.opacity_level), + z_index = int(c.z_index), + layer = int(c.layer_index), + } + + switch v in c.cel { + case ase.Com_Image_Cel: + cel.width = int(v.width) + cel.height = int(v.height) + cel.raw = v.pixels + + case ase.Raw_Cel: + cel.width = int(v.width) + cel.height = int(v.height) + cel.raw = v.pixels + + case ase.Linked_Cel: + for l in frames[v].cels { + if l.layer == cel.layer { + cel.height = l.height + cel.width = l.width + cel.raw = l.raw + cel.link = int(v) + } + } + + case ase.Com_Tilemap_Cel: + cel.tilemap = Tilemap { + width = int(v.width), + height = int(v.height), + x_flip = uint(v.bitmask_x), // Bitmask for X flip + y_flip = uint(v.bitmask_y), // Bitmask for Y flip + diag_flip = uint(v.bitmask_diagonal), // Bitmask for diagonal flip (swap X/Y axis) + tiles = make([]int, len(v.tiles), alloc) or_return, + } + + for &n, p in cel.tilemap.tiles { + switch t in v.tiles[p] { + case ase.BYTE: n = int(t) + case ase.WORD: n = int(t) + case ase.DWORD: n = int(t) + } + } + } + + append(&cels, cel) or_return + + case ase.Cel_Extra_Chunk: + if ase.Cel_Extra_Flag.Precise in c.flags { + extra := Precise_Bounds { + fixed.to_f64(c.x), fixed.to_f64(c.y), + fixed.to_f64(c.width), fixed.to_f64(c.height), + } + cels[len(cels)-1].extra = extra + } + + case ase.Layer_Chunk: + lay := Layer { + name = c.name, + opacity = int(c.opacity) if layer_valid_opacity else 255, + index = len(lays), + blend_mode = Blend_Mode(c.blend_mode), + visiable = .Visiable in c.flags, + tileset = int(c.tileset_index), + is_background = .Background in c.flags, + } + + when !ASE_USE_BUGGED_SAT { + if !hue_sat_warn && (lay.blend_mode == .Saturation || lay.blend_mode == .Hue) { + log.infof("Layer: \"%v\"; \"%v\" blend mode is bugged in Aseprite, in ways we can't replicate.", lay.name, lay.blend_mode) + log.info("By default we use a fixed version. Compile with `ASE_USE_BUGGED_SAT=true` to use a bugged version.") + hue_sat_warn = true + } + } + + + if c.child_level != 0 { + #reverse for l in all_lays { + if l.type == .Group { + if .Visiable not_in l.flags { + lay.visiable = false + break + } + if l.child_level == 0 { + break + } + } + } + } + + append(&lays, lay) or_return + append(&all_lays, &c) or_return + + case ase.Tags_Chunk: + for t in c { + tag := Tag { + int(t.from_frame), + int(t.to_frame), + t.loop_direction, + t.name, + } + append(&tags, tag) or_return + } + + case ase.Palette_Chunk: + if int(c.last_index) >= len(pal) { + resize(&pal, int(c.last_index)+1) or_return + } + + for i in c.first_index..=c.last_index { + if int(i) >= len(pal) { + err = Palette_Error.Color_Index_Out_of_Bounds + return + } + + if n, ok := c.entries[i].name.(string); ok { + pal[i].name = n + } + pal[i].color = c.entries[i].color + } + + case ase.Old_Palette_256_Chunk: + if has_new { continue } + for p in c { + first := len(pal) + int(p.entries_to_skip) + last := first + len(p.colors) + if last >= len(pal) { + resize(&pal, last) or_return + } + + for i in first..<last { + if i >= len(pal) { + err = Palette_Error.Color_Index_Out_of_Bounds + return + } + pal[i].color.rgb = p.colors[i] + if p.colors[i] != 0 { + pal[i].color.a = 255 + } + } + } + + case ase.Old_Palette_64_Chunk: + if has_new { continue } + for p in c { + first := len(pal) + int(p.entries_to_skip) + last := first + len(p.colors) + if last >= len(pal) { + resize(&pal, last) or_return + } + + for i in first..<last { + if i >= len(pal) { + err = Palette_Error.Color_Index_Out_of_Bounds + return + } + if max(p.colors[i].r, p.colors[i].b, p.colors[i].g) > 63 { + err = Palette_Error.Color_Index_Out_of_Bounds + return + } + + // https://github.com/alpine-alpaca/asefile/blob/2274c354cea6764f85597252a0d2228e64709348/src/palette.rs#L134 + // Scale such that 0 -> 0 & 63 -> 255 + pal[i].color.r = p.colors[i].r << 2 | p.colors[i].r >> 4 + pal[i].color.g = p.colors[i].g << 2 | p.colors[i].g >> 4 + pal[i].color.b = p.colors[i].b << 2 | p.colors[i].b >> 4 + if p.colors[i] != 0 { + pal[i].color.a = 255 + } + } + } + + case ase.Tileset_Chunk: + ts: Tileset + ts.id = int(c.id) + ts.width = int(c.width) + ts.height = int(c.height) + ts.name = c.name + + if t, ok := c.compressed.(ase.Tileset_Compressed); ok { + ts.tiles = (Pixels)(t) + } + + append(&all_ts, ts) or_return + + case ase.Slice_Chunk: + sl: Slice + sl.name = c.name + sl.flags = c.flags + sl.keys = make([]Slice_Key, len(c.keys)) + + for &key, pos in sl.keys { + c_key := c.keys[pos] + key.frame = int(c_key.frame_num) + + key.x = int(c_key.x) + key.y = int(c_key.y) + key.w = int(c_key.width) + key.h = int(c_key.height) + + if center, ok := c_key.center.(ase.Slice_Center); ok { + key.center = { + int(center.x), int(center.y), + int(center.width), int(center.height), + } + } + + if pivot, ok := c_key.pivot.(ase.Slice_Pivot); ok { + key.pivot = { int(pivot.x), int(pivot.y) } + } + } + append(&sls, sl) + } + } + + frame.cels = cels[:] + append(&frames, frame) or_return + } + + info^ = { + frames = frames[:], + layers = lays[:], + tags = tags[:], + tilesets = all_ts[:], + slices = sls[:], + palette = pal[:], + md = md, + allocator = alloc, + } + + return nil +} + diff --git a/tools/compile_assets/aseprite/utils/image.odin b/tools/compile_assets/aseprite/utils/image.odin new file mode 100644 index 0000000..ded33b7 --- /dev/null +++ b/tools/compile_assets/aseprite/utils/image.odin @@ -0,0 +1,443 @@ +package aseprite_file_handler_utility + +import "base:runtime" +import ir "base:intrinsics" + +import "core:slice" +import "core:mem" + +@(require) import "core:fmt" +@(require) import "core:log" + +import ase ".." + + +// Only Uses the first frame +get_image_from_doc :: proc(doc: ^ase.Document, frame := 0, alloc := context.allocator) -> (img: Image, err: Errors) { + context.allocator = alloc + + if len(doc.frames) <= frame { + return {}, Image_Error.Frame_Index_Out_Of_Bounds + } + + info: Info + get_info(doc, &info) or_return + defer destroy(&info) + + return get_image_from_frame(info.frames[frame], info) +} + +get_image_from_doc_frame :: proc( + frame: ase.Frame, info: Info, +) -> (img: Image, err: Errors) { + + raw_frame := get_frame(frame, info.allocator) or_return + defer delete(raw_frame.cels, info.allocator) + + return get_image_from_frame(raw_frame, info) +} + +get_image_from_cel :: proc( + cel: Cel, layer: Layer, info: Info, +) -> (img: Image, err: Errors) { + img.width = cel.width + img.height = cel.height + img.bpp = .RGBA + + if cel.tilemap.tiles != nil { + ts := info.tilesets[layer.tileset] + c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return + defer delete(c.raw) + + img.data = make([]byte, c.width * c.height * 4, info.allocator) or_return + if !layer.is_background && info.md.bpp == .Indexed { + img_p := mem.slice_data_cast([]Pixel, img.data) + c := info.palette[info.md.trans_idx].color + c.a = 0 + slice.fill(img_p, c) + } + write_cel(img.data[:], c, layer, info.md, info.palette) or_return + + } else { + img.data = make([]byte, cel.width * cel.height * 4, info.allocator) or_return + if !layer.is_background && info.md.bpp == .Indexed { + img_p := mem.slice_data_cast([]Pixel, img.data) + c := info.palette[info.md.trans_idx].color + c.a = 0 + slice.fill(img_p, c) + } + write_cel(img.data[:], cel, layer, info.md, info.palette) or_return + } + + return +} + +get_image_from_frame :: proc( + frame: Frame, info: Info, +) -> (img: Image, err: Errors) { + + img.md = info.md + img.bpp = .RGBA + img.data = get_image_bytes(frame, info) or_return + return +} + +get_image :: proc { + get_image_from_doc, + get_image_from_doc_frame, + get_image_from_frame, + get_image_from_cel, +} + + +// Only Uses the First Frame +get_image_bytes_from_doc :: proc(doc: ^ase.Document, frame := 0, alloc := context.allocator) -> (img: []byte, err: Errors) { + context.allocator = alloc + md := get_metadata(doc.header) + + raw_frame := get_frame(doc.frames[frame]) or_return + defer delete(raw_frame.cels) + + layers := get_layers(doc) or_return + defer delete(layers) + + palette := get_palette(doc) or_return + defer delete(palette) + + ts := get_tileset(doc) or_return + defer delete(ts) + + info := Info{layers=layers, palette=palette, tilesets=ts, allocator=alloc, md=md} + + return get_image_bytes(raw_frame, info) +} + +get_image_bytes_from_doc_frame :: proc( + frame: ase.Frame, info: Info, +) -> (img: []byte, err: Errors) { + + raw_frame := get_frame(frame, info.allocator) or_return + defer destroy(raw_frame, info.allocator) + return get_image_bytes(raw_frame, info) +} + +get_image_bytes_from_cel :: proc ( + cel: Cel, layer: Layer, info: Info, +) -> (img: []byte, err: Errors) { + if cel.tilemap.tiles != nil { + ts := info.tilesets[layer.tileset] + c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return + defer delete(c.raw) + + img = make([]byte, c.width * c.height * 4, info.allocator) or_return + if !layer.is_background && info.md.bpp == .Indexed { + img_p := mem.slice_data_cast([]Pixel, img) + c := info.palette[info.md.trans_idx].color + c.a = 0 + slice.fill(img_p, c) + } + write_cel(img, c, layer, info.md, info.palette) or_return + + } else { + img = make([]byte, cel.width * cel.height * 4, info.allocator) or_return + if !layer.is_background && info.md.bpp == .Indexed { + img_p := mem.slice_data_cast([]Pixel, img) + c := info.palette[info.md.trans_idx].color + c.a = 0 + slice.fill(img_p, c) + } + write_cel(img, cel, layer, info.md, info.palette) or_return + } + + return +} + +get_image_bytes_from_frame :: proc( + frame: Frame, info: Info, +) -> (img: []byte, err: Errors) { + context.allocator = info.allocator + + img = make([]byte, info.md.width * info.md.height * 4) or_return + if len(frame.cels) == 0 { return } + + if !slice.is_sorted_by(frame.cels, cel_less) { + slice.sort_by(frame.cels, cel_less) + } + + if !info.layers[0].is_background && info.md.bpp == .Indexed { + img_p := mem.slice_data_cast([]Pixel, img) + c := info.palette[info.md.trans_idx].color + c.a = 0 + slice.fill(img_p, c) + } + + for cel in frame.cels { + lay := info.layers[cel.layer] + if !lay.visiable { continue } + + if cel.tilemap.tiles != nil { + ts := info.tilesets[lay.tileset] + c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return + defer delete(c.raw) + + write_cel(img, c, lay, info.md, info.palette) or_return + + } else { + write_cel(img, cel, lay, info.md, info.palette) or_return + } + } + + return +} + +get_image_bytes :: proc { + get_image_bytes_from_doc, + get_image_bytes_from_doc_frame, + get_image_bytes_from_cel, + get_image_bytes_from_frame, +} + + +get_all_images :: proc(doc: ^ase.Document, alloc := context.allocator) -> (imgs: []Image, err: Errors) { + context.allocator = alloc + imgs = make([]Image, len(doc.frames)) or_return + defer if err != nil { destroy(imgs)} + + info: Info + get_info(doc, &info) or_return + defer destroy(&info) + + for frame, p in info.frames { + imgs[p] = get_image(frame, info) or_return + } + + return +} + +get_all_images_bytes :: proc(doc: ^ase.Document, alloc := context.allocator) -> (imgs: [][]byte, err: Errors) { + context.allocator = alloc + imgs = make([][]byte, len(doc.frames)) or_return + defer if err != nil { destroy(imgs) } + + md := get_metadata(doc.header) + + layers := get_layers(doc) or_return + defer delete(layers) + + palette := get_palette(doc) or_return + defer delete(palette) + + ts := get_tileset(doc) or_return + defer delete(ts) + + info := Info{layers=layers, palette=palette, tilesets=ts, allocator=alloc, md=md} + + for frame, p in doc.frames { + imgs[p] = get_image_bytes(frame, info) or_return + } + + return +} + + +get_cels_as_imgs :: proc(doc: ^ase.Document, frame_idx := 0, alloc := context.allocator) -> (res: []Image, err: Errors) { + context.allocator = alloc + + info: Info + get_info(doc, &info) or_return + defer destroy(&info) + + if len(info.frames) < frame_idx { + return nil, .Frame_Index_Out_Of_Bounds + } + + frame := info.frames[frame_idx] + + res = make([]Image, len(frame.cels)) or_return + if len(frame.cels) == 0 { return } + + if !slice.is_sorted_by(frame.cels, cel_less) { + slice.sort_by(frame.cels, cel_less) + } + + for cel, i in frame.cels { + lay := info.layers[cel.layer] + if !lay.visiable { continue } + + res[i] = get_image_from_cel(cel, lay, info) or_return + } + + return +} + + +get_all_cels_as_imgs :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Image, err: Errors) { + context.allocator = alloc + + info: Info + get_info(doc, &info) or_return + defer destroy(&info) + + imgs := make([dynamic]Image) or_return + + for frame in info.frames { + if len(frame.cels) == 0 { continue } + + if !slice.is_sorted_by(frame.cels, cel_less) { + slice.sort_by(frame.cels, cel_less) + } + + for cel in frame.cels { + lay := info.layers[cel.layer] + if !lay.visiable { continue } + + img := get_image_from_cel(cel, lay, info) or_return + append(&imgs, img) + } + } + + return imgs[:], nil +} + +cel_from_tileset :: proc(cel: Cel, ts: Tileset, chans: Pixel_Depth, alloc: runtime.Allocator) -> (c: Cel, err: Errors) { + c = cel + c.width = cel.tilemap.width * ts.width + c.height = cel.tilemap.height * ts.height + ch := int(chans)/8 + + if (ts.height * ts.width * len(c.tilemap.tiles) * ch) != (c.width * c.height * ch) { + return {}, .Tileset_Cel_Sizes_Mismatch + } + + c.raw = make([]byte, ts.height * ts.width * len(c.tilemap.tiles) * ch, alloc) or_return + + for h in 0..<c.tilemap.height { + for w in 0..<c.tilemap.width { + s := (h * c.tilemap.width * ts.height + w) * ts.width * ch + s1 := c.tilemap.tiles[h * c.tilemap.width + w] * ts.width * ts.height * ch + + for y in 0..<ts.height { + copy(c.raw[s+(y*ts.width*c.tilemap.width*ch):][:ch*ts.width], ts.tiles[s1+(y*ts.width*ch):]) + } + } + } + + return +} + +// Write a cel to an image's data. Assumes tilemaps & linked cels have already been handled. +write_cel :: proc ( + buf: []byte, cel: Cel, layer: Layer, md: Metadata, + pal: Palette = nil, +) -> (err: Errors) { + // TODO: Allow for both arbitrary reads & writes, i.e. cropping. + + if len(cel.raw) <= 0 { + fast_log(.Debug, "No Cel data to write.") + return + } + + if len(buf) < (md.height * md.width * 4) { + fast_log(.Error, "Image buffer size is smaller than Metadata.") + return .Buffer_To_Small + } + + when UTILS_DEBUG_MODE { + switch md.bpp { + case .Indexed: + if pal == nil { + fast_log(.Error, "Indexed Color Mode. No Palette") + return .Indexed_BPP_No_Palette + } + fallthrough + case .Grayscale, .RGBA: + if len(cel.raw) % (int(md.bpp) / 8) != 0 { + fast_log(.Error, "Size of cel not a multipule of channels. ") + return .Cel_Size_Not_Of_BPP + } + case: + fast_log(.Error, "Invalid Color Mode: ", md.bpp) + return .Invalid_BPP + } + } + + /*offset := [2]int { + abs(cel.x) if cel.x < 0 else 0, + abs(cel.y) if cel.y < 0 else 0, + }*/ + offset := [2]int { + abs(min(0, cel.x)), + abs(min(0, cel.y)), + } + + bounds := Bounds { + x = clamp(cel.x, 0, md.width), + y = clamp(cel.y, 0, md.height), + width = clamp(cel.width, 0, md.width), + height = clamp(cel.height, 0, md.height), + } + + when UTILS_DEBUG_MODE { + if !( bounds.x <= md.width && bounds.y <= md.height \ + && bounds.width <= md.width && bounds.height <= md.height \ + && offset.x <= md.width && offset.y <= md.height \ + && bounds.x >= 0 && bounds.y >= 0 \ + && bounds.width >= 0 && bounds.height >= 0 \ + && offset.x >= 0 && offset.y >= 0 ) { + fast_log(.Error, "Cel out of bounds of Image bounds.") + return .Cel_Out_Of_Bounds, + } + } + + y_loop: for y in 0..<bounds.height { + for x in 0..<bounds.width { + pix: [4]byte + idx := (y + offset.y) * cel.width + x + offset.x + + // Convert to RGBA + switch md.bpp { + case .Indexed: + if cel.raw[idx] == md.trans_idx { + continue + } + pix = pal[cel.raw[idx]].color + + case .Grayscale: + pix.rgb = cel.raw[idx * 2] + pix.a = cel.raw[idx * 2 + 1] + + case .RGBA: + // Note(blob): + // This is comparable to slice casting before & idxing in to that. + // `mem.slice_data_cast()` or `slice.reinterpret()` + // On average is slightly faster but more variable in optimized builds + pix = ir.unaligned_load((^[4]byte)(&cel.raw[idx * 4])) + } + + if pix.a != 0 { + iidx := ((y + bounds.y) * md.width + x + bounds.x) * 4 + if len(buf) <= iidx { + break y_loop + } + + ipix := (^[4]byte)(&buf[iidx]) + + if ipix.a != 0 { + // Blend pixels) + p := ir.unaligned_load(ipix) + a := alpha(cel.opacity, layer.opacity) + pix = blend(p, pix, a, layer.blend_mode) or_return + + } else { + // Merge Alpha & Opacities + pix.a = u8(alpha(i32(pix.a), alpha(cel.opacity, layer.opacity))) + } + + ir.unaligned_store(ipix, pix) + } + } + } + + return +} + 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 +} + diff --git a/tools/compile_assets/aseprite/utils/types.odin b/tools/compile_assets/aseprite/utils/types.odin new file mode 100644 index 0000000..bf2cd0c --- /dev/null +++ b/tools/compile_assets/aseprite/utils/types.odin @@ -0,0 +1,306 @@ +package aseprite_file_handler_utility + +import "base:runtime" +import "core:time" + +import ase ".." + + +// Errors +Palette_Error :: enum { + None, + Color_Index_Out_of_Bounds, +} +Blend_Error :: enum { + None, + Invalid_Mode, + Unequal_Image_Sizes, +} +Image_Error :: enum { + None, + Frame_Index_Out_Of_Bounds, + Indexed_BPP_No_Palette, + Invalid_BPP, + Cel_Out_Of_Bounds, + Cel_Size_Not_Of_BPP, + Buffer_To_Small, + Buffer_Size_Not_Match_Metadata, + Buffer_Not_RGBA, +} +Animation_Error :: enum { + None, + Tag_Not_Found, + Tag_Index_Out_Of_Bounds, +} + +Tileset_Error :: enum { + None, + Tileset_Cel_Sizes_Mismatch, +} + +Sprite_Sheet_Error :: enum { + None, + Sprite_Size_to_Small, + Invalid_Alignment, + Invalid_Offset, + Invalid_Count, + Invalid_Spacing, + Invalid_Boarder, +} + +Errors :: union #shared_nil { + runtime.Allocator_Error, + Image_Error, + Animation_Error, + Tileset_Error, + Blend_Error, + Palette_Error, + Sprite_Sheet_Error, +} + +// Raw Types +B_Pixel :: [4]i32 +Pixel :: [4]byte +Pixels :: []byte + +Vec2 :: [2]int + +// ASE Document types + +Precise_Bounds :: struct { + // Truely fixpoint but I anit dealing with that shit + x, y, width, height: f64, +} + +Bounds :: struct { + using pos: Vec2, + width, height: int, +} + +Cel :: struct { + using bounds: Bounds, + opacity: int, + link: int, + layer: int, + z_index: int, // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5 + raw: Pixels `fmt:"-"`, + tilemap: Tilemap, + extra: Maybe(Precise_Bounds), +} + +Tilemap :: struct { + width: int, + height: int, + x_flip: uint, + y_flip: uint, + diag_flip: uint, + tiles: []int, +} + +Layer :: struct { + name: string, + opacity: int, + visiable: bool, + is_background: bool, + index: int, + blend_mode: Blend_Mode, + tileset: int, +} + +Frame :: struct { + duration: i64, // in milliseconds + cels: []Cel, +} + +Tag :: struct { + from: int, + to: int, + direction: ase.Tag_Loop_Dir, + name: string, +} + +Palette :: []Color + +Color :: struct { + using color: Pixel, + name: string, +} + +// Bits per pixel +Pixel_Depth :: enum { + Indexed = 8, + Grayscale = 16, + RGBA = 32, +} + +// Not needed RN. We'll only ever handle sRGB. +Color_Space :: enum { + None, + sRGB, + ICC, +} + +Metadata :: struct { + width: int, + height: int, + bpp: Pixel_Depth, + trans_idx: u8, +} + +Slice_Key :: struct { + frame: int, + x, y: int, + w, h: int, + center: [4]int, + pivot: [2]int, +} + +Slice :: struct { + flags: ase.Slice_Flags, + name: string, + keys: []Slice_Key, +} + +Tileset :: struct { + id: int, + width: int, + height: int, + num: int, + name: string, + tiles: Pixels, +} + + +Info :: struct { + frames: []Frame, + layers: []Layer, + tags: []Tag, + tilesets: []Tileset, + slices: []Slice, + palette: Palette, + + md: Metadata, + allocator: runtime.Allocator, +} + + + +// Sprite Sheet +Sprite_Info :: struct { + size: [2]int, // Size of a sprite. + spacing: [2]int, // Spacing between each sprite. + boarder: [2]int, // Boarder between sheet & image edge. + count: int, // Sprites per row. +} + +// Govern's how Frames are writen a Sprite +Sprite_Write_Rules :: struct { + + // What point on the Frame & Sprite to align. + // Effective when Frame.size < Sprite.size. + align: Sprite_Alignment, + + // Offset from the alignment point. + offset: [2]int, + + // Resulting Sheet's Background Colour + background_colour: [4]u8, + + // Shrinks Frame to the bounds of its visable pixels. + // NOTE(blob): + // Using this may slighly change the positioning. + // This is expected & intended behaviour. + shrink_to_pixels: bool, + + // Whether to use the Background Colour + // of the ase file or use `background_colour`; + // Only used for `Indexed` colour mode. + use_index_bg_colour: bool, + + // Whether to ignore Background layers. + ingore_bg_layers: bool, + + // Whether to ingore the Sprite size being + // smaller then the Frame frame. + ingore_sprite_size: bool, +} + +// Sprite Sheet Alignment https://www.desmos.com/geometry/miqzk9ijus +Sprite_Alignment :: enum { + Top_Left, Top_Center, Top_Right, + Mid_Left, Mid_Center, Mid_Right, + Bot_Left, Bot_Center, Bot_Right, + + // Default Alignment + Base = Top_Left, + + // Some helpers... cause why not. + Top = Top_Center, + Middle = Mid_Center, + Bottom = Bot_Center, + Left = Mid_Left, + Right = Mid_Right, +} + + +// Precomputed Types. They own all their data. +Image :: struct { + using md: Metadata, + data: Pixels `fmt:"-"`, +} + +Animation :: struct { + using md: Metadata, + fps: int, + length: time.Duration, + frames: []Pixels, +} + +Sprite_Sheet :: struct { + using img: Image, + info: Sprite_Info, +} + + + +Blend_Mode :: enum { + Unspecified = -1, + Src = -2, + Merge = -3, + Neg_BW = -4, + Red_Tint = -5, + Blue_Tint = -6, + Dst_Over = -7, + + Normal = 00, + Multiply = 01, + Screen = 02, + Overlay = 03, + Darken = 04, + Lighten = 05, + Color_Dodge = 06, + Color_Burn = 07, + Hard_Light = 08, + Soft_Light = 09, + Difference = 10, + Exclusion = 11, + Hue = 12, + Saturation = 13, + Color = 14, + Luminosity = 15, + Addition = 16, + Subtract = 17, + Divide = 18, +} + + +DEFAULT_SPRITE_WRITE_RULES :: Sprite_Write_Rules { + align = .Mid_Center, + offset = {0, 0}, + background_colour = {0, 0, 0, 0}, + shrink_to_pixels = false, + use_index_bg_colour = true, + ingore_bg_layers = false, + ingore_sprite_size = false, +} + diff --git a/tools/compile_assets/aseprite/utils/utils_internal.odin b/tools/compile_assets/aseprite/utils/utils_internal.odin new file mode 100644 index 0000000..6915fe5 --- /dev/null +++ b/tools/compile_assets/aseprite/utils/utils_internal.odin @@ -0,0 +1,110 @@ +package aseprite_file_handler_utility + +import ir "base:intrinsics" +import "base:runtime" +import "core:reflect" +import "core:strconv" +import "core:slice" + +@(require) import "core:fmt" +@(require) import "core:log" + +_ :: reflect + + +@(private) +fast_log_str :: proc(lvl: log.Level, str: string, loc := #caller_location) { + logger := context.logger + if logger.procedure == nil { return } + if lvl < logger.lowest_level { return } + logger.procedure(logger.data, lvl, str, logger.options, loc) +} + +@(private) +fast_log_str_enum :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_enum(T) { + logger := context.logger + if logger.procedure == nil { return } + if lvl < logger.lowest_level { return } + + s := reflect.enum_string(val) + buf := make([]u8, len(str) + len(sep) + len(s)) + defer delete(buf) + + n := copy(buf[:], str) + n += copy(buf[n:], sep) + copy(buf[n:], s) + + logger.procedure(logger.data, lvl, string(buf), logger.options, loc) +} + +@(private) +fast_log_str_num :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_integer(T) { + logger := context.logger + if logger.procedure == nil { return } + if lvl < logger.lowest_level { return } + + nb: [32]u8 + s := strconv.append_int(nb[:], i64(val), 10) + buf := make([]u8, len(str) + len(sep) + len(s)) + defer delete(buf) + + n := copy(buf[:], str) + n += copy(buf[n:], sep) + copy(buf[n:], s) + + logger.procedure(logger.data, lvl, string(buf), logger.options, loc) +} + +@(private) +fast_log :: proc {fast_log_str, fast_log_str_enum, fast_log_str_num} + + +// Internal Debugging Tool. +format_pixels :: proc(img: Image, x := 4, y := 4, alloc := context.allocator) -> (str: string, err: runtime.Allocator_Error) #optional_allocator_error { + ch := int(img.bpp) >> 3 + + size := len(img.data) * 3 /*pixel in bytes*/ \ + + (len(img.data)/ch - 1) /*comas*/ \ + + (len(img.data)*2/ch) /*<space>|*/ \ + + (img.width*img.height*4) /*tile gap*/ + + sb := make([dynamic]byte, 0, size) or_return + buf: [3]byte + + for n in 0..<len(img.data)/ch { + if n %% (img.width) == 0 { + append(&sb, '|') or_return + } + + s := strconv.write_int(buf[:], i64(img.data[n*ch]), 10) + for _ in 0..<3-len(s) { + append(&sb, ' ') or_return + } + append(&sb, s) or_return + + for i in 1..<ch { + append(&sb, ',') + s = strconv.write_int(buf[:], i64(img.data[n*ch+i]), 10) + for _ in 0..<3-len(s) { + append(&sb, ' ') or_return + } + append(&sb, s) or_return + } + append(&sb, '|') or_return + + if (n+1) %% img.width == 0 { + append(&sb, '\n') or_return + }else if (n+1) %% x == 0 { + append(&sb, " |") or_return + } + if (n+1) %% (img.width * y) == 0 { + append(&sb, '\n') or_return + } + } + + return string(sb[:]), nil +} + +fill_colour :: proc(data: []u8, colour: [4]u8) { + slice.fill(slice.reinterpret([][4]u8, data), colour) +} |
