diff options
| author | Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> | 2026-05-08 17:22:58 +0300 |
|---|---|---|
| committer | Tomi Valkeinen <tomi.valkeinen@ideasonboard.com> | 2026-05-08 17:22:58 +0300 |
| commit | 4e2b291a4acdc2cbd39f005c88bda363bc06bd34 (patch) | |
| tree | e90048d5973ad1164b109d575cf577af7daf50be /subprojects/pixpat/pixpat-native/src/pattern.h | |
| parent | 8f94b39040e79eccd9312ed1e467fe8ebfab8860 (diff) | |
| parent | e0b7d30fd437292c88141fb08d60681870b86c6e (diff) | |
Merge commit 'e0b7d30fd437292c88141fb08d60681870b86c6e' as 'subprojects/pixpat'
Diffstat (limited to 'subprojects/pixpat/pixpat-native/src/pattern.h')
| -rw-r--r-- | subprojects/pixpat/pixpat-native/src/pattern.h | 597 |
1 files changed, 597 insertions, 0 deletions
diff --git a/subprojects/pixpat/pixpat-native/src/pattern.h b/subprojects/pixpat/pixpat-native/src/pattern.h new file mode 100644 index 0000000..fbee683 --- /dev/null +++ b/subprojects/pixpat/pixpat-native/src/pattern.h @@ -0,0 +1,597 @@ +#pragma once + +#include <cmath> +#include <cstdint> + +#include "color.h" +#include "layout.h" +#include "params.h" + +namespace pixpat::patterns +{ + +// Patterns emit opaque pixels (a=kNormMax) unless they encode their +// own alpha (e.g. `plain`'s ARGB form). Alpha-bearing sinks +// (ARGB8888 etc) therefore see the pattern's chosen alpha; convert +// paths propagate the source's actual `a` instead (a=0 for X-only +// sources). +// +// A pattern is an instance with: +// using Pixel = RGB16 | YUV16; +// explicit Pat(const Params&) noexcept; +// Pixel sample(size_t x, size_t y, size_t W, size_t H) const noexcept; +// bool ready() const noexcept; // optional, default true +// Patterns that don't read params ignore the constructor argument. + +namespace detail +{ +// 8-bit -> normalized 16 byte-replication. e.g. 255 -> 0xFFFF, +// 1 -> 0x0101. +constexpr RGB16 rgb8(uint8_t r, uint8_t g, uint8_t b) noexcept +{ + return RGB16{ + uint16_t((uint16_t(r) << 8) | r), + uint16_t((uint16_t(g) << 8) | g), + uint16_t((uint16_t(b) << 8) | b), + kNormMax, + }; +} + +// 12-bit -> normalized 16 bit-replication. +constexpr YUV16 yuv12(uint16_t y, uint16_t u, uint16_t v) noexcept +{ + return YUV16{ + uint16_t((y << 4) | (y >> 8)), + uint16_t((u << 4) | (u >> 8)), + uint16_t((v << 4) | (v >> 8)), + kNormMax, + }; +} +} // namespace detail + +// "kmstest" default pattern: white border + diagonals; blue rails on +// the top/left edges; red rails on the bottom/right; an 8-step color +// gradient block in the center. +struct Kmstest { + using Pixel = RGB16; + + explicit Kmstest(const Params&) noexcept { + } + + RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept + { + using detail::rgb8; + const size_t mw = 20; + const size_t xm1 = mw; + const size_t xm2 = W - mw - 1; + const size_t ym1 = mw; + const size_t ym2 = H - mw - 1; + + if (x == xm1 || x == xm2 || y == ym1 || y == ym2) + return rgb8(255, 255, 255); + if (x < xm1 && y < ym1) + return rgb8(255, 255, 255); + if ((x == 0 || x == W - 1) && (y < ym1 || y > ym2)) + return rgb8(255, 255, 255); + if ((y == 0 || y == H - 1) && (x < xm1 || x > xm2)) + return rgb8(255, 255, 255); + if (x < xm1 && (y > ym1 && y < ym2)) + return rgb8(0, 0, 255); + if (y < ym1 && (x > xm1 && x < xm2)) + return rgb8(0, 0, 255); + if (x > xm2 && (y > ym1 && y < ym2)) + return rgb8(255, 0, 0); + if (y > ym2 && (x > xm1 && x < xm2)) + return rgb8(255, 0, 0); + if (x > xm1 && x < xm2 && y > ym1 && y < ym2) { + if (x == y || W - x == H - y) + return rgb8(255, 255, 255); + if (W - x - 1 == y || x == H - y - 1) + return rgb8(255, 255, 255); + const int t = int((x - xm1 - 1) * 8 / (xm2 - xm1 - 1)); + const unsigned c = unsigned((y - ym1 - 1) % 256); + unsigned r = 0, g = 0, b = 0; + switch (t) { + case 0: r = c; break; + case 1: g = c; break; + case 2: b = c; break; + case 3: g = b = c; break; + case 4: r = b = c; break; + case 5: r = g = c; break; + case 6: r = g = b = c; break; + case 7: break; + } + return rgb8(uint8_t(r), uint8_t(g), uint8_t(b)); + } + return rgb8(0, 0, 0); + } +}; + +// SMPTE RP 219-1:2014 color bar pattern. Emits YUV directly with +// pixel values defined by the spec in BT.709 / Limited range. Pass +// `rec=BT709, range=Limited` for spec-correct output; other ColorSpec +// settings produce visibly-wrong colors when the sink crosses to RGB +// (the matrix the caller picked is applied to BT.709-encoded values). +// Callers are trusted — pixpat does not override the spec for them. +struct Smpte { + using Pixel = YUV16; + + explicit Smpte(const Params&) noexcept { + } + + YUV16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept + { + using detail::yuv12; + constexpr YUV16 gray40 = yuv12(1658, 2048, 2048); + constexpr YUV16 white75 = yuv12(2884, 2048, 2048); + constexpr YUV16 yellow75 = yuv12(2694, 704, 2171); + constexpr YUV16 cyan75 = yuv12(2325, 2356, 704); + constexpr YUV16 green75 = yuv12(2136, 1012, 827); + constexpr YUV16 magenta75 = yuv12(1004, 3084, 3269); + constexpr YUV16 red75 = yuv12( 815, 1740, 3392); + constexpr YUV16 blue75 = yuv12( 446, 3392, 1925); + constexpr YUV16 cyan100 = yuv12(3015, 2459, 256); + constexpr YUV16 blue100 = yuv12( 509, 3840, 1884); + constexpr YUV16 yellow100 = yuv12(3507, 256, 2212); + constexpr YUV16 black = yuv12( 256, 2048, 2048); + constexpr YUV16 white100 = yuv12(3760, 2048, 2048); + constexpr YUV16 red100 = yuv12(1001, 1637, 3840); + constexpr YUV16 gray15 = yuv12( 782, 2048, 2048); + + constexpr YUV16 black_m2 = yuv12( 186, 2048, 2048); + constexpr YUV16 black_p2 = yuv12( 326, 2048, 2048); + constexpr YUV16 black_p4 = yuv12( 396, 2048, 2048); + + constexpr size_t M = 1024; + const size_t xs = x * M; + const size_t a = W * M; + const size_t c = (a * 3 / 4) / 7; + const size_t d = a / 8; + + const size_t pattern1_height = (H * 7) / 12; + const size_t pattern2_height = pattern1_height + (H / 12); + const size_t pattern3_height = pattern2_height + (H / 12); + + if (y < pattern1_height) { + if (xs < d || xs >= (a - d)) + return gray40; + const size_t bar = (xs - d) / c; + switch (bar) { + case 0: return white75; + case 1: return yellow75; + case 2: return cyan75; + case 3: return green75; + case 4: return magenta75; + case 5: return red75; + default: return blue75; + } + } + + if (y < pattern2_height) { + if (xs < d) return cyan100; + if (xs >= (a - d)) return blue100; + return white75; + } + + if (y < pattern3_height) { + if (xs < d) return yellow100; + if (xs >= (a - d)) return red100; + const size_t ramp_w = a - 2 * d; + const size_t ramp_x = xs - d; + const uint16_t y_val = uint16_t(256 + (3760 - 256) * ramp_x / ramp_w); + return yuv12(y_val, 2048, 2048); + } + + // pattern4 (PLUGE) + const size_t c0 = d; + const size_t c1 = c0 + c * 3 / 2; + const size_t c2 = c1 + 2 * c; + const size_t c3 = c2 + c * 5 / 6; + + if (xs < c0) return gray15; + if (xs < c1) return black; + if (xs < c2) return white100; + if (xs < c3) return black; + if (xs >= a - d) return gray15; + if (xs >= a - d - c) return black; + + const size_t step = (xs - c3) / (c / 3); + switch (step) { + case 0: return black_m2; + case 1: return black; + case 2: return black_p2; + case 3: return black; + default: return black_p4; + } + } +}; + +// Solid fill from a hex color string. Reads `color=<hex>` from +// params; the value is parsed by Params::get_hex_color (8/16-bpc, +// alpha-first if present, optional `0x` prefix). Missing or +// malformed `color` leaves ready()=false and the dispatcher fails +// the call. +struct Plain { + using Pixel = RGB16; + + explicit Plain(const Params& p) noexcept + { + if (auto c = p.get_hex_color("color")) { + color_ = *c; + ready_ = true; + } + } + + bool ready() const noexcept { + return ready_; + } + + RGB16 sample(size_t, size_t, size_t, size_t) const noexcept + { + return color_; + } + +private: + RGB16 color_{}; + bool ready_{ false }; +}; + +namespace detail +{ +// Linear ramp 0..kNormMax across [0, span-1]. span<=1 returns kNormMax. +constexpr uint16_t ramp16(size_t pos, size_t span) noexcept +{ + if (span <= 1) + return kNormMax; + return uint16_t((uint64_t(pos) * kNormMax) / (span - 1)); +} +} // namespace detail + +// Black/white checkerboard. Reads optional `cell=<N>` (positive +// integer; default 8) for cell size in pixels. +struct Checker { + using Pixel = RGB16; + + explicit Checker(const Params& p) noexcept + { + if (p.get("cell")) { + auto n = p.get_int("cell"); + if (!n || *n <= 0) { + ready_ = false; + return; + } + cell_ = size_t(*n); + } + } + + bool ready() const noexcept { + return ready_; + } + + RGB16 sample(size_t x, size_t y, size_t, size_t) const noexcept + { + const bool dark = (((x / cell_) ^ (y / cell_)) & 1u) != 0; + return dark ? RGB16{ 0, 0, 0, kNormMax } + : RGB16{ kNormMax, kNormMax, kNormMax, kNormMax }; + } + +private: + size_t cell_{ 8 }; + bool ready_{ true }; +}; + +namespace detail +{ +// Pick one of (R, G, B, gray) given a stripe index in [0, 4) and a +// scalar ramp value. Used by hramp/vramp. +constexpr RGB16 rgb_gray_stripe(size_t stripe, uint16_t v) noexcept +{ + switch (stripe) { + case 0: return RGB16{ v, 0, 0, kNormMax }; + case 1: return RGB16{ 0, v, 0, kNormMax }; + case 2: return RGB16{ 0, 0, v, kNormMax }; + default: return RGB16{ v, v, v, kNormMax }; + } +} +} // namespace detail + +// Four horizontal stripes — R, G, B, gray — each a 0..max ramp +// along x. Per-channel and luma quantization in one pattern. +struct Hramp { + using Pixel = RGB16; + + explicit Hramp(const Params&) noexcept { + } + + RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept + { + const size_t stripe = (H == 0) ? 0 : (y * 4) / H; + return detail::rgb_gray_stripe(stripe, detail::ramp16(x, W)); + } +}; + +// Four vertical columns — R, G, B, gray — each a 0..max ramp +// along y. Same coverage as hramp, rotated 90°. +struct Vramp { + using Pixel = RGB16; + + explicit Vramp(const Params&) noexcept { + } + + RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept + { + const size_t col = (W == 0) ? 0 : (x * 4) / W; + return detail::rgb_gray_stripe(col, detail::ramp16(y, H)); + } +}; + +// Diagonal RGB ramp: R sweeps with x, G with y, B with x+y. +struct Dramp { + using Pixel = RGB16; + + explicit Dramp(const Params&) noexcept { + } + + RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept + { + const uint16_t r = detail::ramp16(x, W); + const uint16_t g = detail::ramp16(y, H); + const size_t span = (W + H >= 2) ? (W + H - 1) : 1; + const uint16_t b = detail::ramp16(x + y, span); + return RGB16{ r, g, b, kNormMax }; + } +}; + +namespace detail +{ +// Seven-region color sequence used by hbar/vbar: +// white, red, white, green, white, blue, white. The white separators +// between R/G/B make per-channel offsets at the band boundaries +// visible. +constexpr RGB16 bar_color7(size_t band) noexcept +{ + switch (band) { + case 1: return rgb8(255, 0, 0); + case 3: return rgb8( 0, 255, 0); + case 5: return rgb8( 0, 0, 255); + default: return rgb8(255, 255, 255); + } +} +} // namespace detail + +// Vertical bar (full image height, narrow along x) over a black +// background. `pos` is the left edge in pixels (signed; negative +// values clip at the left edge); `width` is the bar thickness in +// pixels (default 32). The bar is split into 7 equal-height regions +// colored white/red/white/green/white/blue/white. +struct VBarRGB { + using Pixel = RGB16; + + explicit VBarRGB(const Params& p) noexcept + { + auto pp = p.get_int("pos"); + if (!pp) { + ready_ = false; + return; + } + pos_ = *pp; + if (p.get("width")) { + auto w = p.get_int("width"); + if (!w || *w <= 0) { + ready_ = false; + return; + } + width_ = size_t(*w); + } + } + + bool ready() const noexcept { + return ready_; + } + + RGB16 sample(size_t x, size_t y, size_t, size_t H) const noexcept + { + const long long sx = static_cast<long long>(x); + const long long lo = pos_; + const long long hi = lo + static_cast<long long>(width_); + if (sx < lo || sx >= hi) + return detail::rgb8(0, 0, 0); + const size_t band = (H == 0) ? 0 : (y * 7) / H; + return detail::bar_color7(band); + } + +private: + int pos_{}; + size_t width_{ 32 }; + bool ready_{ true }; +}; + +// Horizontal bar: vbar rotated 90°. `pos` is the top edge in pixels; +// `width` is the bar thickness in pixels (default 32). The bar spans +// the full image width and is split into 7 equal-width regions +// colored white/red/white/green/white/blue/white. +struct HBarRGB { + using Pixel = RGB16; + + explicit HBarRGB(const Params& p) noexcept + { + auto pp = p.get_int("pos"); + if (!pp) { + ready_ = false; + return; + } + pos_ = *pp; + if (p.get("width")) { + auto w = p.get_int("width"); + if (!w || *w <= 0) { + ready_ = false; + return; + } + width_ = size_t(*w); + } + } + + bool ready() const noexcept { + return ready_; + } + + RGB16 sample(size_t x, size_t y, size_t W, size_t) const noexcept + { + const long long sy = static_cast<long long>(y); + const long long lo = pos_; + const long long hi = lo + static_cast<long long>(width_); + if (sy < lo || sy >= hi) + return detail::rgb8(0, 0, 0); + const size_t band = (W == 0) ? 0 : (x * 7) / W; + return detail::bar_color7(band); + } + +private: + int pos_{}; + size_t width_{ 32 }; + bool ready_{ true }; +}; + +// Same shape as VBarRGB but emits YUV16 directly. The five unique colors +// (black bg + white/red/green/blue bar regions) are precomputed from +// `spec` at construction so the cross-kind pass is a no-op when the +// sink is YUV. Use the RGB-native `VBarRGB` for RGB sinks instead — it +// avoids the YUV→RGB pass that this variant would incur there. +struct VBarYUV { + using Pixel = YUV16; + + explicit VBarYUV(const Params& p, ColorSpec spec) noexcept + { + auto pp = p.get_int("pos"); + if (!pp) { + ready_ = false; + return; + } + pos_ = *pp; + if (p.get("width")) { + auto w = p.get_int("width"); + if (!w || *w <= 0) { + ready_ = false; + return; + } + width_ = size_t(*w); + } + const ColorCoeffs c = coeffs_for(spec); + using X = ColorXfm<RGB16, YUV16>; + bg_ = X::apply(detail::rgb8( 0, 0, 0), c); + bands_[0] = X::apply(detail::rgb8(255, 255, 255), c); + bands_[1] = X::apply(detail::rgb8(255, 0, 0), c); + bands_[2] = bands_[0]; + bands_[3] = X::apply(detail::rgb8( 0, 255, 0), c); + bands_[4] = bands_[0]; + bands_[5] = X::apply(detail::rgb8( 0, 0, 255), c); + bands_[6] = bands_[0]; + } + + bool ready() const noexcept { + return ready_; + } + + YUV16 sample(size_t x, size_t y, size_t, size_t H) const noexcept + { + const long long sx = static_cast<long long>(x); + const long long lo = pos_; + const long long hi = lo + static_cast<long long>(width_); + if (sx < lo || sx >= hi) + return bg_; + const size_t band = (H == 0) ? 0 : (y * 7) / H; + return bands_[band]; + } + +private: + YUV16 bg_{}; + YUV16 bands_[7]{}; + int pos_{}; + size_t width_{ 32 }; + bool ready_{ true }; +}; + +// YUV-native counterpart to HBarRGB. See VBarYUV. +struct HBarYUV { + using Pixel = YUV16; + + explicit HBarYUV(const Params& p, ColorSpec spec) noexcept + { + auto pp = p.get_int("pos"); + if (!pp) { + ready_ = false; + return; + } + pos_ = *pp; + if (p.get("width")) { + auto w = p.get_int("width"); + if (!w || *w <= 0) { + ready_ = false; + return; + } + width_ = size_t(*w); + } + const ColorCoeffs c = coeffs_for(spec); + using X = ColorXfm<RGB16, YUV16>; + bg_ = X::apply(detail::rgb8( 0, 0, 0), c); + bands_[0] = X::apply(detail::rgb8(255, 255, 255), c); + bands_[1] = X::apply(detail::rgb8(255, 0, 0), c); + bands_[2] = bands_[0]; + bands_[3] = X::apply(detail::rgb8( 0, 255, 0), c); + bands_[4] = bands_[0]; + bands_[5] = X::apply(detail::rgb8( 0, 0, 255), c); + bands_[6] = bands_[0]; + } + + bool ready() const noexcept { + return ready_; + } + + YUV16 sample(size_t x, size_t y, size_t W, size_t) const noexcept + { + const long long sy = static_cast<long long>(y); + const long long lo = pos_; + const long long hi = lo + static_cast<long long>(width_); + if (sy < lo || sy >= hi) + return bg_; + const size_t band = (W == 0) ? 0 : (x * 7) / W; + return bands_[band]; + } + +private: + YUV16 bg_{}; + YUV16 bands_[7]{}; + int pos_{}; + size_t width_{ 32 }; + bool ready_{ true }; +}; + +// Centered radial cosine zone plate: 0.5 + 0.5 * cos(k * (cx² + cy²)) +// with cx, cy measured from the image center and k chosen so the +// local frequency hits Nyquist at the longer edge — i.e. the pattern +// uses every spatial frequency the grid can resolve. +struct Zoneplate { + using Pixel = RGB16; + + explicit Zoneplate(const Params&) noexcept { + } + + RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept + { + const double max_dim = double(W > H ? W : H); + // Local frequency d(k r²)/dr = 2 k r. At r = max_dim/2 the + // frequency reaches π/pixel (Nyquist), giving k = π / max_dim. + const double k = 3.14159265358979323846 / (max_dim > 0 ? max_dim : 1.0); + const double cx = double(x) - 0.5 * double(W); + const double cy = double(y) - 0.5 * double(H); + const double phase = k * (cx * cx + cy * cy); + const double v = 0.5 + 0.5 * std::cos(phase); + const double scaled = v * 65535.0; + const uint16_t g = (scaled < 0.0) ? uint16_t(0) + : (scaled > 65535.0) ? kNormMax + : uint16_t(scaled + 0.5); + return RGB16{ g, g, g, kNormMax }; + } +}; + +} // namespace pixpat::patterns |
