diff --git a/include/compositor.h b/include/compositor.h index 8bfac3b..8ccc833 100644 --- a/include/compositor.h +++ b/include/compositor.h @@ -24,6 +24,18 @@ enum easing_t { EASE_IN_OUT_CUBIC }; +enum anchor_point_t { + ANCHOR_TOP_LEFT, + ANCHOR_TOP_CENTER, + ANCHOR_TOP_RIGHT, + ANCHOR_CENTER_LEFT, + ANCHOR_CENTER, + ANCHOR_CENTER_RIGHT, + ANCHOR_BOTTOM_LEFT, + ANCHOR_BOTTOM_CENTER, + ANCHOR_BOTTOM_RIGHT, +}; + extern "C" { typedef struct compositor_t compositor_t; @@ -39,7 +51,8 @@ uint32_t compositor_insert(compositor_t* comp, uint32_t fb_width, uint32_t fb_height, int32_t x, int32_t y, - int32_t z_order, float scale = 1.0f); + int32_t z_order, float scale, + anchor_point_t anchor); // Remove element by ID void compositor_remove(compositor_t* comp, uint32_t id); diff --git a/src/compositor.cpp b/src/compositor.cpp index f59a865..f0a483b 100644 --- a/src/compositor.cpp +++ b/src/compositor.cpp @@ -70,6 +70,7 @@ struct element_t { bool visible; float scale; float alpha = 1.0f; + anchor_point_t anchor; // Anchor point for positioning and scaling uint32_t id; // Unique identifier std::vector occupied_tiles; // List of occupied tiles animation_t anim; @@ -154,29 +155,35 @@ static inline void get_element_bounds(const element_t* elem, int32_t& out_x0, int32_t& out_y0, int32_t& out_x1, int32_t& out_y1) { - if (elem->scale == 1.0f) { - // Fast path for 1.0 scale - out_x0 = elem->x; - out_y0 = elem->y; - out_x1 = elem->x + (int32_t)elem->fb_width; - out_y1 = elem->y + (int32_t)elem->fb_height; + // Calculate scaled dimensions, rounding to nearest integer to prevent jitter + int32_t scaled_width = (int32_t)std::round(elem->fb_width * elem->scale); + int32_t scaled_height = (int32_t)std::round(elem->fb_height * elem->scale); + + float anchor_x_factor = 0.0f; + float anchor_y_factor = 0.0f; + + // Determine the anchor's proportional position within the element + switch (elem->anchor) { + case ANCHOR_TOP_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 0.0f; break; + case ANCHOR_TOP_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 0.0f; break; + case ANCHOR_TOP_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 0.0f; break; + case ANCHOR_CENTER_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 0.5f; break; + case ANCHOR_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 0.5f; break; + case ANCHOR_CENTER_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 0.5f; break; + case ANCHOR_BOTTOM_LEFT: anchor_x_factor = 0.0f; anchor_y_factor = 1.0f; break; + case ANCHOR_BOTTOM_CENTER: anchor_x_factor = 0.5f; anchor_y_factor = 1.0f; break; + case ANCHOR_BOTTOM_RIGHT: anchor_x_factor = 1.0f; anchor_y_factor = 1.0f; break; } - else { - // Scaled path - int32_t scaled_width = (int32_t)(elem->fb_width * elem->scale); - int32_t scaled_height = (int32_t)(elem->fb_height * elem->scale); - // Apply anchor point transformation (bottom-center) - float x_offset = (elem->fb_width * 0.5f) * (1.0f - elem->scale); - float y_offset = (elem->fb_height) * (1.0f - elem->scale); + // The element's (x, y) represents the anchor point's position on screen. + // We calculate the top-left corner (out_x0, out_y0) by offsetting + // from the anchor's position. + out_x0 = elem->x - (int32_t)std::round(scaled_width * anchor_x_factor); + out_y0 = elem->y - (int32_t)std::round(scaled_height * anchor_y_factor); - // Use std::round to avoid 1-pixel jiggle - out_x0 = elem->x + (int32_t)std::round(x_offset); - out_y0 = elem->y + (int32_t)std::round(y_offset); - - out_x1 = out_x0 + scaled_width; - out_y1 = out_y0 + scaled_height; - } + // The bottom-right corner is the top-left plus the scaled dimensions. + out_x1 = out_x0 + scaled_width; + out_y1 = out_y0 + scaled_height; } // --- Spatial Hashing & Dirty Rects --- @@ -256,8 +263,6 @@ static void update_spatial_hash(compositor_t* comp, element_t* elem) { } } -// --- Draw Order --- - // Uses an incremental insertion sort to place an element in the correct z-order position. static void update_draw_order(compositor_t* comp, uint32_t id) { element_t* elem = find_element(comp, id); @@ -286,12 +291,8 @@ static void update_draw_order(compositor_t* comp, uint32_t id) { comp->draw_order.insert(it_insert, id); } -// --- Public C-Style API Functions --- - extern "C" { -// --- Lifecycle --- - // Advances all animations by a given time delta. void compositor_tick(compositor_t* comp, float delta_time_sec) { if (!comp) return; @@ -448,15 +449,14 @@ void compositor_destroy(compositor_t* comp) { delete comp; // Uses the default C++ destructor for cleanup } -// --- Element Management --- - // Inserts a new element into the compositor and returns a unique ID. uint32_t compositor_insert(compositor_t* comp, const uint32_t* fb, uint32_t fb_width, uint32_t fb_height, int32_t x, int32_t y, - int32_t z_order, float scale) { + int32_t z_order, float scale, + anchor_point_t anchor) { if (!comp) { // Cannot set error if comp is NULL, must return 0 return 0; @@ -483,6 +483,7 @@ uint32_t compositor_insert(compositor_t* comp, .z_order = z_order, .visible = true, .scale = scale, + .anchor = anchor, .id = id }; comp->elements_map[id] = new_elem; @@ -567,9 +568,7 @@ void compositor_update(compositor_t* comp, uint32_t id, comp->last_error.clear(); } -// --- Optimized Setters --- - -// Optimized setter for an element's (x, y) position. +// setter for an element's (x, y) position. void compositor_set_position(compositor_t* comp, uint32_t id, int32_t x, int32_t y) { if (!comp) return; @@ -589,7 +588,7 @@ void compositor_set_position(compositor_t* comp, uint32_t id, comp->last_error.clear(); } -// Optimized setter for an element's z-order. +// setter for an element's z-order. void compositor_set_zorder(compositor_t* comp, uint32_t id, int32_t z_order) { if (!comp) return; @@ -606,7 +605,7 @@ void compositor_set_zorder(compositor_t* comp, uint32_t id, comp->last_error.clear(); } -// Optimized setter for an element's visibility. +// setter for an element's visibility. void compositor_set_visible(compositor_t* comp, uint32_t id, bool visible) { if (!comp) return; @@ -629,7 +628,7 @@ void compositor_set_visible(compositor_t* comp, uint32_t id, comp->last_error.clear(); } -// Optimized setter for an element's scale. +// setter for an element's scale. void compositor_set_scale(compositor_t* comp, uint32_t id, float scale) { if (!comp) return; @@ -654,7 +653,7 @@ void compositor_set_scale(compositor_t* comp, uint32_t id, comp->last_error.clear(); } -// Optimized setter for an element's alpha. +// setter for an element's alpha. void compositor_set_alpha(compositor_t* comp, uint32_t id, float alpha) { if (!comp) return; element_t* elem = find_element(comp, id); @@ -679,7 +678,30 @@ void compositor_set_alpha(compositor_t* comp, uint32_t id, float alpha) { } } -// --- Animation --- +// setter for an element's anchor point. +void compositor_set_anchor(compositor_t* comp, uint32_t id, anchor_point_t anchor) { + if (!comp) return; + element_t* elem = find_element(comp, id); + if (!elem) { + set_error(comp, "Element with ID " + std::to_string(id) + " not found to set anchor."); + return; + } + if (elem->anchor == anchor) { + comp->last_error.clear(); + return; + } + + // Changing anchor changes bounds if scale != 1.0, so treat it like a move/scale. + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + + elem->anchor = anchor; + + update_spatial_hash(comp, elem); + + // Clear last error on success + comp->last_error.clear(); +} void compositor_animate_move(compositor_t* comp, uint32_t id, int32_t to_x, int32_t to_y, @@ -1060,7 +1082,7 @@ const uint32_t* compositor_compose(compositor_t* comp) { // --- Blending Loop (with SIMD) --- int32_t x = blend_x0; -#if defined(__SSE2__) || (defined(_M_AMD64) || defined(_M_X64)) +#if defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(_M_IX86) // Pre-calculate fixed values for the SIMD loop const __m128i v_zero = _mm_setzero_si128(); const __m128i v_one = _mm_set1_epi16(1); diff --git a/src/main.cpp b/src/main.cpp index 654f4c1..b9558f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,8 +13,16 @@ int main() { const int HEIGHT = 720; const int CHAR_WIDTH = 400; const int CHAR_HEIGHT = 600; - const int CHAR_Y_OFFSET = 140; - const int POS_Y = HEIGHT - CHAR_HEIGHT - CHAR_Y_OFFSET; + + // --- New Character Positioning Constants --- + // The vertical baseline for character sprites, making positioning independent of sprite height. + const int CHAR_BASELINE_Y = HEIGHT - 140; + // The horizontal center for the character on the left. + const int LEFT_CHAR_CENTER_X = 60 + (CHAR_WIDTH / 2); + // The horizontal center for the character on the right. + const int RIGHT_CHAR_CENTER_X = WIDTH - 60 - (CHAR_WIDTH / 2); + // An off-screen position to the left for transitions, based on the final position. + const int OFFSCREEN_LEFT_X = LEFT_CHAR_CENTER_X - 660; window_t* window = window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine"); if (!window) { @@ -31,66 +39,72 @@ int main() { uint32_t* bg = image_load_resized("assets/background.png", WIDTH, HEIGHT); if (bg) { - uint32_t bg_id = compositor_insert(compositor, bg, WIDTH, HEIGHT, 0, 0, 0); + uint32_t bg_id = compositor_insert(compositor, bg, WIDTH, HEIGHT, 0, 0, 0, 1.0f, ANCHOR_TOP_LEFT); compositor_set_alpha(compositor, bg_id, 1.0f); } uint32_t* textbox = image_load_resized("assets/textbox.png", WIDTH, 180); if (textbox) { - compositor_insert(compositor, textbox, WIDTH, 180, 0, HEIGHT - 180, 3); + // Position via the bottom-left corner for easier alignment with screen bottom + compositor_insert(compositor, textbox, WIDTH, 180, 0, HEIGHT, 3, 1.0f, ANCHOR_BOTTOM_LEFT); } uint32_t* maksw_happy = image_load_resized("assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT); + // Insert off-screen left, anchored at bottom-center for natural scaling uint32_t maksw_happy_id = compositor_insert(compositor, maksw_happy, - CHAR_WIDTH, CHAR_HEIGHT, -600, POS_Y, 1); + CHAR_WIDTH, CHAR_HEIGHT, OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); - uint32_t* maksw_neutral = image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT); + uint32_t* maksw_neutral = image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT); + // Insert at the final left position, anchored at bottom-center uint32_t maksw_neutral_id = compositor_insert(compositor, maksw_neutral, - CHAR_WIDTH, CHAR_HEIGHT, 60, POS_Y, 1); - compositor_set_visible(compositor, maksw_neutral_id, false); + CHAR_WIDTH, CHAR_HEIGHT, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); + compositor_set_visible(compositor, maksw_neutral_id, false); uint32_t* wikor = image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT); - uint32_t wikor_id = compositor_insert(compositor, wikor, - CHAR_WIDTH, CHAR_HEIGHT, WIDTH - CHAR_WIDTH - 60, POS_Y, 2); + // Insert at the final right position, anchored at bottom-center + uint32_t wikor_id = compositor_insert(compositor, wikor, + CHAR_WIDTH, CHAR_HEIGHT, RIGHT_CHAR_CENTER_X, CHAR_BASELINE_Y, 2, 1.0f, ANCHOR_BOTTOM_CENTER); text_t* name_text = text_init(300, 100); - text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f); + text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f); + // Text is anchored at top-left, positioned relative to textbox uint32_t name_id = compositor_insert(compositor, text_getfb(name_text), - 300, 100, 60, HEIGHT - 180, 4); + 300, 100, 60, HEIGHT - 180, 4, 1.0f, ANCHOR_TOP_LEFT); text_t* dialogue_text = text_init(WIDTH - 140, 160); - text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f); + text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f); + // Text is anchored at top-left, positioned relative to textbox uint32_t dialogue_id = compositor_insert(compositor, text_getfb(dialogue_text), - WIDTH - 140, 160, 60, HEIGHT - 130, 4); + WIDTH - 140, 160, 60, HEIGHT - 130, 4, 1.0f, ANCHOR_TOP_LEFT); - uint32_t* mask = image_load_resized("assets/mask.png", 1280, 1080); - // okay so this requires some math - // inserting at 0,0 (top-left anchor) means it will cover the whole screen - // we need to insert it above the screen, so -1080 - uint32_t mask_id = compositor_insert(compositor, mask, 1280, 1080, 0, -1080, 5); - compositor_set_visible(compositor, mask_id, false); + uint32_t* mask = image_load_resized("assets/mask.png", 1280, 1080); + // Use top-left anchor for full-screen wipes + uint32_t mask_id = compositor_insert(compositor, mask, 1280, 1080, 0, -1080, 5, 1.0f, ANCHOR_TOP_LEFT); + compositor_set_visible(compositor, mask_id, false); - uint32_t* mask2 = image_load_resized("assets/mask2.png", 1280, 1080); - // here, we put it in the center, as this is the slide out - uint32_t mask2_id = compositor_insert(compositor, mask2, 1280, 1080, 0, 0, 5); - compositor_set_visible(compositor, mask2_id, false); + uint32_t* mask2 = image_load_resized("assets/mask2.png", 1280, 1080); + // Use top-left anchor for full-screen wipes + uint32_t mask2_id = compositor_insert(compositor, mask2, 1280, 1080, 0, 0, 5, 1.0f, ANCHOR_TOP_LEFT); + compositor_set_visible(compositor, mask2_id, false); - uint32_t* black = (uint32_t*)malloc(WIDTH * HEIGHT * sizeof(uint32_t)); + uint32_t* black = (uint32_t*)malloc(WIDTH * HEIGHT * sizeof(uint32_t)); for (int i = 0; i < WIDTH * HEIGHT; i++) { black[i] = 0xFF000000; } - uint32_t black_id = compositor_insert(compositor, black, WIDTH, HEIGHT, 0, 0, 6); - compositor_set_visible(compositor, black_id, false); - compositor_set_alpha(compositor, black_id, 0.0f); + // Use top-left anchor for full-screen fade + uint32_t black_id = compositor_insert(compositor, black, WIDTH, HEIGHT, 0, 0, 6, 1.0f, ANCHOR_TOP_LEFT); + compositor_set_visible(compositor, black_id, false); + compositor_set_alpha(compositor, black_id, 0.0f); - compositor_animate_move(compositor, maksw_happy_id, 60, POS_Y, 1.0f, EASE_OUT_CUBIC); - compositor_animate_scale(compositor, maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, true); - text_push(name_text, "maksw", 0.0f, true); + // Animate character into position using the new centered coordinates + compositor_animate_move(compositor, maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1.0f, EASE_OUT_CUBIC); + compositor_animate_scale(compositor, maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, true); + text_push(name_text, "maksw", 0.0f, true); text_push(dialogue_text, "Hey there! Welcome to the Obwarzanek Narrative Engine demo. " "This is a simple visual novel engine showcasing text rendering, " "image compositing, and basic animation features.", - 30.0f, true); + 30.0f, true); bool running = true; @@ -132,19 +146,19 @@ int main() { last_report_time = current_time; // Mark the current time as the new report start } - static bool mouse_was_down = false; - bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT); - bool mouse_clicked = mouse_is_down && !mouse_was_down; - mouse_was_down = mouse_is_down; + static bool mouse_was_down = false; + bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT); + bool mouse_clicked = mouse_is_down && !mouse_was_down; + mouse_was_down = mouse_is_down; - static bool key_was_down = false; - bool key_is_down = window_is_key_down(window, KEY_SPACE); - bool key_pressed = key_is_down && !key_was_down; - key_was_down = key_is_down; + static bool key_was_down = false; + bool key_is_down = window_is_key_down(window, KEY_SPACE); + bool key_pressed = key_is_down && !key_was_down; + key_was_down = key_is_down; // don't judge me, it will work for now - static unsigned int lines = 0; - static bool dialog_finished = false; + static unsigned int lines = 0; + static bool dialog_finished = false; static bool transitioning = false; if (mouse_clicked || key_pressed) { @@ -152,11 +166,12 @@ int main() { text_show_all_current(dialogue_text); } else { + // this will bite me switch (lines) { case 0: // line 2 - compositor_animation_complete_all(compositor); - compositor_animate_scale(compositor, maksw_happy_id, 1.0f, 0.2f, EASE_IN_OUT_CUBIC); + compositor_animation_complete_all(compositor); + compositor_animate_scale(compositor, maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); compositor_animate_scale(compositor, wikor_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC); text_clear(dialogue_text); text_clear(name_text); @@ -168,20 +183,21 @@ int main() { // line 3 compositor_animation_complete_all(compositor); - compositor_set_visible(compositor, mask_id, true); + compositor_set_visible(compositor, mask_id, true); compositor_animate_move(compositor, mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC, false); - transitioning = true; + transitioning = true; lines++; break; case 2: // (line 4), fade out compositor_animation_complete_all(compositor); + compositor_animate_scale(compositor, maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - compositor_set_visible(compositor, black_id, true); - compositor_animate_fade(compositor, black_id, 1.0f, 1.0f, EASE_IN_CUBIC, false); + compositor_set_visible(compositor, black_id, true); + compositor_animate_fade(compositor, black_id, 1.0f, 1.0f, EASE_IN_CUBIC, false); - dialog_finished = true; + dialog_finished = true; lines++; break; default: @@ -196,18 +212,18 @@ int main() { if (transitioning) { if (compositor_is_element_animating(compositor, mask_id) == false) { compositor_set_visible(compositor, maksw_happy_id, false); - compositor_set_visible(compositor, maksw_neutral_id, true); - compositor_animate_scale(compositor, maksw_neutral_id, 1.05f, 0.5f, EASE_IN_OUT_CUBIC); + compositor_set_visible(compositor, maksw_neutral_id, true); + compositor_set_scale(compositor, maksw_neutral_id, 1.05f); text_clear(dialogue_text); text_clear(name_text); text_push(name_text, "maksw", 0.0f, true); text_push(dialogue_text, "why so.. serious?", 30.0f, true); - compositor_set_visible(compositor, mask_id, false); + compositor_set_visible(compositor, mask_id, false); compositor_set_visible(compositor, mask2_id, true); - compositor_animate_move(compositor, mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC, false); + compositor_animate_move(compositor, mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC, false); - transitioning = false; + transitioning = false; } } @@ -215,7 +231,7 @@ int main() { compositor_mark_content_dirty(compositor, black_id); if (compositor_is_element_animating(compositor, black_id) == false) { running = false; - } + } } if (text_nextframe(name_text, delta_time_sec)) @@ -227,13 +243,14 @@ int main() { window_blit(window, fb); } + // no raii because i'm tired already if (maksw_happy) image_free(maksw_happy); - if (maksw_neutral) image_free(maksw_neutral); + if (maksw_neutral) image_free(maksw_neutral); if (wikor) image_free(wikor); - if (black) free(black); - if (mask) image_free(mask); - if (mask2) image_free(mask2); + if (black) free(black); + if (mask) image_free(mask); + if (mask2) image_free(mask2); if (bg) image_free(bg); if (textbox) image_free(textbox); @@ -244,4 +261,4 @@ int main() { window_destroy(window); return 0; -} +} \ No newline at end of file