#include "effects/flow.hpp" #include #include #include "core/parallel.hpp" #include "effects/noise_field.hpp" #include "effects/sampling.hpp" namespace effector { const std::vector& FlowEffect::params() const { static const std::vector 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(std::max(0L, paramInt(values, "flow_strength", 30)))), scale(static_cast(std::max(4L, paramInt(values, "flow_scale", 160)))), octaves(static_cast(std::clamp(paramInt(values, "flow_octaves", 3), 1L, 6L))), swirl(paramChoice(values, "flow_mode", "displace") == "swirl"), travel(static_cast(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(reach) + 2; Rng steady = at.steady(); const WarpField field(steady, width + 2 * margin, height + 2 * margin, settings.scale, settings.octaves, settings.swirl); std::vector orbits(static_cast(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 firstShifts(static_cast(settings.octaves)); std::vector secondShifts(static_cast(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(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(i)]; shift.x = margin + radius * cosine; shift.y = margin + radius * sine; } radius *= 0.5; } Image frame(width, height); parallelFor(static_cast(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(row)); const double y = static_cast(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(col) * 3); } } }); return frame; } } // namespace effector