The run sorting moves to effects/sort_runs.hpp so both share it; Pixel sort renders exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
90 lines
3.8 KiB
C++
90 lines
3.8 KiB
C++
#include "effects/pixelsort.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#include "core/parallel.hpp"
|
|
#include "effects/sampling.hpp"
|
|
#include "effects/sort_runs.hpp"
|
|
#include "effects/source_fill.hpp"
|
|
|
|
namespace effector {
|
|
|
|
const std::vector<ParamSpec>& PixelSortEffect::params() const {
|
|
static const std::vector<ParamSpec> specs = {
|
|
ParamSpec::makeDouble("sort_low", "Lower threshold", 0.25, 0.0, 1.0,
|
|
"Pixels darker than this break a run and stay where they are."),
|
|
ParamSpec::makeDouble("sort_high", "Upper threshold", 0.8, 0.0, 1.0,
|
|
"Pixels brighter than this break a run and stay where they are."),
|
|
ParamSpec::makeChoice("sort_key", "Sort by", "luma", {"luma", "hue", "saturation"},
|
|
"What orders the pixels inside a run."),
|
|
ParamSpec::makeChoice("sort_direction", "Direction", "horizontal",
|
|
{"horizontal", "vertical"}, "Whether rows or columns are sorted."),
|
|
ParamSpec::makeBool("sort_reverse", "Reverse", false,
|
|
"Sort from high to low instead of from low to high."),
|
|
ParamSpec::makeDouble("sort_line_chance", "Line chance", 1.0, 0.0, 1.0,
|
|
"Chance of each row (or column) being sorted at all."),
|
|
ParamSpec::makeInt("sort_max_run", "Longest run", 0, 0, 8192,
|
|
"Each run is cut after a random length up to this many pixels, which "
|
|
"frays the edges of the sorted streaks (0 = no limit)."),
|
|
};
|
|
return specs;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct LinePlan {
|
|
bool sorted = false;
|
|
std::uint64_t seed = 0; // for the run lengths, when they are limited
|
|
};
|
|
|
|
} // namespace
|
|
|
|
Image PixelSortEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
|
const ParamValues& values) const {
|
|
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
|
|
|
const int low = static_cast<int>(std::clamp(paramDouble(values, "sort_low", 0.25), 0.0, 1.0) * 255.0 + 0.5);
|
|
const int high = static_cast<int>(std::clamp(paramDouble(values, "sort_high", 0.8), 0.0, 1.0) * 255.0 + 0.5);
|
|
const std::string keyName = paramChoice(values, "sort_key", "luma");
|
|
const bool vertical = paramChoice(values, "sort_direction", "horizontal") == "vertical";
|
|
const bool reverse = paramBool(values, "sort_reverse", false);
|
|
const double lineChance = std::clamp(paramDouble(values, "sort_line_chance", 1.0), 0.0, 1.0);
|
|
const int maxRun = static_cast<int>(std::max(0L, paramInt(values, "sort_max_run", 0)));
|
|
|
|
Image canvas = canvasFromSources(sources, width, height, rng);
|
|
if (vertical) canvas = transposed(canvas);
|
|
const int lines = canvas.height();
|
|
const int span = canvas.width();
|
|
|
|
// Which lines are sorted, and a seed for each line's run lengths, are drawn
|
|
// serially; the lines then sort in parallel, each from its own generator.
|
|
std::vector<LinePlan> plan(static_cast<std::size_t>(lines));
|
|
for (auto& line : plan) {
|
|
line.sorted = chance(rng.next(), lineChance);
|
|
if (maxRun > 0) line.seed = rng.next();
|
|
}
|
|
|
|
SortRule rule;
|
|
rule.low = low;
|
|
rule.high = high;
|
|
rule.key = sortKeyNamed(keyName);
|
|
rule.reverse = reverse;
|
|
rule.maxRun = maxRun;
|
|
|
|
parallelFor(static_cast<std::size_t>(lines), [&](std::size_t begin, std::size_t end) {
|
|
std::vector<std::pair<int, std::array<std::uint8_t, 3>>> run;
|
|
for (std::size_t line = begin; line < end; ++line) {
|
|
if (!plan[line].sorted) continue;
|
|
Rng runRng(plan[line].seed);
|
|
sortLine(canvas.row(static_cast<int>(line)), span, rule, runRng, run);
|
|
}
|
|
});
|
|
|
|
return vertical ? transposed(canvas) : canvas;
|
|
}
|
|
|
|
} // namespace effector
|