From c2484a05a02f50e1ed14a7527ee6507326277ef2 Mon Sep 17 00:00:00 2001 From: KonAki Date: Mon, 12 Oct 2026 13:45:27 +0800 Subject: add stage module: sprites with layers, slots, tweens, backgrounds with pan and zoom --- kon/konstage.c | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ kon/konstage.h | 141 +++++++++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 kon/konstage.c create mode 100644 kon/konstage.h diff --git a/kon/konstage.c b/kon/konstage.c new file mode 100644 index 0000000..1b958be --- /dev/null +++ b/kon/konstage.c @@ -0,0 +1,317 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konstage.c + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "konstage.h" + +#include +#include + +static int valid(const kon_stage_t *stage, int index) +{ + return index >= 0 && index < stage->capacity && stage->sprites[index].used; +} + +static uint32_t tintOf(float alpha, float brightness) +{ + uint32_t a, b; + + if (!(alpha > 0.0f)) alpha = 0.0f; + if (alpha > 1.0f) alpha = 1.0f; + if (!(brightness > 0.0f)) brightness = 0.0f; + if (brightness > 1.0f) brightness = 1.0f; + a = (uint32_t)(alpha * 255.0f + 0.5f); + b = (uint32_t)(brightness * 255.0f + 0.5f); + return (a << 24) | (b << 16) | (b << 8) | b; +} + +static kon_rect_t wholeOf(const kon_image *image, const kon_rect_t *src) +{ + kon_rect_t whole = {0, 0, 0, 0}; + + if (!image) return whole; + if (src && src->w > 0 && src->h > 0) return *src; + whole.w = image->width; + whole.h = image->height; + return whole; +} + +/*** stage ***/ + +static void resetView(kon_stageBackground_t *back) +{ + kon_tweenSet(&back->zoom, 1.0f); + kon_tweenSet(&back->focusX, 0.5f); + kon_tweenSet(&back->focusY, 0.5f); +} + +void kon_stageInit(kon_stage_t *stage, kon_stageSprite_t *sprites, int capacity, int width, int height) +{ + kon_stage_t zero = {0}; + int i; + + *stage = zero; + stage->width = width; + stage->height = height; + stage->sprites = sprites; + stage->capacity = capacity > 0 ? capacity : 0; + for (i = 0; i < stage->capacity; i++) memset(&sprites[i], 0, sizeof sprites[i]); + stage->slotX[KON_SLOT_FAR_LEFT] = 0.12f; + stage->slotX[KON_SLOT_LEFT] = 0.3f; + stage->slotX[KON_SLOT_CENTER] = 0.5f; + stage->slotX[KON_SLOT_RIGHT] = 0.7f; + stage->slotX[KON_SLOT_FAR_RIGHT] = 0.88f; + stage->filter = KON_FILTER_NEAREST; + kon_tweenSet(&stage->back.alpha, 1.0f); + kon_tweenSet(&stage->old.alpha, 1.0f); + resetView(&stage->back); + resetView(&stage->old); +} + +void kon_stageBackground(kon_stage_t *stage, const kon_image *image, kon_fit_t fit, float seconds) +{ + if (seconds > 0.0f && stage->back.image && image) { + stage->old = stage->back; + kon_tweenSet(&stage->old.alpha, 1.0f); + stage->back.image = image; + stage->back.fit = fit; + resetView(&stage->back); + kon_tweenStart(&stage->back.alpha, 0.0f, 1.0f, seconds, KON_EASE_LINEAR); + return; + } + stage->old.image = NULL; + stage->back.image = image; + stage->back.fit = fit; + resetView(&stage->back); + if (seconds > 0.0f && image) kon_tweenStart(&stage->back.alpha, 0.0f, 1.0f, seconds, KON_EASE_LINEAR); + else kon_tweenSet(&stage->back.alpha, 1.0f); +} + +void kon_stageBackgroundView(kon_stage_t *stage, float zoom, float focusX, float focusY, float seconds, kon_ease_t ease) +{ + kon_tweenTo(&stage->back.zoom, zoom, seconds, ease); + kon_tweenTo(&stage->back.focusX, focusX, seconds, ease); + kon_tweenTo(&stage->back.focusY, focusY, seconds, ease); +} + +int kon_stageAdd(kon_stage_t *stage, const kon_image *image, const kon_rect_t *src, kon_layer_t layer) +{ + int i; + + for (i = 0; i < stage->capacity; i++) { + kon_stageSprite_t *s = &stage->sprites[i]; + int p; + + if (s->used) continue; + memset(s, 0, sizeof *s); + s->used = 1; + s->image = image; + s->src = wholeOf(image, src); + s->anchorX = 0.5f; + s->anchorY = 1.0f; + s->layer = layer; + for (p = 0; p < KON_PROP_COUNT; p++) kon_tweenSet(&s->prop[p], 0.0f); + kon_tweenSet(&s->prop[KON_PROP_SCALE], 1.0f); + kon_tweenSet(&s->prop[KON_PROP_BRIGHTNESS], 1.0f); + return i; + } + return -1; +} + +void kon_stageRemove(kon_stage_t *stage, int index) +{ + if (valid(stage, index)) stage->sprites[index].used = 0; +} + +void kon_stageLayer(kon_stage_t *stage, int index, kon_layer_t layer) +{ + if (valid(stage, index)) stage->sprites[index].layer = layer; +} + +void kon_stageFrame(kon_stage_t *stage, int index, const kon_image *image, const kon_rect_t *src) +{ + if (!valid(stage, index)) return; + stage->sprites[index].image = image; + stage->sprites[index].src = wholeOf(image, src); +} + +void kon_stageTween(kon_stage_t *stage, int index, kon_prop_t prop, float to, float seconds, kon_ease_t ease) +{ + if ((int)prop < 0 || prop >= KON_PROP_COUNT || !valid(stage, index)) return; + kon_tweenTo(&stage->sprites[index].prop[prop], to, seconds, ease); +} + +void kon_stageMove(kon_stage_t *stage, int index, float x, float y, float seconds, kon_ease_t ease) +{ + kon_stageTween(stage, index, KON_PROP_X, x, seconds, ease); + kon_stageTween(stage, index, KON_PROP_Y, y, seconds, ease); +} + +void kon_stageFade(kon_stage_t *stage, int index, float alpha, float seconds, kon_ease_t ease) +{ + kon_stageTween(stage, index, KON_PROP_ALPHA, alpha, seconds, ease); +} + +void kon_stagePlace(kon_stage_t *stage, int index, kon_slot_t slot, float seconds, kon_ease_t ease) +{ + if ((int)slot < 0 || slot >= KON_SLOT_COUNT) return; + kon_stageMove(stage, index, stage->slotX[slot] * (float)stage->width, (float)stage->height, seconds, ease); +} + +void kon_stageHide(kon_stage_t *stage, int index, float seconds) +{ + if (!valid(stage, index)) return; + kon_stageFade(stage, index, 0.0f, seconds, KON_EASE_LINEAR); + stage->sprites[index].hiding = 1; +} + +void kon_stageUpdate(kon_stage_t *stage, float dt) +{ + int i, p; + + kon_tweenStep(&stage->back.alpha, dt); + kon_tweenStep(&stage->back.zoom, dt); + kon_tweenStep(&stage->back.focusX, dt); + kon_tweenStep(&stage->back.focusY, dt); + if (stage->old.image && kon_tweenDone(&stage->back.alpha)) stage->old.image = NULL; + + for (i = 0; i < stage->capacity; i++) { + kon_stageSprite_t *s = &stage->sprites[i]; + + if (!s->used) continue; + for (p = 0; p < KON_PROP_COUNT; p++) kon_tweenStep(&s->prop[p], dt); + if (s->hiding && kon_tweenDone(&s->prop[KON_PROP_ALPHA]) && kon_tweenValue(&s->prop[KON_PROP_ALPHA]) <= 0.0f) s->used = 0; + } +} + +int kon_stageBusy(const kon_stage_t *stage) +{ + int i, p; + + if (stage->old.image || !kon_tweenDone(&stage->back.alpha)) return 1; + if (!kon_tweenDone(&stage->back.zoom) || !kon_tweenDone(&stage->back.focusX) || !kon_tweenDone(&stage->back.focusY)) return 1; + for (i = 0; i < stage->capacity; i++) { + if (!stage->sprites[i].used) continue; + for (p = 0; p < KON_PROP_COUNT; p++) { + if (!kon_tweenDone(&stage->sprites[i].prop[p])) return 1; + } + } + return 0; +} + +/*** drawing ***/ + +static float unit(float v) +{ + if (!(v >= 0.0f)) return v < 0.0f ? 0.0f : 0.5f; + return v > 1.0f ? 1.0f : v; +} + +/* where the picture starts along one axis. it is centred when it is smaller than the view, otherwise the focus is put in the middle and + the picture is kept over the whole view */ +static float startOf(float view, float size, float focus) +{ + float start; + + if (size < view) return (view - size) * 0.5f; + start = view * 0.5f - focus * size; + if (start > 0.0f) start = 0.0f; + if (start < view - size) start = view - size; + return start; +} + +static void drawBackground(const kon_stage_t *stage, kon_framebuffer_t *fb, const kon_stageBackground_t *back) +{ + kon_rect_t src; + float sx, sy, zoom, x, y, viewW = (float)stage->width, viewH = (float)stage->height; + uint32_t tint; + + if (!back->image || back->fit == KON_FIT_NONE) return; + src = wholeOf(back->image, NULL); + if (src.w <= 0 || src.h <= 0 || stage->width <= 0 || stage->height <= 0) return; + zoom = kon_tweenValue(&back->zoom); + if (!(zoom > 0.0f)) return; + + sx = viewW / (float)src.w; + sy = viewH / (float)src.h; + switch (back->fit) { + case KON_FIT_COVER: if (sx < sy) sx = sy; else sy = sx; break; + case KON_FIT_CONTAIN: if (sx > sy) sx = sy; else sy = sx; break; + case KON_FIT_CENTER: sx = sy = 1.0f; break; + case KON_FIT_STRETCH: + case KON_FIT_NONE: break; + } + sx *= zoom; + sy *= zoom; + x = startOf(viewW, (float)src.w * sx, unit(kon_tweenValue(&back->focusX))); + y = startOf(viewH, (float)src.h * sy, unit(kon_tweenValue(&back->focusY))); + if (back->fit == KON_FIT_CENTER && zoom == 1.0f) { + x = floorf(x); + y = floorf(y); + } + tint = tintOf(kon_tweenValue(&back->alpha), 1.0f); + kon_drawImageTint(fb, back->image, src, x, y, 0.0f, 0.0f, sx, sy, 0.0f, stage->filter, tint); +} + +void kon_stageDrawBackground(const kon_stage_t *stage, kon_framebuffer_t *fb) +{ + drawBackground(stage, fb, &stage->old); + drawBackground(stage, fb, &stage->back); +} + +/* sprites go out in order of z, then of index. each pass picks the next one after the last drawn, so nothing is sorted or allocated */ +void kon_stageDraw(const kon_stage_t *stage, kon_framebuffer_t *fb, kon_layer_t layer) +{ + int lastZ = 0, lastIndex = -1, first = 1; + + for (;;) { + int i, pick = -1; + + for (i = 0; i < stage->capacity; i++) { + const kon_stageSprite_t *s = &stage->sprites[i]; + + if (!s->used || s->layer != layer) continue; + if (!first && (s->z < lastZ || (s->z == lastZ && i <= lastIndex))) continue; + if (pick < 0 || s->z < stage->sprites[pick].z) pick = i; + } + if (pick < 0) return; + + { + const kon_stageSprite_t *s = &stage->sprites[pick]; + float scale = kon_tweenValue(&s->prop[KON_PROP_SCALE]); + float sx = s->flip ? -scale : scale; + float ox = s->anchorX * (float)s->src.w, oy = s->anchorY * (float)s->src.h; + + if (s->flip) ox = (float)s->src.w - ox; + if (s->image && scale > 0.0f) { + kon_drawImageTint(fb, s->image, s->src, kon_tweenValue(&s->prop[KON_PROP_X]), kon_tweenValue(&s->prop[KON_PROP_Y]), + ox, oy, sx, scale, 0.0f, stage->filter, + tintOf(kon_tweenValue(&s->prop[KON_PROP_ALPHA]), kon_tweenValue(&s->prop[KON_PROP_BRIGHTNESS]))); + } + lastZ = s->z; + lastIndex = pick; + first = 0; + } + } +} diff --git a/kon/konstage.h b/kon/konstage.h new file mode 100644 index 0000000..6c3b909 --- /dev/null +++ b/kon/konstage.h @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konstage.h + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KONSTAGE_H +#define KONSTAGE_H + +#include "konsofren.h" +#include "kon2d.h" + +/*** definition ***/ + +/* the picture behind everything. none leaves the framebuffer as it is, so a map or a scene drawn first shows through */ +typedef enum kon_fit { + KON_FIT_NONE, + KON_FIT_STRETCH, /* fills the view, squashing the picture if the shapes differ */ + KON_FIT_COVER, /* fills the view keeping the shape, the edges that don't fit are cut off */ + KON_FIT_CONTAIN, /* the whole picture keeping the shape, the rest of the view is left alone */ + KON_FIT_CENTER /* the picture as it is, in the middle */ +} kon_fit_t; + +/* sprites are drawn in two goes so the game can put its text box in between. back is under the box, front is over it, + so a see through effect on a sprite does not turn into a smear over the text */ +typedef enum kon_layer { + KON_LAYER_BACK, + KON_LAYER_FRONT +} kon_layer_t; + +/* standing places across the view, where a sprite goes with kon_stagePlace. their x is stage->slotX, as a share of the width */ +typedef enum kon_slot { + KON_SLOT_FAR_LEFT, + KON_SLOT_LEFT, + KON_SLOT_CENTER, + KON_SLOT_RIGHT, + KON_SLOT_FAR_RIGHT, + KON_SLOT_COUNT +} kon_slot_t; + +/* what kon_stageTween can glide */ +typedef enum kon_prop { + KON_PROP_X, + KON_PROP_Y, + KON_PROP_ALPHA, /* 0 to 1 */ + KON_PROP_SCALE, + KON_PROP_BRIGHTNESS, /* 1 as drawn, lower darkens it, the usual way to show who is not talking */ + KON_PROP_COUNT +} kon_prop_t; + +typedef struct kon_stageSprite { + int used; + const kon_image *image; + kon_rect_t src; /* w == 0 is the whole image */ + kon_tween_t prop[KON_PROP_COUNT]; + float anchorX, anchorY; /* the point of the sprite, 0 to 1, that sits at x, y. 0.5, 1 is the middle of its feet */ + int flip; + kon_layer_t layer; + int z; /* higher is in front inside its layer, sprites with the same z go in the order they were added */ + int hiding; /* removes itself when it has faded out */ +} kon_stageSprite_t; + +typedef struct kon_stageBackground { + const kon_image *image; + kon_fit_t fit; + kon_tween_t alpha; + kon_tween_t zoom, focusX, focusY; +} kon_stageBackground_t; + +/* the stage does not own its sprites or the images, sprites is room for capacity of them */ +typedef struct kon_stage { + int width, height; /* the view, in the pixels you draw at */ + kon_stageSprite_t *sprites; + int capacity; + float slotX[KON_SLOT_COUNT]; + kon_filter_t filter; /* for sprites and the background that get scaled */ + kon_stageBackground_t back, old; +} kon_stage_t; + +/*** functions ***/ + +void kon_stageInit(kon_stage_t *stage, kon_stageSprite_t *sprites, int capacity, int width, int height); + +/* the background fades in over the old one for seconds, 0 swaps at once. image NULL clears it */ +void kon_stageBackground(kon_stage_t *stage, const kon_image *image, kon_fit_t fit, float seconds); + +/* pans and zooms the background, which is what a CG wants. zoom 1 shows it as the fit says and 2 shows half of it. focus is the point of + the picture, 0 to 1 across and down, kept in the middle of the view. a picture that fills the view never lets its edge slide in. + a new background starts at zoom 1, focus 0.5 */ +void kon_stageBackgroundView(kon_stage_t *stage, float zoom, float focusX, float focusY, float seconds, kon_ease_t ease); + +/* a new sprite at 0, 0 in view, not faded in. returns its index or -1 when there is no room. src NULL is the whole image. + it stands on the bottom middle of itself, change anchorX and anchorY on the sprite for anything else */ +int kon_stageAdd(kon_stage_t *stage, const kon_image *image, const kon_rect_t *src, kon_layer_t layer); +void kon_stageRemove(kon_stage_t *stage, int index); + +/* moves a sprite to the other side of the text box. they start in the back layer, behind the box, which is where characters belong */ +void kon_stageLayer(kon_stage_t *stage, int index, kon_layer_t layer); + +/* swaps the picture, for a new expression. the sprite keeps where it is */ +void kon_stageFrame(kon_stage_t *stage, int index, const kon_image *image, const kon_rect_t *src); + +void kon_stageTween(kon_stage_t *stage, int index, kon_prop_t prop, float to, float seconds, kon_ease_t ease); + +/* shorthands for a move, a fade and for walking to a slot along the bottom of the view */ +void kon_stageMove(kon_stage_t *stage, int index, float x, float y, float seconds, kon_ease_t ease); +void kon_stageFade(kon_stage_t *stage, int index, float alpha, float seconds, kon_ease_t ease); +void kon_stagePlace(kon_stage_t *stage, int index, kon_slot_t slot, float seconds, kon_ease_t ease); + +/* fades out and removes itself */ +void kon_stageHide(kon_stage_t *stage, int index, float seconds); + +void kon_stageUpdate(kon_stage_t *stage, float dt); + +/* 1 while anything is still gliding or fading, for a script that waits for it */ +int kon_stageBusy(const kon_stage_t *stage); + +/* draw the background and then the back layer, then your text box, then the front layer. without a background pass 0 */ +void kon_stageDrawBackground(const kon_stage_t *stage, kon_framebuffer_t *fb); +void kon_stageDraw(const kon_stage_t *stage, kon_framebuffer_t *fb, kon_layer_t layer); + +#endif -- cgit