Effector: C++/CMake image effects app

Port of the Python prototype to C++20 with a CMake build:

- effector_core: GUI-free static library (effects, image IO via gdk-pixbuf,
  declarative param specs, registry, portable seeded RNG, thread helper)
- scanlines effect: plans strips from the seeded RNG, then copies pixels in
  parallel with memcpy and doubling horizontal tiling
- gtkmm 4 interface: native file dialogs, widgets generated from the param
  specs, coalesced background renders, preview as a GDK memory texture
- CLI: flags generated from the effect registry, headless rendering with no
  display required
- ctest suite covering the RNG, registry, effect geometry and image round trip

The Python prototype is kept under python-reference/.
This commit is contained in:
hans
2026-09-18 14:28:13 +02:00
commit 925a719da0
36 changed files with 2859 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
#include "effects/scanlines.hpp"
#include <algorithm>
#include <cstring>
#include <thread>
#include <vector>
#include "core/parallel.hpp"
namespace effector {
const std::vector<ParamSpec>& ScanlinesEffect::params() const {
static const std::vector<ParamSpec> specs = {
ParamSpec::makeInt("strip_height", "Strip height", 8, 1, 512,
"Height in pixels of each extracted line."),
ParamSpec::makeInt("jitter", "Height jitter", 0, 0, 256,
"Random +/- variation added to the strip height (0 = uniform)."),
ParamSpec::makeBool("keep_order", "Cycle images in order", false,
"Take strips from the images in order instead of picking randomly."),
};
return specs;
}
namespace {
struct Strip {
int dstY;
int height;
int sourceIndex;
int sx;
int sy;
};
} // namespace
Image ScanlinesEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const {
if (sources.empty()) throw EffectError("No source images.");
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
const int stripHeight = std::max(1L, paramInt(values, "strip_height", 8));
const int jitter = static_cast<int>(std::max(0L, paramInt(values, "jitter", 0)));
const bool keepOrder = paramBool(values, "keep_order", false);
// The plan is drawn sequentially so that a seed always gives the same
// layout; the pixel copying afterwards is what gets parallelised.
std::vector<Strip> plan;
plan.reserve(static_cast<std::size_t>(height / stripHeight) + 8);
int y = 0;
std::size_t index = 0;
while (y < height) {
const int sourceIndex =
keepOrder ? static_cast<int>(index++ % sources.size())
: static_cast<int>(rng.below(sources.size() - 1));
const Image& src = sources[static_cast<std::size_t>(sourceIndex)];
int h = stripHeight;
if (jitter > 0) h += static_cast<int>(rng.inRange(-jitter, jitter));
h = std::max(1, std::min({h, height - y, src.height()}));
const int sy = static_cast<int>(rng.inRange(0, src.height() - h));
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
plan.push_back({y, h, sourceIndex, sx, sy});
y += h;
}
Image canvas(width, height);
parallelFor(plan.size(), [&](std::size_t begin, std::size_t end) {
for (std::size_t i = begin; i < end; ++i) {
const Strip& strip = plan[i];
const Image& src = sources[static_cast<std::size_t>(strip.sourceIndex)];
const int copyWidth = std::min(width, src.width() - strip.sx);
const std::size_t copyBytes = static_cast<std::size_t>(copyWidth) * 3;
for (int line = 0; line < strip.height; ++line) {
const std::uint8_t* in = src.row(strip.sy + line) + static_cast<std::size_t>(strip.sx) * 3;
std::uint8_t* out = canvas.row(strip.dstY + line);
std::memcpy(out, in, copyBytes);
// Tile horizontally when the source is narrower than the canvas,
// doubling the filled span on each pass.
std::size_t filled = copyBytes;
const std::size_t total = static_cast<std::size_t>(width) * 3;
while (filled < total) {
const std::size_t chunk = std::min(filled, total - filled);
std::memcpy(out + filled, out, chunk);
filled += chunk;
}
}
}
});
return canvas;
}
} // namespace effector
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "core/effect.hpp"
namespace effector {
// Fills the canvas top to bottom with horizontal strips cut at random positions
// from the source images.
class ScanlinesEffect : public Effect {
public:
std::string name() const override { return "scanlines"; }
std::string title() const override { return "Scanlines"; }
std::string description() const override {
return "Stacks horizontal strips cut at random positions from the source images, "
"line by line, until the canvas is full.";
}
const std::vector<ParamSpec>& params() const override;
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
const ParamValues& values) const override;
};
} // namespace effector