summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-12 13:45:27 +0800
committerKonAki <konaki@anokonaki.org>2026-10-12 13:45:27 +0800
commitc2484a05a02f50e1ed14a7527ee6507326277ef2 (patch)
treec06de475b5b58d5c10a70b848cdcdaa08b7c8052
parent6dd2b5cb0cc6a9a1b1856be5e5198c3692e5f74d (diff)
add stage module: sprites with layers, slots, tweens, backgrounds with pan and zoom
-rw-r--r--kon/konstage.c317
-rw-r--r--kon/konstage.h141
2 files changed, 458 insertions, 0 deletions
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 <math.h>
+#include <string.h>
+
+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