Make video projects, with a moving channel split as the first video effect
A project can now be a clip: Setup has a Video switch with a length, a frame rate and presets that set the canvas too (Instagram story, reel and feed video, TikTok / YouTube Short, Square loop, Full HD clip). The preview renders the whole clip, shrunk to a memory budget, and loops it with play/pause and a scale; Save As... renders it again at full size and pipes the frames into ffmpeg as .mp4, .mov, .mkv, .webm or .gif, with a progress window that can cancel. --duration, --fps and --video-preset do the same from the command line. Effects learn about time through FrameTime and Effect::renderFrame, which falls back to render, so nothing that holds still changes. Channel split now sways, shakes and rolls a tracking bar down the picture in a video; its motion params are marked forVideo() and hidden in a still project, and a still renders byte for byte as before. renderFrames starts every frame from the same seed and renders the still stages ahead of the first moving one only once. The version moves to 0.6.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -440,6 +440,80 @@ void testRecentProjects(Gtk::Window& window, const std::filesystem::path& dir) {
|
||||
"opened projects are listed newest first, a shared name with its folder");
|
||||
}
|
||||
|
||||
// Visible labels in the params grid saying `text`.
|
||||
bool showsLabel(Gtk::Widget& root, const std::string& text) {
|
||||
for (auto* label : all<Gtk::Label>(root)) {
|
||||
if (label->get_text() == text && label->is_drawable()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void testVideo(Gtk::Window& window, const std::filesystem::path& dir) {
|
||||
std::cout << "video\n";
|
||||
std::filesystem::create_directories(dir);
|
||||
const auto path = dir / "moving.json";
|
||||
effector::Project project;
|
||||
project.stages.push_back({effector::effectByName("channelsplit"), {}}); // needs no sources
|
||||
effector::saveProject(path.string(), project);
|
||||
window.activate_action("win.open-recent",
|
||||
Glib::Variant<Glib::ustring>::create(Glib::filename_to_uri(path.string())));
|
||||
pump(300);
|
||||
|
||||
auto* stack = all<Gtk::Stack>(window).front();
|
||||
auto* toggle = named<Gtk::Switch>(window, "video");
|
||||
auto* preset = named<Gtk::DropDown>(window, "video-preset");
|
||||
auto* duration = named<Gtk::SpinButton>(window, "video-duration");
|
||||
auto* width = named<Gtk::SpinButton>(window, "canvas-width");
|
||||
auto* height = named<Gtk::SpinButton>(window, "canvas-height");
|
||||
auto* playback = named<Gtk::Box>(window, "playback");
|
||||
check(toggle != nullptr && preset != nullptr && duration != nullptr && width != nullptr &&
|
||||
height != nullptr && playback != nullptr,
|
||||
"the video switch, its settings and the playback bar are there");
|
||||
if (toggle == nullptr || preset == nullptr || duration == nullptr || width == nullptr ||
|
||||
height == nullptr || playback == nullptr) {
|
||||
return;
|
||||
}
|
||||
check(isAncestor(*stack->get_child_by_name("setup"), *toggle), "the video switch is on Setup");
|
||||
check(!toggle->get_active() && !preset->is_drawable() && !playback->get_visible(),
|
||||
"a still project shows no video settings and no playback");
|
||||
check(!showsLabel(window, "Wobble"), "and no motion params for the channel split");
|
||||
|
||||
toggle->set_active(true);
|
||||
pump(200);
|
||||
check(showsLabel(window, "Wobble") && showsLabel(window, "Tracking bar"),
|
||||
"a video shows the channel split's motion params");
|
||||
stack->set_visible_child("setup");
|
||||
pump(100);
|
||||
check(preset->is_drawable() && duration->is_drawable(), "and its preset and length");
|
||||
|
||||
preset->set_selected(1); // Instagram story, the first preset
|
||||
pump(200);
|
||||
check(width->get_value() == 1080 && height->get_value() == 1920 && duration->get_value() == 15,
|
||||
"Instagram story picks a 1080 × 1920 canvas and 15 s");
|
||||
duration->set_value(0.5);
|
||||
pump(100);
|
||||
check(preset->get_selected() == 0, "changing the length drops the preset to Custom");
|
||||
|
||||
for (int waited = 0; waited < 200 && !playback->get_visible(); ++waited) pump(50);
|
||||
check(playback->get_visible(), "the clip is rendered and plays in the preview");
|
||||
auto* scale = all<Gtk::Scale>(*playback).empty() ? nullptr : all<Gtk::Scale>(*playback).front();
|
||||
const double before = scale != nullptr ? scale->get_value() : -1.0;
|
||||
pump(250);
|
||||
check(scale != nullptr && scale->get_value() != before, "and it moves on by itself");
|
||||
|
||||
window.activate_action("win.save-project");
|
||||
pump(100);
|
||||
const auto saved = effector::loadProject(path.string());
|
||||
check(saved.video.enabled && saved.video.duration == 0.5 && saved.width == 1080,
|
||||
"the project is saved as a video");
|
||||
|
||||
toggle->set_active(false);
|
||||
pump(300);
|
||||
check(!playback->get_visible() && !showsLabel(window, "Wobble"),
|
||||
"switched back to a still, the playback and motion params go");
|
||||
stack->set_visible_child("chain");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -467,6 +541,7 @@ int main(int argc, char** argv) {
|
||||
runTests(*window);
|
||||
testHelpWindows(*window);
|
||||
testRecentProjects(*window, dataDir / "projects");
|
||||
testVideo(*window, dataDir / "video");
|
||||
app->remove_window(*window);
|
||||
});
|
||||
const int status = app->run(argc > 0 ? 1 : 0, argv);
|
||||
|
||||
Reference in New Issue
Block a user