From 18a66b021c1816055be24a6d27a71033997e7e77 Mon Sep 17 00:00:00 2001 From: baketnk Date: Thu, 24 Sep 2026 12:46:29 -0400 Subject: [PATCH] Polish FrameYap panel with rounded neon controls --- AGENTS.md | 8 +++-- docs/overlay.md | 7 +++-- src/panel_surface.cpp | 72 ++++++++++++++++++++++++++----------------- tests/panel_test.cpp | 2 ++ 4 files changed, 57 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d3bdd71..9dd6d05 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - Default builds and tests are offline and hardware-free. No implicit package/model downloads, microphone recording, input injection or OpenVR initialization. -- Hardware tests must be opt-in and distinguish API discovery from delivered input, - transcription quality, performance and human headset acceptance. +- For changes intended for Frame, a local build is not completion: after offline + 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 use broad cleanup/restart commands. Stop only processes this project owns. - Keep audio, transcripts, model weights, credentials and private logs out of Git. diff --git a/docs/overlay.md b/docs/overlay.md index 6b61815..25b52bb 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -19,8 +19,11 @@ CJK coverage is claimed). `src/panel_surface.*` renders **one 1000×680 RGBA canvas** for review, settings, 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, -shallow curved accent, and dark cards borrow kouseki's VR visual language, with -an independently implemented CPU renderer and a baked halo rather than GPU bloom. +shallow curved accent, and dark cards borrow kouseki's VR visual language. Rounded +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 feedback; static frames are reused. The caller may call `draw(Panel)` at 10 ms intervals. Tracking transforms do not require repainting the canvas. diff --git a/src/panel_surface.cpp b/src/panel_surface.cpp index a2a8efa..9fc3082 100644 --- a/src/panel_surface.cpp +++ b/src/panel_surface.cpp @@ -15,10 +15,17 @@ using Color = std::array; 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 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 { int x, y, w, h; 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, @@ -117,28 +124,33 @@ struct PanelSurface::Impl { 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); } - void border(Rect r, Color c, int thickness = 1) { - rect({r.x, r.y, r.w, thickness}, c); rect({r.x, r.y + r.h - thickness, r.w, thickness}, c); - rect({r.x, r.y, thickness, r.h}, c); rect({r.x + r.w - thickness, r.y, thickness, r.h}, c); + void blend(int x, int y, Color color, float amount) { + auto* dst = pixels.data() + (size_t(y) * W + x) * 4; + for (int k = 0; k < 3; ++k) + dst[k] = static_cast(dst[k] * (1.f - amount) + color[k] * amount); } - void neon(Rect r, Color c) { - // Baked halo + bright core: no extra overlay, mesh, bloom pass or animation. - for (int spread = 5; spread >= 1; --spread) { - Color halo = background; - for (int k = 0; k < 3; ++k) halo[k] += (c[k] - halo[k]) / (spread * 3 + 2); - border({r.x - spread, r.y - spread, r.w + spread * 2, r.h + spread * 2}, halo); - } - border(r, c, 2); + void rounded(Rect r, int radius, Color fill, Color edge, float glow = 0.f, int stroke = 1) { + // Distance-field antialiasing and restrained baked glow. No second texture or GPU pass. + constexpr int halo = 10; + for (int y = std::max(0, r.y - halo); y < std::min(H, r.y + r.h + halo); ++y) + for (int x = std::max(0, r.x - halo); x < std::min(W, r.x + r.w + halo); ++x) { + 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) { + 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() { // Independently rasterized version of kouseki's HUD visual language: // 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) { 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 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 distance = rounded_distance(x, y, 10, 10, W - 20, H - 20, 18.f); 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 t = float(x) / W; @@ -252,14 +264,16 @@ struct PanelSurface::Impl { text("FrameYap", 32, 61, 40, ink, 300); text("ON-DEVICE / REVIEW FIRST", 280, 58, 22, muted, 720); 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); - 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); - rect({32, 198, 936, 1}, {44, 61, 79, 255}); if (!settings) { - rect({32, 214, 936, 176}, card); - rect({32, 214, 3, 176}, {50, 102, 119, 255}); + rounded({32, 212, 936, 178}, 16, {22, 34, 49, 255}, {53, 103, 122, 255}, .18f); 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, panel.transcript.empty() ? muted : ink, 952); @@ -275,7 +289,7 @@ struct PanelSurface::Impl { 32, 486, 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) { const auto& b = buttons[i]; if (!visible(b.id)) continue; @@ -284,12 +298,14 @@ struct PanelSurface::Impl { (mounting(b.id) && *mounting(b.id) == mount); bool hover = std::find(hovered.begin(), hovered.end(), int(i)) != hovered.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} : - hover || selected ? Color{27, 53, 68, 255} : card); - Color accent = b.id == Control::Record && panel.recording ? pink : cyan; - if (on && (hover || selected || b.id == Control::Record)) neon(b.r, accent); - else border(b.r, on ? Color{58, 79, 101, 255} : Color{30, 40, 53, 255}); - if (selected) rect({b.r.x + 1, b.r.y + 1, 4, b.r.h - 2}, cyan); + const Color fill = !on ? Color{17, 23, 33, 255} : down ? Color{35, 77, 88, 255} : + hover || selected ? Color{26, 55, 68, 255} : card; + const Color accent = b.id == Control::Record && panel.recording ? pink : cyan; + const bool highlighted = on && (hover || selected || b.id == Control::Record || + (b.id == Control::Insert && panel.transcript.size())); + 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; 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); diff --git a/tests/panel_test.cpp b/tests/panel_test.cpp index b994f1d..e55d455 100644 --- a/tests/panel_test.cpp +++ b/tests/panel_test.cpp @@ -34,6 +34,8 @@ int main(int argc, char** argv) { assert(surface.pixels()[(40 * PanelSurface::width + 40) * 4 + 3] == 255); // 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, 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, 280, 610).action == UiAction::Cancel); no_action(surface.pointer_up(0, 100, 610));