|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +#include "onion_skin_plugin.hpp" |
| 3 | +#include "onion_skin_render_data.hpp" |
| 4 | +#include "onion_skin_renderer.hpp" |
| 5 | +#include "xstudio/media_reader/image_buffer.hpp" |
| 6 | +#include "xstudio/bookmark/bookmark.hpp" |
| 7 | +#include "xstudio/plugin_manager/plugin_base.hpp" |
| 8 | +#include "xstudio/utility/blind_data.hpp" |
| 9 | + |
| 10 | +#include <algorithm> |
| 11 | +#include <cmath> |
| 12 | +#include <variant> |
| 13 | + |
| 14 | +using namespace xstudio; |
| 15 | +using namespace xstudio::ui::viewport; |
| 16 | + |
| 17 | +OnionSkinPlugin::OnionSkinPlugin( |
| 18 | + caf::actor_config &cfg, const utility::JsonStore &init_settings) |
| 19 | + : plugin::HUDPluginBase(cfg, "Annotation Onion Skin", init_settings, 10.0f) { |
| 20 | + |
| 21 | + frames_before_ = add_integer_attribute("Frames Before", "Before", 3, 0, 20); |
| 22 | + add_hud_settings_attribute(frames_before_); |
| 23 | + frames_before_->set_tool_tip( |
| 24 | + "Maximum frame distance to look back for annotations"); |
| 25 | + frames_before_->set_redraw_viewport_on_change(true); |
| 26 | + |
| 27 | + frames_after_ = add_integer_attribute("Frames After", "After", 3, 0, 20); |
| 28 | + add_hud_settings_attribute(frames_after_); |
| 29 | + frames_after_->set_tool_tip( |
| 30 | + "Maximum frame distance to look ahead for annotations"); |
| 31 | + frames_after_->set_redraw_viewport_on_change(true); |
| 32 | + |
| 33 | + base_opacity_ = |
| 34 | + add_float_attribute("Base Opacity", "Opacity", 0.4f, 0.05f, 1.0f, 0.05f); |
| 35 | + add_hud_settings_attribute(base_opacity_); |
| 36 | + base_opacity_->set_tool_tip("Opacity of the nearest neighboring annotation"); |
| 37 | + base_opacity_->set_redraw_viewport_on_change(true); |
| 38 | + |
| 39 | + opacity_falloff_ = |
| 40 | + add_float_attribute("Opacity Falloff", "Falloff", 0.5f, 0.1f, 1.0f, 0.05f); |
| 41 | + add_hud_settings_attribute(opacity_falloff_); |
| 42 | + opacity_falloff_->set_tool_tip( |
| 43 | + "Multiplier applied per frame step further from current frame"); |
| 44 | + opacity_falloff_->set_redraw_viewport_on_change(true); |
| 45 | + |
| 46 | + use_original_colours_ = add_boolean_attribute( |
| 47 | + "Use Original Colours", "Orig Colours", false); |
| 48 | + add_hud_settings_attribute(use_original_colours_); |
| 49 | + use_original_colours_->set_tool_tip( |
| 50 | + "When enabled, keep annotation colours and only reduce opacity. " |
| 51 | + "When disabled, tint with Previous/Next colours."); |
| 52 | + use_original_colours_->set_redraw_viewport_on_change(true); |
| 53 | + |
| 54 | + past_tint_ = add_colour_attribute( |
| 55 | + "Previous Tint", "Prev Tint", utility::ColourTriplet(1.0f, 0.3f, 0.3f)); |
| 56 | + add_hud_settings_attribute(past_tint_); |
| 57 | + past_tint_->set_tool_tip("Tint colour for annotations from previous frames"); |
| 58 | + past_tint_->set_redraw_viewport_on_change(true); |
| 59 | + |
| 60 | + future_tint_ = add_colour_attribute( |
| 61 | + "Next Tint", "Next Tint", utility::ColourTriplet(0.3f, 1.0f, 0.3f)); |
| 62 | + add_hud_settings_attribute(future_tint_); |
| 63 | + future_tint_->set_tool_tip("Tint colour for annotations from future frames"); |
| 64 | + future_tint_->set_redraw_viewport_on_change(true); |
| 65 | + |
| 66 | + add_hud_description( |
| 67 | + "Shows annotations from neighboring frames as semi-transparent, " |
| 68 | + "color-tinted overlays on the current frame."); |
| 69 | + |
| 70 | + frames_before_->set_preference_path("/plugin/annotation_onion_skin/frames_before"); |
| 71 | + frames_after_->set_preference_path("/plugin/annotation_onion_skin/frames_after"); |
| 72 | + base_opacity_->set_preference_path("/plugin/annotation_onion_skin/base_opacity"); |
| 73 | + opacity_falloff_->set_preference_path("/plugin/annotation_onion_skin/opacity_falloff"); |
| 74 | + use_original_colours_->set_preference_path("/plugin/annotation_onion_skin/use_original_colours"); |
| 75 | + past_tint_->set_preference_path("/plugin/annotation_onion_skin/past_tint"); |
| 76 | + future_tint_->set_preference_path("/plugin/annotation_onion_skin/future_tint"); |
| 77 | +} |
| 78 | + |
| 79 | +plugin::ViewportOverlayRendererPtr |
| 80 | +OnionSkinPlugin::make_overlay_renderer(const std::string & /*viewport_name*/) { |
| 81 | + return plugin::ViewportOverlayRendererPtr(new OnionSkinRenderer()); |
| 82 | +} |
| 83 | + |
| 84 | +utility::BlindDataObjectPtr OnionSkinPlugin::onscreen_render_data( |
| 85 | + const media_reader::ImageBufPtr &image, |
| 86 | + const std::string & /*viewport_name*/, |
| 87 | + const utility::Uuid & /*playhead_uuid*/, |
| 88 | + const bool /*is_hero_image*/, |
| 89 | + const bool /*images_are_in_grid_layout*/) const { |
| 90 | + |
| 91 | + if (!visible() || !image) |
| 92 | + return {}; |
| 93 | + |
| 94 | + const int current_frame = image.playhead_logical_frame(); |
| 95 | + const int range_before = static_cast<int>(frames_before_->value()); |
| 96 | + const int range_after = static_cast<int>(frames_after_->value()); |
| 97 | + const float base_opac = base_opacity_->value(); |
| 98 | + const float falloff = opacity_falloff_->value(); |
| 99 | + const bool orig_colours = use_original_colours_->value(); |
| 100 | + const auto &prev_colour = past_tint_->value(); |
| 101 | + const auto &next_colour = future_tint_->value(); |
| 102 | + |
| 103 | + if (range_before == 0 && range_after == 0) |
| 104 | + return {}; |
| 105 | + |
| 106 | + // ── Update bookmark cache ── |
| 107 | + // image.bookmarks() carries bookmarks covering the current frame. |
| 108 | + // We cache them keyed by logical frame to find neighbors later. |
| 109 | + // |
| 110 | + // Invalidation: when revisiting a frame, if its bookmarks changed |
| 111 | + // (different UUIDs or count), we clear the entire cache. This handles |
| 112 | + // bookmark deletion, media changes, and bookmark additions. |
| 113 | + const auto &frame_bookmarks = image.bookmarks(); |
| 114 | + { |
| 115 | + std::lock_guard<std::mutex> lock(cache_mutex_); |
| 116 | + |
| 117 | + auto it = frame_bookmark_cache_.find(current_frame); |
| 118 | + if (it != frame_bookmark_cache_.end()) { |
| 119 | + bool changed = (it->second.size() != frame_bookmarks.size()); |
| 120 | + if (!changed) { |
| 121 | + for (size_t i = 0; i < it->second.size(); ++i) { |
| 122 | + if (it->second[i]->detail_.uuid_ != |
| 123 | + frame_bookmarks[i]->detail_.uuid_) { |
| 124 | + changed = true; |
| 125 | + break; |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + if (changed) { |
| 130 | + frame_bookmark_cache_.clear(); |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + if (!frame_bookmarks.empty()) { |
| 135 | + frame_bookmark_cache_[current_frame] = frame_bookmarks; |
| 136 | + } else { |
| 137 | + frame_bookmark_cache_.erase(current_frame); |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + // Collect current frame's annotation pointers — skip these when |
| 142 | + // walking neighbors (same annotation spans multiple frames). |
| 143 | + std::set<const void *> current_annotations; |
| 144 | + for (const auto &bm : frame_bookmarks) { |
| 145 | + if (bm && bm->annotation_ && bm->annotation_->user_data()) |
| 146 | + current_annotations.insert(bm->annotation_->user_data()); |
| 147 | + } |
| 148 | + |
| 149 | + // ── Helpers ── |
| 150 | + auto tint_colour = [](const utility::ColourTriplet &c, |
| 151 | + const utility::ColourTriplet &tint) -> utility::ColourTriplet { |
| 152 | + return {c.r * tint.r, c.g * tint.g, c.b * tint.b}; |
| 153 | + }; |
| 154 | + |
| 155 | + auto make_canvas_copy = [&](const ui::canvas::Canvas &src, float opacity, |
| 156 | + const utility::ColourTriplet &tint, |
| 157 | + bool keep_original) -> ui::canvas::Canvas { |
| 158 | + ui::canvas::Canvas out(src); |
| 159 | + for (auto it = out.begin(); it != out.end(); ++it) { |
| 160 | + auto item = *it; |
| 161 | + std::visit( |
| 162 | + [&](auto &v) { |
| 163 | + using T = std::decay_t<decltype(v)>; |
| 164 | + if constexpr (std::is_same_v<T, ui::canvas::Stroke>) { |
| 165 | + v.set_opacity(v.opacity() * opacity); |
| 166 | + if (!keep_original) |
| 167 | + v.set_colour(tint_colour(v.colour(), tint)); |
| 168 | + } else if constexpr (std::is_same_v<T, ui::canvas::Caption>) { |
| 169 | + v.set_opacity(v.opacity() * opacity); |
| 170 | + v.set_bg_opacity(v.background_opacity() * opacity); |
| 171 | + if (!keep_original) |
| 172 | + v.set_colour(tint_colour(v.colour(), tint)); |
| 173 | + } else { |
| 174 | + v.opacity *= opacity; |
| 175 | + if (!keep_original) |
| 176 | + v.colour = tint_colour(v.colour, tint); |
| 177 | + } |
| 178 | + }, |
| 179 | + item); |
| 180 | + out.overwrite_item(it, item); |
| 181 | + } |
| 182 | + return out; |
| 183 | + }; |
| 184 | + |
| 185 | + // Opacity falls off with distance: nearest = base_opac, farther = less. |
| 186 | + auto compute_opacity = [&](int distance) -> float { |
| 187 | + return base_opac * std::pow(falloff, static_cast<float>(distance - 1)); |
| 188 | + }; |
| 189 | + |
| 190 | + // ── Find neighbor annotations from cache (distance-bounded) ── |
| 191 | + struct Candidate { |
| 192 | + const ui::canvas::Canvas *canvas; |
| 193 | + int abs_distance; |
| 194 | + float opacity; |
| 195 | + utility::ColourTriplet tint; |
| 196 | + }; |
| 197 | + std::vector<Candidate> candidates; |
| 198 | + |
| 199 | + { |
| 200 | + std::lock_guard<std::mutex> lock(cache_mutex_); |
| 201 | + |
| 202 | + // Walk backward — stop when distance exceeds range_before. |
| 203 | + if (range_before > 0) { |
| 204 | + auto it = frame_bookmark_cache_.lower_bound(current_frame); |
| 205 | + if (it != frame_bookmark_cache_.begin()) { |
| 206 | + auto pit = it; |
| 207 | + while (pit != frame_bookmark_cache_.begin()) { |
| 208 | + --pit; |
| 209 | + int dist = current_frame - pit->first; |
| 210 | + if (dist > range_before) |
| 211 | + break; |
| 212 | + for (const auto &bm : pit->second) { |
| 213 | + if (!bm || !bm->annotation_ || !bm->annotation_->user_data()) |
| 214 | + continue; |
| 215 | + const auto *canvas = static_cast<const ui::canvas::Canvas *>( |
| 216 | + bm->annotation_->user_data()); |
| 217 | + if (!canvas || canvas->empty()) |
| 218 | + continue; |
| 219 | + if (current_annotations.count(canvas)) |
| 220 | + continue; |
| 221 | + candidates.push_back( |
| 222 | + {canvas, dist, compute_opacity(dist), prev_colour}); |
| 223 | + break; |
| 224 | + } |
| 225 | + } |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + // Walk forward — stop when distance exceeds range_after. |
| 230 | + if (range_after > 0) { |
| 231 | + auto it = frame_bookmark_cache_.upper_bound(current_frame); |
| 232 | + while (it != frame_bookmark_cache_.end()) { |
| 233 | + int dist = it->first - current_frame; |
| 234 | + if (dist > range_after) |
| 235 | + break; |
| 236 | + for (const auto &bm : it->second) { |
| 237 | + if (!bm || !bm->annotation_ || !bm->annotation_->user_data()) |
| 238 | + continue; |
| 239 | + const auto *canvas = static_cast<const ui::canvas::Canvas *>( |
| 240 | + bm->annotation_->user_data()); |
| 241 | + if (!canvas || canvas->empty()) |
| 242 | + continue; |
| 243 | + if (current_annotations.count(canvas)) |
| 244 | + continue; |
| 245 | + candidates.push_back( |
| 246 | + {canvas, dist, compute_opacity(dist), next_colour}); |
| 247 | + break; |
| 248 | + } |
| 249 | + ++it; |
| 250 | + } |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + if (candidates.empty()) |
| 255 | + return {}; |
| 256 | + |
| 257 | + // Render farthest first so closest onion skin draws on top. |
| 258 | + std::sort(candidates.begin(), candidates.end(), |
| 259 | + [](const auto &a, const auto &b) { return a.abs_distance > b.abs_distance; }); |
| 260 | + |
| 261 | + std::vector<ui::canvas::Canvas> canvases; |
| 262 | + canvases.reserve(candidates.size()); |
| 263 | + for (const auto &c : candidates) { |
| 264 | + canvases.push_back(make_canvas_copy(*c.canvas, c.opacity, c.tint, orig_colours)); |
| 265 | + } |
| 266 | + |
| 267 | + return std::make_shared<OnionSkinRenderData>(std::move(canvases)); |
| 268 | +} |
| 269 | + |
| 270 | + |
| 271 | +extern "C" { |
| 272 | +plugin_manager::PluginFactoryCollection *plugin_factory_collection_ptr() { |
| 273 | + return new plugin_manager::PluginFactoryCollection( |
| 274 | + std::vector<std::shared_ptr<plugin_manager::PluginFactory>>( |
| 275 | + {std::make_shared<plugin_manager::PluginFactoryTemplate<OnionSkinPlugin>>( |
| 276 | + OnionSkinPlugin::PLUGIN_UUID, |
| 277 | + "AnnotationOnionSkin", |
| 278 | + plugin_manager::PluginFlags::PF_HEAD_UP_DISPLAY | |
| 279 | + plugin_manager::PluginFlags::PF_VIEWPORT_OVERLAY, |
| 280 | + true, |
| 281 | + "RodeoFX", |
| 282 | + "Annotation Onion Skinning Overlay")})); |
| 283 | +} |
| 284 | +} |
0 commit comments