summaryrefslogtreecommitdiff
path: root/pixpat-native/src/pattern.h
diff options
context:
space:
mode:
authorTomi Valkeinen <tomi.valkeinen@ideasonboard.com>2026-05-08 17:22:58 +0300
committerTomi Valkeinen <tomi.valkeinen@ideasonboard.com>2026-05-08 17:22:58 +0300
commite0b7d30fd437292c88141fb08d60681870b86c6e (patch)
tree7d7f4e94cbec0f4f494042f7cbf39c7c8e7234fe /pixpat-native/src/pattern.h
Squashed 'subprojects/pixpat/' content from commit d444626
git-subtree-dir: subprojects/pixpat git-subtree-split: d444626e6ba988ec6d487800721e447f94b1eaf5
Diffstat (limited to 'pixpat-native/src/pattern.h')
-rw-r--r--pixpat-native/src/pattern.h597
1 files changed, 597 insertions, 0 deletions
diff --git a/pixpat-native/src/pattern.h b/pixpat-native/src/pattern.h
new file mode 100644
index 0000000..fbee683
--- /dev/null
+++ b/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