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:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user