Onion Skin View Widget
Onion Skin View Widget Overview
The Onion Skin View Widget renders a temporal window of spatial data (PointData, LineData, MaskData) centered on the current time frame, with alpha-graded temporal fading. Elements at the current time are drawn at full opacity; elements further away in time progressively fade according to a configurable alpha curve (linear, exponential, or Gaussian). This “onion skin” visualization is commonly used in animation and motion analysis to show the trajectory and shape evolution of tracked objects over a short time window.
Unlike TemporalProjectionViewWidget which collapses all time frames into a single view, OnionSkinViewWidget displays only a configurable window around the current frame, making temporal proximity visible through transparency. Unlike trial-aligned plots (LinePlotWidget, EventPlotWidget, PSTHWidget, HeatmapWidget), this widget does not use PlotAlignmentState or GatherResult. Instead, it reads spatial data directly from PointData, LineData, and MaskData within a temporal window centered on the current TimeFrameIndex.
Users can add multiple data keys of each type, configure temporal window size (frames behind/ahead), alpha curve settings, and rendering parameters (point size, line width), and interact via zoom, pan, and click-to-select.
Architecture
The widget follows the standard plot widget directory layout:
Plots/OnionSkinViewWidget/
├── CMakeLists.txt
├── OnionSkinViewWidgetRegistration.hpp # EditorRegistry registration module
├── OnionSkinViewWidgetRegistration.cpp
├── Core/
│ ├── OnionSkinViewState.hpp # QObject state class (EditorState subclass)
│ └── OnionSkinViewState.cpp
├── Rendering/
│ ├── OnionSkinViewOpenGLWidget.hpp # OpenGL rendering + mouse interaction
│ └── OnionSkinViewOpenGLWidget.cpp
└── UI/
├── OnionSkinViewWidget.hpp # Main composite widget (axes + OpenGL + layout)
├── OnionSkinViewWidget.cpp
├── OnionSkinViewWidget.ui
├── OnionSkinViewPropertiesWidget.hpp # Properties/settings panel
├── OnionSkinViewPropertiesWidget.cpp
└── OnionSkinViewPropertiesWidget.ui
Component Overview
| Component | Responsibility |
|---|---|
OnionSkinViewWidget (UI) |
Top-level Qt widget — composes OpenGL canvas, horizontal axis, vertical axis |
OnionSkinViewPropertiesWidget (UI) |
Properties panel — data key management (point + line + mask), temporal window, alpha curve, rendering controls, axis range controls |
OnionSkinViewOpenGLWidget (Rendering) |
OpenGL canvas — temporal window scene building, alpha-graded rendering, mouse interaction |
OnionSkinViewState (Core) |
Serializable state — view state, axis ranges, data keys, temporal window, alpha curve, rendering parameters |
OnionSkinViewWidgetRegistration |
Registers type with EditorRegistry (state, view, properties factories) |
EditorState Pattern
The Onion Skin View Widget follows the project’s EditorState architecture:
OnionSkinViewStateinherits fromEditorState- Registered with
EditorRegistryviaOnionSkinViewWidgetModule::registerTypes() OnionSkinViewWidgetis the view factory,OnionSkinViewPropertiesWidgetis the properties factory- Both share the same
OnionSkinViewStateinstance - Properties placed in Right zone, view placed in Center zone (default)
- The
create_editor_customfactory inOnionSkinViewWidgetRegistrationhandles coupling the view and properties widgets, connecting range controls and forwardingtimePositionSelectedtoEditorRegistry::setCurrentTime()
State Management
OnionSkinViewStateData
The serializable state struct contains all persistent configuration:
struct OnionSkinViewStateData {
std::string instance_id;
std::string display_name = "Onion Skin View";
CorePlotting::ViewStateData view_state;
HorizontalAxisStateData horizontal_axis;
VerticalAxisStateData vertical_axis;
// Data keys
std::vector<std::string> point_data_keys;
std::vector<std::string> line_data_keys;
std::vector<std::string> mask_data_keys;
// Temporal window
int window_behind = 5; ///< Samples before current time
int window_ahead = 5; ///< Samples after current time
// Alpha curve
std::string alpha_curve = "linear"; ///< "linear", "exponential", "gaussian"
float min_alpha = 0.1f;
float max_alpha = 1.0f;
// Rendering
float point_size = 8.0f;
bool highlight_current = true; ///< Draw current frame with distinct color/size
};Signal Architecture
OnionSkinViewState emits fine-grained signals to separate expensive scene rebuilds from cheap matrix updates:
| Signal | Trigger |
|---|---|
viewStateChanged |
Zoom, pan, or bounds changed (matrix update only — no scene rebuild) |
pointDataKeyAdded / pointDataKeyRemoved / pointDataKeysCleared |
Point data key changes |
lineDataKeyAdded / lineDataKeyRemoved / lineDataKeysCleared |
Line data key changes |
maskDataKeyAdded / maskDataKeyRemoved / maskDataKeysCleared |
Mask data key changes |
windowBehindChanged / windowAheadChanged |
Temporal window size changed |
alphaCurveChanged / minAlphaChanged / maxAlphaChanged |
Alpha curve parameters changed |
highlightCurrentChanged |
Current-frame highlight toggle changed |
stateChanged (inherited) |
Any change requiring full state update |
Performance-critical separation: Zoom and pan operations emit only viewStateChanged(), which updates the projection matrix without triggering an expensive scene rebuild. Temporal window, alpha curve, data key, and rendering signals all trigger scene rebuilds via the _scene_dirty flag.
Axis States
OnionSkinViewState owns both axis states:
HorizontalAxisState— X-axis (generic value axis). See Plot Widget Guide for full axis documentation.VerticalAxisState— Y-axis (generic value axis). Supports the “silent update” pattern to prevent feedback loops during zoom/pan.
Both axis states synchronize bidirectionally with the view state, using the same pattern as TemporalProjectionViewWidget, ScatterPlotWidget, and ACFWidget.
Data Sources
Overview
The widget supports three spatial data types, each added as multiple keys from the DataManager:
| Data Type | Rendering Method | Description |
|---|---|---|
PointData |
SceneRenderer via SceneBuilder::addGlyphs() |
Points rendered as circle glyphs with per-point temporal alpha |
LineData |
SceneRenderer via polyline primitives |
Lines rendered as polylines with per-line temporal alpha |
MaskData |
Contour extraction → polyline rendering | Mask boundaries rendered as polyline contours with per-contour temporal alpha |
PointData
Each PointData key in point_data_keys is loaded via DataManager::getData<PointData>(key). Points within the temporal window [current - window_behind, current + window_ahead] are mapped using CorePlotting::SpatialMapper::mapPointsInWindow(), which returns TimedMappedElement objects containing world-space coordinates, associated EntityIDs, and temporal offset from the current frame.
Points are rendered as circle glyphs via SceneBuilder::addGlyphs() → SceneRenderer. This shares the same glyph rendering infrastructure used by EventPlotWidget, ScatterPlotWidget, and TemporalProjectionViewWidget.
LineData
Each LineData key in line_data_keys is loaded via DataManager::getData<LineData>(key). Lines within the temporal window are mapped using CorePlotting::SpatialMapper::mapLinesInWindow(), which returns TimedOwningLineView objects containing vertex data and temporal offset.
Lines are rendered as polylines within the SceneRenderer. This shares line rendering infrastructure used by LinePlotWidget and TemporalProjectionViewWidget.
MaskData
Each MaskData key in mask_data_keys is loaded via DataManager::getData<MaskData>(key). Mask boundaries within the temporal window are extracted as contour polylines using CorePlotting::SpatialMapper::mapMaskContoursInWindow(), which uses the MaskContourMapper to convert binary masks into polyline contours with temporal offset information.
This is unique to OnionSkinViewWidget — no other plot widget currently renders MaskData. The mask contour extraction handles the conversion from binary pixel masks to smooth polyline boundaries for GPU rendering. Because masks are rendered as contour polylines, they share the same line rendering parameters (width, alpha) and are blended identically with LineData contours.
DataManager Observer
The properties widget registers a DataManager observer to keep all three combo boxes (point, line, mask) current when data is added or removed:
// Constructor
_dm_observer_id = _data_manager->addObserver([this]() {
_populatePointComboBox();
_populateLineComboBox();
_populateMaskComboBox();
});
// Destructor — critical to avoid dangling references
if (_data_manager && _dm_observer_id != -1) {
_data_manager->removeObserver(_dm_observer_id);
}See Plot Widget Guide — DataManager Integration for the full observer contract.
Rendering Pipeline
Data Flow
OnionSkinViewState (data keys + window + alpha + rendering params)
↓
DataManager::getData<PointData/LineData/MaskData>(key)
↓
SpatialMapper::mapPointsInWindow() → TimedMappedElement[]
SpatialMapper::mapLinesInWindow() → TimedOwningLineView[]
SpatialMapper::mapMaskContoursInWindow() → TimedOwningLineView[]
↓
AlphaCurve::computeTemporalAlpha(offset, half_width, curve, min, max)
↓
Bounding box computation (auto-fit from data)
↓
setXBounds() / setYBounds() → view state + axis sync
↓
SceneBuilder: addGlyphs() (points) + polylines (lines + mask contours)
— each element's color includes computed temporal alpha
— depth-sorted: farthest temporal distance drawn first (back-to-front)
↓
SceneRenderer::uploadScene() → SceneRenderer::render()
Temporal Alpha Computation
The AlphaCurve module (CorePlotting/DataTypes/AlphaCurve.hpp) computes per-element alpha values based on temporal distance from the current frame:
| Curve Type | Formula | Behavior |
|---|---|---|
| Linear | alpha = max - (max - min) * |offset| / half_width |
Uniform fade |
| Exponential | alpha = min + (max - min) * exp(-k * |offset| / half_width) |
Rapid initial fade, slow tail |
| Gaussian | alpha = min + (max - min) * exp(-|offset|² / (2σ²)) |
Smooth bell-curve fade |
Parameters:
offset: frame distance from current time (0 = current frame)half_width: max(window_behind, window_ahead) — the normalization rangemin_alpha/max_alpha: alpha range (default 0.1–1.0)
Rendering Order
Elements are sorted by decreasing temporal distance (drawn back-to-front) so that blending works correctly with GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA. The current frame is drawn last (on top) with the highest alpha.
When highlight_current is enabled, the current frame’s elements are rendered with a distinct color:
| Element Type | Current Frame Color | Non-Current Frame Color |
|---|---|---|
| Points | Bright orange-red (1.0, 0.3, 0.1) | Blue (0.2, 0.5, 0.9) |
| Lines | Bright orange-red (1.0, 0.3, 0.1) | Green (0.2, 0.7, 0.4) |
| Mask contours | Bright orange-red (1.0, 0.3, 0.1) | Orange (0.8, 0.5, 0.2) |
Auto-Fit Bounds
After gathering all windowed data, the widget computes a tight bounding box from all point positions, line vertices, and mask contour vertices. Bounds update is triggered when data keys change (via the _needs_bounds_update flag).
Interaction
Zoom and Pan
The widget uses the shared PlotInteractionHelpers for consistent zoom/pan behavior across all widgets:
| Input | Action |
|---|---|
| Mouse wheel | X-zoom only |
| Shift + wheel | Y-zoom only |
| Ctrl + wheel | Uniform zoom (both axes) |
| Click + drag | Pan in both axes |
These are the same shared interaction helpers used across all plot widgets — see Plot Widget Guide.
Entity Selection
- Click on a point at the current frame: nearest-point lookup with 15-pixel tolerance; emits
entitySelected(EntityId) - Double-click: emits
entityDoubleClicked(EntityId)for frame navigation
Point hit testing uses a brute-force nearest-point search limited to the current frame’s points.
Properties Panel
The OnionSkinViewPropertiesWidget provides controls organized into logical sections:
Data Key Management
Three separate sections, one for each data type:
- Point data key combo box + Add/Remove buttons — lists all
PointDatakeys from DataManager - Point data table — shows currently added point data keys
- Line data key combo box + Add/Remove buttons — lists all
LineDatakeys from DataManager - Line data table — shows currently added line data keys
- Mask data key combo box + Add/Remove buttons — lists all
MaskDatakeys from DataManager - Mask data table — shows currently added mask data keys
Temporal Window Controls
- Window Behind spinbox — number of frames before current time (default: 5)
- Window Ahead spinbox — number of frames after current time (default: 5)
Alpha Curve Controls
- Alpha curve combo box — Linear / Exponential / Gaussian
- Min alpha spinbox — minimum opacity for farthest frames (default: 0.1)
- Max alpha spinbox — maximum opacity for current frame (default: 1.0)
Rendering Controls
- Line width spinbox — global line/contour thickness (default: 2.0)
- Highlight current checkbox — toggles distinct current-frame coloring
Axis Range Controls
When setPlotWidget() is called (during registration wiring), the properties panel creates:
- X-Axis Range Controls —
HorizontalAxisRangeControlsin a collapsibleSection - Y-Axis Range Controls —
VerticalAxisRangeControlsin a collapsibleSection
Visual Customization
Point Glyph Style (Per-Key)
Each PointData key added to the widget has its own independent glyph style, stored as a CorePlotting::GlyphStyleData struct (shape, size, hex color, alpha). This follows the same shared glyph infrastructure used by ScatterPlotWidget and TemporalProjectionViewWidget.
Data Model
Serializable state is stored in OnionSkinViewStateData:
std::map<std::string, CorePlotting::GlyphStyleData> point_key_glyph_styles;Each key maps to a GlyphStyleData with default values {GlyphType::Circle, 8.0f, "#007bff", 1.0f} assigned when the key is first added. The map is serialized via rfl::json alongside all other state fields.
Live QObject Layer
OnionSkinViewState also owns a parallel map of GlyphStyleState objects (one per key):
std::map<std::string, std::unique_ptr<GlyphStyleState>> _point_glyph_style_states;GlyphStyleState is a QObject wrapper around GlyphStyleData that emits styleChanged when the user edits any property. The state class connects each GlyphStyleState::styleChanged to sync point_key_glyph_styles[key], mark dirty, emit glyphStyleChanged(), and emit pointKeyGlyphStyleChanged(key). This two-level design keeps the serializable struct free of QObject and mirrors the VerticalAxisState / VerticalAxisStateData split used throughout the codebase.
Key lifecycle: - addPointDataKey(key) → calls _createGlyphStyleStateForKey(key), which loads any existing serialized style (from point_key_glyph_styles) or inserts the default. - removePointDataKey(key) → erases both the GlyphStyleData entry and the GlyphStyleState. - clearPointDataKeys() → clears both maps. - fromJson() → clears _point_glyph_style_states and recreates them from the deserialized point_key_glyph_styles, ensuring the live and serialized layers stay in sync.
Properties Panel Integration
The “Point Glyph Options” collapsible Section in OnionSkinViewPropertiesWidget contains a single GlyphStyleControls widget. Because there are multiple point keys, the controls are context-sensitive: they rebind to the GlyphStyleState of whichever row is currently selected in the point data table, and are disabled when no row is selected.
The rebind is implemented in _updateGlyphStyleControls(), which is called from:
_onPointTableSelectionChanged()— whenever the table selection changes_onStatePointKeyAdded()/_onStatePointKeyRemoved()— after the table is repopulated
GlyphStyleControls::setGlyphStyleState(state) disconnects from the previous GlyphStyleState, connects to the new one, refreshes all controls, and enables/disables the widget.
Rendering
OnionSkinViewOpenGLWidget::rebuildScene() calls _state->getPointKeyGlyphStyle(key) for each point key to retrieve the current GlyphStyleData. Glyph type, size, and base color are read from the per-key style and used when building RenderableGlyphBatch entries. Temporal alpha modulates the base color’s alpha channel; when highlight_current is enabled the current frame uses a hardcoded orange-red override color regardless of the per-key style.
Testing
Existing Tests
tests/WhiskerToolbox/Plots/OnionSkinViewWidget/OnionSkinViewState.test.cpp— State creation, serialization round-trip, axis state compositiontests/WhiskerToolbox/Plots/OnionSkinViewWidget/OnionSkinViewPropertiesWidget.test.cpp— Properties widget DataManager integration (combo box population, observer refresh on data add/remove, destruction cleanup, stale key handling for point/line/mask keys)
See Also
- Plot Widget Guide — Shared architecture, axis types, DataManager integration
- Onion Skin View Roadmap — Development roadmap for upcoming features
- TemporalProjectionViewWidget — Full-projection counterpart (all-time spatial overlay)
- LinePlotWidget — Shares line rendering infrastructure
- ScatterPlotWidget — Shares point glyph rendering infrastructure
- EventPlotWidget — Shares glyph rendering via
SceneRenderer