Polish FrameYap panel with rounded neon controls

This commit is contained in:
baketnk committed 2026-09-24 12:46:29 -04:00
1 parent 04b404a653
commit 18a66b021c
4 files changed
+57 -32

No files matched your search

+6 -2
View File
@@ -13,8 +13,12 @@ past observations; it never proves current device availability or grants a live
placeholder installer as functional or conflate an OpenVR app key with a store ID. placeholder installer as functional or conflate an OpenVR app key with a store ID.
- Default builds and tests are offline and hardware-free. No implicit package/model - Default builds and tests are offline and hardware-free. No implicit package/model
downloads, microphone recording, input injection or OpenVR initialization. downloads, microphone recording, input injection or OpenVR initialization.
- Hardware tests must be opt-in and distinguish API discovery from delivered input, - For changes intended for Frame, a local build is not completion: after offline
transcription quality, performance and human headset acceptance. checks, upload/install the tested native build on the Frame test device and verify
the deployed version when device access is authorized. If unavailable, report
explicitly that the change is local-only; do not imply the headset was updated.
- Live hardware tests remain opt-in and must distinguish API discovery from delivered
input, transcription quality, performance and human headset acceptance.
- Preserve existing SSH and user sessions; never terminate SSH/session processes or - Preserve existing SSH and user sessions; never terminate SSH/session processes or
use broad cleanup/restart commands. Stop only processes this project owns. use broad cleanup/restart commands. Stop only processes this project owns.
- Keep audio, transcripts, model weights, credentials and private logs out of Git. - Keep audio, transcripts, model weights, credentials and private logs out of Git.
+5 -2
View File
@@ -19,8 +19,11 @@ CJK coverage is claimed).
`src/panel_surface.*` renders **one 1000×680 RGBA canvas** for review, settings, `src/panel_surface.*` renders **one 1000×680 RGBA canvas** for review, settings,
status and controls. One OpenVR handle receives it with `SetOverlayRaw`; tabs do status and controls. One OpenVR handle receives it with `SetOverlayRaw`; tabs do
not create extra overlays or render targets. The rounded mint-to-blue perimeter, not create extra overlays or render targets. The rounded mint-to-blue perimeter,
shallow curved accent, and dark cards borrow kouseki's VR visual language, with shallow curved accent, and dark cards borrow kouseki's VR visual language. Rounded
an independently implemented CPU renderer and a baked halo rather than GPU bloom. preview, status and control surfaces use independently rasterized antialiased edges
and restrained baked neon halos rather than GPU bloom. The recording indicator and
selected controls remain distinguishable by their labels, not color alone. Rounded
control hit areas exclude their clipped corners.
Rendering/uploads occur only for changed content, page, settings, or pointer Rendering/uploads occur only for changed content, page, settings, or pointer
feedback; static frames are reused. The caller may call `draw(Panel)` at 10 ms feedback; static frames are reused. The caller may call `draw(Panel)` at 10 ms
intervals. Tracking transforms do not require repainting the canvas. intervals. Tracking transforms do not require repainting the canvas.
+44 -28
View File
@@ -15,10 +15,17 @@ using Color = std::array<unsigned char, 4>;
constexpr Color background{12, 16, 27, 255}, card{20, 28, 43, 255}; constexpr Color background{12, 16, 27, 255}, card{20, 28, 43, 255};
constexpr Color ink{230, 240, 249, 255}, muted{151, 173, 193, 255}; constexpr Color ink{230, 240, 249, 255}, muted{151, 173, 193, 255};
constexpr Color cyan{31, 240, 164, 255}, pink{255, 110, 135, 255}; constexpr Color cyan{31, 240, 164, 255}, pink{255, 110, 135, 255};
float rounded_distance(float px, float py, int x, int y, int w, int h, float radius) {
const float dx = std::abs(px - (x + w / 2.f)) - (w / 2.f - radius);
const float dy = std::abs(py - (y + h / 2.f)) - (h / 2.f - radius);
return std::hypot(std::max(dx, 0.f), std::max(dy, 0.f)) +
std::min(std::max(dx, dy), 0.f) - radius;
}
struct Rect { struct Rect {
int x, y, w, h; int x, y, w, h;
bool contains(float px, float py) const { bool contains(float px, float py) const {
return std::isfinite(px) && std::isfinite(py) && px >= x && px < x + w && py >= y && py < y + h; return std::isfinite(px) && std::isfinite(py) && px >= x && px < x + w && py >= y && py < y + h &&
rounded_distance(px, py, x, y, w, h, std::min(16, h / 3)) <= 0.f;
} }
}; };
enum class Control { Review, Settings, Prev, Next, Record, Cancel, Insert, Enter, Quit, enum class Control { Review, Settings, Prev, Next, Record, Cancel, Insert, Enter, Quit,
@@ -117,28 +124,33 @@ struct PanelSurface::Impl {
for (int x = std::max(r.x, 0); x < std::min(W, r.x + r.w); ++x) for (int x = std::max(r.x, 0); x < std::min(W, r.x + r.w); ++x)
std::copy(c.begin(), c.end(), pixels.begin() + (size_t(y) * W + x) * 4); std::copy(c.begin(), c.end(), pixels.begin() + (size_t(y) * W + x) * 4);
} }
void border(Rect r, Color c, int thickness = 1) { void blend(int x, int y, Color color, float amount) {
rect({r.x, r.y, r.w, thickness}, c); rect({r.x, r.y + r.h - thickness, r.w, thickness}, c); auto* dst = pixels.data() + (size_t(y) * W + x) * 4;
rect({r.x, r.y, thickness, r.h}, c); rect({r.x + r.w - thickness, r.y, thickness, r.h}, c); for (int k = 0; k < 3; ++k)
dst[k] = static_cast<unsigned char>(dst[k] * (1.f - amount) + color[k] * amount);
} }
void neon(Rect r, Color c) { void rounded(Rect r, int radius, Color fill, Color edge, float glow = 0.f, int stroke = 1) {
// Baked halo + bright core: no extra overlay, mesh, bloom pass or animation. // Distance-field antialiasing and restrained baked glow. No second texture or GPU pass.
for (int spread = 5; spread >= 1; --spread) { constexpr int halo = 10;
Color halo = background; for (int y = std::max(0, r.y - halo); y < std::min(H, r.y + r.h + halo); ++y)
for (int k = 0; k < 3; ++k) halo[k] += (c[k] - halo[k]) / (spread * 3 + 2); for (int x = std::max(0, r.x - halo); x < std::min(W, r.x + r.w + halo); ++x) {
border({r.x - spread, r.y - spread, r.w + spread * 2, r.h + spread * 2}, halo); const float d = rounded_distance(x + .5f, y + .5f, r.x, r.y, r.w, r.h, radius);
} if (glow > 0.f && d > 0.f && d < halo) {
border(r, c, 2); const float falloff = 1.f - d / halo;
blend(x, y, edge, glow * falloff * falloff);
}
if (d < .5f) blend(x, y, fill, std::clamp(.5f - d, 0.f, 1.f));
if (d > -stroke - .5f && d < .5f)
blend(x, y, edge, std::clamp(d + stroke + .5f, 0.f, 1.f) *
std::clamp(.5f - d, 0.f, 1.f));
}
} }
void frame() { void frame() {
// Independently rasterized version of kouseki's HUD visual language: // Independently rasterized version of kouseki's HUD visual language:
// rounded mint-to-blue perimeter and a second shallow curved accent. // rounded mint-to-blue perimeter and a second shallow curved accent.
for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) { for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
if (x > 34 && x < W - 34 && y > 34 && y < H - 34) continue; if (x > 34 && x < W - 34 && y > 34 && y < H - 34) continue;
const float qx = std::abs(x - W / 2.f) - (W / 2.f - 28.f); const float distance = rounded_distance(x, y, 10, 10, W - 20, H - 20, 18.f);
const float qy = std::abs(y - H / 2.f) - (H / 2.f - 28.f);
const float distance = std::hypot(std::max(qx, 0.f), std::max(qy, 0.f)) +
std::min(std::max(qx, qy), 0.f) - 18.f;
const float edge = std::abs(distance); const float edge = std::abs(distance);
const float strength = edge <= 1.f ? 1.f : .30f * std::max(0.f, 1.f - (edge - 1.f) / 6.f); const float strength = edge <= 1.f ? 1.f : .30f * std::max(0.f, 1.f - (edge - 1.f) / 6.f);
const float t = float(x) / W; const float t = float(x) / W;
@@ -252,14 +264,16 @@ struct PanelSurface::Impl {
text("FrameYap", 32, 61, 40, ink, 300); text("FrameYap", 32, 61, 40, ink, 300);
text("ON-DEVICE / REVIEW FIRST", 280, 58, 22, muted, 720); text("ON-DEVICE / REVIEW FIRST", 280, 58, 22, muted, 720);
text(mount_label(mount), 756, 58, 24, cyan, 968); text(mount_label(mount), 756, 58, 24, cyan, 968);
rect({32, 84, 7, 34}, panel.recording ? pink : cyan); rounded({32, 78, 936, 48}, 13, {18, 31, 44, 255},
panel.recording ? Color{115, 64, 83, 255} : Color{45, 82, 98, 255},
panel.recording ? .16f : 0.f);
rounded({46, 95, 13, 13}, 6, panel.recording ? pink : cyan,
panel.recording ? pink : cyan, panel.recording ? .40f : .20f);
auto status = wrap(panel.status, 27, 790); auto status = wrap(panel.status, 27, 790);
text(status.front(), 54, 109, 27, ink, 844); text(status.front(), 70, 109, 27, ink, 844);
if (status.size() > 1) text("[...]", 864, 109, 23, muted, 968); if (status.size() > 1) text("[...]", 864, 109, 23, muted, 968);
rect({32, 198, 936, 1}, {44, 61, 79, 255});
if (!settings) { if (!settings) {
rect({32, 214, 936, 176}, card); rounded({32, 212, 936, 178}, 16, {22, 34, 49, 255}, {53, 103, 122, 255}, .18f);
rect({32, 214, 3, 176}, {50, 102, 119, 255});
for (size_t i = 0; i < lines_per_page && page * lines_per_page + i < lines.size(); ++i) for (size_t i = 0; i < lines_per_page && page * lines_per_page + i < lines.size(); ++i)
text(lines[page * lines_per_page + i], 48, 246 + int(i) * 40, 32, text(lines[page * lines_per_page + i], 48, 246 + int(i) * 40, 32,
panel.transcript.empty() ? muted : ink, 952); panel.transcript.empty() ? muted : ink, 952);
@@ -275,7 +289,7 @@ struct PanelSurface::Impl {
32, 486, 23, muted, 968); 32, 486, 23, muted, 968);
text("Tracking lost? Wrist placement falls back to world space.", 32, 518, 23, muted, 968); text("Tracking lost? Wrist placement falls back to world space.", 32, 518, 23, muted, 968);
} }
rect({32, 556, 936, 1}, {44, 61, 79, 255}); rect({32, 550, 936, 1}, {39, 65, 80, 255});
for (size_t i = 0; i < buttons.size(); ++i) { for (size_t i = 0; i < buttons.size(); ++i) {
const auto& b = buttons[i]; const auto& b = buttons[i];
if (!visible(b.id)) continue; if (!visible(b.id)) continue;
@@ -284,12 +298,14 @@ struct PanelSurface::Impl {
(mounting(b.id) && *mounting(b.id) == mount); (mounting(b.id) && *mounting(b.id) == mount);
bool hover = std::find(hovered.begin(), hovered.end(), int(i)) != hovered.end(); bool hover = std::find(hovered.begin(), hovered.end(), int(i)) != hovered.end();
bool down = std::find(pressed.begin(), pressed.end(), int(i)) != pressed.end(); bool down = std::find(pressed.begin(), pressed.end(), int(i)) != pressed.end();
rect(b.r, !on ? Color{18, 23, 34, 255} : down ? Color{37, 74, 89, 255} : const Color fill = !on ? Color{17, 23, 33, 255} : down ? Color{35, 77, 88, 255} :
hover || selected ? Color{27, 53, 68, 255} : card); hover || selected ? Color{26, 55, 68, 255} : card;
Color accent = b.id == Control::Record && panel.recording ? pink : cyan; const Color accent = b.id == Control::Record && panel.recording ? pink : cyan;
if (on && (hover || selected || b.id == Control::Record)) neon(b.r, accent); const bool highlighted = on && (hover || selected || b.id == Control::Record ||
else border(b.r, on ? Color{58, 79, 101, 255} : Color{30, 40, 53, 255}); (b.id == Control::Insert && panel.transcript.size()));
if (selected) rect({b.r.x + 1, b.r.y + 1, 4, b.r.h - 2}, cyan); rounded(b.r, std::min(16, b.r.h / 3), fill,
!on ? Color{35, 46, 59, 255} : highlighted ? accent : Color{67, 93, 112, 255},
highlighted ? (hover || down ? .40f : .23f) : 0.f, highlighted ? 2 : 1);
const auto label = b.id == Control::Record && panel.recording ? "Stop" : b.label; const auto label = b.id == Control::Record && panel.recording ? "Stop" : b.label;
text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, 27, on ? ink : Color{81, 96, 113, 255}, b.r.x + b.r.w - 8); text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, 27, on ? ink : Color{81, 96, 113, 255}, b.r.x + b.r.w - 8);
if (mounting(b.id) && selected) text("ON", b.r.x + b.r.w - 56, b.r.y + 38, 23, cyan, b.r.x + b.r.w - 12); if (mounting(b.id) && selected) text("ON", b.r.x + b.r.w - 56, b.r.y + 38, 23, cyan, b.r.x + b.r.w - 12);
+2
View File
@@ -34,6 +34,8 @@ int main(int argc, char** argv) {
assert(surface.pixels()[(40 * PanelSurface::width + 40) * 4 + 3] == 255); assert(surface.pixels()[(40 * PanelSurface::width + 40) * 4 + 3] == 255);
// Disabled controls never emit insertion/submission. Retry/Cancel remain available. // Disabled controls never emit insertion/submission. Retry/Cancel remain available.
no_action(click(surface, 480, 610)); no_action(click(surface, 680, 610)); no_action(click(surface, 480, 610)); no_action(click(surface, 680, 610));
no_action(click(surface, 32, 574)); // clipped visual corners are not invisible hit targets
no_action(click(surface, 207, 575));
assert(click(surface, 100, 610).action == UiAction::BeginRecord); assert(click(surface, 100, 610).action == UiAction::BeginRecord);
assert(click(surface, 280, 610).action == UiAction::Cancel); assert(click(surface, 280, 610).action == UiAction::Cancel);
no_action(surface.pointer_up(0, 100, 610)); no_action(surface.pointer_up(0, 100, 610));