diff --git a/include/audio.h b/include/audio.h index d7c1ddc..6382682 100644 --- a/include/audio.h +++ b/include/audio.h @@ -12,6 +12,7 @@ extern "C" { #endif typedef struct audio_t audio_t; +typedef uint32_t audio_stream_id_t; // If the return type is bool, false means failure. @@ -34,7 +35,7 @@ void audio_free_data(uint8_t* data); // data must remain valid until the stream ends or is stopped // loop: if true, the stream restarts on EOF // 0 means failure, valid IDs start from 1 -uint32_t audio_play(audio_t* audio, +audio_stream_id_t audio_play(audio_t* audio, const uint8_t* data, int size, int channels, @@ -42,22 +43,22 @@ uint32_t audio_play(audio_t* audio, bool loop); // Stop and remove stream by ID -bool audio_stop(audio_t* audio, uint32_t id); +bool audio_stop(audio_t* audio, audio_stream_id_t id); -bool audio_pause(audio_t* audio, uint32_t id); -bool audio_resume(audio_t* audio, uint32_t id); +bool audio_pause(audio_t* audio, audio_stream_id_t id); +bool audio_resume(audio_t* audio, audio_stream_id_t id); // Returns true if the stream is active (playing or paused) -bool audio_is_active(audio_t* audio, uint32_t id); +bool audio_is_active(audio_t* audio, audio_stream_id_t id); // Returns true if the stream is currently playing (not paused, not stopped) -bool audio_is_playing(audio_t* audio, uint32_t id); +bool audio_is_playing(audio_t* audio, audio_stream_id_t id); // Per-stream volume, immediate effect, range [0.0, 1.0] -bool audio_set_volume(audio_t* audio, uint32_t id, float volume); +bool audio_set_volume(audio_t* audio, audio_stream_id_t id, float volume); // Animate per-stream volume linearly over time, driven by audio_tick() -bool audio_animate_volume(audio_t* audio, uint32_t id, +bool audio_animate_volume(audio_t* audio, audio_stream_id_t id, float to_volume, float duration_sec); // Global volume, immediate effect, range [0.0, 1.0] diff --git a/include/compositor.h b/include/compositor.h index 6a49780..175a594 100644 --- a/include/compositor.h +++ b/include/compositor.h @@ -41,6 +41,7 @@ extern "C" { #endif typedef struct compositor_t compositor_t; +typedef uint32_t element_handle_t; // If the return type is bool, false means failure. @@ -55,7 +56,7 @@ void compositor_destroy(compositor_t* comp); // FB format is ARGB8888 and it always will be // The compositor takes a reference, caller must keep alive until removed // 0 means failure, valid IDs start from 1 -uint32_t compositor_insert(compositor_t* comp, +element_handle_t compositor_insert(compositor_t* comp, const uint32_t* fb, uint32_t fb_width, uint32_t fb_height, @@ -64,24 +65,18 @@ uint32_t compositor_insert(compositor_t* comp, anchor_point_t anchor); // Remove element by ID -bool compositor_remove(compositor_t* comp, uint32_t id); +bool compositor_remove(compositor_t* comp, element_handle_t id); -// AIO Update element properties, immediate effect -bool compositor_update(compositor_t* comp, uint32_t id, - int32_t x, int32_t y, - int32_t z_order, float scale, - bool visible, float alpha); - -// Individual setters, immediate effect -bool compositor_set_position(compositor_t* comp, uint32_t id, +// Setters, immediate effect +bool compositor_set_position(compositor_t* comp, element_handle_t id, int32_t x, int32_t y); -bool compositor_set_zorder(compositor_t* comp, uint32_t id, +bool compositor_set_zorder(compositor_t* comp, element_handle_t id, int32_t z_order); -bool compositor_set_scale(compositor_t* comp, uint32_t id, +bool compositor_set_scale(compositor_t* comp, element_handle_t id, float scale); -bool compositor_set_visible(compositor_t* comp, uint32_t id, +bool compositor_set_visible(compositor_t* comp, element_handle_t id, bool visible); -bool compositor_set_alpha(compositor_t* comp, uint32_t id, +bool compositor_set_alpha(compositor_t* comp, element_handle_t id, float alpha); // Advances all animations by a given time delta. @@ -91,31 +86,31 @@ void compositor_tick(compositor_t* comp, float delta_time_sec); // Queue means the animation will start after the previous animation completed // Not queueing will play the animation alongside any current animation // You cannot stack the same type of animation without queueing, the second call will cancel the first call. -bool compositor_animate_move(compositor_t* comp, uint32_t id, +bool compositor_animate_move(compositor_t* comp, element_handle_t id, int32_t to_x, int32_t to_y, - float duration_sec, easing_t easing, bool queue); -bool compositor_animate_fade(compositor_t* comp, uint32_t id, + float duration_sec, easing_t easing, bool queue, bool parallel); +bool compositor_animate_fade(compositor_t* comp, element_handle_t id, float to_alpha, - float duration_sec, easing_t easing, bool queue); -bool compositor_animate_scale(compositor_t* comp, uint32_t id, + float duration_sec, easing_t easing, bool queue, bool parallel); +bool compositor_animate_scale(compositor_t* comp, element_handle_t id, float to_scale, - float duration_sec, easing_t easing, bool queue); + float duration_sec, easing_t easing, bool queue, bool parallel); // Immediately finish all animations on a specific object (optionally only N queued, 0 = all), or every object. -bool compositor_animation_finish_element(compositor_t* comp, uint32_t id, int num_queued /*0=all*/); +bool compositor_animation_finish_element(compositor_t* comp, element_handle_t id, int num_queued /*0=all*/); void compositor_animation_finish_all(compositor_t* comp); // Returns true if any element is animating or has queued animations. bool compositor_is_animating(compositor_t* comp); // Returns true if the specified element is animating or has queued animations. -bool compositor_is_element_animating(compositor_t* comp, uint32_t id); +bool compositor_is_element_animating(compositor_t* comp, element_handle_t id); // Compose all visible elements and return framebuffer pointer const uint32_t* compositor_compose(compositor_t* comp); // Mark an element's content as dirty for redrawing -bool compositor_mark_content_dirty(compositor_t* comp, uint32_t id); +bool compositor_mark_content_dirty(compositor_t* comp, element_handle_t id); void compositor_mark_all_dirty(compositor_t* comp); #ifdef __cplusplus diff --git a/include/window.h b/include/window.h index 86e0327..af66b6a 100644 --- a/include/window.h +++ b/include/window.h @@ -53,6 +53,8 @@ extern "C" { #include #endif +typedef struct { int x, y; } mouse_pos_t; + // creates a window with given width, height and title, title can be utf8 window_t* window_create(int width, int height, const char* title); // destroys the window @@ -66,7 +68,7 @@ bool window_is_key_down(window_t* s, int key_scancode); // returns true if the given mouse button is currently held down bool window_is_mouse_button_down(window_t* s, int button_index); // gets the current mouse position in window coordinates -void window_get_mouse_position(window_t* s, int* x, int* y); +mouse_pos_t window_get_mouse_position(window_t* s); #ifdef __cplusplus } diff --git a/src/audio.cpp b/src/audio.cpp index 1e61149..0340e91 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -186,7 +186,7 @@ void audio_free_data(uint8_t* data) { } } -uint32_t audio_play(audio_t* audio, const uint8_t* data, int size, +audio_stream_id_t audio_play(audio_t* audio, const uint8_t* data, int size, int channels, int rate, bool loop) { if (!audio || !data) return 0; @@ -218,12 +218,12 @@ uint32_t audio_play(audio_t* audio, const uint8_t* data, int size, return id; } -bool audio_stop(audio_t* audio, uint32_t id) { +bool audio_stop(audio_t* audio, audio_stream_id_t id) { if (!audio) return false; return audio->streams.erase(id) > 0; // destructor closes the device } -bool audio_pause(audio_t* audio, uint32_t id) { +bool audio_pause(audio_t* audio, audio_stream_id_t id) { if (!audio) return false; auto it = audio->streams.find(id); if (it == audio->streams.end()) return false; @@ -235,7 +235,7 @@ bool audio_pause(audio_t* audio, uint32_t id) { return true; } -bool audio_resume(audio_t* audio, uint32_t id) { +bool audio_resume(audio_t* audio, audio_stream_id_t id) { if (!audio) return false; auto it = audio->streams.find(id); if (it == audio->streams.end()) return false; @@ -247,19 +247,19 @@ bool audio_resume(audio_t* audio, uint32_t id) { return true; } -bool audio_is_active(audio_t* audio, uint32_t id) { +bool audio_is_active(audio_t* audio, audio_stream_id_t id) { if (!audio) return false; return audio->streams.count(id) > 0; } -bool audio_is_playing(audio_t* audio, uint32_t id) { +bool audio_is_playing(audio_t* audio, audio_stream_id_t id) { if (!audio) return false; auto it = audio->streams.find(id); if (it == audio->streams.end()) return false; return !it->second.paused; } -bool audio_set_volume(audio_t* audio, uint32_t id, float volume) { +bool audio_set_volume(audio_t* audio, audio_stream_id_t id, float volume) { if (!audio) return false; auto it = audio->streams.find(id); if (it == audio->streams.end()) return false; @@ -269,7 +269,7 @@ bool audio_set_volume(audio_t* audio, uint32_t id, float volume) { return true; } -bool audio_animate_volume(audio_t* audio, uint32_t id, +bool audio_animate_volume(audio_t* audio, audio_stream_id_t id, float to_volume, float duration_sec) { if (!audio) return false; auto it = audio->streams.find(id); diff --git a/src/compositor.cpp b/src/compositor.cpp index e9917ff..f88a6ef 100644 --- a/src/compositor.cpp +++ b/src/compositor.cpp @@ -65,6 +65,7 @@ struct queued_animation_t { bool affects_position; bool affects_scale; bool affects_alpha; + bool parallel = false; }; struct element_t { @@ -250,7 +251,18 @@ static void update_spatial_hash(compositor_t* comp, element_t* elem) { for (int32_t ty = tile_y0; ty < tile_y1; ty++) { for (int32_t tx = tile_x0; tx < tile_x1; tx++) { uint32_t tile_idx = ty * comp->tiles_x + tx; - comp->tile_map[tile_idx].push_back(elem->id); + auto& tile_list = comp->tile_map[tile_idx]; + + auto it_insert = std::lower_bound( + tile_list.begin(), + tile_list.end(), + elem->z_order, + [comp](uint32_t current_id, int32_t target_z) { + return comp->elements_map[current_id].z_order < target_z; + } + ); + + tile_list.insert(it_insert, elem->id); elem->occupied_tiles.push_back(tile_idx); } } @@ -287,123 +299,145 @@ static void update_draw_order(compositor_t* comp, uint32_t id) { extern "C" { // Advances all animations by a given time delta. -void compositor_tick(compositor_t* comp, float delta_time_sec) { - if (!comp) return; + void compositor_tick(compositor_t* comp, float delta_time_sec) { + if (!comp) return; - for (auto& pair : comp->elements_map) { - element_t* elem = &pair.second; - if (!elem->valid) continue; - animation_t* anim = &elem->anim; + for (auto& pair : comp->elements_map) { + element_t* elem = &pair.second; + if (!elem->valid) continue; + animation_t* anim = &elem->anim; - // Check if we need to start a queued animation - if (anim->type == ANIM_NONE && !elem->anim_queue.empty()) { - queued_animation_t& next = elem->anim_queue.front(); - - anim->type = next.type; - anim->animating_position = next.affects_position; - anim->animating_scale = next.affects_scale; - anim->animating_alpha = next.affects_alpha; - - if (next.affects_position) { - anim->start_x = elem->x; - anim->start_y = elem->y; - anim->end_x = next.to_x; - anim->end_y = next.to_y; - anim->position_anim.progress_sec = 0.0f; - anim->position_anim.duration_sec = next.duration_sec; - anim->position_anim.easing = next.easing; + // Are we currently busy doing ANYTHING? + bool is_busy = anim->animating_position || anim->animating_alpha || anim->animating_scale; + + // If idle, pop the next block of queued animations! + if (!is_busy && !elem->anim_queue.empty()) { + + while (!elem->anim_queue.empty()) { + queued_animation_t& next = elem->anim_queue.front(); + + // Route the queued data to the active state + if (next.affects_position) { + anim->animating_position = true; + anim->start_x = elem->x; + anim->start_y = elem->y; + anim->end_x = next.to_x; + anim->end_y = next.to_y; + anim->position_anim.progress_sec = 0.0f; + anim->position_anim.duration_sec = next.duration_sec; + anim->position_anim.easing = next.easing; + } + if (next.affects_alpha) { + anim->animating_alpha = true; + anim->start_alpha = elem->alpha; + anim->end_alpha = next.to_alpha; + anim->alpha_anim.progress_sec = 0.0f; + anim->alpha_anim.duration_sec = next.duration_sec; + anim->alpha_anim.easing = next.easing; + // Auto-show if fading in from 0 + if (elem->alpha == 0.0f && next.to_alpha > 0.0f) { + elem->visible = true; + } + } + if (next.affects_scale) { + anim->animating_scale = true; + anim->start_scale = elem->scale; + anim->end_scale = next.to_scale; + anim->scale_anim.progress_sec = 0.0f; + anim->scale_anim.duration_sec = next.duration_sec; + anim->scale_anim.easing = next.easing; + } + + // Remove it from the queue + elem->anim_queue.erase(elem->anim_queue.begin()); + + // THE MAGIC TRICK: + // Look at the NEXT item. If it exists and wants to be parallel, + // loop around and pop it immediately! Otherwise, break and wait. + if (elem->anim_queue.empty() || !elem->anim_queue.front().parallel) { + break; + } + } + + // Re-evaluate busy status since we just popped new animations + is_busy = anim->animating_position || anim->animating_alpha || anim->animating_scale; } - if (next.affects_alpha) { - anim->start_alpha = elem->alpha; - anim->end_alpha = next.to_alpha; - anim->alpha_anim.progress_sec = 0.0f; - anim->alpha_anim.duration_sec = next.duration_sec; - anim->alpha_anim.easing = next.easing; - } - if (next.affects_scale) { - anim->start_scale = elem->scale; - anim->end_scale = next.to_scale; - anim->scale_anim.progress_sec = 0.0f; - anim->scale_anim.duration_sec = next.duration_sec; - anim->scale_anim.easing = next.easing; - } - - elem->anim_queue.erase(elem->anim_queue.begin()); - } - // Skip if nothing is animating - if (anim->type == ANIM_NONE) continue; + // Skip ticking if nothing is animating at all + if (!is_busy) continue; - mark_element_dirty(comp, elem); - bool position_changed = false; - bool all_done = true; - - // Update position animation with its own timing - if (anim->animating_position) { - anim->position_anim.progress_sec += delta_time_sec; - float t_raw = anim->position_anim.progress_sec / anim->position_anim.duration_sec; - - if (t_raw >= 1.0f) { - elem->x = anim->end_x; - elem->y = anim->end_y; - anim->animating_position = false; - } else { - float t_eased = apply_easing(anim->position_anim.easing, t_raw); - elem->x = (int32_t)std::round(lerp(static_cast(anim->start_x), - static_cast(anim->end_x), t_eased)); - elem->y = (int32_t)std::round(lerp(static_cast(anim->start_y), - static_cast(anim->end_y), t_eased)); - all_done = false; - } - position_changed = true; - } - - // Update alpha animation with its own timing - if (anim->animating_alpha) { - anim->alpha_anim.progress_sec += delta_time_sec; - float t_raw = anim->alpha_anim.progress_sec / anim->alpha_anim.duration_sec; - - if (t_raw >= 1.0f) { - elem->alpha = anim->end_alpha; - if (elem->alpha == 0.0f) elem->visible = false; - anim->animating_alpha = false; - } else { - float t_eased = apply_easing(anim->alpha_anim.easing, t_raw); - elem->alpha = lerp(anim->start_alpha, anim->end_alpha, t_eased); - all_done = false; - } - } - - // Update scale animation with its own timing - if (anim->animating_scale) { - anim->scale_anim.progress_sec += delta_time_sec; - float t_raw = anim->scale_anim.progress_sec / anim->scale_anim.duration_sec; - - if (t_raw >= 1.0f) { - elem->scale = anim->end_scale; - anim->animating_scale = false; - } else { - float t_eased = apply_easing(anim->scale_anim.easing, t_raw); - elem->scale = lerp(anim->start_scale, anim->end_scale, t_eased); - all_done = false; - } - position_changed = true; - } - - // If all animations are done, mark as complete - if (all_done) { - anim->type = ANIM_NONE; - } - - // Update spatial hash - if (position_changed) { - clear_spatial_hash(comp, elem->id); - update_spatial_hash(comp, elem); - } else { mark_element_dirty(comp, elem); + bool position_changed = false; + + // --- UPDATE ACTIVE ANIMATIONS --- + + // Update Position + if (anim->animating_position) { + anim->position_anim.progress_sec += delta_time_sec; + float t_raw = anim->position_anim.progress_sec / anim->position_anim.duration_sec; + + if (t_raw >= 1.0f) { + elem->x = anim->end_x; + elem->y = anim->end_y; + anim->animating_position = false; + } + else { + float t_eased = apply_easing(anim->position_anim.easing, t_raw); + // Note: using whatever lerp function you have in your engine + elem->x = anim->start_x + (anim->end_x - anim->start_x) * t_eased; + elem->y = anim->start_y + (anim->end_y - anim->start_y) * t_eased; + } + position_changed = true; + } + + // Update Alpha + if (anim->animating_alpha) { + anim->alpha_anim.progress_sec += delta_time_sec; + float t_raw = anim->alpha_anim.progress_sec / anim->alpha_anim.duration_sec; + + if (t_raw >= 1.0f) { + elem->alpha = anim->end_alpha; + if (elem->alpha <= 0.0f) elem->visible = false; + anim->animating_alpha = false; + } + else { + float t_eased = apply_easing(anim->alpha_anim.easing, t_raw); + elem->alpha = anim->start_alpha + (anim->end_alpha - anim->start_alpha) * t_eased; + } + } + + // Update Scale + if (anim->animating_scale) { + anim->scale_anim.progress_sec += delta_time_sec; + float t_raw = anim->scale_anim.progress_sec / anim->scale_anim.duration_sec; + + if (t_raw >= 1.0f) { + elem->scale = anim->end_scale; + anim->animating_scale = false; + } + else { + float t_eased = apply_easing(anim->scale_anim.easing, t_raw); + elem->scale = anim->start_scale + (anim->end_scale - anim->start_scale) * t_eased; + } + position_changed = true; + } + + // Cleanup empty state marker + if (!anim->animating_position && !anim->animating_alpha && !anim->animating_scale) { + anim->type = ANIM_NONE; + } + + // Update spatial hash + if (position_changed) { + clear_spatial_hash(comp, elem->id); + update_spatial_hash(comp, elem); + } + else { + // Just redrawing pixels, didn't move + mark_element_dirty(comp, elem); + } } } -} // Allocates and initializes a new compositor instance. compositor_t* compositor_init(uint32_t width, uint32_t height) { @@ -443,7 +477,7 @@ void compositor_destroy(compositor_t* comp) { } // Inserts a new element into the compositor and returns a unique ID. -uint32_t compositor_insert(compositor_t* comp, +element_handle_t compositor_insert(compositor_t* comp, const uint32_t* fb, uint32_t fb_width, uint32_t fb_height, @@ -490,7 +524,7 @@ uint32_t compositor_insert(compositor_t* comp, } // Removes an element from the compositor. -bool compositor_remove(compositor_t* comp, uint32_t id) { +bool compositor_remove(compositor_t* comp, element_handle_t id) { if (!comp) { return false; } @@ -521,52 +555,8 @@ bool compositor_remove(compositor_t* comp, uint32_t id) { return true; } -// Updates all properties of an element in a single "heavy" call. -bool compositor_update(compositor_t* comp, uint32_t id, - int32_t x, int32_t y, - int32_t z_order, float scale, - bool visible, float alpha) { - if (!comp) return false; - - element_t* elem = find_element(comp, id); - if (!elem) { - return false; - } - - // Mark old area dirty (based on current position/size) - mark_element_dirty(comp, elem); - clear_spatial_hash(comp, id); // Clear hash entries for old position/size - - // Update properties - elem->x = x; - elem->y = y; - - // Check for z_order change - bool z_order_changed = (elem->z_order != z_order); - elem->z_order = z_order; - - elem->scale = scale; - elem->visible = visible; - - // Update the spatial hash and mark the new area dirty - update_spatial_hash(comp, elem); - - // Update draw order if z_order changed - if (z_order_changed) { - update_draw_order(comp, id); - } - - elem->alpha = std::max(0.0f, std::min(1.0f, alpha)); - - if (elem->alpha == 0.0f) { - mark_element_dirty(comp, elem); - } - - return true; -} - // setter for an element's (x, y) position. -bool compositor_set_position(compositor_t* comp, uint32_t id, int32_t x, int32_t y) { +bool compositor_set_position(compositor_t* comp, element_handle_t id, int32_t x, int32_t y) { if (!comp) { return false; } @@ -588,22 +578,29 @@ bool compositor_set_position(compositor_t* comp, uint32_t id, int32_t x, int32_t } // setter for an element's z-order. -bool compositor_set_zorder(compositor_t* comp, uint32_t id, int32_t z_order) { +bool compositor_set_zorder(compositor_t* comp, element_handle_t id, int32_t z_order) { if (!comp) return false; element_t* elem = find_element(comp, id); if (!elem) { return false; } if (elem->z_order != z_order) { + // Must clear from spatial hash BEFORE changing z_order to safely re-insert + mark_element_dirty(comp, elem); + clear_spatial_hash(comp, id); + elem->z_order = z_order; update_draw_order(comp, id); + + // Re-add to spatial hash, which will now insert it using the new z_order + update_spatial_hash(comp, elem); } return true; } // setter for an element's visibility. -bool compositor_set_visible(compositor_t* comp, uint32_t id, bool visible) { +bool compositor_set_visible(compositor_t* comp, element_handle_t id, bool visible) { if (!comp) return false; element_t* elem = find_element(comp, id); if (!elem) { @@ -624,7 +621,7 @@ bool compositor_set_visible(compositor_t* comp, uint32_t id, bool visible) { } // setter for an element's scale. -bool compositor_set_scale(compositor_t* comp, uint32_t id, float scale) { +bool compositor_set_scale(compositor_t* comp, element_handle_t id, float scale) { if (!comp) return false; element_t* elem = find_element(comp, id); if (!elem) { @@ -645,7 +642,7 @@ bool compositor_set_scale(compositor_t* comp, uint32_t id, float scale) { } // setter for an element's alpha. -bool compositor_set_alpha(compositor_t* comp, uint32_t id, float alpha) { +bool compositor_set_alpha(compositor_t* comp, element_handle_t id, float alpha) { if (!comp) return false; element_t* elem = find_element(comp, id); if (!elem) { @@ -671,7 +668,7 @@ bool compositor_set_alpha(compositor_t* comp, uint32_t id, float alpha) { } // setter for an element's anchor point. -bool compositor_set_anchor(compositor_t* comp, uint32_t id, anchor_point_t anchor) { +bool compositor_set_anchor(compositor_t* comp, element_handle_t id, anchor_point_t anchor) { if (!comp) { return false; } @@ -695,9 +692,9 @@ bool compositor_set_anchor(compositor_t* comp, uint32_t id, anchor_point_t ancho return true; } -bool compositor_animate_move(compositor_t* comp, uint32_t id, +bool compositor_animate_move(compositor_t* comp, element_handle_t id, int32_t to_x, int32_t to_y, - float duration_sec, easing_t easing, bool queue) { + float duration_sec, easing_t easing, bool queue, bool parallel) { if (!comp) { return false; } @@ -716,6 +713,7 @@ bool compositor_animate_move(compositor_t* comp, uint32_t id, queued.to_x = to_x; queued.to_y = to_y; queued.is_move_fade = false; + queued.parallel = parallel; queued.affects_position = true; queued.affects_scale = false; queued.affects_alpha = false; @@ -743,8 +741,8 @@ bool compositor_animate_move(compositor_t* comp, uint32_t id, return true; } -bool compositor_animate_scale(compositor_t* comp, uint32_t id, - float to_scale, float duration_sec, easing_t easing, bool queue) { +bool compositor_animate_scale(compositor_t* comp, element_handle_t id, + float to_scale, float duration_sec, easing_t easing, bool queue, bool parallel) { if (!comp) { return false; } @@ -762,6 +760,7 @@ bool compositor_animate_scale(compositor_t* comp, uint32_t id, queued.duration_sec = duration_sec; queued.to_scale = to_scale; queued.is_move_fade = false; + queued.parallel = parallel; queued.affects_position = false; queued.affects_scale = true; queued.affects_alpha = false; @@ -787,8 +786,8 @@ bool compositor_animate_scale(compositor_t* comp, uint32_t id, return true; } -bool compositor_animate_fade(compositor_t* comp, uint32_t id, - float to_alpha, float duration_sec, easing_t easing, bool queue) { +bool compositor_animate_fade(compositor_t* comp, element_handle_t id, + float to_alpha, float duration_sec, easing_t easing, bool queue, bool parallel) { if (!comp) { return false; } @@ -808,6 +807,7 @@ bool compositor_animate_fade(compositor_t* comp, uint32_t id, queued.duration_sec = duration_sec; queued.to_alpha = to_alpha; queued.is_move_fade = false; + queued.parallel = parallel; queued.affects_position = false; queued.affects_scale = false; queued.affects_alpha = true; @@ -838,7 +838,7 @@ bool compositor_animate_fade(compositor_t* comp, uint32_t id, } // Finish all or up to num_queued queued animations for a specific element -bool compositor_animation_finish_element(compositor_t* comp, uint32_t id, int num_queued) { +bool compositor_animation_finish_element(compositor_t* comp, element_handle_t id, int num_queued) { if (!comp) { return false; } @@ -982,7 +982,7 @@ void compositor_mark_all_dirty(compositor_t* comp) { } // Marks the tiles occupied by a specific element as dirty. -bool compositor_mark_content_dirty(compositor_t* comp, uint32_t id) { +bool compositor_mark_content_dirty(compositor_t* comp, element_handle_t id) { if (!comp) { return false; } @@ -1016,7 +1016,7 @@ bool compositor_is_animating(compositor_t* comp) { } // Returns true if the specified element is animating or has queued animations. -bool compositor_is_element_animating(compositor_t* comp, uint32_t id) { +bool compositor_is_element_animating(compositor_t* comp, element_handle_t id) { if (!comp) return false; element_t* elem = find_element(comp, id); if (!elem) { @@ -1038,7 +1038,7 @@ const uint32_t* compositor_compose(compositor_t* comp) { // OpenMP 2.0 requires signed integers for loop counters int total_tiles = (int)(comp->tiles_y * comp->tiles_x); - // Uncomment when you're ready to test threading again! + // Uncomment when ready to test threading again! //#pragma omp parallel for schedule(dynamic, 8) for (int i = 0; i < total_tiles; i++) { uint32_t ty = (uint32_t)(i / comp->tiles_x); @@ -1061,7 +1061,7 @@ const uint32_t* compositor_compose(compositor_t* comp) { } // SPATIAL HASH: Only process elements that actually touch this tile - std::vector local_draw_list = comp->tile_map[tile_idx]; + const std::vector& local_draw_list = comp->tile_map[tile_idx]; // If nothing is in this tile, we're done here if (local_draw_list.empty()) { @@ -1069,11 +1069,6 @@ const uint32_t* compositor_compose(compositor_t* comp) { continue; } - // Sort the local elements by z-order - std::sort(local_draw_list.begin(), local_draw_list.end(), [comp](uint32_t a, uint32_t b) { - return comp->elements_map[a].z_order < comp->elements_map[b].z_order; - }); - // Draw only the local elements for (uint32_t element_id : local_draw_list) { const element_t* elem = find_element(comp, element_id); diff --git a/src/main.cpp b/src/main.cpp index 62405da..0d28b3f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,370 +1,635 @@ -// this file is a clusterfuck that works as a demo of the internals. -// frontend is hard. todo a real state machine. -// want branching? if you try really hard. #include #include #include #include #include -#include +#include +#include +#include #include #include #include #include #include -// smartass approach -struct WindowHandle { - window_t* ptr; - WindowHandle(window_t* w) : ptr(w) {} - ~WindowHandle() { if (ptr) window_destroy(ptr); } - operator window_t* () { return ptr; } +// --------------------------------------------------------------------------- +// RAII handles +// --------------------------------------------------------------------------- + +// A generic RAII wrapper. +template +struct EngineHandle { + T* ptr; + + // Constructor + EngineHandle(T* p = nullptr) : ptr(p) {} + + // Destructor + ~EngineHandle() { if (ptr) DestroyFn(ptr); } + + // DELETE Copy Constructor (Prevents accidental cloning) + EngineHandle(const EngineHandle&) = delete; + + // DELETE Copy Assignment (Prevents accidental overwrites) + EngineHandle& operator=(const EngineHandle&) = delete; + + // ALLOW Move Constructor (Safely transfer ownership) + EngineHandle(EngineHandle&& other) noexcept : ptr(other.ptr) { + other.ptr = nullptr; // Nullify the old pointer so it doesn't get destroyed + } + + // ALLOW Move Assignment + EngineHandle& operator=(EngineHandle&& other) noexcept { + if (this != &other) { + if (ptr) DestroyFn(ptr); // Clean up our current resource first + ptr = other.ptr; // Steal the resource from the other handle + other.ptr = nullptr; // Nullify the old one + } + return *this; + } + + // Implicit conversion operator + operator T* () const { return ptr; } }; -struct CompositorHandle { - compositor_t* ptr; - CompositorHandle(compositor_t* c) : ptr(c) {} - ~CompositorHandle() { if (ptr) compositor_destroy(ptr); } - operator compositor_t* () { return ptr; } +using WindowHandle = EngineHandle; +using CompositorHandle = EngineHandle; +using TextHandle = EngineHandle; +using AudioHandle = EngineHandle; +using AudioDataHandle = EngineHandle; +using ImageHandle = EngineHandle; + +// --------------------------------------------------------------------------- +// Script interpreter +// --------------------------------------------------------------------------- + +// A command is an opcode plus a closure that does the actual work. +// Keeping the closure allows the command set to directly capture backend +// pointers without a fat context struct threaded through everything. +// +// The closure returns true when the interpreter should YIELD (stop executing +// commands this frame and wait). Returning false means continue immediately +// to the next command. + +struct Command { + std::string opcode; // for debugging + std::function execute; }; -struct TextHandle { - text_t* ptr; - TextHandle(text_t* t) : ptr(t) {} - ~TextHandle() { if (ptr) text_destroy(ptr); } - operator text_t* () { return ptr; } +enum class WaitKind { + NONE, + CLICK, // wait for mouse click or space + ANIM_ALL, // wait until compositor_is_animating() == false + ANIM_ELEM, // wait until a specific element stops animating + END, // script finished, keep rendering until told to quit }; -struct ImageHandle { - uint32_t* ptr; - ImageHandle(uint32_t* p) : ptr(p) {} - ~ImageHandle() { if (ptr) image_free(ptr); } - operator uint32_t* () { return ptr; } +struct Interpreter { + std::vector script; + size_t pc = 0; + + WaitKind waiting = WaitKind::NONE; + element_handle_t wait_elem = 0; // valid when waiting == ANIM_ELEM + + // Callbacks into the engine, set before first tick + compositor_t* compositor = nullptr; + text_t* dialogue = nullptr; + text_t* name_box = nullptr; + + // Advance the interpreter by one frame. + // clicked: true on the frame a left-click or spacebar press was detected. + // Returns true when the script has fully ended and the engine may quit. + bool tick(bool clicked) { + + // ---- resolve current wait condition -------------------------------- + switch (waiting) { + + case WaitKind::CLICK: + if (clicked) { + // If the typewriter is still running, snap it and stay on + // this line - let the player see the full text first. + if (text_is_animating(dialogue)) { + text_show_all_current(dialogue); + return false; + } + waiting = WaitKind::NONE; + } + else { + return false; + } + break; + + case WaitKind::ANIM_ALL: + if (compositor_is_animating(compositor)) + return false; + waiting = WaitKind::NONE; + break; + + case WaitKind::ANIM_ELEM: + if (compositor_is_element_animating(compositor, wait_elem)) + return false; + waiting = WaitKind::NONE; + break; + + case WaitKind::END: + return true; + + case WaitKind::NONE: + break; + } + + // ---- burn through commands until a yield --------------------------- + while (pc < script.size()) { + const Command& cmd = script[pc++]; + bool yield = cmd.execute(); + if (yield) return false; + } + + // Ran off the end of the script without an explicit END. + waiting = WaitKind::END; + return true; + } }; -struct AudioHandle { - audio_t* ptr; - AudioHandle(audio_t* a) : ptr(a) {} - ~AudioHandle() { if (ptr) audio_destroy(ptr); } - operator audio_t* () { return ptr; } -}; +// --------------------------------------------------------------------------- +// Script builder helpers +// +// These push Command objects onto the script vector. They capture the +// interpreter and backend pointers by reference/value as appropriate. +// All helpers return void and operate on a passed-in script vector so the +// build function stays readable. +// --------------------------------------------------------------------------- -struct AudioDataHandle { - uint8_t* ptr; - AudioDataHandle(uint8_t* p) : ptr(p) {} - ~AudioDataHandle() { if (ptr) audio_free_data(ptr); } - operator uint8_t* () { return ptr; } -}; +using Script = std::vector; + +static void cmd_speaker(Script& s, text_t* name_box, + element_handle_t name_id, compositor_t* comp, + const char* speaker) +{ + s.push_back({ "SPEAKER", [=]() -> bool { + text_clear(name_box); + if (speaker && speaker[0]) + text_push(name_box, speaker, 0.0f, true); + compositor_mark_content_dirty(comp, name_id); + return false; + } }); +} + +static void cmd_line(Script& s, text_t* dialogue, + element_handle_t dialogue_id, compositor_t* comp, + const char* text, float speed = 30.0f) +{ + s.push_back({ "LINE", [=]() -> bool { + text_clear(dialogue); + text_push(dialogue, text, speed, true); + compositor_mark_content_dirty(comp, dialogue_id); + return false; + } }); +} + +static void cmd_wait_click(Script& s, Interpreter& interp) +{ + s.push_back({ "WAIT_CLICK", [&interp]() -> bool { + interp.waiting = WaitKind::CLICK; + return true; + } }); +} + +static void cmd_wait_anim_all(Script& s, Interpreter& interp) +{ + s.push_back({ "WAIT_ANIM_ALL", [&interp]() -> bool { + interp.waiting = WaitKind::ANIM_ALL; + return true; + } }); +} + +static void cmd_wait_anim_elem(Script& s, Interpreter& interp, + element_handle_t elem) +{ + s.push_back({ "WAIT_ANIM_ELEM", [&interp, elem]() -> bool { + interp.waiting = WaitKind::ANIM_ELEM; + interp.wait_elem = elem; + return true; + } }); +} + +static void cmd_finish_all(Script& s, compositor_t* comp) +{ + s.push_back({ "ANIM_FINISH_ALL", [=]() -> bool { + compositor_animation_finish_all(comp); + return false; + } }); +} + +static void cmd_anim_scale(Script& s, compositor_t* comp, + element_handle_t elem, float to, + float dur, easing_t ease, bool queue, bool parallel) +{ + s.push_back({ "ANIM_SCALE", [=]() -> bool { + compositor_animate_scale(comp, elem, to, dur, ease, queue, parallel); + return false; + } }); +} + +static void cmd_anim_move(Script& s, compositor_t* comp, + element_handle_t elem, + int32_t x, int32_t y, + float dur, easing_t ease, bool queue, bool parallel) +{ + s.push_back({ "ANIM_MOVE", [=]() -> bool { + compositor_animate_move(comp, elem, x, y, dur, ease, queue, parallel); + return false; + } }); +} + +static void cmd_anim_fade(Script& s, compositor_t* comp, + element_handle_t elem, float to_alpha, + float dur, easing_t ease, bool queue, bool parallel) +{ + s.push_back({ "ANIM_FADE", [=]() -> bool { + compositor_animate_fade(comp, elem, to_alpha, dur, ease, queue, parallel); + return false; + } }); +} + +static void cmd_set_visible(Script& s, compositor_t* comp, + element_handle_t elem, bool visible) +{ + s.push_back({ "ELEM_SHOW/HIDE", [=]() -> bool { + compositor_set_visible(comp, elem, visible); + return false; + } }); +} + +static void cmd_set_scale(Script& s, compositor_t* comp, + element_handle_t elem, float scale) +{ + s.push_back({ "ELEM_SET_SCALE", [=]() -> bool { + compositor_set_scale(comp, elem, scale); + return false; + } }); +} + +static void cmd_set_alpha(Script& s, compositor_t* comp, + element_handle_t elem, float alpha) +{ + s.push_back({ "ELEM_SET_ALPHA", [=]() -> bool { + compositor_set_alpha(comp, elem, alpha); + return false; + } }); +} + +static void cmd_audio_fade(Script& s, audio_t* audio, + audio_stream_id_t id, + float to_vol, float dur) +{ + s.push_back({ "MUSIC_FADE", [=]() -> bool { + audio_animate_volume(audio, id, to_vol, dur); + return false; + } }); +} + +static void cmd_end(Script& s, Interpreter& interp) +{ + s.push_back({ "END", [&interp]() -> bool { + interp.waiting = WaitKind::END; + return true; + } }); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- -// the clusterfuck begins int main() { const int WIDTH = 1280; const int HEIGHT = 720; const int CHAR_WIDTH = 400; const int CHAR_HEIGHT = 600; - // Character Positioning Constants - // The vertical baseline for character sprites, making positioning independent of sprite height. + const int CENTER_X = WIDTH / 2; 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; + const int OFFSCREEN_RIGHT_X = RIGHT_CHAR_CENTER_X + 660; + + // ---- window / compositor / audio --------------------------------------- WindowHandle window(window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine")); - if (!window) { - printf("Failed to create window\n"); - return 1; - } + if (!window) { printf("Failed to create window\n"); return 1; } CompositorHandle compositor(compositor_init(WIDTH, HEIGHT)); - if (!compositor) { - printf("Failed to initialize compositor\n"); - return 1; - } + if (!compositor) { printf("Failed to initialize compositor\n"); return 1; } - AudioHandle audio(audio_init()); - if (!audio) { - printf("Failed to initialize audio\n"); - return 1; - } + AudioHandle audio(audio_init()); + if (!audio) { printf("Failed to initialize audio\n"); return 1; } audio_set_global_volume(audio, 1.0f); - int music_size, music_channels, music_rate, music_total_samples; - AudioDataHandle music_data(audio_load_madpcm("assets/Freikugeln.wav", &music_size, &music_channels, &music_rate, &music_total_samples)); - if (!music_data) { - printf("Failed to load audio\n"); - return 1; - } + int music_size, music_channels, music_rate, music_total_samples; + AudioDataHandle music_data(audio_load_madpcm( + "assets/Freikugeln.wav", + &music_size, &music_channels, &music_rate, &music_total_samples)); + if (!music_data) { printf("Failed to load audio\n"); return 1; } - uint32_t music_id = audio_play(audio, music_data, music_size, music_channels, music_rate, true); + audio_stream_id_t music_id = audio_play( + audio, music_data, music_size, music_channels, music_rate, true); + + // ---- assets / compositor elements ------------------------------------- ImageHandle bg(image_load_resized("assets/background.png", WIDTH, HEIGHT)); if (bg) { - uint32_t bg_id = compositor_insert(compositor, bg, WIDTH, HEIGHT, 0, 0, 0, 1.0f, ANCHOR_TOP_LEFT); + element_handle_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); } ImageHandle textbox(image_load_resized("assets/textbox.png", WIDTH, 180)); - if (textbox) { - // 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); - } + if (textbox) + compositor_insert(compositor, textbox, WIDTH, 180, + 0, HEIGHT, 3, 1.0f, ANCHOR_BOTTOM_LEFT); ImageHandle 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, OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); + element_handle_t maksw_happy_id = compositor_insert( + compositor, maksw_happy, CHAR_WIDTH, CHAR_HEIGHT, + OFFSCREEN_LEFT_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); ImageHandle 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, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1, 1.0f, ANCHOR_BOTTOM_CENTER); + element_handle_t maksw_neutral_id = compositor_insert( + compositor, maksw_neutral, 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); - ImageHandle wikor(image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT)); - // 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); + ImageHandle wikor_img(image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT)); + element_handle_t wikor_id = compositor_insert( + compositor, wikor_img, CHAR_WIDTH, CHAR_HEIGHT, + OFFSCREEN_RIGHT_X, CHAR_BASELINE_Y, 2, 1.0f, ANCHOR_BOTTOM_CENTER); TextHandle name_text(text_init(300, 100)); 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), + element_handle_t name_id = compositor_insert( + compositor, text_getfb(name_text), 300, 100, 60, HEIGHT - 180, 4, 1.0f, ANCHOR_TOP_LEFT); TextHandle dialogue_text(text_init(WIDTH - 140, 160)); 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), + element_handle_t dialogue_id = compositor_insert( + compositor, text_getfb(dialogue_text), WIDTH - 140, 160, 60, HEIGHT - 130, 4, 1.0f, ANCHOR_TOP_LEFT); + // mask: a wipe graphic, starts off-screen above, slides down to cover ImageHandle 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); + element_handle_t mask_id = compositor_insert( + compositor, mask, 1280, 1080, 0, -1080, 5, 1.0f, ANCHOR_TOP_LEFT); compositor_set_visible(compositor, mask_id, false); + // mask2: slides off downward to reveal the new scene ImageHandle 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); + element_handle_t mask2_id = compositor_insert( + compositor, mask2, 1280, 1080, 0, 0, 5, 1.0f, ANCHOR_TOP_LEFT); compositor_set_visible(compositor, mask2_id, false); + // ---- build the script ------------------------------------------------- + // + // Shorthand references so the builder lines stay readable. + compositor_t* comp = compositor; + text_t* dlg = dialogue_text; + text_t* nmb = name_text; + element_handle_t dlg_id = dialogue_id; + + Interpreter interp; + interp.compositor = comp; + interp.dialogue = dlg; + interp.name_box = nmb; + + Script& S = interp.script; + + // Convenience aliases so each script line stays short. + // All lambdas below capture by value (element handles, pointers) which is + // safe because all resources outlive the interpreter. + + auto speaker = [&](const char* who) { + cmd_speaker(S, nmb, name_id, comp, who); + }; + auto line = [&](const char* text, float speed = 30.0f) { + cmd_line(S, dlg, dlg_id, comp, text, speed); + }; + auto wait_click = [&]() { cmd_wait_click(S, interp); }; + auto wait_anim_all = [&]() { cmd_wait_anim_all(S, interp); }; + auto wait_anim = [&](element_handle_t e) { cmd_wait_anim_elem(S, interp, e); }; + auto finish_all = [&]() { cmd_finish_all(S, comp); }; + auto anim_scale = [&](element_handle_t e, float to, float dur, + easing_t ease, bool q = false, bool p = false) { + cmd_anim_scale(S, comp, e, to, dur, ease, q, p); + }; + auto anim_move = [&](element_handle_t e, int32_t x, int32_t y, + float dur, easing_t ease, bool q = false, bool p = false) { + cmd_anim_move(S, comp, e, x, y, dur, ease, q, p); + }; + auto anim_fade = [&](element_handle_t e, float a, float dur, + easing_t ease, bool q = false, bool p = false) { + cmd_anim_fade(S, comp, e, a, dur, ease, q, p); + }; + auto show = [&](element_handle_t e) { cmd_set_visible(S, comp, e, true); }; + auto hide = [&](element_handle_t e) { cmd_set_visible(S, comp, e, false); }; + auto scale = [&](element_handle_t e, float v) { cmd_set_scale(S, comp, e, v); }; + auto alpha = [&](element_handle_t e, float v) { cmd_set_alpha(S, comp, e, v); }; + + // ------------------------------------------------------------------ + // Scene 1 - maksw slides in, introduces the engine + // ------------------------------------------------------------------ + + // Slide maksw_happy in from off-screen, bob on arrival, scale up as + // "active speaker". No WAIT_ANIM here - dialogue starts immediately + // and the animation plays alongside the typewriter, which feels natural. + anim_move(maksw_happy_id, CENTER_X, CHAR_BASELINE_Y, + 1.5f, EASE_OUT_CUBIC, true); + anim_scale(maksw_happy_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC, /*queue=*/true, /*parallel=*/false); + speaker("maksw"); + line("Hello and welcome to the Obwarzanek Narrative Engine demo!"); + wait_click(); + + line("Today I'll present to you-"); + wait_click(); + + anim_move(wikor_id, RIGHT_CHAR_CENTER_X, CHAR_BASELINE_Y, 0.4f, EASE_OUT_CUBIC); + anim_move(maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y - 20, + 0.3f, EASE_OUT_CUBIC); + anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("Yeah, hello to you too."); + wait_click(); + + anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("maksw"); + line("Oh Wiktor, what a nice surprise!"); + wait_click(); + + line("...Do you need anything?"); + wait_click(); + + anim_scale(maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("No, I was just walking by."); + wait_click(); + + line("Cool engine though."); + wait_click(); + + // ------------------------------------------------------------------ + // Transition - wipe in, swap scene, wipe out + // + // Phase 1: mask slides down from above to cover the screen. + // We WAIT on that specific element so we know exactly when + // the screen is fully hidden - no guessing, no hack cases. + // + // Phase 2: while hidden, swap sprites and set up the new scene state. + // All of this is instant (no animation), which is fine because + // the mask is covering everything. + // + // Phase 3: hide mask, show mask2 (already covering screen at y=0), + // slide mask2 downward to reveal the new scene. + // We don't WAIT here - dialogue runs alongside the wipe-out, + // which looks intentional and cinematic. + // ------------------------------------------------------------------ + + // Phase 1 - cover + finish_all(); + show(mask_id); + anim_move(mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC); + wait_anim(mask_id); // block until mask fully covers the screen + + // Phase 2 - swap (mask is covering, player sees nothing) + hide(maksw_happy_id); + show(maksw_neutral_id); + scale(maksw_neutral_id, 1.0f); + scale(wikor_id, 1.05f); + + // Phase 3 - uncover + hide(mask_id); + show(mask2_id); + // mask2 starts at y=0 (covering screen), animate it off downward + anim_move(mask2_id, 0, HEIGHT, 0.5f, EASE_OUT_CUBIC); + // Dialogue fires immediately - text appears as the wipe reveals the scene + speaker("wikor"); + line("Anyways, how's your day going?"); + wait_click(); + + // ------------------------------------------------------------------ + // Scene 2 - neutral maksw, wikor is chatty + // ------------------------------------------------------------------ + + finish_all(); + anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("maksw"); + line("You hear what I'm hearing?"); + wait_click(); + + finish_all(); + anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("What? That music?"); + wait_click(); + + finish_all(); + anim_scale(wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("maksw"); + line("Yep! Freikugeln by Johann Strauss II!"); + wait_click(); + + finish_all(); + anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + anim_scale(wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC); + speaker("wikor"); + line("Sure thing..."); + wait_click(); + + finish_all(); + line("Like I know who he is."); + wait_click(); + + // ------------------------------------------------------------------ + // Ending - fade to black, fade out music, then quit + // ------------------------------------------------------------------ + + finish_all(); + anim_scale(maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC); + // Reuse mask as a simple alpha-fade overlay (no positional wipe needed) + show(mask_id); + alpha(mask_id, 0.0f); + anim_fade(mask_id, 1.0f, 0.8f, EASE_IN_OUT_CUBIC); + cmd_audio_fade(S, audio, music_id, 0.0f, 0.6f); + wait_anim(mask_id); // wait until black, then exit + cmd_end(S, interp); + + // ---- main loop -------------------------------------------------------- + bool running = true; auto last_frame_time = std::chrono::high_resolution_clock::now(); auto last_report_time = last_frame_time; - uint64_t frame_count = 0; - float highest_frame_time = 0.0f; - float frame_time_sum = 0.0f; + float highest_frame_time = 0.0f; + float frame_time_sum = 0.0f; const std::chrono::duration REPORT_INTERVAL = std::chrono::seconds(1); + bool mouse_was_down = false; + bool key_was_down = false; + while (running) { if (window_pollevent(window)) running = false; auto current_time = std::chrono::high_resolution_clock::now(); std::chrono::duration delta_duration = current_time - last_frame_time; - float delta_time_sec = delta_duration.count(); + float dt = delta_duration.count(); last_frame_time = current_time; + // FPS reporting frame_count++; - frame_time_sum += delta_time_sec; - if (delta_time_sec > highest_frame_time) highest_frame_time = delta_time_sec; - - std::chrono::duration time_since_last_report = current_time - last_report_time; - - if (time_since_last_report >= REPORT_INTERVAL) { + frame_time_sum += dt; + if (dt > highest_frame_time) highest_frame_time = dt; + std::chrono::duration since_report = current_time - last_report_time; + if (since_report >= REPORT_INTERVAL) { if (frame_count > 0) { - float avg_delta_time = frame_time_sum / (float)frame_count; - float fps = 1.0f / avg_delta_time; - float actual_duration = time_since_last_report.count(); - + float fps = 1.0f / (frame_time_sum / (float)frame_count); printf("FPS: %.2f (over %llu frames in %.2f seconds), highest frame time: %.2fms\n", - fps, frame_count, actual_duration, highest_frame_time * 1000.0f); + fps, frame_count, since_report.count(), highest_frame_time * 1000.0f); } - frame_count = 0; - frame_time_sum = 0.0f; - highest_frame_time = 0.0f; + frame_count = 0; frame_time_sum = 0.0f; highest_frame_time = 0.0f; last_report_time = current_time; } - static bool mouse_was_down = false; + // Edge-detect click and keypress 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; + bool clicked = (mouse_is_down && !mouse_was_down) || + (key_is_down && !key_was_down); + mouse_was_down = mouse_is_down; key_was_down = key_is_down; - // don't judge me, it will work for now - static unsigned int lines = 1; - static bool dialog_finished = false; - static bool transitioning = false; + // Tick the interpreter + bool script_done = interp.tick(clicked); + if (script_done) running = false; - if (mouse_clicked || key_pressed || lines == 1 /* hack */) { - if (text_is_animating(dialogue_text)) { - text_show_all_current(dialogue_text); - } - else { - switch (lines) { - case 1: - compositor_animate_move(compositor, maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 1.0f, EASE_OUT_CUBIC, false); - 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, "Hello and welcome to the Obwarzanek Narrative Engine demo!", 30.0f, true); - lines++; - break; - case 2: - compositor_finish_animations_all(compositor); - text_clear(dialogue_text); - text_push(dialogue_text, "Today I'll present to you-", 30.0f, true); - lines++; - break; - case 3: - compositor_finish_animations_all(compositor); - compositor_animate_scale(compositor, maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - compositor_animate_move(compositor, maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y - 20, 0.1f, EASE_OUT_CUBIC, false); - compositor_animate_move(compositor, maksw_happy_id, LEFT_CHAR_CENTER_X, CHAR_BASELINE_Y, 0.1f, EASE_IN_CUBIC, true); - compositor_animate_scale(compositor, wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, false); - text_clear(dialogue_text); - text_clear(name_text); - text_push(name_text, "wikor", 0.0f, true); - text_push(dialogue_text, "Yeah, hello to you too.", 30.0f, true); - lines++; - break; - case 4: - compositor_finish_animations_all(compositor); - compositor_animate_scale(compositor, wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - compositor_animate_scale(compositor, maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, false); - text_clear(dialogue_text); - text_clear(name_text); - text_push(name_text, "maksw", 0.0f, true); - text_push(dialogue_text, "Oh Wiktor, what a nice suprise!", 30.0f, true); - lines++; - break; - case 5: - compositor_finish_animations_all(compositor); - text_clear(dialogue_text); - text_push(dialogue_text, "Do you need anything?", 30.0f, true); - lines++; - break; - case 6: - compositor_finish_animations_all(compositor); - compositor_animate_scale(compositor, maksw_happy_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - compositor_animate_scale(compositor, wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, false); - text_clear(dialogue_text); - text_clear(name_text); - text_push(name_text, "wikor", 0.0f, true); - text_push(dialogue_text, "No, i was just walking by.", 30.0f, true); - lines++; - break; - case 7: - compositor_finish_animations_all(compositor); - text_clear(dialogue_text); - text_push(dialogue_text, "Cool engine though.", 30.0f, true); - lines++; - break; - case 8: - // fade transition - compositor_animation_complete_all(compositor); - compositor_set_visible(compositor, mask_id, true); - compositor_animate_move(compositor, mask_id, 0, 0, 0.5f, EASE_OUT_CUBIC, false); - transitioning = true; - lines++; - break; - case 9: - // transitioning blocks input, empty case not to trigger default and desync lines from actual line - break; - case 10: - compositor_animation_complete_all(compositor); - compositor_animate_scale(compositor, wikor_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - compositor_animate_scale(compositor, maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, false); - text_clear(dialogue_text); - text_clear(name_text); - text_push(name_text, "maksw", 0.0f, true); - text_push(dialogue_text, "Can't complain, the winter break is just a week away. Plus the compositor is finally cooperating.", 30.0f, true); - lines++; - break; - case 11: - compositor_animation_complete_all(compositor); - compositor_animate_scale(compositor, maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - compositor_animate_scale(compositor, wikor_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, false); - text_clear(dialogue_text); - text_clear(name_text); - text_push(name_text, "wikor", 0.0f, true); - text_push(dialogue_text, "Sure thing..", 30.0f, true); - lines++; - break; - case 12: - compositor_animation_complete_all(compositor); - text_clear(dialogue_text); - text_push(dialogue_text, "You're talking to me like i know anything about computers.", 30.0f, true); - lines++; - break; - case 13: - // fade to black and end - compositor_animation_complete_all(compositor); - compositor_animate_scale(compositor, maksw_neutral_id, 1.0f, 0.3f, EASE_IN_OUT_CUBIC, false); - // reuse mask - compositor_set_visible(compositor, mask_id, true); - compositor_set_alpha(compositor, mask_id, 0.0f); - compositor_animate_fade(compositor, mask_id, 1.0f, 0.8f, EASE_IN_OUT_CUBIC, false); + // Tick subsystems + compositor_tick(compositor, dt); - audio_animate_volume(audio, music_id, 0.0f, 0.6f); - - dialog_finished = true; - lines++; - break; - default: - running = false; - break; - } - } - } - - compositor_tick(compositor, delta_time_sec); - - if (transitioning) { - if (!compositor_is_element_animating(compositor, mask_id)) { - compositor_set_visible(compositor, maksw_happy_id, false); - compositor_set_visible(compositor, maksw_neutral_id, true); - compositor_set_scale(compositor, maksw_neutral_id, 1.0f); - compositor_set_scale(compositor, wikor_id, 1.05f); - text_clear(dialogue_text); - text_clear(name_text); - text_push(name_text, "wikor", 0.0f, true); - text_push(dialogue_text, "Anyways, how's your day going?", 30.0f, true); - - 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); - transitioning = false; - lines++; - } - } - - if (dialog_finished) { - //compositor_mark_content_dirty(compositor, mask_id); - if (!compositor_is_element_animating(compositor, mask_id)) { - running = false; - } - } - - if (text_nextframe(name_text, delta_time_sec)) - compositor_mark_content_dirty(compositor, name_id); - if (text_nextframe(dialogue_text, delta_time_sec)) - compositor_mark_content_dirty(compositor, dialogue_id); + if (text_nextframe(name_text, dt)) compositor_mark_content_dirty(compositor, name_id); + if (text_nextframe(dialogue_text, dt)) compositor_mark_content_dirty(compositor, dialogue_id); const uint32_t* fb = compositor_compose(compositor); window_blit(window, fb); - audio_tick(audio, delta_time_sec); + audio_tick(audio, dt); } - audio_stop(audio, music_id); - + audio_stop(audio, music_id); return 0; -} +} \ No newline at end of file diff --git a/src/window.cpp b/src/window.cpp index 2308c37..0a7e449 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -103,9 +103,9 @@ bool window_is_mouse_button_down(window_t* s, int button) { return false; } -void window_get_mouse_position(window_t* s, int* x, int* y) { - if (x) *x = s->mouse_x; - if (y) *y = s->mouse_y; +mouse_pos_t window_get_mouse_position(window_t* s) { + mouse_pos_t pos = { s->mouse_x, s->mouse_y }; + return pos; } void window_blit(window_t* s, const uint32_t* fb) {