Add Focus pull, a video effect that pulls Blur in and out of focus
Blur's box blur moves to effects/box_blur.hpp so both share it; Blur renders exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,11 @@ commit that made it. The interface shows this file under Changelog in its menu.
|
|||||||
through its image, or sliding sideways, while the strips stay put. On the
|
through its image, or sliding sideways, while the strips stay put. On the
|
||||||
command line it is `-e striproll` with `--roll-speed`, `--roll-axis`,
|
command line it is `-e striproll` with `--roll-speed`, `--roll-axis`,
|
||||||
`--roll-direction` and the rest.
|
`--roll-direction` and the rest.
|
||||||
|
- Focus pull, a video effect: a lens being focused, either the whole
|
||||||
|
picture blurring as Blur blurs it and coming back sharp, or a band of
|
||||||
|
sharpness moving down the picture and back up. On the command line it is
|
||||||
|
`-e focuspull` with `--focus-mode`, `--focus-radius`, `--focus-band` and
|
||||||
|
`--focus-speed`.
|
||||||
- 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
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ add_library(effector_core STATIC
|
|||||||
src/effects/datamosh.cpp
|
src/effects/datamosh.cpp
|
||||||
src/effects/feedback.cpp
|
src/effects/feedback.cpp
|
||||||
src/effects/filmgate.cpp
|
src/effects/filmgate.cpp
|
||||||
|
src/effects/focuspull.cpp
|
||||||
src/effects/flow.cpp
|
src/effects/flow.cpp
|
||||||
src/effects/grain.cpp
|
src/effects/grain.cpp
|
||||||
src/effects/monochrome.cpp
|
src/effects/monochrome.cpp
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "effects/datamosh.hpp"
|
#include "effects/datamosh.hpp"
|
||||||
#include "effects/feedback.hpp"
|
#include "effects/feedback.hpp"
|
||||||
#include "effects/filmgate.hpp"
|
#include "effects/filmgate.hpp"
|
||||||
|
#include "effects/focuspull.hpp"
|
||||||
#include "effects/flow.hpp"
|
#include "effects/flow.hpp"
|
||||||
#include "effects/grain.hpp"
|
#include "effects/grain.hpp"
|
||||||
#include "effects/monochrome.hpp"
|
#include "effects/monochrome.hpp"
|
||||||
@@ -53,6 +54,7 @@ const std::vector<EffectPtr>& effects() {
|
|||||||
std::make_shared<FilmGateEffect>(),
|
std::make_shared<FilmGateEffect>(),
|
||||||
std::make_shared<DatamoshEffect>(),
|
std::make_shared<DatamoshEffect>(),
|
||||||
std::make_shared<StripRollEffect>(),
|
std::make_shared<StripRollEffect>(),
|
||||||
|
std::make_shared<FocusPullEffect>(),
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-64
@@ -1,10 +1,9 @@
|
|||||||
#include "effects/blur.hpp"
|
#include "effects/blur.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstring>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/parallel.hpp"
|
#include "effects/box_blur.hpp"
|
||||||
#include "effects/source_fill.hpp"
|
#include "effects/source_fill.hpp"
|
||||||
|
|
||||||
namespace effector {
|
namespace effector {
|
||||||
@@ -23,57 +22,6 @@ const std::vector<ParamSpec>& BlurEffect::params() const {
|
|||||||
return specs;
|
return specs;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// One separable box blur pass. `stride` is the distance in bytes between two
|
|
||||||
// neighbouring samples along the blurred axis, `count` how many of them there
|
|
||||||
// are; the window is clamped at both ends.
|
|
||||||
void boxPass(const std::uint8_t* in, std::uint8_t* out, int count, std::size_t stride, int radius) {
|
|
||||||
const int window = 2 * radius + 1;
|
|
||||||
int sum[3] = {0, 0, 0};
|
|
||||||
|
|
||||||
for (int c = 0; c < 3; ++c) {
|
|
||||||
sum[c] = in[c] * (radius + 1);
|
|
||||||
for (int i = 1; i <= radius; ++i) {
|
|
||||||
sum[c] += in[static_cast<std::size_t>(std::min(i, count - 1)) * stride + c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
for (int c = 0; c < 3; ++c) {
|
|
||||||
out[static_cast<std::size_t>(i) * stride + c] =
|
|
||||||
static_cast<std::uint8_t>(sum[c] / window);
|
|
||||||
}
|
|
||||||
const int add = std::min(i + radius + 1, count - 1);
|
|
||||||
const int drop = std::max(i - radius, 0);
|
|
||||||
for (int c = 0; c < 3; ++c) {
|
|
||||||
sum[c] += in[static_cast<std::size_t>(add) * stride + c] -
|
|
||||||
in[static_cast<std::size_t>(drop) * stride + c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void blurHorizontal(const Image& src, Image& dst, int radius) {
|
|
||||||
parallelFor(static_cast<std::size_t>(src.height()), [&](std::size_t begin, std::size_t end) {
|
|
||||||
for (std::size_t y = begin; y < end; ++y) {
|
|
||||||
boxPass(src.row(static_cast<int>(y)), dst.row(static_cast<int>(y)), src.width(), 3,
|
|
||||||
radius);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void blurVertical(const Image& src, Image& dst, int radius) {
|
|
||||||
const std::size_t stride = src.stride();
|
|
||||||
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) {
|
|
||||||
const std::size_t offset = x * 3;
|
|
||||||
boxPass(src.data() + offset, dst.data() + offset, src.height(), stride, radius);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
Image BlurEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
Image BlurEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||||
const ParamValues& values) const {
|
const ParamValues& values) const {
|
||||||
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
||||||
@@ -94,17 +42,7 @@ Image BlurEffect::render(const std::vector<Image>& sources, int width, int heigh
|
|||||||
fillFromSource(canvas, src, sx, sy);
|
fillFromSource(canvas, src, sx, sy);
|
||||||
}
|
}
|
||||||
|
|
||||||
Image scratch(width, height);
|
boxBlur(canvas, radius, passes, direction);
|
||||||
for (int pass = 0; pass < passes; ++pass) {
|
|
||||||
if (direction != "vertical") {
|
|
||||||
blurHorizontal(canvas, scratch, std::min(radius, std::max(1, width - 1)));
|
|
||||||
std::swap(canvas, scratch);
|
|
||||||
}
|
|
||||||
if (direction != "horizontal") {
|
|
||||||
blurVertical(canvas, scratch, std::min(radius, std::max(1, height - 1)));
|
|
||||||
std::swap(canvas, scratch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/image.hpp"
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// One separable box blur pass. `stride` is the distance in bytes between two
|
||||||
|
// neighbouring samples along the blurred axis, `count` how many of them there
|
||||||
|
// are; the window is clamped at both ends.
|
||||||
|
inline void boxPass(const std::uint8_t* in, std::uint8_t* out, int count, std::size_t stride,
|
||||||
|
int radius) {
|
||||||
|
const int window = 2 * radius + 1;
|
||||||
|
int sum[3] = {0, 0, 0};
|
||||||
|
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
sum[c] = in[c] * (radius + 1);
|
||||||
|
for (int i = 1; i <= radius; ++i) {
|
||||||
|
sum[c] += in[static_cast<std::size_t>(std::min(i, count - 1)) * stride + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
out[static_cast<std::size_t>(i) * stride + c] =
|
||||||
|
static_cast<std::uint8_t>(sum[c] / window);
|
||||||
|
}
|
||||||
|
const int add = std::min(i + radius + 1, count - 1);
|
||||||
|
const int drop = std::max(i - radius, 0);
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
sum[c] += in[static_cast<std::size_t>(add) * stride + c] -
|
||||||
|
in[static_cast<std::size_t>(drop) * stride + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void blurHorizontal(const Image& src, Image& dst, int radius) {
|
||||||
|
parallelFor(static_cast<std::size_t>(src.height()), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t y = begin; y < end; ++y) {
|
||||||
|
boxPass(src.row(static_cast<int>(y)), dst.row(static_cast<int>(y)), src.width(), 3,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void blurVertical(const Image& src, Image& dst, int radius) {
|
||||||
|
const std::size_t stride = src.stride();
|
||||||
|
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) {
|
||||||
|
const std::size_t offset = x * 3;
|
||||||
|
boxPass(src.data() + offset, dst.data() + offset, src.height(), stride, radius);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blurs `canvas` in place: `passes` box blurs of `radius` along the axes
|
||||||
|
// `direction` names ("both", "horizontal" or "vertical"). Shared by the blur
|
||||||
|
// and the focus pull effects.
|
||||||
|
inline void boxBlur(Image& canvas, int radius, int passes, const std::string& direction) {
|
||||||
|
const int width = canvas.width();
|
||||||
|
const int height = canvas.height();
|
||||||
|
Image scratch(width, height);
|
||||||
|
for (int pass = 0; pass < passes; ++pass) {
|
||||||
|
if (direction != "vertical") {
|
||||||
|
blurHorizontal(canvas, scratch, std::min(radius, std::max(1, width - 1)));
|
||||||
|
std::swap(canvas, scratch);
|
||||||
|
}
|
||||||
|
if (direction != "horizontal") {
|
||||||
|
blurVertical(canvas, scratch, std::min(radius, std::max(1, height - 1)));
|
||||||
|
std::swap(canvas, scratch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#include "effects/focuspull.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <numbers>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/parallel.hpp"
|
||||||
|
#include "effects/box_blur.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
const std::vector<ParamSpec>& FocusPullEffect::params() const {
|
||||||
|
static const std::vector<ParamSpec> specs = {
|
||||||
|
ParamSpec::makeChoice("focus_mode", "Mode", "breathe", {"breathe", "band"},
|
||||||
|
"breathe pulls the whole picture out of focus and back; band "
|
||||||
|
"moves a band of sharpness down the picture and back up."),
|
||||||
|
ParamSpec::makeInt("focus_radius", "Radius", 12, 0, 256,
|
||||||
|
"Half width in pixels of the blur at its strongest."),
|
||||||
|
ParamSpec::makeDouble("focus_band", "Band", 0.3, 0.05, 1.0,
|
||||||
|
"Height of the sharp band as a share of the picture, with as much "
|
||||||
|
"again fading into the blur; band mode only."),
|
||||||
|
ParamSpec::makeDouble("focus_speed", "Speed", 0.25, 0.0, 10.0,
|
||||||
|
"Pulls there and back per second, rounded so that the clip loops "
|
||||||
|
"without a jump; 0 holds still.",
|
||||||
|
0.05, 2),
|
||||||
|
};
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Two passes of a box blur come close enough to a lens.
|
||||||
|
constexpr int kPasses = 2;
|
||||||
|
|
||||||
|
struct Settings {
|
||||||
|
bool band;
|
||||||
|
int radius;
|
||||||
|
double bandShare;
|
||||||
|
double speed;
|
||||||
|
|
||||||
|
explicit Settings(const ParamValues& values)
|
||||||
|
: band(paramChoice(values, "focus_mode", "breathe") == "band"),
|
||||||
|
radius(static_cast<int>(std::max(0L, paramInt(values, "focus_radius", 12)))),
|
||||||
|
bandShare(std::clamp(paramDouble(values, "focus_band", 0.3), 0.05, 1.0)),
|
||||||
|
speed(std::max(0.0, paramDouble(values, "focus_speed", 0.25))) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Image blurred(const Image& input, int radius) {
|
||||||
|
Image out = input;
|
||||||
|
if (radius > 0) boxBlur(out, radius, kPasses, "both");
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lays `over` on `under` in place, row y at `mix(y)` out of 256.
|
||||||
|
template <typename Mix>
|
||||||
|
void blendRows(Image& under, const Image& over, Mix mix) {
|
||||||
|
const std::size_t bytes = static_cast<std::size_t>(under.width()) * 3;
|
||||||
|
parallelFor(static_cast<std::size_t>(under.height()), [&](std::size_t begin, std::size_t end) {
|
||||||
|
for (std::size_t y = begin; y < end; ++y) {
|
||||||
|
const int weight = mix(static_cast<int>(y));
|
||||||
|
if (weight <= 0) continue;
|
||||||
|
std::uint8_t* out = under.row(static_cast<int>(y));
|
||||||
|
const std::uint8_t* in = over.row(static_cast<int>(y));
|
||||||
|
for (std::size_t i = 0; i < bytes; ++i) {
|
||||||
|
out[i] = static_cast<std::uint8_t>(out[i] + (in[i] - out[i]) * weight / 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Image FocusPullEffect::animate(Image input, const ParamValues& values, const Moment& at) const {
|
||||||
|
const Settings settings(values);
|
||||||
|
if (settings.radius <= 0) return input;
|
||||||
|
const int height = input.height();
|
||||||
|
|
||||||
|
// Where this frame is in time: how far the pull has gone, easing out from
|
||||||
|
// the start and back to it once a cycle.
|
||||||
|
const double t = settings.speed > 0.0 ? at.cycle(settings.speed) : 0.0;
|
||||||
|
const double pull = (1.0 - std::cos(2.0 * std::numbers::pi * t)) * 0.5;
|
||||||
|
|
||||||
|
if (!settings.band) {
|
||||||
|
// A radius between two whole ones is the two blurs mixed, so the
|
||||||
|
// focus drifts smoothly rather than in steps.
|
||||||
|
const double radius = settings.radius * pull;
|
||||||
|
const int lower = static_cast<int>(radius);
|
||||||
|
const int mix = static_cast<int>(std::lround((radius - lower) * 256.0));
|
||||||
|
Image frame = blurred(input, lower);
|
||||||
|
if (mix > 0) blendRows(frame, blurred(input, lower + 1), [mix](int) { return mix; });
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The band's middle goes from the top to the bottom and back; the blur
|
||||||
|
// grows over as much again as the band is high on either side of it.
|
||||||
|
const double middle = pull * (height - 1);
|
||||||
|
const double half = settings.bandShare * height * 0.5;
|
||||||
|
const double fade = std::max(1.0, settings.bandShare * height);
|
||||||
|
const Image soft = blurred(input, settings.radius);
|
||||||
|
blendRows(input, soft, [&](int y) {
|
||||||
|
const double away = std::abs(y - middle) - half;
|
||||||
|
return static_cast<int>(std::lround(std::clamp(away / fade, 0.0, 1.0) * 256.0));
|
||||||
|
});
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "effects/video_effect.hpp"
|
||||||
|
|
||||||
|
namespace effector {
|
||||||
|
|
||||||
|
// The blur effect in motion, as a lens being focused: either the whole
|
||||||
|
// picture drifts out of focus and back, or a band of sharpness travels down
|
||||||
|
// the picture and back up with the rest blurred, as a tilted lens shows it.
|
||||||
|
//
|
||||||
|
// The pull goes there and back once a cycle, so the clip loops without a
|
||||||
|
// jump; nothing in it is random.
|
||||||
|
class FocusPullEffect : public VideoEffect {
|
||||||
|
public:
|
||||||
|
std::string name() const override { return "focuspull"; }
|
||||||
|
std::string title() const override { return "Focus pull"; }
|
||||||
|
std::string description() const override {
|
||||||
|
return "Blurs the picture as Blur does, pulling it out of focus and back, or moving a "
|
||||||
|
"band of sharpness up and down it.";
|
||||||
|
}
|
||||||
|
const std::vector<ParamSpec>& params() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Image animate(Image input, const ParamValues& values, const Moment& at) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace effector
|
||||||
@@ -2210,6 +2210,9 @@ int main() {
|
|||||||
{"gate_scratches", 0L}}},
|
{"gate_scratches", 0L}}},
|
||||||
{"datamosh", {{"mosh_rate", 0.4}, {"mosh_block", 8L}, {"mosh_bleed", true}}, {},
|
{"datamosh", {{"mosh_rate", 0.4}, {"mosh_block", 8L}, {"mosh_bleed", true}}, {},
|
||||||
{{"mosh_rate", 0.0}}},
|
{{"mosh_rate", 0.0}}},
|
||||||
|
{"focuspull", {{"focus_radius", 5L}}, {}, {{"focus_radius", 0L}}},
|
||||||
|
{"focuspull", {{"focus_mode", std::string("band")}, {"focus_speed", 1.0}}, {},
|
||||||
|
{{"focus_mode", std::string("band")}, {"focus_radius", 0L}}},
|
||||||
};
|
};
|
||||||
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