Let several text stages share one named look

A picture with a title, a caption and a credit meant typing the same
font, size and colour into every text stage, and changing the look meant
typing them all again. A project now holds named styles: a stage follows
one, and editing it moves every stage that follows it at once.

The front-ends learn nothing effect-specific. A ParamSpec can be marked
.styled(), saying a style carries it rather than the stage, and
Effect::styleKind() names the pool it comes from -- so styleSpecs(kind)
is to a style what params() is to a stage, and a second text-ish effect
would slot into the styles that already exist. The look is what a style
carries; the words and the placement stay on the stage, which is what
lets two stages share a typography in two different corners.

stageValues() resolves one in a single place, and a stage keeps a full
set of values of its own beside the style, so switching it to Custom,
dropping the style, or opening the project in a build that does not know
it renders exactly the same picture. A style moves stages together; it
never holds the only copy of anything.

The command line builds no styles and passes none, so it renders what it
always did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Si
2026-09-19 21:55:55 +02:00
co-authored by Claude Opus 5
parent eb417bb41a
commit 3aade6f361
17 changed files with 890 additions and 131 deletions
+20 -3
View File
@@ -11,8 +11,26 @@ std::vector<const ChainStage*> enabledStages(const std::vector<ChainStage>& stag
return running;
}
ParamValues stageValues(const ChainStage& stage, const StyleSet& styles) {
ParamValues values = stage.effect->defaults();
for (const auto& [key, value] : stage.values) values[key] = value;
const Style* style = findStyle(styles, stage.effect->styleKind(), stage.style);
if (style != nullptr) {
// Only the params the effect marked styled(): the words and the
// placement stay the stage's own, which is what lets two stages share
// one look without sharing a position.
for (const auto& spec : stage.effect->params()) {
if (!spec.inStyle) continue;
const auto found = style->values.find(spec.key);
if (found != style->values.end()) values[spec.key] = found->second;
}
}
return values;
}
Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image>& sources,
int width, int height, Rng& rng) {
int width, int height, Rng& rng, const StyleSet& styles) {
if (stages.empty()) throw EffectError("The effect chain is empty.");
for (const auto& stage : stages) {
if (!stage.effect) throw EffectError("The effect chain has an empty stage.");
@@ -28,8 +46,7 @@ Image renderChain(const std::vector<ChainStage>& stages, const std::vector<Image
for (std::size_t i = 0; i < running.size(); ++i) {
const auto& stage = *running[i];
auto values = stage.effect->defaults();
for (const auto& [key, value] : stage.values) values[key] = value;
const auto values = stageValues(stage, styles);
if (i == 0) {
image = stage.effect->render(sources, width, height, rng, values);