animated example

This commit is contained in:
Jorijn van der Graaf 2026-05-02 00:03:24 +02:00
commit c9fd1b1585
17 changed files with 576 additions and 465 deletions

Binary file not shown.

View file

@ -0,0 +1,55 @@
# VulkanAnimated
A live HUD demo: three `Observable<float>`s drive `ProgressBar`s, three
`Observable<std::string>`s drive `Text` labels, and an FPS readout in
the corner ticks every frame. Everything updates from a single
`onUpdate` listener — no `Invalidate()` / `Redraw()` calls.
## What it shows
- **`Observable<T>` data flow**: change a value in `onUpdate`, the
next frame's `RebuildFrame` re-emits the draw list with the new value
automatically. No tree rebuild.
- **`ProgressBar::bindValue(obs, lo, hi)`** — the bar fill normalises the
observable's current value into 0..1 each frame.
- **`Text::bind(observable)`** — the displayed string is sourced from
the observable each frame, replacing any baked-in runs.
- **Composition pattern**: a small lambda helper builds one HP/MP-style
row (`Text` + `ProgressBar` inside an `HStack`) given the observables
and a colour, then the row is dropped into the parent `VStack` like
any other widget.
- **Different update rates per observable**: `health` oscillates at 0.7
rad/s, `mana` at 1.3, `charge` advances linearly modulo 1 — visible
proof that each observable updates independently.
## Run
```bash
cd examples/VulkanAnimated
crafter-build -r
```
You should see "Animated HUD" with three coloured bars (red HP, blue MP,
yellow Charge) all moving at different rates, with the FPS readout in
the top-right ticking once per frame.
Click `Quit` to exit.
## Notes
The whole tick handler is just:
```cpp
EventListener<FrameTime> tick(&window.onUpdate, [&](FrameTime ft){
t += ft.delta.count();
health = 0.5f + 0.5f * std::sin(t * 0.7f);
mana = std::abs(std::sin(t * 1.3f));
charge = std::fmod(t * 0.3f, 1.0f);
healthLabel = std::format("HP {:>3.0f} / 100", health.Get() * 100);
// …
});
```
That's the entire animation system. There is deliberately no
`Animation<T>` / tween primitive in the library — drive observables
from any source you like.

View file

@ -0,0 +1,115 @@
#include "vulkan/vulkan.h"
import Crafter.Graphics;
import Crafter.Event;
import Crafter.Math;
import std;
using namespace Crafter;
// A simple "game HUD" demo: three Observable<float> drive ProgressBars at
// different rates / colours, while Observable<std::string> labels feed the
// Text widgets next to them. UIScene::RebuildFrame re-emits each frame, so
// the only application code needed is "update the observables in
// onUpdate" — no manual Invalidate / Redraw calls.
int main() {
Device::Initialize();
Window window(1280, 720, "VulkanAnimated");
window.StartInit();
window.FinishInit();
Font font("Inter.ttf");
UI::Theme theme = UI::themes::default_dark();
UI::UIScene scene;
scene.Initialize(window, "ui.comp.spv");
scene.background(UI::Color{0.06f, 0.07f, 0.10f, 1.0f});
// ─── Observables ─────────────────────────────────────────────────────
UI::Observable<float> health{1.0f};
UI::Observable<float> mana {0.5f};
UI::Observable<float> charge{0.0f};
UI::Observable<std::string> healthLabel;
UI::Observable<std::string> manaLabel;
UI::Observable<std::string> chargeLabel;
UI::Observable<std::string> fpsLabel;
// ─── Per-frame tick: drive the observables. UIScene re-emits on
// onUpdate, so any read of these values is automatically picked
// up in the next frame's draw list.
float t = 0.0f;
EventListener<FrameTime> tick(&window.onUpdate, [&](FrameTime ft) {
const float dt = static_cast<float>(ft.delta.count());
t += dt;
// Three offset waves at different rates / phases.
health = 0.5f + 0.5f * std::sin(t * 0.7f);
mana = std::abs(std::sin(t * 1.3f));
charge = std::fmod(t * 0.3f, 1.0f);
healthLabel = std::format("HP {:>3.0f} / 100", health.Get() * 100.0f);
manaLabel = std::format("MP {:>3.0f} / 100", mana.Get() * 100.0f);
chargeLabel = std::format("Charge {:>3.0f}%", charge.Get() * 100.0f);
fpsLabel = (dt > 0.0f) ? std::format("{:>5.1f} fps", 1.0f / dt) : std::string{"---.- fps"};
});
// ─── Helper: one HP/MP-style row (label on the left, bar on the right).
// Build into a local (avoids returning a reference to a temporary
// that dies at the end of the chain expression).
auto bar = [&](UI::Observable<std::string>& label,
UI::Observable<float>& value,
UI::Color fg) -> UI::HStack {
UI::HStack h;
h.width(UI::Length::Frac(1))
.spacing(12)
.children(
UI::Text{}.bind(label).font(font).size(16)
.width(UI::Length::Px(160)),
UI::ProgressBar{}
.bindValue(value, 0.0f, 1.0f)
.foreground(fg)
.size(UI::Length::Frac(1), UI::Length::Px(20))
);
return h;
};
scene.Root(
UI::VStack{}
.padding(28)
.spacing(16)
.children(
UI::HStack{}
.width(UI::Length::Frac(1))
.children(
UI::Text{"Animated HUD"}.font(font).size(28),
UI::Spacer{},
UI::Text{}.bind(fpsLabel).font(font).size(16)
.color(UI::Color{0.55f, 0.85f, 1.0f, 1.0f})
),
UI::Text{"Three Observable<float>s drive the bars; "
"Observable<std::string>s drive the labels."}
.font(font).size(14).color(UI::Color{0.65f, 0.65f, 0.65f, 1}),
bar(healthLabel, health, UI::Color{0.90f, 0.30f, 0.30f, 1.0f}),
bar(manaLabel, mana, UI::Color{0.30f, 0.55f, 0.95f, 1.0f}),
bar(chargeLabel, charge, UI::Color{0.95f, 0.85f, 0.30f, 1.0f}),
UI::Spacer{},
UI::HStack{}
.width(UI::Length::Frac(1))
.spacing(8)
.children(
UI::Spacer{},
UI::Button{"Quit"}.font(font).style(theme.danger)
.onClick([]{ std::_Exit(0); })
)
)
);
window.Render();
window.StartUpdate();
window.StartSync();
}

View file

@ -0,0 +1,27 @@
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
std::vector<std::string> graphicsArgs(args.begin(), args.end());
Configuration* graphics = LocalProject({
.projectFile = "../../project.cpp",
.args = graphicsArgs,
});
Configuration cfg;
cfg.path = "./";
cfg.name = "VulkanAnimated";
cfg.outputName = "VulkanAnimated";
ApplyStandardArgs(cfg, args);
cfg.dependencies = { graphics };
std::array<fs::path, 0> ifaces = {};
std::array<fs::path, 1> impls = { "main" };
cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.files.push_back("Inter.ttf");
return cfg;
}