Files
plotter/include/plot/render/style.hpp
T
Martino Ferrari c4c0a49466 style(grid): brighter major grid
grid_major 0x2A3038 -> 0x39414C, still well under axis_line's 0x8A94A6, so the three-way
hierarchy the eye reads stays minor < major < axis. Already fully opaque, so "more
prominent" meant a brighter colour and nothing else.

The tests that classify grid pixels take their colours from the style, so they followed
the change; the one hardcoded mention (grid_pixel()'s doc comment) is updated, and the
minor's relationship to the major is now stated as an order of magnitude rather than the
fifth the old colours happened to give. 361 cases, 0 failed.
2026-10-03 14:42:06 +02:00

171 lines
10 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <plot/core/color.hpp>
namespace plot::render {
/// Every colour and metric the renderer draws with. Defaults are the dark theme
/// the plan's render smoke test asserts against; a caller swaps a field and the
/// next frame picks it up, so nothing here is baked into a shader.
///
/// ## Units: every metric below is **logical**, and `device()` converts it
///
/// A metric a user perceives as a *size* -- a font, a stroke width, a padding, a
/// marker, a swatch, the minimum spacing between ticks -- is written in logical
/// units, so a 12 px label is the same physical size on a 1x panel and a 2x one
/// and a 1 px grid is the same hairline on both. The host states what its display
/// is with `ui_scale` (device pixels per logical unit) and every consumer converts
/// exactly once, through `device()`: `Plot::layout()` when it sizes the label
/// strips, the title strips and the legend's chrome, the renderer when it converts
/// widths and font px.
///
/// The one exception is `dense_threshold_spp`, which is a **density** rather than a
/// size: samples per *rasterised* pixel. It is compared against a samples-per-pixel
/// ratio computed from the framebuffer's own width, so scaling it would make the
/// chosen LOD level depend on the display and not on the data. It is used raw.
///
/// The same rule classifies the metrics that live outside this struct, which are
/// logical too and converted by the same call:
///
/// * `Legend`'s pad, swatch, gap and margin;
/// * `Trace`'s (and `TraceDesc`'s) `width` -- a stroke like `line_width_px`, and
/// the one a plot draws most of, so leaving it raw would invert the
/// trace/axis stroke hierarchy at any scale above 1;
/// * the renderer's marker arm lengths and text offsets.
struct Style {
// --- colours: neither logical nor device, so nothing to scale ------------
Color background = Color::hex(0x101418);
/// The labelled grid. Brighter than the subdivisions and still well under `axis_line`
/// (0x8A94A6): the grid is structure the eye can read, the axis is the frame, and the
/// hierarchy between the three is what keeps a dense plot legible. It is already fully
/// opaque, so "more prominent" means a brighter colour and nothing else.
Color grid_major = Color::hex(0x39414C);
/// The minor grid's colour, **with an alpha** so it blends into the background rather than
/// sitting on it: a subdivision is a reading aid, and at full opacity a dense minor grid
/// reads as a second set of ticks competing with the labelled one. The line shader applies
/// `u_color.a` (see `src/render/shaders.cpp`), so this one blends and the major -- opaque at
/// 0x39414C -- stays the structure the eye picks out. Together with `grid_minor_width` and
/// the dash below, the defaults put a minor line at an order of magnitude less ink than a
/// major one.
Color grid_minor = Color::hex(0x1C2126, 0.5f);
Color axis_line = Color::hex(0x8A94A6);
Color axis_text = Color::hex(0xC8D0DC);
Color cursor = Color::hex(0xFFB020);
Color measure_fill = Color::hex(0xFFB020, 0.15f);
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
/// trace and the line says *where*.
///
/// **Hue-neutral by design**, and that is the whole choice: a saturated colour here would be
/// confusable with a trace, and the library's own default trace colour is a saturated cyan
/// (`Trace`'s default, 0x40C0FF) that a hue-based reading cannot tell from a cyan crosshair --
/// only lightness would separate them, which no reader can rely on. A near-white can never be a
/// saturated trace of *any* hue, and it is still plainly not `cursor`'s orange, which is what
/// the spec asks for.
///
/// The stroke is thin and dashed by default -- see `crosshair_width`, `crosshair_dash_on` and
/// `crosshair_dash_off` below for the values: thinner than a trace so the instrument reads as
/// chrome rather than data, and dashed so it is distinguishable by *form* as well as by
/// lightness.
Color crosshair = Color::hex(0xE6EBF2);
/// The crosshair's stroke and dash, in logical pixels (spec §17). The crosshair is a
/// *passive* hover instrument drawn over the data, so its default **recedes**: a
/// thinner stroke than a trace's (`Trace::width`'s 1.5) so it reads as chrome rather
/// than as data, and a dash so it is distinguishable from the grid and the traces at
/// a glance.
///
/// `crosshair_dash_on` is the ink length and `crosshair_dash_off` the gap; **`off == 0`
/// means solid**, which is the value every *other* line in the library is drawn at, so
/// the one pipeline serves them unchanged (the dash is a fragment-shader discard on
/// `v_along`, so no geometry is added). A `Crosshair` may override all three for one
/// plot without touching this global style.
f32 crosshair_width = 0.75f;
f32 crosshair_dash_on = 4.0f;
f32 crosshair_dash_off = 4.0f;
// --- the scale ----------------------------------------------------------
/// Device pixels per logical unit. 1.0 is the 1x display every default here was
/// chosen on. Supplied by the host (`Renderer::style()` is what an adapter
/// writes), and handed to the plot by `Renderer::draw()` before it lays out, so
/// layout and drawing cannot disagree about it.
f32 ui_scale = 1.0f;
// --- logical metrics: a size a user perceives, scaled by `device()` ------
f32 axis_font_px = 12.0f; ///< logical em the axis text is drawn at.
f32 grid_label_pad = 4.0f; ///< logical gap between a tick label and the axes,
///< and the inset inside a title strip.
/// The two grid classes, each with its own width and its own switch. The grid's
/// *positions* come from the tick set alone -- `Grid::build()` tags every segment
/// with the class of the tick it came from -- so these four decide only how a
/// class is drawn. A class switched off stages no geometry at all: the renderer
/// skips it before the first segment is staged, so a dense plot's minor grid
/// costs nothing rather than being drawn transparent (spec §11). The colours
/// above are already per class.
f32 grid_major_width = 1.0f; ///< logical width of a major (labelled-tick) line.
/// Logical width of a minor (subdivision) line: **thinner than the major hairline**, and
/// sub-pixel on purpose. At 1x that is expressed as *coverage* -- the anti-aliased ramp
/// (`line_aa_px`) lights one column at roughly three quarters of the ink -- and on a HiDPI
/// display as a line that is literally narrower. The point is hierarchy: the labelled grid is
/// structure, the subdivisions are texture.
f32 grid_minor_width = 0.75f;
bool grid_major_visible = true; ///< stage the major grid at all.
bool grid_minor_visible = true; ///< stage the minor grid at all.
/// The minor grid's dash, in logical pixels: ink length and gap. **`off == 0` draws it
/// solid** -- the same convention as the crosshair's and a trace's dash -- which is what a
/// caller that wants the old solid subdivision states. Dashed is the default because an
/// unlabelled line at an unlabelled position should never be mistaken for a labelled one, and
/// because a broken line carries less weight over data than a continuous one. The dash is a
/// fragment discard, never extra geometry, so a dense minor grid costs no more vertices for
/// being dashed.
f32 grid_minor_dash_on = 3.0f;
f32 grid_minor_dash_off = 3.0f;
f32 line_width_px = 1.5f; ///< logical width of the strokes framing the data
///< (axis lines, cursor markers).
/// Logical half-width of the anti-aliased fringe the line shader blends at
/// every stroke edge (the smoothstep ramp runs over `|dist| in [w - aa,
/// w + aa]`). Clamped to at least 0.25 device px at use: below that the
/// ramp is thinner than a fragment and edges alias again. 1.0 is the value
/// every default was chosen on.
f32 line_aa_px = 1.0f;
/// Logical width of a *draggable handle* drawn highlighted: the tab a hovered or
/// grabbed cursor marker is widened to, and the tab a region's hot bound is drawn
/// as -- the latter sized by `Plot::layout()` with this same metric, so the two
/// kinds of handle are one width. Wider than `line_width_px` on purpose: the cue
/// for "this marker will follow your drag" has to be visible on a marker whose
/// own colour may already be the accent.
f32 handle_width_px = 5.0f;
f32 tick_min_px = 60.0f; ///< logical minimum spacing between major ticks, so
///< a 2x display gets the same tick *count*.
// --- device metric: a density, never scaled ------------------------------
f32 dense_threshold_spp = 1.5f; ///< spec §4.3: samples per **device** pixel at or
///< above which a trace is gathered from the LOD
///< pyramid instead of from its raw samples. A
///< density, not a size: it is deliberately not
///< scaled by `ui_scale`, or the chosen level
///< would move with the display's DPI. It selects
///< the gather level, never a primitive: both
///< sides draw through the one instanced
///< anti-aliased quad path.
/// `logical` in device pixels: the single conversion point for every logical
/// metric above, so the arithmetic appears once per consumer instead of a `*
/// ui_scale` scattered over the renderer and the layout.
///
/// A scale that is not positive is not a scale -- a host that has not measured
/// its display, a field nobody filled in -- and converts 1:1 rather than
/// collapsing every strip, font and stroke to nothing.
f32 device(f32 logical) const noexcept {
return ui_scale > 0.0f ? logical * ui_scale : logical;
}
};
} // namespace plot::render