#include "Web_Plot_Session.h" #include "Pixel_Frame.h" #include "render_2D/export.h" #include #include #include #include #include #include #include #include namespace renderive::web { namespace { Color blend(Color first, Color second, double amount) { const auto channel = [amount](std::uint8_t a, std::uint8_t b) { return static_cast(std::clamp( std::lround(a + (b - a) * amount), 0L, 255L)); }; return { channel(first.r, second.r), channel(first.g, second.g), channel(first.b, second.b), channel(first.a, second.a) }; } Color_Map radio_color_map() { constexpr Color stops[] = { {3, 7, 18, 255}, {16, 52, 105, 255}, {23, 163, 184, 255}, {238, 210, 91, 255}, {239, 68, 68, 255} }; std::vector colors; colors.reserve(256); for (int index = 0; index < 256; ++index) { const double position = index / 255.0 * (std::size(stops) - 1); const auto stop = static_cast(std::floor(position)); const auto next = std::min(stop + 1, std::size(stops) - 1); colors.push_back(premultiply(blend(stops[stop], stops[next], position - stop))); } return Color_Map(std::move(colors)); } Time_Of_Day current_time_of_day() { constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL; const auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); return {now % day_ms}; } double gaussian(double x, double center, double width) { const double normalized = (x - center) / width; return std::exp(-0.5 * normalized * normalized); } } // namespace struct Web_Plot_Session::Impl { Scene2D plot; renderive_Owner spectrum_frequency_axis; renderive_Owner spectrum_power_axis; renderive_Owner waterfall_frequency_axis; renderive_Owner waterfall_time_axis; renderive_Owner spectrum; renderive_Owner waterfall; renderive_Owner selection; Demo_Mode mode = Demo_Mode::Fm; double gain_db = 8.0; std::uint64_t frame_index{}; std::mutex mutex; Impl() { plot.init(); plot.set_background_color({3, 7, 18, 255}); plot.set_max_render_fps(30.0); plot.set_viewport_size({960, 600}); const auto root = plot.root_renderable(); constexpr Color axis_color{93, 116, 151, 255}; const Range initial_frequency{101'300'000.0, 103'700'000.0}; { auto attach = plot.attach_builder(); const auto make_group = [&](std::string name) { auto group = detail::make_renderable_group(true); group->set_object_name(std::move(name)); attach.attach(group); attach.add_display_parent(group, root); attach.add_dependency_parent(group, root); return group; }; const auto data = make_group("Web_Data"); const auto axes = make_group("Web_Axes"); const auto overlay = make_group("Web_Overlay"); axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); spectrum_frequency_axis = Frequency_Axis::Builder{&attach} .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) .set<&Frequency_Axis::Properties::coordinates>(initial_frequency) .set<&Frequency_Axis::Properties::precision>(2) .set<&Frequency_Axis::Properties::tick_length>(8) .set<&Frequency_Axis::Properties::sub_tick_length>(4) .set<&Frequency_Axis::Properties::color>(axis_color) .set<&Frequency_Axis::Properties::wheel>(true) .set<&Frequency_Axis::Properties::drag>(true) .build(axes); spectrum_power_axis = Axis::Builder{&attach} .set<&Axis::Properties::orientation>(Orientation::Vertical) .set<&Axis::Properties::coordinates>(Range{-20.0, -120.0}) .set<&Axis::Properties::precision>(0) .set<&Axis::Properties::tick_length>(-8) .set<&Axis::Properties::sub_tick_length>(-4) .set<&Axis::Properties::color>(axis_color) .set<&Axis::Properties::unit_text>("dBm") .build(axes); waterfall_frequency_axis = Frequency_Axis::Builder{&attach} .set<&Frequency_Axis::Properties::orientation>(Orientation::Horizontal) .set<&Frequency_Axis::Properties::coordinates>(initial_frequency) .set<&Frequency_Axis::Properties::precision>(2) .set<&Frequency_Axis::Properties::tick_length>(8) .set<&Frequency_Axis::Properties::sub_tick_length>(4) .set<&Frequency_Axis::Properties::color>(axis_color) .set<&Frequency_Axis::Properties::wheel>(true) .set<&Frequency_Axis::Properties::drag>(true) .build(axes); waterfall_time_axis = Time_Axis::Builder{&attach} .set<&Time_Axis::Properties::orientation>(Orientation::Vertical) .set<&Time_Axis::Properties::visible_count>(72) .set<&Time_Axis::Properties::tick_label_spacing_px>(34) .set<&Time_Axis::Properties::format>("mm:ss") .set<&Time_Axis::Properties::tick_length>(-8) .set<&Time_Axis::Properties::sub_tick_length>(-4) .set<&Time_Axis::Properties::color>(axis_color) .build(axes); spectrum = Spectrum::Builder{&attach} .set<&Spectrum::Properties::frequency_range>(initial_frequency) .set<&Spectrum::Properties::frequency_point_size>(768) .set<&Spectrum::Properties::center_frequency>(102'500'000.0) .set<&Spectrum::Properties::sweep_frequency_range>(Range{102'200'000.0, 102'800'000.0}) .set<&Spectrum::Properties::max_marker_visible>(true) .set<&Spectrum::Properties::sweep_region_visible>(true) .set<&Spectrum::Properties::interpolation_mode>(Line_Interpolation_Mode::Cubic_Value) .build(data, spectrum_frequency_axis, spectrum_power_axis); spectrum->set<&Spectrum::Properties::current_pen>(Pen{ Color{57, 224, 177, 255}, 2.0, Line_Style::Solid, Line_Cap::Round, Line_Join::Round }); spectrum->set<&Spectrum::Properties::current_brush>(Brush{Color{31, 174, 145, 32}, Brush_Style::Solid}); spectrum->set<&Spectrum::Properties::max_pen>(Pen{Color{250, 204, 21, 210}, 1.0}); spectrum->set<&Spectrum::Properties::middle_frequency_pen>(Pen{ Color{56, 189, 248, 220}, 1.0, Line_Style::Dash }); waterfall = Waterfall::Builder{&attach} .set<&Waterfall::Properties::frequency_range>(initial_frequency) .set<&Waterfall::Properties::power_range>(Range{-120.0, -20.0}) .set<&Waterfall::Properties::frequency_bin_count>(768) .set<&Waterfall::Properties::interpolation_mode>(Image_Interpolation_Mode::Bilinear) .set<&Waterfall::Properties::color_map>(radio_color_map()) .build(data, waterfall_frequency_axis, waterfall_time_axis); selection = Selection_Rectangle_Overlay::Builder{&attach} .set<&Selection_Rectangle_Overlay::Properties::selection_brush>(Brush{Color{56, 189, 248, 36}, Brush_Style::Solid}) .set<&Selection_Rectangle_Overlay::Properties::selection_border_pen>(Pen{Color{125, 211, 252, 230}, 1.0, Line_Style::Dash}) .build(overlay, spectrum_frequency_axis, spectrum_power_axis); } apply_layout(plot.viewport_size()); plot.activate_view(); update_model(); (void)plot.render_frame(true); } ~Impl() { plot.deactivate_view(); } void apply_layout(Size viewport) { const int left = viewport.width < 620 ? 54 : 72; const int right = viewport.width < 620 ? 44 : 70; const int top = 16; const int bottom = viewport.height < 480 ? 24 : 32; const int gap = viewport.height < 480 ? 36 : 48; const int content_width = std::max(1, viewport.width - left - right); const int available_height = std::max(2, viewport.height - top - bottom - gap); const int spectrum_height = std::max(1, available_height * 43 / 100); const int waterfall_y = top + spectrum_height + gap; const int waterfall_height = std::max(1, viewport.height - waterfall_y - bottom); spectrum_frequency_axis->update([&](Axis_Properties& state) { state.x = left; state.y = top + spectrum_height; state.pixel_length = static_cast(content_width); }); spectrum_power_axis->update([&](Axis_Properties& state) { state.x = left; state.y = top; state.pixel_length = static_cast(spectrum_height); }); waterfall_frequency_axis->update([&](Axis_Properties& state) { state.x = left; state.y = waterfall_y + waterfall_height; state.pixel_length = static_cast(content_width); }); waterfall_time_axis->update([&](auto& state) { state.x = left; state.y = waterfall_y; state.pixel_length = static_cast(waterfall_height); }); } void update_model() { constexpr int sample_count = 768; const double time = static_cast(frame_index) * 0.075; std::vector samples(sample_count); for (int index = 0; index < sample_count; ++index) { const double x = static_cast(index) / (sample_count - 1); const double noise = std::sin(index * 12.9898 + frame_index * 0.371) * std::sin(index * 0.137 + frame_index * 0.071); double signal{}; switch (mode) { case Demo_Mode::Am: signal = 66.0 * gaussian(x, 0.5, 0.008) + 39.0 * gaussian(x, 0.42, 0.018) + 39.0 * gaussian(x, 0.58, 0.018); break; case Demo_Mode::Fm: { const double moving = 0.5 + std::sin(time) * 0.035; signal = 58.0 * gaussian(x, moving, 0.055) + 25.0 * gaussian(x, moving - 0.11, 0.023) + 25.0 * gaussian(x, moving + 0.11, 0.023); break; } case Demo_Mode::Usb: signal = 61.0 * gaussian(x, 0.57, 0.045) + 28.0 * gaussian(x, 0.68, 0.025); break; case Demo_Mode::Lsb: signal = 61.0 * gaussian(x, 0.43, 0.045) + 28.0 * gaussian(x, 0.32, 0.025); break; } samples[index] = std::clamp(-110.0 + noise * 4.5 + signal + gain_db, -120.0, -20.0); } spectrum->update_samples(samples); if ((frame_index & 1U) == 0U) waterfall->append_row(current_time_of_day(), samples); ++frame_index; } void set_center_frequency(double megahertz) { const double center = megahertz * 1'000'000.0; const double bandwidth = spectrum_frequency_axis->get<&Axis_Properties::coordinates>().size(); const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range); waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range); spectrum->set<&Spectrum::Properties::frequency_range>(range); spectrum->set<&Spectrum::Properties::center_frequency>(center); spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ center - bandwidth * 0.125, center + bandwidth * 0.125 }); waterfall->set<&Waterfall::Properties::frequency_range>(range); } void set_bandwidth(double kilohertz) { const double center = spectrum->get<&Spectrum::Properties::center_frequency>(); const double bandwidth = kilohertz * 1000.0; const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; spectrum_frequency_axis->set<&Axis_Properties::coordinates>(range); waterfall_frequency_axis->set<&Axis_Properties::coordinates>(range); spectrum->set<&Spectrum::Properties::frequency_range>(range); spectrum->set<&Spectrum::Properties::sweep_frequency_range>(Range{ center - bandwidth * 0.125, center + bandwidth * 0.125 }); waterfall->set<&Waterfall::Properties::frequency_range>(range); } std::optional render_pixels() { if (!plot.view_active()) return std::nullopt; update_model(); if (!plot.render_frame(true)) return std::nullopt; std::string pixels; const Color background = plot.background_color(); plot.with_frame([&pixels, background](Image_View image) { pixels = encode_pixel_frame(image, background); }); return pixels.empty() ? std::nullopt : std::optional(std::move(pixels)); } std::optional handle(const Web_Event& event) { std::lock_guard lock(mutex); return std::visit( [this](const auto& value) -> std::optional { using T = std::decay_t; if constexpr (std::is_same_v) { auto pixels = render_pixels(); if (pixels) return Web_Response{Web_Response_Type::Pixels, std::move(*pixels)}; } else if constexpr (std::is_same_v) { const Size previous = plot.viewport_size(); plot.set_viewport_size(value.size); apply_layout(value.size); Resize_Event resized; resized.old_size = previous; resized.new_size = value.size; plot.dispatch_event(resized); } else if constexpr (std::is_same_v) { if (value.type == Event_Type::Show) plot.activate_view(); else if (value.type == Event_Type::Hide) plot.deactivate_view(); plot.dispatch_event(value); } else if constexpr (Event_Object) { plot.dispatch_event(value); } else if constexpr (std::is_same_v) { mode = value.mode; plot.notify_model_dirty(); } else if constexpr (std::is_same_v) { set_center_frequency(value.megahertz); } else if constexpr (std::is_same_v) { set_bandwidth(value.kilohertz); } else if constexpr (std::is_same_v) { gain_db = value.decibels; plot.notify_model_dirty(); } else if constexpr (std::is_same_v) { spectrum->set<&Spectrum::Properties::max_hold_visible>(value.enabled); } else if constexpr (std::is_same_v) { spectrum->set<&Spectrum::Properties::interpolation_mode>( value.enabled ? Line_Interpolation_Mode::Cubic_Value : Line_Interpolation_Mode::Nearest_Sample); waterfall->set<&Waterfall::Properties::interpolation_mode>( value.enabled ? Image_Interpolation_Mode::Bilinear : Image_Interpolation_Mode::Nearest); } else if constexpr (std::is_same_v) { selection->clear_selected_regions(); } return std::nullopt; }, event); } }; Web_Plot_Session::Web_Plot_Session() : impl_(std::make_unique()) {} Web_Plot_Session::~Web_Plot_Session() = default; std::optional Web_Plot_Session::handle(const Web_Event& event) { return impl_->handle(event); } } // namespace renderive::web