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:
@@ -21,6 +21,11 @@ commit that made it. The interface shows this file under Changelog in its menu.
|
|||||||
of noise as Warp does, with the field drifting so the picture ripples like
|
of noise as Warp does, with the field drifting so the picture ripples like
|
||||||
heat haze or water. On the command line it is `-e flow` with
|
heat haze or water. On the command line it is `-e flow` with
|
||||||
`--flow-strength`, `--flow-travel`, `--flow-speed` and the rest.
|
`--flow-strength`, `--flow-travel`, `--flow-speed` and the rest.
|
||||||
|
- Sort sweep, a video effect: runs of pixels sorted as Pixel sort sorts
|
||||||
|
them, only within a window of brightness that sweeps from dark to bright
|
||||||
|
and back, so the streaks spread over one part of the picture after
|
||||||
|
another. On the command line it is `-e sweep` with `--sweep-from`,
|
||||||
|
`--sweep-to`, `--sweep-width`, `--sweep-speed` and the rest.
|
||||||
- A Quality choice under Video on Setup: Small file, Standard or High, for
|
- A Quality choice under Video on Setup: Small file, Standard or High, for
|
||||||
how the saved video is encoded. It is kept in the project, and on the
|
how the saved video is encoded. It is kept in the project, and on the
|
||||||
command line it is `--video-quality small|standard|high`. Standard writes
|
command line it is `--video-quality small|standard|high`. Standard writes
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ add_library(effector_core STATIC
|
|||||||
src/effects/scanlines.cpp
|
src/effects/scanlines.cpp
|
||||||
src/effects/shatter.cpp
|
src/effects/shatter.cpp
|
||||||
src/effects/slitshift.cpp
|
src/effects/slitshift.cpp
|
||||||
|
src/effects/sweep.cpp
|
||||||
src/effects/text.cpp
|
src/effects/text.cpp
|
||||||
src/effects/tiles.cpp
|
src/effects/tiles.cpp
|
||||||
src/effects/tileswap.cpp
|
src/effects/tileswap.cpp
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "effects/scanlines.hpp"
|
#include "effects/scanlines.hpp"
|
||||||
#include "effects/shatter.hpp"
|
#include "effects/shatter.hpp"
|
||||||
#include "effects/slitshift.hpp"
|
#include "effects/slitshift.hpp"
|
||||||
|
#include "effects/sweep.hpp"
|
||||||
#include "effects/text.hpp"
|
#include "effects/text.hpp"
|
||||||
#include "effects/tiles.hpp"
|
#include "effects/tiles.hpp"
|
||||||
#include "effects/tileswap.hpp"
|
#include "effects/tileswap.hpp"
|
||||||
@@ -45,6 +46,7 @@ const std::vector<EffectPtr>& effects() {
|
|||||||
std::make_shared<TileSwapEffect>(),
|
std::make_shared<TileSwapEffect>(),
|
||||||
std::make_shared<TunnelEffect>(),
|
std::make_shared<TunnelEffect>(),
|
||||||
std::make_shared<FlowEffect>(),
|
std::make_shared<FlowEffect>(),
|
||||||
|
std::make_shared<SweepEffect>(),
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include "core/parallel.hpp"
|
#include "core/parallel.hpp"
|
||||||
#include "effects/sampling.hpp"
|
#include "effects/sampling.hpp"
|
||||||
|
#include "effects/sort_runs.hpp"
|
||||||
#include "effects/source_fill.hpp"
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
namespace effector {
|
namespace effector {
|
||||||
@@ -34,39 +35,6 @@ const std::vector<ParamSpec>& PixelSortEffect::params() const {
|
|||||||
|
|
||||||
namespace {
|
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 {
|
struct LinePlan {
|
||||||
bool sorted = false;
|
bool sorted = false;
|
||||||
std::uint64_t seed = 0; // for the run lengths, when they are limited
|
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();
|
if (maxRun > 0) line.seed = rng.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto keyOf = [&](const std::uint8_t* px) {
|
SortRule rule;
|
||||||
if (keyName == "hue") return hue(px);
|
rule.low = low;
|
||||||
if (keyName == "saturation") return saturation(px);
|
rule.high = high;
|
||||||
return luma(px);
|
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) {
|
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;
|
std::vector<std::pair<int, std::array<std::uint8_t, 3>>> run;
|
||||||
for (std::size_t line = begin; line < end; ++line) {
|
for (std::size_t line = begin; line < end; ++line) {
|
||||||
if (!plan[line].sorted) continue;
|
if (!plan[line].sorted) continue;
|
||||||
Rng runRng(plan[line].seed);
|
Rng runRng(plan[line].seed);
|
||||||
std::uint8_t* row = canvas.row(static_cast<int>(line));
|
sortLine(canvas.row(static_cast<int>(line)), span, rule, runRng, 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 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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -2108,6 +2108,8 @@ int main() {
|
|||||||
const std::vector<Moving> moving = {
|
const std::vector<Moving> moving = {
|
||||||
{"tunnel", {{"tunnel_drift", 6L}, {"tunnel_fade", 1.0}}, {}, {{"tunnel_fade", 0.0}}},
|
{"tunnel", {{"tunnel_drift", 6L}, {"tunnel_fade", 1.0}}, {}, {{"tunnel_fade", 0.0}}},
|
||||||
{"flow", {{"flow_mode", std::string("swirl")}}, {}, {{"flow_strength", 0L}}},
|
{"flow", {{"flow_mode", std::string("swirl")}}, {}, {{"flow_strength", 0L}}},
|
||||||
|
{"sweep", {{"sweep_reverse", true}, {"sweep_max_run", 20L}}, {},
|
||||||
|
{{"sweep_line_chance", 0.0}}},
|
||||||
};
|
};
|
||||||
for (const auto& [name, lively, smooth, off] : moving) {
|
for (const auto& [name, lively, smooth, off] : moving) {
|
||||||
const auto effect = effectByName(name);
|
const auto effect = effectByName(name);
|
||||||
|
|||||||
Reference in New Issue
Block a user