diff --git a/build.zig b/build.zig index 57ef24e..fdde523 100644 --- a/build.zig +++ b/build.zig @@ -17,6 +17,10 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); + exe.addIncludePath(b.path("src")); + exe.addCSourceFile(.{ + .file = b.path("src/cute_aseprite.c") + }); exe.linkLibrary(raylib_dep.artifact("raylib")); exe.root_module.addImport("raylib", raylib_dep.module("raylib")); diff --git a/src/app.zig b/src/app.zig index 5c4f1f1..2e87e2a 100644 --- a/src/app.zig +++ b/src/app.zig @@ -2,13 +2,15 @@ const std = @import("std"); const rl = @import("raylib"); const TaskPool = @import("./task-pool.zig"); const FontFace = @import("./font-face.zig"); -const Graph = @import("./graph.zig"); +const Graph = @import("./ui/graph.zig"); const Platform = @import("./platform.zig"); const NIDaq = @import("ni-daq.zig"); const Theme = @import("./theme.zig"); const RectUtils = @import("./rect-utils.zig"); const UI = @import("./ui/root.zig"); const showButton = @import("./ui/button.zig").showButton; +const remap = @import("./utils.zig").remap; +const Aseprite = @import("./aseprite.zig"); const log = std.log.scoped(.app); @@ -22,11 +24,13 @@ const Channel = struct { view_cache: Graph.Cache = .{}, view_rect: Graph.ViewOptions, + dragged_marker: ?enum { from, to, both } = null, + min_value: f64, max_value: f64, samples: union(enum) { owned: []f64, - } + }, }; allocator: Allocator, @@ -44,18 +48,48 @@ view_width: f32 = 5000, ui: UI, +grab_texture: struct { + normal: rl.Texture2D, + hot: rl.Texture2D, + active: rl.Texture2D, +}, + pub fn init( allocator: Allocator, task_pool_options: TaskPool.Options, nidaq_options: NIDaq.Options ) !App { + + // TODO: Maybe store a compressed version of aseprite files when embedding? + // Setup a build step to compress the files + const grab_ase = try Aseprite.init(allocator, @embedFile("./assets/grab-marker.ase")); + defer grab_ase.deinit(); + + const grab_normal_image = grab_ase.getTagImage(grab_ase.getTag("normal") orelse return error.TagNotFound); + defer rl.unloadImage(grab_normal_image); + const grab_normal_texture = rl.loadTextureFromImage(grab_normal_image); + + const grab_hot_image = grab_ase.getTagImage(grab_ase.getTag("hot") orelse return error.TagNotFound); + defer rl.unloadImage(grab_hot_image); + const grab_hot_texture = rl.loadTextureFromImage(grab_hot_image); + + const grab_active_image = grab_ase.getTagImage(grab_ase.getTag("active") orelse return error.TagNotFound); + defer rl.unloadImage(grab_active_image); + const grab_active_texture = rl.loadTextureFromImage(grab_active_image); + return App{ .allocator = allocator, .task_pool = try TaskPool.init(allocator, task_pool_options), .channels = std.ArrayList(Channel).init(allocator), .ni_daq = try NIDaq.init(allocator, nidaq_options), .start_time_ns = std.time.nanoTimestamp(), - .ui = UI.init() + .ui = UI.init(), + + .grab_texture = .{ + .normal = grab_normal_texture, + .hot = grab_hot_texture, + .active = grab_active_texture + } }; } @@ -69,12 +103,11 @@ pub fn deinit(self: *App) void { self.channels.deinit(); self.task_pool.deinit(self.allocator); self.ni_daq.deinit(self.allocator); -} -// fn appendChannel(self: *App) !*Channel { -// try self.channels.append(Channel.init()); -// return &self.channels.items[self.channels.items.len-1]; -// } + rl.unloadTexture(self.grab_texture.normal); + rl.unloadTexture(self.grab_texture.hot); + rl.unloadTexture(self.grab_texture.active); +} fn readSamplesFromFile(allocator: Allocator, file: std.fs.File) ![]f64 { try file.seekTo(0); @@ -103,6 +136,202 @@ fn nanoToSeconds(ns: i128) f32 { return @as(f32, @floatFromInt(ns)) / std.time.ns_per_s; } +pub fn appendChannelFromFile(self: *App, file: std.fs.File) !void { + const samples = try readSamplesFromFile(self.allocator, file); + errdefer self.allocator.free(samples); + + var min_value = samples[0]; + var max_value = samples[0]; + + for (samples) |sample| { + min_value = @min(min_value, sample); + max_value = @max(max_value, sample); + } + + const margin = 0.1; + try self.channels.append(Channel{ + .min_value = min_value, + .max_value = max_value, + .view_rect = .{ + .from = 0, + .to = @floatFromInt(samples.len), + .min_value = min_value + (min_value - max_value) * margin, + .max_value = max_value + (max_value - min_value) * margin + }, + .samples = .{ .owned = samples } + }); +} + +fn showChannelMinimap(self: *App, channel: *Channel, minimap_rect: rl.Rectangle) void { + const MarkerState = enum { + normal, + hot, + active + }; + + var from_state = MarkerState.normal; + var to_state = MarkerState.normal; + var area_state = MarkerState.normal; + + const sample_count = channel.samples.owned.len; + const sample_count_f32: f32 = @floatFromInt(sample_count); + + const grab_marker_width: f32 = @floatFromInt(self.grab_texture.normal.width); + const view_column_min = RectUtils.left(minimap_rect) + grab_marker_width/2; + const view_column_max = RectUtils.right(minimap_rect) - grab_marker_width/2; + + const from_column_position = remap(f32, + 0, + sample_count_f32, + view_column_min, + view_column_max, + channel.view_rect.from + ); + + const to_column_position = remap(f32, + 0, + sample_count_f32, + view_column_min, + view_column_max, + channel.view_rect.to + ); + + const visible_area_rect = rl.Rectangle{ + .x = from_column_position, + .y = minimap_rect.y, + .width = to_column_position - from_column_position, + .height = minimap_rect.height + }; + + const mouse = self.ui.getMousePosition(); + if (RectUtils.isInsideVec2(minimap_rect, mouse)) { + rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.gray); + + const grab_distance = 20; + + if (@abs(mouse.x - from_column_position) < grab_distance) { + from_state = .hot; + } else if (@abs(mouse.x - to_column_position) < grab_distance) { + to_state = .hot; + } else if (RectUtils.isInsideVec2(visible_area_rect, mouse)) { + area_state = .hot; + } + + if (rl.isMouseButtonPressed(.mouse_button_left)) { + if (area_state == .hot) { + channel.dragged_marker = .both; + } else if (from_state == .hot) { + channel.dragged_marker = .from; + } else if (to_state == .hot) { + channel.dragged_marker = .to; + } + } + + } else { + rl.drawRectangleLinesEx(minimap_rect, 1, rl.Color.black); + } + + if (rl.isMouseButtonReleased(.mouse_button_left)) { + channel.dragged_marker = null; + } + + const sample_under_mouse = remap(f32, + view_column_min, + view_column_max, + 0, + sample_count_f32, + mouse.x + ); + + if (channel.dragged_marker) |marker| { + const min_shown_samples = 1000; + switch (marker) { + .from => { + from_state = .active; + channel.view_rect.from = std.math.clamp(sample_under_mouse, 0, channel.view_rect.to-min_shown_samples); + }, + .to => { + to_state = .active; + channel.view_rect.to = std.math.clamp(sample_under_mouse, channel.view_rect.from+min_shown_samples, @as(f32, sample_count_f32)); + }, + .both => { + area_state = .active; + from_state = .active; + to_state = .active; + + var delta = remap(f32, + 0, + minimap_rect.width, + 0, + sample_count_f32, + self.ui.getMouseDelta().x + ); + delta = std.math.clamp(delta, -channel.view_rect.from, sample_count_f32 - channel.view_rect.to); + + channel.view_rect.from += delta; + channel.view_rect.to += delta; + } + } + } + + rl.drawRectangleRec(visible_area_rect, rl.Color.gray); + + { + const texture = switch (from_state) { + .normal => self.grab_texture.normal, + .hot => self.grab_texture.hot, + .active => self.grab_texture.active + }; + + rl.drawTextureV( + texture, + .{ + .x = from_column_position - @as(f32, @floatFromInt(texture.width)) / 2, + .y = RectUtils.top(minimap_rect) + }, + rl.Color.white + ); + } + + { + const texture = switch (to_state) { + .normal => self.grab_texture.normal, + .hot => self.grab_texture.hot, + .active => self.grab_texture.active + }; + + rl.drawTextureV( + texture, + .{ + .x = to_column_position - @as(f32, @floatFromInt(texture.width)) / 2, + .y = RectUtils.top(minimap_rect) + }, + rl.Color.white + ); + } +} + +const graph_height = 128; +const minimap_height = 16; +const channel_height = graph_height + minimap_height; +fn showChannelRow(self: *App, channel: *Channel, channel_rect: rl.Rectangle) void { + const graph_rect, const minimap_rect = RectUtils.horizontalSplit(channel_rect, graph_height); + + { // Graph + Graph.draw( + &self.ui, + &channel.view_cache, + graph_rect, + channel.view_rect, + channel.samples.owned + ); + + rl.drawRectangleLinesEx(graph_rect, 1, rl.Color.black); + } + + self.showChannelMinimap(channel, minimap_rect); +} + pub fn tick(self: *App) !void { // const dt = rl.getFrameTime(); @@ -134,29 +363,7 @@ pub fn tick(self: *App) !void { defer file.close(); // TODO: Handle error - const samples = try readSamplesFromFile(self.allocator, file); - errdefer self.allocator.free(samples); - - var min_value = samples[0]; - var max_value = samples[0]; - - for (samples) |sample| { - min_value = @min(min_value, sample); - max_value = @max(max_value, sample); - } - - const margin = 0.1; - try self.channels.append(Channel{ - .min_value = min_value, - .max_value = max_value, - .view_rect = .{ - .from = 0, - .to = @floatFromInt(samples.len), - .min_value = min_value + (min_value - max_value) * margin, - .max_value = max_value + (max_value - min_value) * margin - }, - .samples = .{ .owned = samples } - }); + try self.appendChannelFromFile(file); } else |err| { // TODO: Show error message to user; log.err("Failed to pick file: {}", .{ err }); @@ -165,22 +372,11 @@ pub fn tick(self: *App) !void { { var channels_stack = UI.Stack.init(RectUtils.shrink(graphs_rect, 10), .top_to_bottom); - for (self.channels.items) |*channel| { - const channel_rect = channels_stack.next(128); - - Graph.draw( - &channel.view_cache, - channel_rect, - channel.view_rect, - channel.samples.owned - ); - - rl.drawRectangleLinesEx(channel_rect, 1, rl.Color.black); + self.showChannelRow(channel, channels_stack.next(channel_height)); } } - // rl.drawLineV( // Vec2.init(0, window_height/2), // Vec2.init(window_width, window_height/2), diff --git a/src/aseprite.zig b/src/aseprite.zig new file mode 100644 index 0000000..88bfb38 --- /dev/null +++ b/src/aseprite.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const rl = @import("raylib"); +const c = @cImport({ + @cInclude("cute_aseprite.h"); +}); + +const assert = std.debug.assert; + +ase: *c.ase_t, + +pub fn init(allocator: std.mem.Allocator, memory: []const u8) !@This() { + _ = allocator; // TODO: Pass allocator to function + + const parsed = c.cute_aseprite_load_from_memory(@ptrCast(memory), @intCast(memory.len), null); + if (parsed == null) { + return error.CuteLoadFromMemory; + } + + return @This(){ + .ase = @ptrCast(parsed) + }; +} + +pub fn deinit(self: @This()) void { + c.cute_aseprite_free(self.ase); +} + +pub fn getTag(self: @This(), name: []const u8) ?c.struct_ase_tag_t { + const tag_count: usize = @intCast(self.ase.tag_count); + for (self.ase.tags[0..tag_count]) |tag| { + const tag_name = std.mem.span(tag.name); + if (std.mem.eql(u8, tag_name, name)) { + return tag; + } + } + return null; +} + +pub fn getFrameImage(self: @This(), frame_index: usize) rl.Image { + const width: usize = @intCast(self.ase.w); + const height: usize = @intCast(self.ase.h); + + var image = rl.genImageColor(@intCast(width), @intCast(height), rl.Color.black.alpha(0)); + assert(@intFromPtr(image.data) != 0); + + image.setFormat(rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8); + + const pixel_count = width * height; + const frame = self.ase.frames[frame_index]; + for (0.., frame.pixels[0..pixel_count]) |pixel_index, pixel| { + const x = @mod(pixel_index, width); + const y = @divFloor(pixel_index, height); + image.drawPixel(@intCast(x), @intCast(y), .{ + .r = pixel.r, + .g = pixel.g, + .b = pixel.b, + .a = pixel.a, + }); + } + + return image; +} + +pub fn getTagImage(self: @This(), tag: c.struct_ase_tag_t) rl.Image { + return getFrameImage(self, @intCast(tag.from_frame)); +} \ No newline at end of file diff --git a/src/assets/grab-marker.ase b/src/assets/grab-marker.ase new file mode 100644 index 0000000..0141da3 Binary files /dev/null and b/src/assets/grab-marker.ase differ diff --git a/src/assets/srcery-pallete.ase b/src/assets/srcery-pallete.ase new file mode 100644 index 0000000..94edd96 Binary files /dev/null and b/src/assets/srcery-pallete.ase differ diff --git a/src/cute_aseprite.c b/src/cute_aseprite.c new file mode 100644 index 0000000..b25f24f --- /dev/null +++ b/src/cute_aseprite.c @@ -0,0 +1,10 @@ +#include + +// TODO: Use zig allocators +// extern void *zig_cute_aseprite_malloc(void* ctx, size_t size); +// extern void zig_cute_aseprite_free(void* ctx, void* mem); + +#define CUTE_ASEPRITE_IMPLEMENTATION +// #define CUTE_ASEPRITE_ALLOC(size, ctx) zig_cute_aseprite_malloc(ctx, size) +// #define CUTE_ASEPRITE_FREE(mem, ctx) zig_cute_aseprite_free(ctx, mem) +#include "cute_aseprite.h" \ No newline at end of file diff --git a/src/cute_aseprite.h b/src/cute_aseprite.h new file mode 100644 index 0000000..216f380 --- /dev/null +++ b/src/cute_aseprite.h @@ -0,0 +1,1358 @@ +/* + ------------------------------------------------------------------------------ + Licensing information can be found at the end of the file. + ------------------------------------------------------------------------------ + + cute_aseprite.h - v1.04 + + To create implementation (the function definitions) + #define CUTE_ASEPRITE_IMPLEMENTATION + in *one* C/CPP file (translation unit) that includes this file + + + SUMMARY + + cute_aseprite.h is a single-file header that implements some functions to + parse .ase/.aseprite files. The entire file is parsed all at once and some + structs are filled out then handed back to you. + + + LIMITATIONS + + Only the "normal" blend mode for layers is supported. As a workaround try + using the "merge down" function in Aseprite to create a normal layer. + Supporting all blend modes would take too much code to be worth it. + + Does not support very old versions of Aseprite (with old palette chunks + 0x0004 or 0x0011). Also does not support deprecrated mask chunk. + + sRGB and ICC profiles are parsed but completely ignored when blending + frames together. If you want these to be used when composing frames you + have to do this yourself. + + + SPECIAL THANKS + + Special thanks to Noel Berry for the blend code in his reference C++ + implementation (https://github.com/NoelFB/blah). + + Special thanks to Richard Mitton for the initial implementation of the + zlib inflater. + + + Revision history: + 1.00 (08/25/2020) initial release + 1.01 (08/31/2020) fixed memleaks, tag parsing bug (crash), blend bugs + 1.02 (02/05/2022) fixed icc profile parse bug, support transparent pal- + ette index, can parse 1.3 files (no tileset support) + 1.03 (11/27/2023) fixed slice pivot parse bug + 1.04 (02/20/2024) chunck 0x0004 support +*/ + +/* + DOCUMENTATION + + Simply load an .ase or .aseprite file from disk or from memory like so. + + ase_t* ase = cute_aseprite_load_from_file("data/player.aseprite", NULL); + + + Then access the fields directly, assuming you have your own `Animation` type. + + int w = ase->w; + int h = ase->h; + Animation anim = { 0 }; // Your custom animation data type. + + for (int i = 0; i < ase->frame_count; ++i) { + ase_frame_t* frame = ase->frames + i; + anim.add_frame(frame->duration_milliseconds, frame->pixels); + } + + + Then free it up when done. + + cute_aseprite_free(ase); + + + DATA STRUCTURES + + Aseprite files have frames, layers, and cels. A single frame is one frame of an + animation, formed by blending all the cels of an animation together. There is + one cel per layer per frame. Each cel contains its own pixel data. + + The frame's pixels are automatically assumed to have been blended by the `normal` + blend mode. A warning is emit if any other blend mode is encountered. Feel free + to update the pixels of each frame with your own implementation of blending + functions. The frame's pixels are merely provided like this for convenience. + + + BUGS AND CRASHES + + This header is quite new and it takes time to test all the parse paths. Don't be + shy about opening a GitHub issue if there's a crash! It's quite easy to update + the parser as long as you upload your .ase file that shows the bug. + + https://github.com/RandyGaul/cute_headers/issues +*/ + +#ifndef CUTE_ASEPRITE_H +#define CUTE_ASEPRITE_H + +typedef struct ase_t ase_t; + +ase_t* cute_aseprite_load_from_file(const char* path, void* mem_ctx); +ase_t* cute_aseprite_load_from_memory(const void* memory, int size, void* mem_ctx); +void cute_aseprite_free(ase_t* aseprite); + +#define CUTE_ASEPRITE_MAX_LAYERS (64) +#define CUTE_ASEPRITE_MAX_SLICES (128) +#define CUTE_ASEPRITE_MAX_PALETTE_ENTRIES (1024) +#define CUTE_ASEPRITE_MAX_TAGS (256) + +#include + +typedef struct ase_color_t ase_color_t; +typedef struct ase_frame_t ase_frame_t; +typedef struct ase_layer_t ase_layer_t; +typedef struct ase_cel_t ase_cel_t; +typedef struct ase_tag_t ase_tag_t; +typedef struct ase_slice_t ase_slice_t; +typedef struct ase_palette_entry_t ase_palette_entry_t; +typedef struct ase_palette_t ase_palette_t; +typedef struct ase_udata_t ase_udata_t; +typedef struct ase_cel_extra_chunk_t ase_cel_extra_chunk_t; +typedef struct ase_color_profile_t ase_color_profile_t; +typedef struct ase_fixed_t ase_fixed_t; + +struct ase_color_t +{ + uint8_t r, g, b, a; +}; + +struct ase_fixed_t +{ + uint16_t a; + uint16_t b; +}; + +struct ase_udata_t +{ + int has_color; + ase_color_t color; + int has_text; + const char* text; +}; + +typedef enum ase_layer_flags_t +{ + ASE_LAYER_FLAGS_VISIBLE = 0x01, + ASE_LAYER_FLAGS_EDITABLE = 0x02, + ASE_LAYER_FLAGS_LOCK_MOVEMENT = 0x04, + ASE_LAYER_FLAGS_BACKGROUND = 0x08, + ASE_LAYER_FLAGS_PREFER_LINKED_CELS = 0x10, + ASE_LAYER_FLAGS_COLLAPSED = 0x20, + ASE_LAYER_FLAGS_REFERENCE = 0x40, +} ase_layer_flags_t; + +typedef enum ase_layer_type_t +{ + ASE_LAYER_TYPE_NORMAL, + ASE_LAYER_TYPE_GROUP, +} ase_layer_type_t; + +struct ase_layer_t +{ + ase_layer_flags_t flags; + ase_layer_type_t type; + const char* name; + ase_layer_t* parent; + float opacity; + ase_udata_t udata; +}; + +struct ase_cel_extra_chunk_t +{ + int precise_bounds_are_set; + ase_fixed_t precise_x; + ase_fixed_t precise_y; + ase_fixed_t w, h; +}; + +struct ase_cel_t +{ + ase_layer_t* layer; + void* pixels; + int w, h; + int x, y; + float opacity; + int is_linked; + uint16_t linked_frame_index; + int has_extra; + ase_cel_extra_chunk_t extra; + ase_udata_t udata; +}; + +struct ase_frame_t +{ + ase_t* ase; + int duration_milliseconds; + ase_color_t* pixels; + int cel_count; + ase_cel_t cels[CUTE_ASEPRITE_MAX_LAYERS]; +}; + +typedef enum ase_animation_direction_t +{ + ASE_ANIMATION_DIRECTION_FORWARDS, + ASE_ANIMATION_DIRECTION_BACKWORDS, + ASE_ANIMATION_DIRECTION_PINGPONG, +} ase_animation_direction_t; + +struct ase_tag_t +{ + int from_frame; + int to_frame; + ase_animation_direction_t loop_animation_direction; + int repeat; + uint8_t r, g, b; + const char* name; + ase_udata_t udata; +}; + +struct ase_slice_t +{ + const char* name; + int frame_number; + int origin_x; + int origin_y; + int w, h; + + int has_center_as_9_slice; + int center_x; + int center_y; + int center_w; + int center_h; + + int has_pivot; + int pivot_x; + int pivot_y; + + ase_udata_t udata; +}; + +struct ase_palette_entry_t +{ + ase_color_t color; + const char* color_name; +}; + +struct ase_palette_t +{ + int entry_count; + ase_palette_entry_t entries[CUTE_ASEPRITE_MAX_PALETTE_ENTRIES]; +}; + +typedef enum ase_color_profile_type_t +{ + ASE_COLOR_PROFILE_TYPE_NONE, + ASE_COLOR_PROFILE_TYPE_SRGB, + ASE_COLOR_PROFILE_TYPE_EMBEDDED_ICC, +} ase_color_profile_type_t; + +struct ase_color_profile_t +{ + ase_color_profile_type_t type; + int use_fixed_gamma; + ase_fixed_t gamma; + uint32_t icc_profile_data_length; + void* icc_profile_data; +}; + +typedef enum ase_mode_t +{ + ASE_MODE_RGBA, + ASE_MODE_GRAYSCALE, + ASE_MODE_INDEXED +} ase_mode_t; + +struct ase_t +{ + ase_mode_t mode; + int w, h; + int transparent_palette_entry_index; + int number_of_colors; + int pixel_w; + int pixel_h; + int grid_x; + int grid_y; + int grid_w; + int grid_h; + int has_color_profile; + ase_color_profile_t color_profile; + ase_palette_t palette; + + int layer_count; + ase_layer_t layers[CUTE_ASEPRITE_MAX_LAYERS]; + + int frame_count; + ase_frame_t* frames; + + int tag_count; + ase_tag_t tags[CUTE_ASEPRITE_MAX_TAGS]; + + int slice_count; + ase_slice_t slices[CUTE_ASEPRITE_MAX_SLICES]; + + void* mem_ctx; +}; + +#endif // CUTE_ASEPRITE_H + +#ifdef CUTE_ASEPRITE_IMPLEMENTATION +#ifndef CUTE_ASEPRITE_IMPLEMENTATION_ONCE +#define CUTE_ASEPRITE_IMPLEMENTATION_ONCE + +#ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE +#endif + +#if !defined(CUTE_ASEPRITE_ALLOC) + #include + #define CUTE_ASEPRITE_ALLOC(size, ctx) malloc(size) + #define CUTE_ASEPRITE_FREE(mem, ctx) free(mem) +#endif + +#if !defined(CUTE_ASEPRITE_UNUSED) + #if defined(_MSC_VER) + #define CUTE_ASEPRITE_UNUSED(x) (void)x + #else + #define CUTE_ASEPRITE_UNUSED(x) (void)(sizeof(x)) + #endif +#endif + +#if !defined(CUTE_ASEPRITE_MEMCPY) + #include // memcpy + #define CUTE_ASEPRITE_MEMCPY memcpy +#endif + +#if !defined(CUTE_ASEPRITE_MEMSET) + #include // memset + #define CUTE_ASEPRITE_MEMSET memset +#endif + +#if !defined(CUTE_ASEPRITE_ASSERT) + #include + #define CUTE_ASEPRITE_ASSERT assert +#endif + +#if !defined(CUTE_ASEPRITE_SEEK_SET) + #include // SEEK_SET + #define CUTE_ASEPRITE_SEEK_SET SEEK_SET +#endif + +#if !defined(CUTE_ASEPRITE_SEEK_END) + #include // SEEK_END + #define CUTE_ASEPRITE_SEEK_END SEEK_END +#endif + +#if !defined(CUTE_ASEPRITE_FILE) + #include // FILE + #define CUTE_ASEPRITE_FILE FILE +#endif + +#if !defined(CUTE_ASEPRITE_FOPEN) + #include // fopen + #define CUTE_ASEPRITE_FOPEN fopen +#endif + +#if !defined(CUTE_ASEPRITE_FSEEK) + #include // fseek + #define CUTE_ASEPRITE_FSEEK fseek +#endif + +#if !defined(CUTE_ASEPRITE_FREAD) + #include // fread + #define CUTE_ASEPRITE_FREAD fread +#endif + +#if !defined(CUTE_ASEPRITE_FTELL) + #include // ftell + #define CUTE_ASEPRITE_FTELL ftell +#endif + +#if !defined(CUTE_ASEPRITE_FCLOSE) + #include // fclose + #define CUTE_ASEPRITE_FCLOSE fclose +#endif + +static const char* s_error_file = NULL; // The filepath of the file being parsed. NULL if from memory. +static const char* s_error_reason; // Used to capture errors during DEFLATE parsing. + +#if !defined(CUTE_ASEPRITE_WARNING) + #define CUTE_ASEPRITE_WARNING(msg) cute_aseprite_warning(msg, __LINE__) + + static int s_error_cline; // The line in cute_aseprite.h where the error was triggered. + void cute_aseprite_warning(const char* warning, int line) + { + s_error_cline = line; + const char *error_file = s_error_file ? s_error_file : "MEMORY"; + printf("WARNING (cute_aseprite.h:%i): %s (%s)\n", s_error_cline, warning, error_file); + } +#endif + +#define CUTE_ASEPRITE_FAIL() do { goto ase_err; } while (0) +#define CUTE_ASEPRITE_CHECK(X, Y) do { if (!(X)) { s_error_reason = Y; CUTE_ASEPRITE_FAIL(); } } while (0) +#define CUTE_ASEPRITE_CALL(X) do { if (!(X)) goto ase_err; } while (0) +#define CUTE_ASEPRITE_DEFLATE_MAX_BITLEN 15 + +// DEFLATE tables from RFC 1951 +static uint8_t s_fixed_table[288 + 32] = { + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, +}; // 3.2.6 +static uint8_t s_permutation_order[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; // 3.2.7 +static uint8_t s_len_extra_bits[29 + 2] = { 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0, 0,0 }; // 3.2.5 +static uint32_t s_len_base[29 + 2] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 0,0 }; // 3.2.5 +static uint8_t s_dist_extra_bits[30 + 2] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13, 0,0 }; // 3.2.5 +static uint32_t s_dist_base[30 + 2] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 0,0 }; // 3.2.5 + +typedef struct deflate_t +{ + uint64_t bits; + int count; + uint32_t* words; + int word_count; + int word_index; + int bits_left; + + int final_word_available; + uint32_t final_word; + + char* out; + char* out_end; + char* begin; + + uint32_t lit[288]; + uint32_t dst[32]; + uint32_t len[19]; + uint32_t nlit; + uint32_t ndst; + uint32_t nlen; +} deflate_t; + +static int s_would_overflow(deflate_t* s, int num_bits) +{ + return (s->bits_left + s->count) - num_bits < 0; +} + +static char* s_ptr(deflate_t* s) +{ + CUTE_ASEPRITE_ASSERT(!(s->bits_left & 7)); + return (char*)(s->words + s->word_index) - (s->count / 8); +} + +static uint64_t s_peak_bits(deflate_t* s, int num_bits_to_read) +{ + if (s->count < num_bits_to_read) + { + if (s->word_index < s->word_count) + { + uint32_t word = s->words[s->word_index++]; + s->bits |= (uint64_t)word << s->count; + s->count += 32; + CUTE_ASEPRITE_ASSERT(s->word_index <= s->word_count); + } + + else if (s->final_word_available) + { + uint32_t word = s->final_word; + s->bits |= (uint64_t)word << s->count; + s->count += s->bits_left; + s->final_word_available = 0; + } + } + + return s->bits; +} + +static uint32_t s_consume_bits(deflate_t* s, int num_bits_to_read) +{ + CUTE_ASEPRITE_ASSERT(s->count >= num_bits_to_read); + uint32_t bits = (uint32_t)(s->bits & (((uint64_t)1 << num_bits_to_read) - 1)); + s->bits >>= num_bits_to_read; + s->count -= num_bits_to_read; + s->bits_left -= num_bits_to_read; + return bits; +} + +static uint32_t s_read_bits(deflate_t* s, int num_bits_to_read) +{ + CUTE_ASEPRITE_ASSERT(num_bits_to_read <= 32); + CUTE_ASEPRITE_ASSERT(num_bits_to_read >= 0); + CUTE_ASEPRITE_ASSERT(s->bits_left > 0); + CUTE_ASEPRITE_ASSERT(s->count <= 64); + CUTE_ASEPRITE_ASSERT(!s_would_overflow(s, num_bits_to_read)); + s_peak_bits(s, num_bits_to_read); + uint32_t bits = s_consume_bits(s, num_bits_to_read); + return bits; +} + +static uint32_t s_rev16(uint32_t a) +{ + a = ((a & 0xAAAA) >> 1) | ((a & 0x5555) << 1); + a = ((a & 0xCCCC) >> 2) | ((a & 0x3333) << 2); + a = ((a & 0xF0F0) >> 4) | ((a & 0x0F0F) << 4); + a = ((a & 0xFF00) >> 8) | ((a & 0x00FF) << 8); + return a; +} + +// RFC 1951 section 3.2.2 +static uint32_t s_build(deflate_t* s, uint32_t* tree, uint8_t* lens, int sym_count) +{ + int n, codes[16], first[16], counts[16] = { 0 }; + CUTE_ASEPRITE_UNUSED(s); + + // Frequency count + for (n = 0; n < sym_count; n++) counts[lens[n]]++; + + // Distribute codes + counts[0] = codes[0] = first[0] = 0; + for (n = 1; n <= 15; ++n) + { + codes[n] = (codes[n - 1] + counts[n - 1]) << 1; + first[n] = first[n - 1] + counts[n - 1]; + } + + for (uint32_t i = 0; i < (uint32_t)sym_count; ++i) + { + uint8_t len = lens[i]; + + if (len != 0) + { + CUTE_ASEPRITE_ASSERT(len < 16); + uint32_t code = (uint32_t)codes[len]++; + uint32_t slot = (uint32_t)first[len]++; + tree[slot] = (code << (32 - (uint32_t)len)) | (i << 4) | len; + } + } + + return (uint32_t)first[15]; +} + +static int s_stored(deflate_t* s) +{ + char* p; + + // 3.2.3 + // skip any remaining bits in current partially processed byte + s_read_bits(s, s->count & 7); + + // 3.2.4 + // read LEN and NLEN, should complement each other + uint16_t LEN = (uint16_t)s_read_bits(s, 16); + uint16_t NLEN = (uint16_t)s_read_bits(s, 16); + uint16_t TILDE_NLEN = ~NLEN; + CUTE_ASEPRITE_CHECK(LEN == TILDE_NLEN, "Failed to find LEN and NLEN as complements within stored (uncompressed) stream."); + CUTE_ASEPRITE_CHECK(s->bits_left / 8 <= (int)LEN, "Stored block extends beyond end of input stream."); + p = s_ptr(s); + CUTE_ASEPRITE_MEMCPY(s->out, p, LEN); + s->out += LEN; + return 1; + +ase_err: + return 0; +} + +// 3.2.6 +static int s_fixed(deflate_t* s) +{ + s->nlit = s_build(s, s->lit, s_fixed_table, 288); + s->ndst = s_build(0, s->dst, s_fixed_table + 288, 32); + return 1; +} + +static int s_decode(deflate_t* s, uint32_t* tree, int hi) +{ + uint64_t bits = s_peak_bits(s, 16); + uint32_t search = (s_rev16((uint32_t)bits) << 16) | 0xFFFF; + int lo = 0; + while (lo < hi) + { + int guess = (lo + hi) >> 1; + if (search < tree[guess]) hi = guess; + else lo = guess + 1; + } + + uint32_t key = tree[lo - 1]; + uint32_t len = (32 - (key & 0xF)); + CUTE_ASEPRITE_ASSERT((search >> len) == (key >> len)); + + s_consume_bits(s, key & 0xF); + return (key >> 4) & 0xFFF; +} + +// 3.2.7 +static int s_dynamic(deflate_t* s) +{ + uint8_t lenlens[19] = { 0 }; + + uint32_t nlit = 257 + s_read_bits(s, 5); + uint32_t ndst = 1 + s_read_bits(s, 5); + uint32_t nlen = 4 + s_read_bits(s, 4); + + for (uint32_t i = 0 ; i < nlen; ++i) + lenlens[s_permutation_order[i]] = (uint8_t)s_read_bits(s, 3); + + // Build the tree for decoding code lengths + s->nlen = s_build(0, s->len, lenlens, 19); + uint8_t lens[288 + 32]; + + for (uint32_t n = 0; n < nlit + ndst;) + { + int sym = s_decode(s, s->len, (int)s->nlen); + switch (sym) + { + case 16: for (uint32_t i = 3 + s_read_bits(s, 2); i; --i, ++n) lens[n] = lens[n - 1]; break; + case 17: for (uint32_t i = 3 + s_read_bits(s, 3); i; --i, ++n) lens[n] = 0; break; + case 18: for (uint32_t i = 11 + s_read_bits(s, 7); i; --i, ++n) lens[n] = 0; break; + default: lens[n++] = (uint8_t)sym; break; + } + } + + s->nlit = s_build(s, s->lit, lens, (int)nlit); + s->ndst = s_build(0, s->dst, lens + nlit, (int)ndst); + return 1; +} + +// 3.2.3 +static int s_block(deflate_t* s) +{ + while (1) + { + int symbol = s_decode(s, s->lit, (int)s->nlit); + + if (symbol < 256) + { + CUTE_ASEPRITE_CHECK(s->out + 1 <= s->out_end, "Attempted to overwrite out buffer while outputting a symbol."); + *s->out = (char)symbol; + s->out += 1; + } + + else if (symbol > 256) + { + symbol -= 257; + uint32_t length = s_read_bits(s, (int)(s_len_extra_bits[symbol])) + s_len_base[symbol]; + int distance_symbol = s_decode(s, s->dst, (int)s->ndst); + uint32_t backwards_distance = s_read_bits(s, s_dist_extra_bits[distance_symbol]) + s_dist_base[distance_symbol]; + CUTE_ASEPRITE_CHECK(s->out - backwards_distance >= s->begin, "Attempted to write before out buffer (invalid backwards distance)."); + CUTE_ASEPRITE_CHECK(s->out + length <= s->out_end, "Attempted to overwrite out buffer while outputting a string."); + char* src = s->out - backwards_distance; + char* dst = s->out; + s->out += length; + + switch (backwards_distance) + { + case 1: // very common in images + CUTE_ASEPRITE_MEMSET(dst, *src, (size_t)length); + break; + default: while (length--) *dst++ = *src++; + } + } + + else break; + } + + return 1; + +ase_err: + return 0; +} + +// 3.2.3 +static int s_inflate(const void* in, int in_bytes, void* out, int out_bytes, void* mem_ctx) +{ + CUTE_ASEPRITE_UNUSED(mem_ctx); + deflate_t* s = (deflate_t*)CUTE_ASEPRITE_ALLOC(sizeof(deflate_t), mem_ctx); + s->bits = 0; + s->count = 0; + s->word_index = 0; + s->bits_left = in_bytes * 8; + + // s->words is the in-pointer rounded up to a multiple of 4 + int first_bytes = (int)((((size_t)in + 3) & (size_t)(~3)) - (size_t)in); + s->words = (uint32_t*)((char*)in + first_bytes); + s->word_count = (in_bytes - first_bytes) / 4; + int last_bytes = ((in_bytes - first_bytes) & 3); + + for (int i = 0; i < first_bytes; ++i) + s->bits |= (uint64_t)(((uint8_t*)in)[i]) << (i * 8); + + s->final_word_available = last_bytes ? 1 : 0; + s->final_word = 0; + for(int i = 0; i < last_bytes; i++) + s->final_word |= ((uint8_t*)in)[in_bytes - last_bytes + i] << (i * 8); + + s->count = first_bytes * 8; + + s->out = (char*)out; + s->out_end = s->out + out_bytes; + s->begin = (char*)out; + + int count = 0; + uint32_t bfinal; + do + { + bfinal = s_read_bits(s, 1); + uint32_t btype = s_read_bits(s, 2); + + switch (btype) + { + case 0: CUTE_ASEPRITE_CALL(s_stored(s)); break; + case 1: s_fixed(s); CUTE_ASEPRITE_CALL(s_block(s)); break; + case 2: s_dynamic(s); CUTE_ASEPRITE_CALL(s_block(s)); break; + case 3: CUTE_ASEPRITE_CHECK(0, "Detected unknown block type within input stream."); + } + + ++count; + } + while (!bfinal); + + CUTE_ASEPRITE_FREE(s, mem_ctx); + return 1; + +ase_err: + CUTE_ASEPRITE_FREE(s, mem_ctx); + return 0; +} + +typedef struct ase_state_t +{ + uint8_t* in; + uint8_t* end; + void* mem_ctx; +} ase_state_t; + +static uint8_t s_read_uint8(ase_state_t* s) +{ + CUTE_ASEPRITE_ASSERT(s->in <= s->end + sizeof(uint8_t)); + uint8_t** p = &s->in; + uint8_t value = **p; + ++(*p); + return value; +} + +static uint16_t s_read_uint16(ase_state_t* s) +{ + CUTE_ASEPRITE_ASSERT(s->in <= s->end + sizeof(uint16_t)); + uint8_t** p = &s->in; + uint16_t value; + value = (*p)[0]; + value |= (((uint16_t)((*p)[1])) << 8); + *p += 2; + return value; +} + +static ase_fixed_t s_read_fixed(ase_state_t* s) +{ + ase_fixed_t value; + value.a = s_read_uint16(s); + value.b = s_read_uint16(s); + return value; +} + +static uint32_t s_read_uint32(ase_state_t* s) +{ + CUTE_ASEPRITE_ASSERT(s->in <= s->end + sizeof(uint32_t)); + uint8_t** p = &s->in; + uint32_t value; + value = (*p)[0]; + value |= (((uint32_t)((*p)[1])) << 8); + value |= (((uint32_t)((*p)[2])) << 16); + value |= (((uint32_t)((*p)[3])) << 24); + *p += 4; + return value; +} + +#ifdef CUTE_ASPRITE_S_READ_UINT64 +// s_read_uint64() is not currently used. +static uint64_t s_read_uint64(ase_state_t* s) +{ + CUTE_ASEPRITE_ASSERT(s->in <= s->end + sizeof(uint64_t)); + uint8_t** p = &s->in; + uint64_t value; + value = (*p)[0]; + value |= (((uint64_t)((*p)[1])) << 8 ); + value |= (((uint64_t)((*p)[2])) << 16); + value |= (((uint64_t)((*p)[3])) << 24); + value |= (((uint64_t)((*p)[4])) << 32); + value |= (((uint64_t)((*p)[5])) << 40); + value |= (((uint64_t)((*p)[6])) << 48); + value |= (((uint64_t)((*p)[7])) << 56); + *p += 8; + return value; +} +#endif + +#define s_read_int16(s) (int16_t)s_read_uint16(s) +#define s_read_int32(s) (int32_t)s_read_uint32(s) + +#ifdef CUTE_ASPRITE_S_READ_BYTES +// s_read_bytes() is not currently used. +static void s_read_bytes(ase_state_t* s, uint8_t* bytes, int num_bytes) +{ + for (int i = 0; i < num_bytes; ++i) { + bytes[i] = s_read_uint8(s); + } +} +#endif + +static const char* s_read_string(ase_state_t* s) +{ + int len = (int)s_read_uint16(s); + char* bytes = (char*)CUTE_ASEPRITE_ALLOC(len + 1, s->mem_ctx); + for (int i = 0; i < len; ++i) { + bytes[i] = (char)s_read_uint8(s); + } + bytes[len] = 0; + return bytes; +} + +static void s_skip(ase_state_t* ase, int num_bytes) +{ + CUTE_ASEPRITE_ASSERT(ase->in <= ase->end + num_bytes); + ase->in += num_bytes; +} + +static char* s_fopen(const char* path, int* size, void* mem_ctx) +{ + CUTE_ASEPRITE_UNUSED(mem_ctx); + char* data = 0; + CUTE_ASEPRITE_FILE* fp = CUTE_ASEPRITE_FOPEN(path, "rb"); + int sz = 0; + + if (fp) { + CUTE_ASEPRITE_FSEEK(fp, 0, CUTE_ASEPRITE_SEEK_END); + sz = (int)CUTE_ASEPRITE_FTELL(fp); + CUTE_ASEPRITE_FSEEK(fp, 0, CUTE_ASEPRITE_SEEK_SET); + data = (char*)CUTE_ASEPRITE_ALLOC(sz + 1, mem_ctx); + CUTE_ASEPRITE_FREAD(data, sz, 1, fp); + data[sz] = 0; + CUTE_ASEPRITE_FCLOSE(fp); + } + + if (size) *size = sz; + return data; +} + +ase_t* cute_aseprite_load_from_file(const char* path, void* mem_ctx) +{ + s_error_file = path; + int sz; + void* file = s_fopen(path, &sz, mem_ctx); + if (!file) { + CUTE_ASEPRITE_WARNING("Unable to find map file."); + return NULL; + } + ase_t* aseprite = cute_aseprite_load_from_memory(file, sz, mem_ctx); + CUTE_ASEPRITE_FREE(file, mem_ctx); + s_error_file = NULL; + return aseprite; +} + +static int s_mul_un8(int a, int b) +{ + int t = (a * b) + 0x80; + return (((t >> 8) + t) >> 8); +} + +static ase_color_t s_blend(ase_color_t src, ase_color_t dst, uint8_t opacity) +{ + src.a = (uint8_t)s_mul_un8(src.a, opacity); + int a = src.a + dst.a - s_mul_un8(src.a, dst.a); + int r, g, b; + if (a == 0) { + r = g = b = 0; + } else { + r = dst.r + (src.r - dst.r) * src.a / a; + g = dst.g + (src.g - dst.g) * src.a / a; + b = dst.b + (src.b - dst.b) * src.a / a; + } + ase_color_t ret = { (uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)a }; + return ret; +} + +static int s_min(int a, int b) +{ + return a < b ? a : b; +} + +static int s_max(int a, int b) +{ + return a < b ? b : a; +} + +static ase_color_t s_color(ase_t* ase, void* src, int index) +{ + ase_color_t result; + if (ase->mode == ASE_MODE_RGBA) { + result = ((ase_color_t*)src)[index]; + } else if (ase->mode == ASE_MODE_GRAYSCALE) { + uint8_t saturation = ((uint8_t*)src)[index * 2]; + uint8_t a = ((uint8_t*)src)[index * 2 + 1]; + result.r = result.g = result.b = saturation; + result.a = a; + } else { + CUTE_ASEPRITE_ASSERT(ase->mode == ASE_MODE_INDEXED); + uint8_t palette_index = ((uint8_t*)src)[index]; + if (palette_index == ase->transparent_palette_entry_index) { + result.r = 0; + result.g = 0; + result.b = 0; + result.a = 0; + } else { + result = ase->palette.entries[palette_index].color; + } + } + return result; +} + +ase_t* cute_aseprite_load_from_memory(const void* memory, int size, void* mem_ctx) +{ + ase_t* ase = (ase_t*)CUTE_ASEPRITE_ALLOC(sizeof(ase_t), mem_ctx); + CUTE_ASEPRITE_MEMSET(ase, 0, sizeof(*ase)); + + ase_state_t state = { 0, 0, 0 }; + ase_state_t* s = &state; + s->in = (uint8_t*)memory; + s->end = s->in + size; + s->mem_ctx = mem_ctx; + + s_skip(s, sizeof(uint32_t)); // File size. + int magic = (int)s_read_uint16(s); + CUTE_ASEPRITE_ASSERT(magic == 0xA5E0); + + ase->frame_count = (int)s_read_uint16(s); + ase->w = s_read_uint16(s); + ase->h = s_read_uint16(s); + uint16_t bpp = s_read_uint16(s) / 8; + if (bpp == 4) ase->mode = ASE_MODE_RGBA; + else if (bpp == 2) ase->mode = ASE_MODE_GRAYSCALE; + else { + CUTE_ASEPRITE_ASSERT(bpp == 1); + ase->mode = ASE_MODE_INDEXED; + } + uint32_t valid_layer_opacity = s_read_uint32(s) & 1; + int speed = s_read_uint16(s); + s_skip(s, sizeof(uint32_t) * 2); // Spec says skip these bytes, as they're zero'd. + ase->transparent_palette_entry_index = s_read_uint8(s); + s_skip(s, 3); // Spec says skip these bytes. + ase->number_of_colors = (int)s_read_uint16(s); + ase->pixel_w = (int)s_read_uint8(s); + ase->pixel_h = (int)s_read_uint8(s); + ase->grid_x = (int)s_read_int16(s); + ase->grid_y = (int)s_read_int16(s); + ase->grid_w = (int)s_read_uint16(s); + ase->grid_h = (int)s_read_uint16(s); + s_skip(s, 84); // For future use (set to zero). + + ase->frames = (ase_frame_t*)CUTE_ASEPRITE_ALLOC((int)(sizeof(ase_frame_t)) * ase->frame_count, mem_ctx); + CUTE_ASEPRITE_MEMSET(ase->frames, 0, sizeof(ase_frame_t) * (size_t)ase->frame_count); + + ase_udata_t* last_udata = NULL; + int was_on_tags = 0; + int tag_index = 0; + + ase_layer_t* layer_stack[CUTE_ASEPRITE_MAX_LAYERS]; + + // Parse all chunks in the .aseprite file. + for (int i = 0; i < ase->frame_count; ++i) { + ase_frame_t* frame = ase->frames + i; + frame->ase = ase; + s_skip(s, sizeof(uint32_t)); // Frame size. + magic = (int)s_read_uint16(s); + CUTE_ASEPRITE_ASSERT(magic == 0xF1FA); + int chunk_count = (int)s_read_uint16(s); + frame->duration_milliseconds = s_read_uint16(s); + if (frame->duration_milliseconds == 0) frame->duration_milliseconds = speed; + s_skip(s, 2); // For future use (set to zero). + uint32_t new_chunk_count = s_read_uint32(s); + if (new_chunk_count) chunk_count = (int)new_chunk_count; + + for (int j = 0; j < chunk_count; ++j) { + uint32_t chunk_size = s_read_uint32(s); + uint16_t chunk_type = s_read_uint16(s); + chunk_size -= (uint32_t)(sizeof(uint32_t) + sizeof(uint16_t)); + uint8_t* chunk_start = s->in; + + switch (chunk_type) { + case 0x0004: // Old Palette chunk (used when there are no colors with alpha in the palette) + { + uint16_t nbPackets = s_read_uint16(s); + for (uint16_t k = 0; k < nbPackets; k++) { + uint16_t maxColor = 0; + uint16_t skip = (uint16_t)s_read_uint8(s); + uint16_t nbColors = (uint16_t)s_read_uint8(s); + if (nbColors == 0) nbColors = 256; + + for (uint16_t l = 0; l < nbColors; l++) { + ase_palette_entry_t entry; + entry.color.r = s_read_uint8(s); + entry.color.g = s_read_uint8(s); + entry.color.b = s_read_uint8(s); + entry.color.a = 255; + entry.color_name = NULL; + ase->palette.entries[skip + l] = entry; + if (skip + l > maxColor) maxColor = skip + l; + } + + ase->palette.entry_count = maxColor+1; + } + + } break; + case 0x2004: // Layer chunk. + { + CUTE_ASEPRITE_ASSERT(ase->layer_count < CUTE_ASEPRITE_MAX_LAYERS); + ase_layer_t* layer = ase->layers + ase->layer_count++; + layer->flags = (ase_layer_flags_t)s_read_uint16(s); + layer->type = (ase_layer_type_t)s_read_uint16(s); + layer->parent = NULL; + int child_level = (int)s_read_uint16(s); + layer_stack[child_level] = layer; + if (child_level) { + layer->parent = layer_stack[child_level - 1]; + } + s_skip(s, sizeof(uint16_t)); // Default layer width in pixels (ignored). + s_skip(s, sizeof(uint16_t)); // Default layer height in pixels (ignored). + int blend_mode = (int)s_read_uint16(s); + if (blend_mode) CUTE_ASEPRITE_WARNING("Unknown blend mode encountered."); + layer->opacity = s_read_uint8(s) / 255.0f; + if (!valid_layer_opacity) layer->opacity = 1.0f; + s_skip(s, 3); // For future use (set to zero). + layer->name = s_read_string(s); + last_udata = &layer->udata; + } break; + + case 0x2005: // Cel chunk. + { + CUTE_ASEPRITE_ASSERT(frame->cel_count < CUTE_ASEPRITE_MAX_LAYERS); + ase_cel_t* cel = frame->cels + frame->cel_count++; + int layer_index = (int)s_read_uint16(s); + cel->layer = ase->layers + layer_index; + cel->x = s_read_int16(s); + cel->y = s_read_int16(s); + cel->opacity = s_read_uint8(s) / 255.0f; + int cel_type = (int)s_read_uint16(s); + s_skip(s, 7); // For future (set to zero). + switch (cel_type) { + case 0: // Raw cel. + cel->w = s_read_uint16(s); + cel->h = s_read_uint16(s); + cel->pixels = CUTE_ASEPRITE_ALLOC(cel->w * cel->h * bpp, mem_ctx); + CUTE_ASEPRITE_MEMCPY(cel->pixels, s->in, (size_t)(cel->w * cel->h * bpp)); + s_skip(s, cel->w * cel->h * bpp); + break; + + case 1: // Linked cel. + cel->is_linked = 1; + cel->linked_frame_index = s_read_uint16(s); + break; + + case 2: // Compressed image cel. + { + cel->w = s_read_uint16(s); + cel->h = s_read_uint16(s); + int zlib_byte0 = s_read_uint8(s); + int zlib_byte1 = s_read_uint8(s); + int deflate_bytes = (int)chunk_size - (int)(s->in - chunk_start); + void* pixels = s->in; + CUTE_ASEPRITE_ASSERT((zlib_byte0 & 0x0F) == 0x08); // Only zlib compression method (RFC 1950) is supported. + CUTE_ASEPRITE_ASSERT((zlib_byte0 & 0xF0) <= 0x70); // Innapropriate window size detected. + CUTE_ASEPRITE_ASSERT(!(zlib_byte1 & 0x20)); // Preset dictionary is present and not supported. + int pixels_sz = cel->w * cel->h * bpp; + void* pixels_decompressed = CUTE_ASEPRITE_ALLOC(pixels_sz, mem_ctx); + int ret = s_inflate(pixels, deflate_bytes, pixels_decompressed, pixels_sz, mem_ctx); + if (!ret) CUTE_ASEPRITE_WARNING(s_error_reason); + cel->pixels = pixels_decompressed; + s_skip(s, deflate_bytes); + } break; + } + last_udata = &cel->udata; + } break; + + case 0x2006: // Cel extra chunk. + { + ase_cel_t* cel = frame->cels + frame->cel_count; + cel->has_extra = 1; + cel->extra.precise_bounds_are_set = (int)s_read_uint32(s); + cel->extra.precise_x = s_read_fixed(s); + cel->extra.precise_y = s_read_fixed(s); + cel->extra.w = s_read_fixed(s); + cel->extra.h = s_read_fixed(s); + s_skip(s, 16); // For future use (set to zero). + } break; + + case 0x2007: // Color profile chunk. + { + ase->has_color_profile = 1; + ase->color_profile.type = (ase_color_profile_type_t)s_read_uint16(s); + ase->color_profile.use_fixed_gamma = (int)s_read_uint16(s) & 1; + ase->color_profile.gamma = s_read_fixed(s); + s_skip(s, 8); // For future use (set to zero). + if (ase->color_profile.type == ASE_COLOR_PROFILE_TYPE_EMBEDDED_ICC) { + // Use the embedded ICC profile. + ase->color_profile.icc_profile_data_length = s_read_uint32(s); + ase->color_profile.icc_profile_data = CUTE_ASEPRITE_ALLOC(ase->color_profile.icc_profile_data_length, mem_ctx); + CUTE_ASEPRITE_MEMCPY(ase->color_profile.icc_profile_data, s->in, ase->color_profile.icc_profile_data_length); + s->in += ase->color_profile.icc_profile_data_length; + } + } break; + + case 0x2018: // Tags chunk. + { + ase->tag_count = (int)s_read_uint16(s); + s_skip(s, 8); // For future (set to zero). + CUTE_ASEPRITE_ASSERT(ase->tag_count < CUTE_ASEPRITE_MAX_TAGS); + for (int k = 0; k < ase->tag_count; ++k) { + ase->tags[k].from_frame = (int)s_read_uint16(s); + ase->tags[k].to_frame = (int)s_read_uint16(s); + ase->tags[k].loop_animation_direction = (ase_animation_direction_t)s_read_uint8(s); + ase->tags[k].repeat = s_read_uint16(s); + s_skip(s, 6); // For future (set to zero). + ase->tags[k].r = s_read_uint8(s); + ase->tags[k].g = s_read_uint8(s); + ase->tags[k].b = s_read_uint8(s); + s_skip(s, 1); // Extra byte (zero). + ase->tags[k].name = s_read_string(s); + } + was_on_tags = 1; + } break; + + case 0x2019: // Palette chunk. + { + ase->palette.entry_count = (int)s_read_uint32(s); + CUTE_ASEPRITE_ASSERT(ase->palette.entry_count <= CUTE_ASEPRITE_MAX_PALETTE_ENTRIES); + int first_index = (int)s_read_uint32(s); + int last_index = (int)s_read_uint32(s); + s_skip(s, 8); // For future (set to zero). + for (int k = first_index; k <= last_index; ++k) { + int has_name = s_read_uint16(s); + ase_palette_entry_t entry; + entry.color.r = s_read_uint8(s); + entry.color.g = s_read_uint8(s); + entry.color.b = s_read_uint8(s); + entry.color.a = s_read_uint8(s); + if (has_name) { + entry.color_name = s_read_string(s); + } else { + entry.color_name = NULL; + } + CUTE_ASEPRITE_ASSERT(k < CUTE_ASEPRITE_MAX_PALETTE_ENTRIES); + ase->palette.entries[k] = entry; + } + } break; + + case 0x2020: // Udata chunk. + { + CUTE_ASEPRITE_ASSERT(last_udata || was_on_tags); + if (was_on_tags && !last_udata) { + CUTE_ASEPRITE_ASSERT(tag_index < ase->tag_count); + last_udata = &ase->tags[tag_index++].udata; + } + int flags = (int)s_read_uint32(s); + if (flags & 1) { + last_udata->has_text = 1; + last_udata->text = s_read_string(s); + } + if (flags & 2) { + last_udata->color.r = s_read_uint8(s); + last_udata->color.g = s_read_uint8(s); + last_udata->color.b = s_read_uint8(s); + last_udata->color.a = s_read_uint8(s); + } + last_udata = NULL; + } break; + + case 0x2022: // Slice chunk. + { + int slice_count = (int)s_read_uint32(s); + int flags = (int)s_read_uint32(s); + s_skip(s, sizeof(uint32_t)); // Reserved. + const char* name = s_read_string(s); + for (int k = 0; k < (int)slice_count; ++k) { + ase_slice_t slice; + CUTE_ASEPRITE_MEMSET(&slice, 0, sizeof(slice)); + slice.name = name; + slice.frame_number = (int)s_read_uint32(s); + slice.origin_x = (int)s_read_int32(s); + slice.origin_y = (int)s_read_int32(s); + slice.w = (int)s_read_uint32(s); + slice.h = (int)s_read_uint32(s); + if (flags & 1) { + // It's a 9-patches slice. + slice.has_center_as_9_slice = 1; + slice.center_x = (int)s_read_int32(s); + slice.center_y = (int)s_read_int32(s); + slice.center_w = (int)s_read_uint32(s); + slice.center_h = (int)s_read_uint32(s); + } + if (flags & 2) { + // Has pivot information. + slice.has_pivot = 1; + slice.pivot_x = (int)s_read_int32(s); + slice.pivot_y = (int)s_read_int32(s); + } + CUTE_ASEPRITE_ASSERT(ase->slice_count < CUTE_ASEPRITE_MAX_SLICES); + ase->slices[ase->slice_count++] = slice; + last_udata = &ase->slices[ase->slice_count - 1].udata; + } + } break; + + default: + s_skip(s, (int)chunk_size); + break; + } + + uint32_t size_read = (uint32_t)(s->in - chunk_start); + CUTE_ASEPRITE_ASSERT(size_read == chunk_size); + } + } + + // Blend all cel pixels into each of their respective frames, for convenience. + for (int i = 0; i < ase->frame_count; ++i) { + ase_frame_t* frame = ase->frames + i; + frame->pixels = (ase_color_t*)CUTE_ASEPRITE_ALLOC((int)(sizeof(ase_color_t)) * ase->w * ase->h, mem_ctx); + CUTE_ASEPRITE_MEMSET(frame->pixels, 0, sizeof(ase_color_t) * (size_t)ase->w * (size_t)ase->h); + ase_color_t* dst = frame->pixels; + for (int j = 0; j < frame->cel_count; ++j) { + ase_cel_t* cel = frame->cels + j; + if (!(cel->layer->flags & ASE_LAYER_FLAGS_VISIBLE)) { + continue; + } + if (cel->layer->parent && !(cel->layer->parent->flags & ASE_LAYER_FLAGS_VISIBLE)) { + continue; + } + while (cel->is_linked) { + ase_frame_t* frame = ase->frames + cel->linked_frame_index; + int found = 0; + for (int k = 0; k < frame->cel_count; ++k) { + if (frame->cels[k].layer == cel->layer) { + cel = frame->cels + k; + found = 1; + break; + } + } + CUTE_ASEPRITE_ASSERT(found); + } + void* src = cel->pixels; + uint8_t opacity = (uint8_t)(cel->opacity * cel->layer->opacity * 255.0f); + int cx = cel->x; + int cy = cel->y; + int cw = cel->w; + int ch = cel->h; + int cl = -s_min(cx, 0); + int ct = -s_min(cy, 0); + int dl = s_max(cx, 0); + int dt = s_max(cy, 0); + int dr = s_min(ase->w, cw + cx); + int db = s_min(ase->h, ch + cy); + int aw = ase->w; + for (int dx = dl, sx = cl; dx < dr; dx++, sx++) { + for (int dy = dt, sy = ct; dy < db; dy++, sy++) { + int dst_index = aw * dy + dx; + ase_color_t src_color = s_color(ase, src, cw * sy + sx); + ase_color_t dst_color = dst[dst_index]; + ase_color_t result = s_blend(src_color, dst_color, opacity); + dst[dst_index] = result; + } + } + } + } + + ase->mem_ctx = mem_ctx; + return ase; +} + +void cute_aseprite_free(ase_t* ase) +{ + for (int i = 0; i < ase->frame_count; ++i) { + ase_frame_t* frame = ase->frames + i; + CUTE_ASEPRITE_FREE(frame->pixels, ase->mem_ctx); + for (int j = 0; j < frame->cel_count; ++j) { + ase_cel_t* cel = frame->cels + j; + CUTE_ASEPRITE_FREE(cel->pixels, ase->mem_ctx); + CUTE_ASEPRITE_FREE((void*)cel->udata.text, ase->mem_ctx); + } + } + for (int i = 0; i < ase->layer_count; ++i) { + ase_layer_t* layer = ase->layers + i; + CUTE_ASEPRITE_FREE((void*)layer->name, ase->mem_ctx); + CUTE_ASEPRITE_FREE((void*)layer->udata.text, ase->mem_ctx); + } + for (int i = 0; i < ase->tag_count; ++i) { + ase_tag_t* tag = ase->tags + i; + CUTE_ASEPRITE_FREE((void*)tag->name, ase->mem_ctx); + } + for (int i = 0; i < ase->slice_count; ++i) { + ase_slice_t* slice = ase->slices + i; + CUTE_ASEPRITE_FREE((void*)slice->udata.text, ase->mem_ctx); + } + if (ase->slice_count) { + CUTE_ASEPRITE_FREE((void*)ase->slices[0].name, ase->mem_ctx); + } + for (int i = 0; i < ase->palette.entry_count; ++i) { + CUTE_ASEPRITE_FREE((void*)ase->palette.entries[i].color_name, ase->mem_ctx); + } + CUTE_ASEPRITE_FREE(ase->color_profile.icc_profile_data, ase->mem_ctx); + CUTE_ASEPRITE_FREE(ase->frames, ase->mem_ctx); + CUTE_ASEPRITE_FREE(ase, ase->mem_ctx); +} + +#endif // CUTE_ASEPRITE_IMPLEMENTATION_ONCE +#endif // CUTE_ASEPRITE_IMPLEMENTATION + +/* + ------------------------------------------------------------------------------ + This software is available under 2 licenses - you may choose the one you like. + ------------------------------------------------------------------------------ + ALTERNATIVE A - zlib license + Copyright (c) 2017 Randy Gaul http://www.randygaul.net + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from + the use of this software. + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + ------------------------------------------------------------------------------ + ALTERNATIVE B - Public Domain (www.unlicense.org) + This is free and unencumbered software released into the public domain. + Anyone is free to copy, modify, publish, use, compile, sell, or distribute this + software, either in source code form or as a compiled binary, for any purpose, + commercial or non-commercial, and by any means. + In jurisdictions that recognize copyright laws, the author or authors of this + software dedicate any and all copyright interest in the software to the public + domain. We make this dedication for the benefit of the public at large and to + the detriment of our heirs and successors. We intend this dedication to be an + overt act of relinquishment in perpetuity of all present and future rights to + this software under copyright law. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ------------------------------------------------------------------------------ +*/ diff --git a/src/main.zig b/src/main.zig index 9d84e7e..192c320 100644 --- a/src/main.zig +++ b/src/main.zig @@ -174,6 +174,21 @@ pub fn main() !void { ); defer app.deinit(); + if (builtin.mode == .Debug) { + { + const sample_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin", .{}); + defer sample_file.close(); + try app.appendChannelFromFile(sample_file); + } + + { + const sample_file = try std.fs.cwd().openFile("samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_IjStim.bin", .{}); + defer sample_file.close(); + try app.appendChannelFromFile(sample_file); + } + } + + while (!rl.windowShouldClose()) { try app.tick(); } diff --git a/src/graph.zig b/src/ui/graph.zig similarity index 83% rename from src/graph.zig rename to src/ui/graph.zig index cceda15..49ff12b 100644 --- a/src/graph.zig +++ b/src/ui/graph.zig @@ -1,14 +1,25 @@ +const builtin = @import("builtin"); const std = @import("std"); const rl = @import("raylib"); -const Theme = @import("./theme.zig"); +const Theme = @import("../theme.zig"); +const UI = @import("./root.zig"); +const remap = @import("../utils.zig").remap; const assert = std.debug.assert; const Vec2 = rl.Vector2; const clamp = std.math.clamp; +const disable_caching = false; + +comptime { + if (builtin.mode != .Debug) { + assert(disable_caching == false); + } +} + pub const ViewOptions = struct { from: f32, // inclusive - to: f32, // exclusive + to: f32, // inclusive min_value: f64, max_value: f64, left_aligned: bool = true, @@ -31,12 +42,25 @@ pub const Cache = struct { pub fn invalidate(self: *Cache) void { self.options = null; } -}; -fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T { - const t = (value - from_min) / (from_max - from_min); - return std.math.lerp(to_min, to_max, t); -} + pub fn draw(self: Cache, rect: rl.Rectangle) void { + if (self.texture) |texture| { + const source = rl.Rectangle{ + .x = 0, + .y = 0, + .width = @floatFromInt(texture.texture.width), + .height = @floatFromInt(-texture.texture.height) + }; + rl.drawTexturePro( + texture.texture, + source, + rect, + rl.Vector2.zero(), + 0, rl.Color.white + ); + } + } +}; fn mapSampleX(draw_rect: rl.Rectangle, view_rect: ViewOptions, index: f64) f64 { return remap( @@ -73,9 +97,9 @@ fn clampIndexUsize(value: f32, size: usize) usize { return @intFromFloat(clamp(value, 0, size_f32)); } -fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { +fn drawSamples(ui: *UI, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { assert(options.left_aligned); // TODO: - assert(options.to > options.from); + assert(options.to >= options.from); if (options.from > @as(f32, @floatFromInt(samples.len))) return; if (options.to < 0) return; @@ -130,13 +154,8 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f } } } else { - rl.beginScissorMode( - @intFromFloat(@round(draw_rect.x)), - @intFromFloat(@round(draw_rect.y)), - @intFromFloat(@round(draw_rect.width)), - @intFromFloat(@round(draw_rect.height)) - ); - defer rl.endScissorMode(); + ui.beginScissorModeRect(draw_rect); + defer ui.endScissorMode(); { const from_index = clampIndexUsize(@floor(options.from), samples.len); @@ -166,7 +185,12 @@ fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f } } -pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { +pub fn draw(ui: *UI, cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void { + if (disable_caching) { + drawSamples(ui, draw_rect, options, samples); + return; + } + if (cache) |c| { const render_width: i32 = @intFromFloat(@ceil(draw_rect.width)); const render_height: i32 = @intFromFloat(@ceil(draw_rect.height)); @@ -191,35 +215,26 @@ pub fn draw(cache: ?*Cache, draw_rect: rl.Rectangle, options: ViewOptions, sampl const render_texture = c.texture.?; if (c.options != null and std.meta.eql(c.options.?, options)) { - const source = rl.Rectangle{ - .x = 0, - .y = 0, - .width = @floatFromInt(render_texture.texture.width), - .height = @floatFromInt(-render_texture.texture.height) - }; - rl.drawTexturePro( - render_texture.texture, - source, - draw_rect, - rl.Vector2.zero(), - 0, rl.Color.white - ); + c.draw(draw_rect); return; } c.options = options; render_texture.begin(); - rl.gl.rlPushMatrix(); - rl.gl.rlTranslatef(-draw_rect.x, -draw_rect.y, 0); + ui.pushTransform(); + ui.transformTranslate(-draw_rect.x, -draw_rect.y); + rl.clearBackground(rl.Color.black.alpha(0)); } - drawSamples(draw_rect, options, samples); + drawSamples(ui, draw_rect, options, samples); if (cache) |c| { - rl.gl.rlPopMatrix(); + ui.popTransform(); const render_texture = c.texture.?; render_texture.end(); + + c.draw(draw_rect); } } \ No newline at end of file diff --git a/src/utils.zig b/src/utils.zig index beb3ad5..a1af17d 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -23,4 +23,9 @@ pub fn drawUnderline(rect: rl.Rectangle, size: f32, color: rl.Color) void { .width = rect.width, .height = size }, color); -} \ No newline at end of file +} + +pub fn remap(comptime T: type, from_min: T, from_max: T, to_min: T, to_max: T, value: T) T { + const t = (value - from_min) / (from_max - from_min); + return std.math.lerp(to_min, to_max, t); +}