Warp's noise field moves to effects/noise_field.hpp so both share it; Warp renders exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
5.0 KiB
C++
119 lines
5.0 KiB
C++
#include "effects/flow.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <vector>
|
|
|
|
#include "core/parallel.hpp"
|
|
#include "effects/noise_field.hpp"
|
|
#include "effects/sampling.hpp"
|
|
|
|
namespace effector {
|
|
|
|
const std::vector<ParamSpec>& FlowEffect::params() const {
|
|
static const std::vector<ParamSpec> specs = {
|
|
ParamSpec::makeInt("flow_strength", "Strength", 30, 0, 4096,
|
|
"How far in pixels the field pushes the image at its strongest."),
|
|
ParamSpec::makeInt("flow_scale", "Scale", 160, 4, 8192,
|
|
"Size in pixels of the largest features in the field."),
|
|
ParamSpec::makeInt("flow_octaves", "Octaves", 3, 1, 6,
|
|
"Finer layers of noise laid over the first; more give a turbulent "
|
|
"edge to the flow."),
|
|
ParamSpec::makeChoice("flow_mode", "Mode", "displace", {"displace", "swirl"},
|
|
"displace pushes along the field; swirl flows along its contour "
|
|
"lines, which curls the image into eddies."),
|
|
ParamSpec::makeInt("flow_travel", "Travel", 80, 0, 4096,
|
|
"Across how many pixels the field drifts in each cycle; the finer "
|
|
"layers drift half as far and turn faster."),
|
|
ParamSpec::makeDouble("flow_speed", "Speed", 0.25, 0.0, 10.0,
|
|
"Cycles per second, rounded so that the clip loops without a jump; "
|
|
"0 holds still.",
|
|
0.05, 2),
|
|
};
|
|
return specs;
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct Settings {
|
|
double strength;
|
|
double scale;
|
|
int octaves;
|
|
bool swirl;
|
|
int travel;
|
|
double speed;
|
|
|
|
explicit Settings(const ParamValues& values)
|
|
: strength(static_cast<double>(std::max(0L, paramInt(values, "flow_strength", 30)))),
|
|
scale(static_cast<double>(std::max(4L, paramInt(values, "flow_scale", 160)))),
|
|
octaves(static_cast<int>(std::clamp(paramInt(values, "flow_octaves", 3), 1L, 6L))),
|
|
swirl(paramChoice(values, "flow_mode", "displace") == "swirl"),
|
|
travel(static_cast<int>(std::max(0L, paramInt(values, "flow_travel", 80)))),
|
|
speed(std::max(0.0, paramDouble(values, "flow_speed", 0.25))) {}
|
|
};
|
|
|
|
// Which way round, and from where, one layer of a field drifts.
|
|
struct Orbit {
|
|
double start; // degrees
|
|
double turn; // +1 or -1
|
|
};
|
|
|
|
} // namespace
|
|
|
|
Image FlowEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
|
|
const Settings settings(values);
|
|
const int width = input.width();
|
|
const int height = input.height();
|
|
if (settings.strength <= 0.0) return input;
|
|
|
|
// What stays put for the whole clip: the field, drawn a circle's radius
|
|
// larger on every side than the picture, so it can drift without running
|
|
// out, and where each of its layers starts its circle, and which way.
|
|
const double reach = settings.travel * 0.5;
|
|
const int margin = static_cast<int>(reach) + 2;
|
|
Rng steady = at.steady();
|
|
const WarpField field(steady, width + 2 * margin, height + 2 * margin, settings.scale,
|
|
settings.octaves, settings.swirl);
|
|
std::vector<Orbit> orbits(static_cast<std::size_t>(settings.octaves) * 2);
|
|
for (Orbit& orbit : orbits) {
|
|
orbit.start = unitDouble(steady.next()) * 360.0;
|
|
orbit.turn = (steady.next() >> 63) != 0 ? 1.0 : -1.0;
|
|
}
|
|
|
|
// Where this frame is in time: layer i has gone i + 1 times round a
|
|
// circle half as wide as the one before in a whole cycle.
|
|
const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0;
|
|
std::vector<NoiseShift> firstShifts(static_cast<std::size_t>(settings.octaves));
|
|
std::vector<NoiseShift> secondShifts(static_cast<std::size_t>(settings.octaves));
|
|
double radius = reach;
|
|
for (int i = 0; i < settings.octaves; ++i) {
|
|
for (int f = 0; f < 2; ++f) {
|
|
const Orbit& orbit = orbits[static_cast<std::size_t>(i * 2 + f)];
|
|
double sine = 0.0;
|
|
double cosine = 1.0;
|
|
sinCosDegrees(orbit.start + orbit.turn * (i + 1) * 360.0 * t, sine, cosine);
|
|
NoiseShift& shift = (f == 0 ? firstShifts : secondShifts)[static_cast<std::size_t>(i)];
|
|
shift.x = margin + radius * cosine;
|
|
shift.y = margin + radius * sine;
|
|
}
|
|
radius *= 0.5;
|
|
}
|
|
|
|
Image frame(width, height);
|
|
parallelFor(static_cast<std::size_t>(height), [&](std::size_t begin, std::size_t end) {
|
|
for (std::size_t row = begin; row < end; ++row) {
|
|
std::uint8_t* out = frame.row(static_cast<int>(row));
|
|
const double y = static_cast<double>(row);
|
|
for (int col = 0; col < width; ++col) {
|
|
const double x = col;
|
|
double dx = 0.0;
|
|
double dy = 0.0;
|
|
field.push(x, y, settings.strength, dx, dy, &firstShifts, &secondShifts);
|
|
sampleBilinear(input, x + dx, y + dy, out + static_cast<std::size_t>(col) * 3);
|
|
}
|
|
}
|
|
});
|
|
return frame;
|
|
}
|
|
|
|
} // namespace effector
|