Add a text effect

Writes a line or several over its input in a chosen family, weight, size
and colour, optionally on a box filled with a colour or with a picture
from disk. Like the blur and the overlay it needs no sources of its own,
so it can lead a chain; given sources it fills the canvas from a randomly
chosen one first.

Two param types rather than effect-specific branches in the front-ends:
Text, free-form text the user types, and Color, a colour written
"#rrggbb". Both travel as strings, so the interface gets an entry and a
colour button, the command line gets a flag (a colour that does not parse
is refused there rather than drawn as black), and a project keeps them
across a round trip.

The glyphs come from Pango, which draws through Cairo without a display,
so the core stays headless — but they are the one thing here that is not
byte-identical across machines: a machine without the chosen family
renders the substitute its fontconfig picks. Everything the effect is
handed, the rng included, is used exactly as every other effect uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-18 20:31:24 +02:00
co-authored by Claude Opus 5
parent c34bc426fc
commit 1d0343b90a
12 changed files with 725 additions and 6 deletions
+21 -1
View File
@@ -2,6 +2,7 @@
#include <charconv>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
@@ -358,8 +359,15 @@ std::string projectToJson(const Project& project, const std::string& directory)
spec.digits);
break;
case ParamSpec::Type::Choice:
out << quote(paramChoice(stage.values, spec.key, spec.defaultChoice));
case ParamSpec::Type::Text:
out << quote(paramText(stage.values, spec.key, spec.defaultChoice));
break;
case ParamSpec::Type::Color: {
std::uint8_t rgb[3];
paramColor(stage.values, spec.key, spec.defaultChoice, rgb);
out << quote(formatColor(rgb));
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.
@@ -452,6 +460,18 @@ Project projectFromJson(const std::string& text, const std::string& directory) {
value->text.empty() ? value->text : resolve(value->text, directory);
}
break;
case ParamSpec::Type::Text:
if (value->kind == Json::Kind::String) stage.values[spec.key] = value->text;
break;
case ParamSpec::Type::Color: {
// A colour that does not parse keeps the default,
// exactly as an unknown choice does.
std::uint8_t rgb[3];
if (value->kind == Json::Kind::String && parseColor(value->text, rgb)) {
stage.values[spec.key] = formatColor(rgb);
}
break;
}
case ParamSpec::Type::Choice:
for (const auto& choice : spec.choices) {
if (value->kind == Json::Kind::String && value->text == choice) {