Add an image overlay effect

Lays a single picture from disk over its input: fitted, filled, stretched,
left at its own size or tiled, centred or pinned to the top left corner,
nudged by an offset and mixed in by its opacity.

The file it needs is a new kind of param, Path, so the interface shows a
chooser and the CLI takes a file name, both without knowing an overlay
exists. Projects write it relative to their own folder, as they do a
source, and Image::load can now hand back the alpha channel instead of
flattening it onto white, so a cut-out PNG keeps its cut-out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-18 19:31:09 +02:00
co-authored by Claude Opus 5
parent 2a64ab115e
commit e2252ecd30
15 changed files with 520 additions and 8 deletions
+13
View File
@@ -348,6 +348,13 @@ std::string projectToJson(const Project& project, const std::string& directory)
case ParamSpec::Type::Choice:
out << quote(paramChoice(stage.values, spec.key, spec.defaultChoice));
break;
case ParamSpec::Type::Path: {
// Written relative to the project folder, exactly as the
// sources are, so a folder still opens once it is moved.
const auto path = paramText(stage.values, spec.key, "");
out << quote(path.empty() ? path : relativeTo(path, directory));
break;
}
}
}
out << (specs.empty() ? "}" : "\n }") << "\n }";
@@ -418,6 +425,12 @@ Project projectFromJson(const std::string& text, const std::string& directory) {
case ParamSpec::Type::Double:
if (value->kind == Json::Kind::Number) stage.values[spec.key] = value->number;
break;
case ParamSpec::Type::Path:
if (value->kind == Json::Kind::String) {
stage.values[spec.key] =
value->text.empty() ? value->text : resolve(value->text, directory);
}
break;
case ParamSpec::Type::Choice:
for (const auto& choice : spec.choices) {
if (value->kind == Json::Kind::String && value->text == choice) {