Files
Vibe-HD-2D/engine/include/hd2d/UI.h
Gabrieal Jimmy 83a1f16806 Sprite sheets, atlases, game UI, time of day, weather presets, headless captures
- BillboardInstance: per-instance uv sub-rect, feet anchor, turn, dithered alpha, flash;
  BillboardBatch::sharedTexture; Texture createEmpty/update/cellUV/cellAnchor/nearest
- SpriteAtlas: fixed-cell runtime atlas with gutters
- hd2d::ui: Octopath-style panels, fonts (Cinzel, EB Garamond), dialogue box with
  typewriter + portraits + choices, menus, nameplates, toasts, ImGui theme
- DayCycle (dawn/day/dusk/night keyframes, sun + moon arcs), Weather overcast/mist/
  lightning, night-aware presets, stars and moon in the sky
- Hidden-window offscreen captures (Window visible=false, Renderer output FBO,
  OffscreenTarget); --screenshot never opens a window
- Camera::worldToScreen; CMake usable via add_subdirectory (HD2D_BUILD_DEMO,
  HD2D_ENGINE_ASSET_DIR)
2026-10-02 15:05:23 -05:00

163 lines
7.0 KiB
C++

#pragma once
// HD-2D style game UI (Octopath Traveler / Triangle Strategy look) drawn with
// Dear ImGui draw lists: translucent navy panels with a gold double border and
// corner gems, serif text with a soft shadow, name plates, menus, progress
// bars, a typewriter dialogue box with choices, and toast notifications.
//
// Everything draws into an ImDrawList (usually ImGui::GetForegroundDrawList()),
// so no ImGui windows are needed. Sizes are in pixels at a 1080 px tall
// viewport; multiply by scaleFor(viewport) (the widgets taking `scale` or a
// viewport do this for you).
//
// Setup: DebugUI::init(), then ui::loadFonts(dir, ui::scaleFor(viewport))
// before the first ImGui frame (or between frames to rebake after a resize).
#include <imgui.h>
#include <string>
#include <string_view>
#include <vector>
namespace hd2d::ui {
// Game fonts. `body` dialogue/menu text (serif), `title` large display face,
// `name` speaker / name plates (small caps serif), `small` captions and toasts.
// ImGui's default font stays the default so debug panels are unaffected.
struct Fonts {
ImFont* body = nullptr;
ImFont* title = nullptr;
ImFont* name = nullptr;
ImFont* small = nullptr;
};
// Nominal font sizes (px at 1080p). Fonts are baked at size * pixelScale so
// text drawn at size * scaleFor(viewport) is pixel-exact.
inline constexpr float kBodySize = 34.0f;
inline constexpr float kTitleSize = 96.0f;
inline constexpr float kNameSize = 28.0f;
inline constexpr float kSmallSize = 26.0f;
// Loads the fonts from `fontDir` (assets/fonts) baked for `pixelScale` and
// rebuilds the ImGui font atlas. Requires an ImGui context (DebugUI::init)
// and must be called outside an ImGui frame. Returns false if a font file is
// missing (the missing slots fall back to the default font).
bool loadFonts(const std::string& fontDir, float pixelScale = 1.0f);
const Fonts& fonts();
// Styles standard ImGui widgets (windows, buttons, input text, sliders, combo,
// checkboxes, tabs, scrollbars) in the same navy + gold palette.
void applyTheme();
// UI scale for a viewport in pixels: 1.0 at 1080 px tall.
float scaleFor(ImVec2 viewport);
// Shared palette.
inline constexpr ImU32 kGold = IM_COL32(222, 189, 120, 255);
inline constexpr ImU32 kGoldDim = IM_COL32(150, 122, 74, 255);
inline constexpr ImU32 kTextColor = IM_COL32(246, 240, 226, 255);
inline constexpr ImU32 kTextMuted = IM_COL32(184, 178, 164, 255);
// `alpha` is the background opacity; `tint` modulates the border/ornament
// colour and its alpha fades the whole panel (shadow, background, border).
// `rounding` is in 1080p pixels; `scale` 0 = scaleFor(ImGui display size).
struct PanelStyle {
float alpha = 0.88f;
bool ornate = true;
float rounding = 6.0f;
ImU32 tint = IM_COL32(255, 255, 255, 255);
float scale = 0.0f;
};
void panel(ImDrawList* dl, ImVec2 min, ImVec2 max, const PanelStyle& style = {});
// Text with a 1px dark drop shadow. `font` may be null (current ImGui font).
void text(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s, bool shadow = true);
// Word-wrapped variant of text(); wrapWidth <= 0 disables wrapping.
void textWrapped(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s,
float wrapWidth, bool shadow = true);
ImVec2 measure(ImFont* font, float size, std::string_view s, float wrapWidth = 0.0f);
void textCentered(ImDrawList* dl, ImFont* font, float size, ImVec2 center, ImU32 color, std::string_view s,
bool shadow = true);
// Big glowing gold title with flourish rules underneath (title screens).
void titleText(ImDrawList* dl, ImVec2 center, std::string_view s, float size);
// Small pill label whose pointer tip sits on `anchorPx` (e.g. a projected
// head position). `accent` colours the border and pointer.
void nameplate(ImDrawList* dl, ImVec2 anchorPx, std::string_view s, ImU32 accent, float scale = 1.0f);
// Vertical option list in a panel with an animated cursor on `selected`.
void menu(ImDrawList* dl, ImVec2 center, const std::vector<std::string>& items, int selected,
std::string_view title = {}, float scale = 1.0f);
// Same as menu(), placed so that `pivot` (0..1 of the panel size) lands on `pos`.
void menuAt(ImDrawList* dl, ImVec2 pos, ImVec2 pivot, const std::vector<std::string>& items, int selected,
std::string_view title = {}, float scale = 1.0f);
// Panel size menu()/menuAt() will use.
ImVec2 menuSize(const std::vector<std::string>& items, std::string_view title = {}, float scale = 1.0f);
// Recessed bar filled to t (0..1) with a vertical gradient of `color`.
void progressBar(ImDrawList* dl, ImVec2 min, ImVec2 max, float t, ImU32 color);
// Small blinking/bobbing gem pointing down ("more text" indicator); `time` drives the animation.
void advanceIndicator(ImDrawList* dl, ImVec2 center, float scale, float time);
struct DialogueLine {
std::string speaker; // empty = no name plate
std::string text; // UTF-8, word-wrapped to the box
ImTextureID portrait = 0; // optional, drawn square on the left inside the box
ImVec2 uv0{0, 0}, uv1{1, 1};
};
// Bottom-centre typewriter dialogue box. Typical loop:
// if (confirmPressed && box.advance()) handle(box.choice());
// box.update(dt); box.draw(viewport);
class DialogueBox {
public:
void start(std::vector<DialogueLine> lines, std::vector<std::string> choices = {}); // choices shown with the last line
bool isOpen() const { return open_; }
void update(float dt);
bool advance(); // reveal the rest of the line, else next line; returns true when the conversation closed
void moveChoice(int delta);
int choice() const { return chosen_; } // chosen index once closed with choices, else -1
void close();
void draw(ImVec2 viewport); // draws into ImGui::GetForegroundDrawList()
bool lineRevealed() const; // current line fully shown
bool choicesVisible() const; // last line revealed and choices pending
float charsPerSecond = 48.0f;
float sentencePause = 0.22f; // extra seconds after . ! ? and an ellipsis
float clausePause = 0.09f; // extra seconds after , ; : and dashes
private:
void stepChar();
std::vector<DialogueLine> lines_;
std::vector<std::string> choices_;
size_t line_ = 0;
size_t revealed_ = 0; // bytes of the current line shown (always on a UTF-8 boundary)
float budget_ = 0.0f; // seconds banked for the typewriter
float time_ = 0.0f;
int selected_ = 0;
int chosen_ = -1;
bool open_ = false;
};
// Stacked notification banners at the top centre that slide in and fade out.
class Toasts {
public:
void push(std::string text, ImU32 accent = IM_COL32(230, 190, 90, 255), float seconds = 3.5f);
void update(float dt);
void draw(ImVec2 viewport); // draws into ImGui::GetForegroundDrawList()
void clear() { items_.clear(); }
private:
struct Item {
std::string text;
ImU32 accent;
float seconds, age = 0.0f;
};
std::vector<Item> items_;
};
} // namespace hd2d::ui