window_get_mouse_position returns a struct, claude wrote me a state machine for main.cpp, compositor queues animations on type instead of one queue, change from uint32_t to *_handle_t, improve compositor performance, generic raii wrapper in main.cpp over multiple structs.

This commit is contained in:
maksw committed 2026-02-27 23:05:35 +01:00
1 parent a12c9a3e8f
commit 02b5bc8b52
7 files changed
+748 -490

No files matched your search

+9 -8
View File
@@ -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]
+18 -23
View File
@@ -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
+3 -1
View File
@@ -53,6 +53,8 @@ extern "C" {
#include <stdint.h>
#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
}
+8 -8
View File
@@ -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);
+173 -178
View File
@@ -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<float>(anim->start_x),
static_cast<float>(anim->end_x), t_eased));
elem->y = (int32_t)std::round(lerp(static_cast<float>(anim->start_y),
static_cast<float>(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<uint32_t> local_draw_list = comp->tile_map[tile_idx];
const std::vector<uint32_t>& 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);
+534 -269
View File
@@ -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 <cstdio>
#include <cstdint>
#include <cstring>
#include <chrono>
#include <vector>
#include <memory>
#include <string>
#include <unordered_map>
#include <functional>
#include <window.h>
#include <text.h>
#include <compositor.h>
#include <image.h>
#include <audio.h>
// 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 <typename T, void(*DestroyFn)(T*)>
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<window_t, window_destroy>;
using CompositorHandle = EngineHandle<compositor_t, compositor_destroy>;
using TextHandle = EngineHandle<text_t, text_destroy>;
using AudioHandle = EngineHandle<audio_t, audio_destroy>;
using AudioDataHandle = EngineHandle<uint8_t, audio_free_data>;
using ImageHandle = EngineHandle<uint32_t, image_free>;
// ---------------------------------------------------------------------------
// 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<bool()> 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<Command> 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<Command>;
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<float> 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<float> 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<float> 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<float> 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;
}
}
+3 -3
View File
@@ -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) {