Add Sort sweep, a video effect that sweeps Pixel sort's window over the brightness
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>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
#include "effects/scanlines.hpp"
|
||||
#include "effects/shatter.hpp"
|
||||
#include "effects/slitshift.hpp"
|
||||
#include "effects/sweep.hpp"
|
||||
#include "effects/text.hpp"
|
||||
#include "effects/tiles.hpp"
|
||||
#include "effects/tileswap.hpp"
|
||||
@@ -45,6 +46,7 @@ const std::vector<EffectPtr>& effects() {
|
||||
std::make_shared<TileSwapEffect>(),
|
||||
std::make_shared<TunnelEffect>(),
|
||||
std::make_shared<FlowEffect>(),
|
||||
std::make_shared<SweepEffect>(),
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
#include "effects/sampling.hpp"
|
||||
#include "effects/sort_runs.hpp"
|
||||
#include "effects/source_fill.hpp"
|
||||
|
||||
namespace effector {
|
||||
@@ -34,39 +35,6 @@ const std::vector<ParamSpec>& PixelSortEffect::params() const {
|
||||
|
||||
namespace {
|
||||
|
||||
int luma(const std::uint8_t* px) { return (px[0] * 54 + px[1] * 183 + px[2] * 19) >> 8; }
|
||||
|
||||
// Hue on a 0..1535 scale (six sectors of 256), in integers.
|
||||
int hue(const std::uint8_t* px) {
|
||||
const int r = px[0], g = px[1], b = px[2];
|
||||
const int hi = std::max({r, g, b});
|
||||
const int lo = std::min({r, g, b});
|
||||
const int range = hi - lo;
|
||||
if (range == 0) return 0;
|
||||
if (hi == r) return g >= b ? (g - b) * 255 / range : 1535 - (b - g) * 255 / range;
|
||||
if (hi == g) return 512 + (b - r) * 255 / range;
|
||||
return 1024 + (r - g) * 255 / range;
|
||||
}
|
||||
|
||||
int saturation(const std::uint8_t* px) {
|
||||
return std::max({px[0], px[1], px[2]}) - std::min({px[0], px[1], px[2]});
|
||||
}
|
||||
|
||||
// The image with rows and columns swapped, so vertical sorting is horizontal
|
||||
// sorting of the transpose.
|
||||
Image transposed(const Image& src) {
|
||||
Image out(src.height(), src.width());
|
||||
parallelFor(static_cast<std::size_t>(src.width()), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t x = begin; x < end; ++x) {
|
||||
std::uint8_t* row = out.row(static_cast<int>(x));
|
||||
for (int y = 0; y < src.height(); ++y) {
|
||||
std::memcpy(row + static_cast<std::size_t>(y) * 3, src.row(y) + x * 3, 3);
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
struct LinePlan {
|
||||
bool sorted = false;
|
||||
std::uint64_t seed = 0; // for the run lengths, when they are limited
|
||||
@@ -99,49 +67,19 @@ Image PixelSortEffect::render(const std::vector<Image>& sources, int width, int
|
||||
if (maxRun > 0) line.seed = rng.next();
|
||||
}
|
||||
|
||||
auto keyOf = [&](const std::uint8_t* px) {
|
||||
if (keyName == "hue") return hue(px);
|
||||
if (keyName == "saturation") return saturation(px);
|
||||
return luma(px);
|
||||
};
|
||||
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);
|
||||
std::uint8_t* row = canvas.row(static_cast<int>(line));
|
||||
|
||||
auto flush = [&](int start) {
|
||||
// A stable sort, so equal keys keep their order on every
|
||||
// standard library.
|
||||
std::stable_sort(run.begin(), run.end(), [&](const auto& a, const auto& b) {
|
||||
return reverse ? a.first > b.first : a.first < b.first;
|
||||
});
|
||||
for (std::size_t i = 0; i < run.size(); ++i) {
|
||||
std::memcpy(row + (static_cast<std::size_t>(start) + i) * 3, run[i].second.data(), 3);
|
||||
}
|
||||
run.clear();
|
||||
};
|
||||
|
||||
int start = 0;
|
||||
int limit = maxRun > 0 ? static_cast<int>(runRng.inRange(1, maxRun)) : span;
|
||||
for (int x = 0; x < span; ++x) {
|
||||
const std::uint8_t* px = row + static_cast<std::size_t>(x) * 3;
|
||||
const int brightness = luma(px);
|
||||
if (brightness < low || brightness > high) {
|
||||
flush(start);
|
||||
start = x + 1;
|
||||
continue;
|
||||
}
|
||||
if (static_cast<int>(run.size()) >= limit) {
|
||||
flush(start);
|
||||
start = x;
|
||||
if (maxRun > 0) limit = static_cast<int>(runRng.inRange(1, maxRun));
|
||||
}
|
||||
run.push_back({keyOf(px), {px[0], px[1], px[2]}});
|
||||
}
|
||||
flush(start);
|
||||
sortLine(canvas.row(static_cast<int>(line)), span, rule, runRng, run);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/image.hpp"
|
||||
#include "core/parallel.hpp"
|
||||
#include "core/rng.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// What orders the pixels inside a run. Shared by the pixel sort and the sort
|
||||
// sweep effects.
|
||||
enum class SortKey { Luma, Hue, Saturation };
|
||||
|
||||
inline SortKey sortKeyNamed(const std::string& name) {
|
||||
if (name == "hue") return SortKey::Hue;
|
||||
if (name == "saturation") return SortKey::Saturation;
|
||||
return SortKey::Luma;
|
||||
}
|
||||
|
||||
inline int sortLuma(const std::uint8_t* px) { return (px[0] * 54 + px[1] * 183 + px[2] * 19) >> 8; }
|
||||
|
||||
// Hue on a 0..1535 scale (six sectors of 256), in integers.
|
||||
inline int sortHue(const std::uint8_t* px) {
|
||||
const int r = px[0], g = px[1], b = px[2];
|
||||
const int hi = std::max({r, g, b});
|
||||
const int lo = std::min({r, g, b});
|
||||
const int range = hi - lo;
|
||||
if (range == 0) return 0;
|
||||
if (hi == r) return g >= b ? (g - b) * 255 / range : 1535 - (b - g) * 255 / range;
|
||||
if (hi == g) return 512 + (b - r) * 255 / range;
|
||||
return 1024 + (r - g) * 255 / range;
|
||||
}
|
||||
|
||||
inline int sortSaturation(const std::uint8_t* px) {
|
||||
return std::max({px[0], px[1], px[2]}) - std::min({px[0], px[1], px[2]});
|
||||
}
|
||||
|
||||
inline int sortKeyOf(SortKey key, const std::uint8_t* px) {
|
||||
switch (key) {
|
||||
case SortKey::Hue: return sortHue(px);
|
||||
case SortKey::Saturation: return sortSaturation(px);
|
||||
case SortKey::Luma: break;
|
||||
}
|
||||
return sortLuma(px);
|
||||
}
|
||||
|
||||
// The image with rows and columns swapped, so vertical sorting is horizontal
|
||||
// sorting of the transpose.
|
||||
inline Image transposed(const Image& src) {
|
||||
Image out(src.height(), src.width());
|
||||
parallelFor(static_cast<std::size_t>(src.width()), [&](std::size_t begin, std::size_t end) {
|
||||
for (std::size_t x = begin; x < end; ++x) {
|
||||
std::uint8_t* row = out.row(static_cast<int>(x));
|
||||
for (int y = 0; y < src.height(); ++y) {
|
||||
std::memcpy(row + static_cast<std::size_t>(y) * 3, src.row(y) + x * 3, 3);
|
||||
}
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// How one line is sorted: the brightness a pixel must lie within (0..255) to
|
||||
// join a run, what orders a run, and the longest a run may grow (0 for no
|
||||
// limit), its lengths drawn from `runRng`.
|
||||
struct SortRule {
|
||||
int low = 0;
|
||||
int high = 255;
|
||||
SortKey key = SortKey::Luma;
|
||||
bool reverse = false;
|
||||
int maxRun = 0;
|
||||
};
|
||||
|
||||
// Sorts every run of one line of `span` pixels in place. `run` is scratch
|
||||
// space, reused from line to line.
|
||||
inline void sortLine(std::uint8_t* row, int span, const SortRule& rule, Rng& runRng,
|
||||
std::vector<std::pair<int, std::array<std::uint8_t, 3>>>& run) {
|
||||
auto flush = [&](int start) {
|
||||
// A stable sort, so equal keys keep their order on every standard
|
||||
// library.
|
||||
std::stable_sort(run.begin(), run.end(), [&](const auto& a, const auto& b) {
|
||||
return rule.reverse ? a.first > b.first : a.first < b.first;
|
||||
});
|
||||
for (std::size_t i = 0; i < run.size(); ++i) {
|
||||
std::memcpy(row + (static_cast<std::size_t>(start) + i) * 3, run[i].second.data(), 3);
|
||||
}
|
||||
run.clear();
|
||||
};
|
||||
|
||||
int start = 0;
|
||||
int limit = rule.maxRun > 0 ? static_cast<int>(runRng.inRange(1, rule.maxRun)) : span;
|
||||
for (int x = 0; x < span; ++x) {
|
||||
const std::uint8_t* px = row + static_cast<std::size_t>(x) * 3;
|
||||
const int brightness = sortLuma(px);
|
||||
if (brightness < rule.low || brightness > rule.high) {
|
||||
flush(start);
|
||||
start = x + 1;
|
||||
continue;
|
||||
}
|
||||
if (static_cast<int>(run.size()) >= limit) {
|
||||
flush(start);
|
||||
start = x;
|
||||
if (rule.maxRun > 0) limit = static_cast<int>(runRng.inRange(1, rule.maxRun));
|
||||
}
|
||||
run.push_back({sortKeyOf(rule.key, px), {px[0], px[1], px[2]}});
|
||||
}
|
||||
flush(start);
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "effects/sweep.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numbers>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
#include "effects/sampling.hpp"
|
||||
#include "effects/sort_runs.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
const std::vector<ParamSpec>& SweepEffect::params() const {
|
||||
static const std::vector<ParamSpec> specs = {
|
||||
ParamSpec::makeDouble("sweep_from", "From", 0.1, 0.0, 1.0,
|
||||
"Brightness the middle of the window starts the sweep at."),
|
||||
ParamSpec::makeDouble("sweep_to", "To", 0.9, 0.0, 1.0,
|
||||
"Brightness the middle of the window turns back at."),
|
||||
ParamSpec::makeDouble("sweep_width", "Window", 0.35, 0.0, 1.0,
|
||||
"How wide a band of brightness is sorted at once; pixels outside "
|
||||
"it break a run and stay where they are."),
|
||||
ParamSpec::makeDouble("sweep_speed", "Speed", 0.25, 0.0, 10.0,
|
||||
"Sweeps there and back per second, rounded so that the clip loops "
|
||||
"without a jump; 0 holds still.",
|
||||
0.05, 2),
|
||||
ParamSpec::makeChoice("sweep_key", "Sort by", "luma", {"luma", "hue", "saturation"},
|
||||
"What orders the pixels inside a run."),
|
||||
ParamSpec::makeChoice("sweep_direction", "Direction", "horizontal",
|
||||
{"horizontal", "vertical"}, "Whether rows or columns are sorted."),
|
||||
ParamSpec::makeBool("sweep_reverse", "Reverse", false,
|
||||
"Sort from high to low instead of from low to high."),
|
||||
ParamSpec::makeDouble("sweep_line_chance", "Line chance", 1.0, 0.0, 1.0,
|
||||
"Chance of each row (or column) being sorted at all."),
|
||||
ParamSpec::makeInt("sweep_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 Settings {
|
||||
double from;
|
||||
double to;
|
||||
double width;
|
||||
double speed;
|
||||
SortKey key;
|
||||
bool vertical;
|
||||
bool reverse;
|
||||
double lineChance;
|
||||
int maxRun;
|
||||
|
||||
explicit Settings(const ParamValues& values)
|
||||
: from(std::clamp(paramDouble(values, "sweep_from", 0.1), 0.0, 1.0)),
|
||||
to(std::clamp(paramDouble(values, "sweep_to", 0.9), 0.0, 1.0)),
|
||||
width(std::clamp(paramDouble(values, "sweep_width", 0.35), 0.0, 1.0)),
|
||||
speed(std::max(0.0, paramDouble(values, "sweep_speed", 0.25))),
|
||||
key(sortKeyNamed(paramChoice(values, "sweep_key", "luma"))),
|
||||
vertical(paramChoice(values, "sweep_direction", "horizontal") == "vertical"),
|
||||
reverse(paramBool(values, "sweep_reverse", false)),
|
||||
lineChance(std::clamp(paramDouble(values, "sweep_line_chance", 1.0), 0.0, 1.0)),
|
||||
maxRun(static_cast<int>(std::max(0L, paramInt(values, "sweep_max_run", 0)))) {}
|
||||
};
|
||||
|
||||
struct LinePlan {
|
||||
bool sorted = false;
|
||||
std::uint64_t seed = 0; // for the run lengths, the same on every frame
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Image SweepEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
|
||||
const Settings settings(values);
|
||||
Image canvas = settings.vertical ? transposed(input) : std::move(input);
|
||||
const int lines = canvas.height();
|
||||
const int span = canvas.width();
|
||||
|
||||
// What stays put for the whole clip: which lines are sorted, and where
|
||||
// their runs are cut.
|
||||
Rng steady = at.steady();
|
||||
std::vector<LinePlan> plan(static_cast<std::size_t>(lines));
|
||||
for (auto& line : plan) {
|
||||
line.sorted = chance(steady.next(), settings.lineChance);
|
||||
line.seed = steady.next();
|
||||
}
|
||||
|
||||
// Where this frame is in time: the middle of the window eases from
|
||||
// `from` to `to` and back once a cycle.
|
||||
const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0;
|
||||
const double ease = (1.0 - std::cos(2.0 * std::numbers::pi * t)) * 0.5;
|
||||
const double middle = settings.from + (settings.to - settings.from) * ease;
|
||||
SortRule rule;
|
||||
rule.low = static_cast<int>(std::lround((middle - settings.width * 0.5) * 255.0));
|
||||
rule.high = static_cast<int>(std::lround((middle + settings.width * 0.5) * 255.0));
|
||||
rule.key = settings.key;
|
||||
rule.reverse = settings.reverse;
|
||||
rule.maxRun = settings.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 settings.vertical ? transposed(canvas) : canvas;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "effects/video_effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// The pixel sort in motion: only pixels inside a window of brightness are
|
||||
// sorted, and the window sweeps from the dark end to the bright end and back,
|
||||
// so the sorted streaks spread over one part of the picture after another,
|
||||
// melt away and return.
|
||||
//
|
||||
// Which lines are sorted, and how their runs are cut, is drawn from
|
||||
// at.steady() and stays put; the window alone moves, there and back once a
|
||||
// cycle, so the clip loops without a jump.
|
||||
class SweepEffect : public VideoEffect {
|
||||
public:
|
||||
std::string name() const override { return "sweep"; }
|
||||
std::string title() const override { return "Sort sweep"; }
|
||||
std::string description() const override {
|
||||
return "Sorts runs of pixels as Pixel sort does, only within a window of brightness that "
|
||||
"sweeps from dark to bright and back.";
|
||||
}
|
||||
const std::vector<ParamSpec>& params() const override;
|
||||
|
||||
protected:
|
||||
Image animate(Image input, const ParamValues& values, const Moment& at) const override;
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user