/* * Copyright (C) 2018 Vito Caputo - * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3 as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "list.h" #include "stage.h" typedef struct v2f_t { float x, y; } v2f_t; typedef struct aabb_t { v2f_t min, max; } aabb_t; typedef struct stage_cached_t { int width, height; SDL_Texture *texture; } stage_cached_t; struct stage_node_t { list_head_t nodes; char name[STAGE_NODE_NAME_MAX]; stage_cached_t cached; aabb_t aabb; /* node coordinates expressed in the space of -1.0...1.0 */ stage_map_t aabb_map; v2f_t position; /* position of node, optionally used to move aabb as a whole */ stage_map_t position_map; v2f_t origin; /* origin of node, 0,0 being center of aabb (the default) this determines how position relates to aabb. -1...1 being aabb.min ... aabb.max */ float alpha; /* alpha for the texture when composited */ double angle; /* angle for the texture when composited */ unsigned active:1; /* node is active */ unsigned locked:1; /* node is locked */ unsigned statik:1; /* node is static */ stage_render_func_t render; /* render object into a texture */ stage_free_func_t free; /* free object */ void *object; /* object */ }; struct stage_t { list_head_t layers[STAGE_LAYERS_MAX]; SDL_Renderer *renderer; /* target renderer */ float aspect_ratio; /* width/height ratio of stage, -1.0...1.0 range of node coordinates map to this ratio. */ v2f_t position; /* position of the stage (defaults to 0,0) */ float alpha; /* alpha to apply to all nodes */ stage_map_t aabb_map; /* aabb map for the stage, nodes inherit this setting at create time */ stage_map_t position_map; /* position map for the stage, nodes inherit this setting at create time */ }; /* minimally initialize a node to carry an object */ static void node_init(stage_node_t *node, const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { strncpy(node->name, name, sizeof(node->name)); node->render = render_func; node->free = free_func; node->object = object; } /* allocate a node, no list manipulation occurs, this purely creates a node in isolation and assigns its associated name, object and functions */ static stage_node_t * node_new(const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { stage_node_t *node; node = calloc(1, sizeof(stage_node_t)); if (!node) return NULL; node_init(node, name, object, render_func, free_func); return node; } /* free a node, no list manipulation occurs, this is purely cleanup of the node and its associated texture and object */ static void node_free(stage_node_t *node) { if (node->free) node->free(node->object); if (node->cached.texture) SDL_DestroyTexture(node->cached.texture); free(node); } /* returns a new node installed at the specified layer */ stage_node_t * stage_node_new(stage_t *stage, int layer, const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { stage_node_t *node; node = node_new(name, object, render_func, free_func); if (!node) return NULL; node->aabb_map = stage->aabb_map; node->position_map = stage->position_map; list_add_tail(&node->nodes, &stage->layers[layer]); return node; } /* replace a given node's object only - retain render/free hooks and everything else, invalidates cache - DOES NOT FREE OLD OBJECT */ /* FIXME: this is being used as a kludge to swap frames in an svg - * eliminate this bullshit and add support for animations or at least frames, * the stage really shouldn't have anything to do with it, we can dick with * the svg behind its back. I just don't currently have the helpers for creating * svg nodes actually giving out the bare svg handle currently, so the stage_node_t * is all I have to dick with at the moment. Next time! This really dicks with * the resource lifecycle - if the stage frees the node, which object was active * at the time and which object still needs to be freed because it wasn't in * the stage when the node was killed? It's totally shitty, and I'm aware. */ void stage_node_set_object(const stage_t *stage, stage_node_t *node, void *object) { if (node->cached.texture) { SDL_DestroyTexture(node->cached.texture); node->cached.texture = NULL; } node->object = object; } /* FIXME: GROSS fuck me */ void stage_node_get_object(const stage_t *stage, stage_node_t *node, void **res_object) { *res_object = node->object; } /* replaces a given node's object-related properties but otherwise keeping the existing state */ void stage_node_replace(const stage_t *stage, stage_node_t *node, const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { if (node->free) node->free(node->object); if (node->cached.texture) { SDL_DestroyTexture(node->cached.texture); node->cached.texture = NULL; } node_init(node, name, object, render_func, free_func); } /* frees a given node removing it from the stage */ stage_node_t * stage_node_free(stage_t *stage, stage_node_t *node) { if (node) { list_del(&node->nodes); node_free(node); } return NULL; } /* set the alpha on a node */ void stage_node_set_alpha(const stage_t *stage, stage_node_t *node, float alpha) { /* TODO: this should probably mark something dirty for when we stop always recomposing everything */ node->alpha = alpha; } /* set the aabb for a node */ void stage_node_set_aabb(const stage_t *stage, stage_node_t *node, const aabb_t *aabb) { node->aabb = *aabb; } /* get the aabb for a node */ void stage_node_get_aabb(const stage_t *stage, const stage_node_t *node, aabb_t *res_aabb) { *res_aabb = node->aabb; } /* set the position for a node */ void stage_node_set_position(const stage_t *stage, stage_node_t *node, v2f_t *position) { node->position = *position; } /* get the position for a node */ void stage_node_get_position(const stage_t *stage, const stage_node_t *node, v2f_t *res_position) { *res_position = node->position; } /* set the origin for a node */ void stage_node_set_origin(const stage_t *stage, stage_node_t *node, v2f_t *origin) { node->origin = *origin; } /* get the origin for a node */ void stage_node_get_origin(const stage_t *stage, const stage_node_t *node, v2f_t *res_origin) { *res_origin = node->origin; } /* set a node's active state (participates in rendering) */ void stage_node_set_active(const stage_t *stage, stage_node_t *node, int active) { node->active = active; } /* set a node's locked state (doesn't get freed by clears) */ void stage_node_set_locked(const stage_t *stage, stage_node_t *node, int locked) { node->locked = locked; } /* set a node to static (isn't influenced by external position/origin/aabb changes, solely its own positin/origin/aabb) */ void stage_node_set_static(const stage_t *stage, stage_node_t *node, int statik) { /* I deliberately spelled statik to avoid language keyword collision */ node->statik = statik; } /* set a node's layer */ void stage_node_set_layer(stage_t *stage, stage_node_t *node, int layer) { /* TODO: assert layer sanity */ list_del(&node->nodes); list_add_tail(&node->nodes, &stage->layers[layer]); } void stage_node_set_angle(const stage_t *stage, stage_node_t *node, double angle) { node->angle = angle; } void stage_node_set_aabb_map(const stage_t *stage, stage_node_t *node, stage_map_t map) { node->aabb_map = map; } void stage_node_set_position_map(const stage_t *stage, stage_node_t *node, stage_map_t map) { node->position_map = map; } void stage_node_get_angle(const stage_t *stage, stage_node_t *node, double *res_angle) { *res_angle = node->angle; } /* create a new stage with the given aspect ratio */ stage_t * stage_new(SDL_Renderer *renderer, float aspect_ratio) { stage_t *stage; int i; stage = calloc(1, sizeof(stage_t)); if (!stage) return NULL; for (i = 0; i < STAGE_LAYERS_MAX; i++) INIT_LIST_HEAD(&stage->layers[i]); stage->renderer = renderer; stage->aspect_ratio = aspect_ratio; return stage; } static void _stage_clear(stage_t *stage, int force) { stage_node_t *node, *_node; int i; for (i = 0; i < STAGE_LAYERS_MAX; i++) { list_for_each_entry_safe(node, _node, &stage->layers[i], nodes) { if (force || !node->locked) stage_node_free(stage, node); } } } /* free everything in the stage, but keep the stage around */ /* probably good hygiene to clear everything when there's no intention of * reusing nodes at the start of a new context, in case something is left * around unintentionally. */ void stage_clear(stage_t *stage) { _stage_clear(stage, 0); } /* free the supplied stage and all associated textures and objects */ stage_t * stage_free(stage_t *stage) { if (stage) { _stage_clear(stage, 1); free(stage); } return NULL; } /* fit a stage to the supplied dimensions, returns the fitted dimensions in the result pointers */ /* the idea is this can be used in a window resize hook to enforce the stage's aspect ratio */ void stage_fit(float aspect_ratio, int width, int height, int *res_width, int *res_height) { float full_width = aspect_ratio * ((float)height); if (full_width == width) { /* perfect fit */ *res_width = width; *res_height = height; } else if (full_width > width) { /* height is too large */ *res_height = (1.0f / aspect_ratio) * ((float)width); *res_width = width; } else { /* width is too large */ *res_width = full_width; *res_height = height; } } /* set the global alpha factor for the stage, the individual node alphas are multiplied by this value @ render time. */ void stage_set_alpha(stage_t *stage, float alpha) { stage->alpha = alpha; } /* get the global alpha factor for the stage */ void stage_get_alpha(stage_t *stage, float *res_alpha) { *res_alpha = stage->alpha; } /* set the global position for the stage, all nodes are shifted by this @ render time */ void stage_set_position(stage_t *stage, v2f_t *position) { stage->position = *position; } /* get the global position for the stage */ void stage_get_position(stage_t *stage, v2f_t *res_position) { *res_position = stage->position; } /* set the aabb mapping for the stage */ void stage_set_aabb_map(stage_t *stage, stage_map_t map) { stage->aabb_map = map; } /* set the position mapping for the stage */ void stage_set_position_map(stage_t *stage, stage_map_t map) { stage->position_map = map; } static void map_stage_rect(const stage_t *stage, const SDL_Rect *stage_rect, stage_map_t map, float *res_w, float *res_h) { switch (map) { case STAGE_MAP_FILL: *res_w = stage_rect->w; *res_h = stage_rect->h; break; case STAGE_MAP_MINSQ: if (stage_rect->w < stage_rect->h) { *res_w = stage_rect->w; *res_h = stage_rect->w; } else { *res_w = stage_rect->h; *res_h = stage_rect->h; } break; case STAGE_MAP_MAXSQ: if (stage_rect->w > stage_rect->h) { *res_w = stage_rect->w; *res_h = stage_rect->w; } else { *res_w = stage_rect->h; *res_h = stage_rect->h; } break; } } /* translate a node's aabb within the given stage to a rect witin stage_rect, storing result in *res_rect */ static void node_to_rect(const stage_t *stage, const stage_node_t *node, const SDL_Rect *stage_rect, SDL_Rect *res_rect) { float aabb_w = (node->aabb.max.x - node->aabb.min.x); float aabb_h = (node->aabb.max.y - node->aabb.min.y); float stage_w, stage_h; /* center in stage_rect */ res_rect->x = stage_rect->x + floorf((float)stage_rect->w * .5f); res_rect->y = stage_rect->y + floorf((float)stage_rect->h * .5f); /* compute virtual stage dimensions according to the node's aabb_map setting for aabb calculations */ map_stage_rect(stage, stage_rect, node->aabb_map, &stage_w, &stage_h); /* res dimensions are simply aabb dimensions scaled by stage dimensions according to node->aabb_map */ res_rect->w = floorf(aabb_w * .5f * stage_w); res_rect->h = floorf(aabb_h * .5f * stage_h); /* apply node->aabb (scaled relative to stage_rect) */ res_rect->x += floorf(node->aabb.min.x * .5f * stage_w); res_rect->y += -floorf(node->aabb.max.y * .5f * stage_h); if (!node->statik) { /* apply stage->position (relative to stage_rect) */ res_rect->x += floorf(stage->position.x * .5f * (float)stage_rect->w); res_rect->y += -floorf(stage->position.y * .5f * (float)stage_rect->h); } /* compute virtual stage dimensions according to the node's position_map setting for position calculations */ map_stage_rect(stage, stage_rect, node->position_map, &stage_w, &stage_h); res_rect->x += floorf(node->position.x * .5f * (float)stage_w); res_rect->y += -floorf(node->position.y * .5f * (float)stage_h); /* apply node->origin (scaled relative to scaled aabb_w) (this probably needs to be inverted) */ res_rect->x += floorf(node->origin.x * .5f * (float)res_rect->w); res_rect->y += -floorf(node->origin.y * .5f * (float)res_rect->h); /* Prevent producing 0-dimensioned non-zero rects even if it's technicaly incorrect, * what's likely going on is the window has been resized very small. Rather * than allowing things to become completely invisible in such circumstances, * give them at least a single pixel. */ if (res_rect->w == 0 && aabb_w > 0.f) res_rect->w = 1; if (res_rect->h == 0 && aabb_h > 0.f) res_rect->h = 1; } static void render_nodes(const stage_t *stage, const list_head_t *head, SDL_Renderer *renderer, SDL_Rect *dest_rect) { stage_node_t *node; list_for_each_entry(node, head, nodes) { SDL_Rect node_rect; if (!node->active) continue; /* scale the node's aabb stage coordinates to destination renderer coordinates */ node_to_rect(stage, node, dest_rect, &node_rect); /* if we have a cached texture, see if the dimensions changed */ if (node->cached.texture && (node_rect.w != node->cached.width || node_rect.h != node->cached.height)) { SDL_DestroyTexture(node->cached.texture); node->cached.texture = NULL; } /* Tell the node to draw its cached texture */ /* The draw function may either use the existing texture when non-NULL or destroy and create a new one, * whatever is most convenient for it. * If the texture is NULL, a create & render must be performed, and 1 returned. * If the texture is non-NULL, and there's no animation or anything to be done, 0 may simply be returned. * Otherwise, if supplied a non-NULL texture and it's modified, 1 must be returned. */ /* XXX: at this time, the return value is ignored (it always composites the stage even when unchanged) */ node->render(renderer, node->object, node_rect.w, node_rect.h, &node->cached.texture); node->cached.width = node_rect.w; node->cached.height = node_rect.h; SDL_SetTextureAlphaMod(node->cached.texture, stage->alpha * node->alpha * 255.0f); /* copy the texture to renderer */ if (node->angle == 0) SDL_RenderCopy(renderer, node->cached.texture, NULL, &node_rect); else SDL_RenderCopyEx(renderer, node->cached.texture, NULL, &node_rect, node->angle, NULL, SDL_FLIP_NONE); } } /* XXX: ANOTHER KLUDGE FIXME */ /* we shouldn't need to let this out of the stage, but for the mouse aiming * it was easier this way for now. */ void stage_get_output_size(stage_t *stage, int *res_width, int *res_height) { SDL_GetRendererOutputSize(stage->renderer, res_width, res_height); } /* render the supplied stage into the supplied renderer */ /* the aspect_ratio of the stage will be enforced in filling the logical size * of the renderer. Unless the aspect ratio matches precisely, stage_render() * will center the rendering and leave blank borders on the edges left empty. * The renderer may be sized precisely by first calling stage_fit(). */ void stage_render(const stage_t *stage) { SDL_Rect rect; int i, width, height; /* XXX TODO: investigate renderer viewports and scale factors */ SDL_GetRendererOutputSize(stage->renderer, &width, &height); stage_fit(stage->aspect_ratio, width, height, &rect.w, &rect.h); rect.x = (width - rect.w) / 2; rect.y = (height - rect.h) / 2; SDL_RenderClear(stage->renderer); /* TODO: handle -1 on error */ for (i = 0; i < STAGE_LAYERS_MAX; i++) render_nodes(stage, &stage->layers[i], stage->renderer, &rect); } /* lookup a node from a name */ stage_node_t * stage_node_lookup_name(const stage_t *stage, const char *name) { stage_node_t *node; int i; for (i = 0; i < STAGE_LAYERS_MAX; i++) { list_for_each_entry(node, &stage->layers[i], nodes) { if (!strncmp(node->name, name, sizeof(node->name))) return node; } } return NULL; } /* lookup a node from a cartesian renderer coordinate */ stage_node_t * stage_node_lookup_cartesian(const stage_t *stage, int x, int y) { stage_node_t *node; SDL_Rect rect; int i, width, height; /* FIXME: copy-pasta with render, factor out */ SDL_GetRendererOutputSize(stage->renderer, &width, &height); stage_fit(stage->aspect_ratio, width, height, &rect.w, &rect.h); rect.x = (width - rect.w) / 2; rect.y = (height - rect.h) / 2; /* this is currently very simple: find the first top-most node * including the specified coordinate. */ for (i = STAGE_LAYERS_MAX - 1; i >= 0; i--) { list_for_each_entry(node, &stage->layers[i], nodes) { SDL_Rect node_rect; if (!node->active) continue; node_to_rect(stage, node, &rect, &node_rect); if (x >= node_rect.x && x < node_rect.x + node_rect.w && y >= node_rect.y && y < node_rect.y + node_rect.h) return node; } } return NULL; }