#include "Web_Plot_Session.h" #include "Pixel_Frame.h" #include "Core2/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 { Plot_Core plot; std::shared_ptr spectrum_frequency_axis; std::shared_ptr spectrum_power_axis; std::shared_ptr waterfall_frequency_axis; std::shared_ptr waterfall_time_axis; std::shared_ptr spectrum; std::shared_ptr waterfall; std::shared_ptr 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(); const auto data = plot.create_renderable_node(root, "Web_Data"); const auto axes = plot.create_renderable_node(root, "Web_Axes"); const auto overlay = plot.create_renderable_node(root, "Web_Overlay"); axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel); constexpr Color axis_color{93, 116, 151, 255}; const Range initial_frequency{101'300'000.0, 103'700'000.0}; spectrum_frequency_axis = Frequency_Axis::Builder(axes, Orientation::Horizontal) .set_coord_range(initial_frequency) .set_label_precision(2) .set_tick_length(8) .set_sub_tick_length(4) .set_color(axis_color) .set_use_wheel(true) .set_use_drag(true) .build(); spectrum_power_axis = Axis::Builder(axes, Orientation::Vertical) .set_coord_range({-20.0, -120.0}) .set_label_precision(0) .set_tick_length(-8) .set_sub_tick_length(-4) .set_color(axis_color) .set_unit_text("dBm") .build(); waterfall_frequency_axis = Frequency_Axis::Builder(axes, Orientation::Horizontal) .set_coord_range(initial_frequency) .set_label_precision(2) .set_tick_length(8) .set_sub_tick_length(4) .set_color(axis_color) .set_use_wheel(true) .set_use_drag(true) .build(); waterfall_time_axis = Time_Axis::Builder(axes, Orientation::Vertical) .set_visible_time_point_count(72) .set_tick_label_spacing_px(34) .set_time_format("mm:ss") .set_tick_length(-8) .set_sub_tick_length(-4) .set_color(axis_color) .build(); spectrum = Spectrum::Builder(data, spectrum_frequency_axis, spectrum_power_axis) .set_frequency_range(initial_frequency) .set_frequency_point_size(768) .set_center_frequency(102'500'000.0) .set_sweep_frequency_range({102'200'000.0, 102'800'000.0}) .set_max_marker_visible(true) .set_sweep_region_visible(true) .set_interpolation_mode(Line_Interpolation_Mode::Cubic_Value) .build(); spectrum->set_current_pen({Color{57, 224, 177, 255}, 2.0, Line_Style::Solid, Line_Cap::Round, Line_Join::Round}); spectrum->set_current_brush({Color{31, 174, 145, 32}, Brush_Style::Solid}); spectrum->set_max_pen({Color{250, 204, 21, 210}, 1.0}); spectrum->set_middle_frequency_pen({Color{56, 189, 248, 220}, 1.0, Line_Style::Dash}); waterfall = Waterfall::Builder(data, waterfall_frequency_axis, waterfall_time_axis) .set_frequency_range(initial_frequency) .set_power_range({-120.0, -20.0}) .set_frequency_bin_count(768) .set_interpolation_mode(Image_Interpolation_Mode::Bilinear) .set_color_map(radio_color_map()) .build(); selection = Selection_Rectangle_Overlay::Builder( overlay, spectrum_frequency_axis, spectrum_power_axis) .set_rect_brush({Color{56, 189, 248, 36}, Brush_Style::Solid}) .set_border_pen({Color{125, 211, 252, 230}, 1.0, Line_Style::Dash}) .build(); 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->set_x(left); spectrum_frequency_axis->set_y(top + spectrum_height); spectrum_frequency_axis->set_pixel_length(static_cast(content_width)); spectrum_power_axis->set_x(left); spectrum_power_axis->set_y(top); spectrum_power_axis->set_pixel_length(static_cast(spectrum_height)); waterfall_frequency_axis->set_x(left); waterfall_frequency_axis->set_y(waterfall_y + waterfall_height); waterfall_frequency_axis->set_pixel_length(static_cast(content_width)); waterfall_time_axis->set_x(left); waterfall_time_axis->set_y(waterfall_y); waterfall_time_axis->set_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->coord_range().size(); const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; spectrum_frequency_axis->set_coord_range(range); waterfall_frequency_axis->set_coord_range(range); spectrum->set_frequency_range(range); spectrum->set_center_frequency(center); spectrum->set_sweep_frequency_range({center - bandwidth * 0.125, center + bandwidth * 0.125}); waterfall->set_frequency_range(range); } void set_bandwidth(double kilohertz) { const double center = spectrum->center_frequency(); const double bandwidth = kilohertz * 1000.0; const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5}; spectrum_frequency_axis->set_coord_range(range); waterfall_frequency_axis->set_coord_range(range); spectrum->set_frequency_range(range); spectrum->set_sweep_frequency_range({center - bandwidth * 0.125, center + bandwidth * 0.125}); waterfall->set_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 (std::is_base_of_v) { 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_max_hold_visible(value.enabled); } else if constexpr (std::is_same_v) { spectrum->set_interpolation_mode( value.enabled ? Line_Interpolation_Mode::Cubic_Value : Line_Interpolation_Mode::Nearest_Sample); waterfall->set_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