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
+70
View File
@@ -0,0 +1,70 @@
#include <chrono>
#include <cstdio>
#include <iomanip>
#include <iostream>
#include "cli/options.hpp"
#include "core/effect.hpp"
#include "core/image.hpp"
#include "core/registry.hpp"
#include "core/rng.hpp"
#include "gui/app.hpp"
namespace {
int renderHeadless(const effector::Options& options) {
const auto effect = effector::effectByName(options.effect);
std::vector<effector::LoadFailure> failures;
auto sources = effector::loadImages(options.images, failures);
for (const auto& failure : failures) {
std::cerr << "effector: skipping " << failure.path << ": " << failure.message << "\n";
}
if (sources.empty()) {
std::cerr << "effector: no usable source images\n";
return 2;
}
const std::uint64_t seed = options.seed.value_or(effector::Rng::randomSeed());
effector::Rng rng(seed);
auto values = effect->defaults();
for (const auto& [key, value] : options.params) values[key] = value;
const auto start = std::chrono::steady_clock::now();
const auto image = effect->render(sources, options.width, options.height, rng, values);
const auto renderMillis =
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
image.save(options.output);
std::cout << options.output << " " << options.width << "x" << options.height
<< " effect=" << effect->name() << " seed=" << seed << " render="
<< std::fixed << std::setprecision(1) << renderMillis << " ms\n";
return 0;
}
} // namespace
int main(int argc, char** argv) {
try {
const auto options = effector::parseOptions(argc, argv);
if (options.showHelp) {
std::cout << effector::usage(argv[0]);
return 0;
}
if (options.listEffects) {
for (const auto& effect : effector::effects()) {
std::cout << effect->name() << "\t" << effect->title() << " - "
<< effect->description() << "\n";
}
return 0;
}
if (!options.output.empty()) return renderHeadless(options);
return effector::runGui(options, argc, argv);
} catch (const std::exception& error) {
std::cerr << "effector: " << error.what() << "\n";
return 1;
}
}