From 3aade6f361aba930c7a3dff270270b3ccac22069 Mon Sep 17 00:00:00 2001 From: Si Date: Sat, 19 Sep 2026 21:55:55 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 32 ++++- CMakeLists.txt | 1 + src/core/chain.cpp | 23 ++- src/core/chain.hpp | 15 +- src/core/effect.cpp | 20 +-- src/core/effect.hpp | 6 + src/core/params.cpp | 23 +++ src/core/params.hpp | 12 ++ src/core/project.cpp | 230 ++++++++++++++++++----------- src/core/project.hpp | 9 +- src/core/style.cpp | 39 +++++ src/core/style.hpp | 39 +++++ src/effects/text.cpp | 31 ++-- src/effects/text.hpp | 1 + src/gui/window.cpp | 336 +++++++++++++++++++++++++++++++++++++++++-- src/gui/window.hpp | 34 ++++- tests/core_tests.cpp | 170 ++++++++++++++++++++++ 17 files changed, 890 insertions(+), 131 deletions(-) create mode 100644 src/core/style.cpp create mode 100644 src/core/style.hpp diff --git a/CLAUDE.md b/CLAUDE.md index b35d896..3c5b70f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,17 @@ travel as `ParamValues` (a `map>`), read back with `paramInt`/`paramDouble`/`paramBool`/`paramChoice`. +A param can also be marked `.styled()`, which says a *named style* carries it +rather than the stage it sits on: the font and the colour of a text stage, but +never its words or its placement. The pool a style comes from is named by +`Effect::styleKind()` (empty by default — the effect takes no style; `"text"` +for the text effect), and effects answering the same kind share one pool, so a +second text-ish effect would slot into the styles that already exist. The +front-ends know nothing more than that: the interface builds the style row and +the project file writes a style's params from `styleSpecs(kind)` +(`src/core/style.cpp`), which is to a style exactly what `params()` is to a +stage. + Whether an effect works from the loaded photographs is part of the same contract: `Effect::needsSources()` (default true, false for blur). Only the first stage ever sees the sources, so `chainNeedsSources` (`src/core/chain.cpp`) @@ -82,6 +93,18 @@ it, which is why the status line and the file name never mention a stage that did not run. A chain with every stage disabled throws rather than rendering nothing. +### A style is a look several stages share + +`ChainStage::style` names a style, and `stageValues` (`src/core/chain.cpp`) is +the one place that resolves one: an effect's defaults, then the stage's own +values, then the style's values for the params it carries. A stage always 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 the +style renders exactly the same picture — a style moves stages together, it +never holds the only copy of anything. The interface writes an edited param to +both the style and the stage for that reason. `renderChain` takes the pool as a +trailing argument; the command line builds no styles and passes none. + The interface builds the chain in the sidebar ("Effect chain": add from the menu, reorder, remove, switch off with the check on the row, one stage selected at a time with its params below), the CLI builds it from repeated `-e`: @@ -104,12 +127,15 @@ pixels, so a project saved as A4 reopens as A4. ### Projects are plain JSON `src/core/project.cpp` reads and writes the whole setup — source paths, the -chain with its params (and its `"enabled": false`, written only for a stage -that is switched off), canvas and seed — as a small JSON file, with its own +named styles, the chain with its params (and its `"enabled": false` and +`"style"`, each written only when there is something to say), canvas and seed — as a small JSON file, with its own minimal reader rather than a JSON dependency. Writing is driven by the effects' `ParamSpec`s, not by whatever happens to sit in a stage's `ParamValues`, so a key an effect does not declare is never written and never read back: an old -project keeps the defaults for params that did not exist when it was saved. +project keeps the defaults for params that did not exist when it was saved. The +same leniency covers styles: one of a kind this build does not know, one with +no name or a repeated one, and a stage naming a style the file does not hold +are each dropped on their own rather than refusing the project. Source paths under the project's own folder are written relative, so a folder can be moved or shared whole. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f6e3c5..a6f3eec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(effector_core STATIC src/core/project.cpp src/core/registry.cpp src/core/rng.cpp + src/core/style.cpp src/effects/blur.cpp src/effects/grain.cpp src/effects/monochrome.cpp diff --git a/src/core/chain.cpp b/src/core/chain.cpp index d7a5051..33e2d0c 100644 --- a/src/core/chain.cpp +++ b/src/core/chain.cpp @@ -11,8 +11,26 @@ std::vector enabledStages(const std::vector& 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& stages, const std::vector& 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& stages, const std::vectordefaults(); - 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); diff --git a/src/core/chain.hpp b/src/core/chain.hpp index 2b712d6..d779998 100644 --- a/src/core/chain.hpp +++ b/src/core/chain.hpp @@ -7,6 +7,7 @@ #include "core/image.hpp" #include "core/params.hpp" #include "core/rng.hpp" +#include "core/style.hpp" namespace effector { @@ -25,8 +26,18 @@ struct ChainStage { // the chain, so a stage can be switched off and back on while trying // things out without being taken apart and rebuilt. bool enabled = true; + // The style this stage follows, by name, in the pool its effect names with + // styleKind(); empty when the stage stands on its own. A stage keeps a + // full set of values either way, so a style that is dropped or renamed + // away leaves the stage rendering exactly what it rendered before. + std::string style = {}; }; +// The values a stage actually runs with: its effect's defaults, then the +// stage's own values, then the values of the style it follows for the params +// that style carries. The one place a style is resolved. +ParamValues stageValues(const ChainStage& stage, const StyleSet& styles); + // The stages that actually run, in order, as pointers into `stages`. A // disabled stage draws nothing from the rng and is handed no image, so a chain // renders exactly as it would if that stage had never been added -- this is @@ -39,8 +50,10 @@ std::vector enabledStages(const std::vector& stag // // All stages share one Rng and run strictly in sequence, so a seed still // reproduces the cascade byte for byte. Throws when no stage is left to run. +// `styles` is the project's pool; a chain built without one -- the command +// line's -- passes none and renders from the stages alone. Image renderChain(const std::vector& stages, const std::vector& sources, - int width, int height, Rng& rng); + int width, int height, Rng& rng, const StyleSet& styles = {}); // Whether the chain has to be given source images. Only the first stage that // runs ever sees them — every later one works on the stage before it — so this diff --git a/src/core/effect.cpp b/src/core/effect.cpp index d6590a0..e28ef8b 100644 --- a/src/core/effect.cpp +++ b/src/core/effect.cpp @@ -4,25 +4,7 @@ namespace effector { ParamValues Effect::defaults() const { ParamValues values; - for (const auto& spec : params()) { - switch (spec.type) { - case ParamSpec::Type::Int: - values[spec.key] = static_cast(spec.defaultValue); - break; - case ParamSpec::Type::Double: - values[spec.key] = spec.defaultValue; - break; - case ParamSpec::Type::Bool: - values[spec.key] = spec.defaultValue != 0.0; - break; - case ParamSpec::Type::Choice: - case ParamSpec::Type::Path: - case ParamSpec::Type::Text: - case ParamSpec::Type::Color: - values[spec.key] = spec.defaultChoice; - break; - } - } + for (const auto& spec : params()) values[spec.key] = paramDefault(spec); return values; } diff --git a/src/core/effect.hpp b/src/core/effect.hpp index af60c9a..1becff7 100644 --- a/src/core/effect.hpp +++ b/src/core/effect.hpp @@ -32,6 +32,12 @@ public: // chain that starts with one of them needs no sources at all. virtual bool needsSources() const { return true; } + // The pool of named styles this effect's styled params are drawn from, or + // empty when it takes no style. Effects that answer the same kind share + // one pool, so a stage can be pointed at a look its neighbours already + // use. An effect with no kind must declare no styled params. + virtual std::string styleKind() const { return {}; } + virtual Image render(const std::vector& sources, int width, int height, Rng& rng, const ParamValues& values) const = 0; diff --git a/src/core/params.cpp b/src/core/params.cpp index 76e2f19..fc95c52 100644 --- a/src/core/params.cpp +++ b/src/core/params.cpp @@ -10,6 +10,12 @@ std::string ParamSpec::flag() const { return flag; } +ParamSpec ParamSpec::styled() const { + ParamSpec copy = *this; + copy.inStyle = true; + return copy; +} + ParamSpec ParamSpec::makeInt(std::string key, std::string label, long defaultValue, long minimum, long maximum, std::string help, long step) { ParamSpec spec; @@ -103,6 +109,23 @@ const ParamValue* find(const ParamValues& values, const std::string& key) { } // namespace +ParamValue paramDefault(const ParamSpec& spec) { + switch (spec.type) { + case ParamSpec::Type::Int: + return static_cast(spec.defaultValue); + case ParamSpec::Type::Double: + return spec.defaultValue; + case ParamSpec::Type::Bool: + return spec.defaultValue != 0.0; + case ParamSpec::Type::Choice: + case ParamSpec::Type::Path: + case ParamSpec::Type::Text: + case ParamSpec::Type::Color: + break; + } + return spec.defaultChoice; +} + long paramInt(const ParamValues& values, const std::string& key, long fallback) { if (const auto* value = find(values, key)) { if (const auto* v = std::get_if(value)) return *v; diff --git a/src/core/params.hpp b/src/core/params.hpp index eba8421..a834cc5 100644 --- a/src/core/params.hpp +++ b/src/core/params.hpp @@ -24,9 +24,17 @@ struct ParamSpec { int digits = 2; std::string defaultChoice; std::vector choices; + // Whether a named style carries this param rather than the stage it sits + // on: see core/style.hpp. + bool inStyle = false; std::string flag() const; // "strip_height" -> "--strip-height" + // A copy of this spec marked as carried by a style, so that a declaration + // reads makeDouble("font_size", ...).styled() and the factories keep the + // signatures they have. + ParamSpec styled() const; + static ParamSpec makeInt(std::string key, std::string label, long defaultValue, long minimum, long maximum, std::string help = {}, long step = 1); static ParamSpec makeDouble(std::string key, std::string label, double defaultValue, @@ -53,6 +61,10 @@ struct ParamSpec { using ParamValue = std::variant; using ParamValues = std::map; +// The value a spec stands for when nothing has been set: one place, so that +// an effect's defaults and a style's are filled the same way. +ParamValue paramDefault(const ParamSpec& spec); + long paramInt(const ParamValues& values, const std::string& key, long fallback); double paramDouble(const ParamValues& values, const std::string& key, double fallback); bool paramBool(const ParamValues& values, const std::string& key, bool fallback); diff --git a/src/core/project.cpp b/src/core/project.cpp index 1a2eb3a..71d7b46 100644 --- a/src/core/project.cpp +++ b/src/core/project.cpp @@ -325,6 +325,96 @@ std::string resolve(const std::string& path, const std::string& directory) { return (std::filesystem::path(directory) / file).lexically_normal().string(); } +// The params of a stage or of a style, written from the specs rather than from +// whatever the values happen to hold: a key the effect does not declare is +// never written, so it can never be read back either. +void writeParams(std::ostream& out, const std::vector& specs, const ParamValues& values, + const std::string& directory) { + out << "{"; + bool first = true; + for (const auto& spec : specs) { + out << (first ? "\n" : ",\n") << " " << quote(spec.key) << ": "; + first = false; + switch (spec.type) { + case ParamSpec::Type::Bool: + out << (paramBool(values, spec.key, spec.defaultValue != 0.0) ? "true" : "false"); + break; + case ParamSpec::Type::Int: + out << paramInt(values, spec.key, static_cast(spec.defaultValue)); + break; + case ParamSpec::Type::Double: + out << number(paramDouble(values, spec.key, spec.defaultValue), spec.digits); + break; + case ParamSpec::Type::Choice: + case ParamSpec::Type::Text: + out << quote(paramText(values, spec.key, spec.defaultChoice)); + break; + case ParamSpec::Type::Color: { + std::uint8_t rgb[3]; + paramColor(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. + const auto path = paramText(values, spec.key, ""); + out << quote(path.empty() ? path : relativeTo(path, directory)); + break; + } + } + } + out << (specs.empty() ? "}" : "\n }"); +} + +// The mirror image: only declared keys are read, each behind the gate its type +// asks for, so anything else in the file leaves the default standing. +void readParams(const std::vector& specs, const Json& params, + const std::string& directory, ParamValues& values) { + for (const auto& spec : specs) { + const auto* value = params.member(spec.key); + if (value == nullptr) continue; + switch (spec.type) { + case ParamSpec::Type::Bool: + if (value->kind == Json::Kind::Bool) values[spec.key] = value->boolean; + break; + case ParamSpec::Type::Int: + if (value->kind == Json::Kind::Number) { + values[spec.key] = static_cast(std::llround(value->number)); + } + break; + case ParamSpec::Type::Double: + if (value->kind == Json::Kind::Number) values[spec.key] = value->number; + break; + case ParamSpec::Type::Path: + if (value->kind == Json::Kind::String) { + values[spec.key] = + value->text.empty() ? value->text : resolve(value->text, directory); + } + break; + case ParamSpec::Type::Text: + if (value->kind == Json::Kind::String) 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)) { + 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) { + values[spec.key] = choice; + } + } + break; + } + } +} + } // namespace std::string projectToJson(const Project& project, const std::string& directory) { @@ -353,6 +443,21 @@ std::string projectToJson(const Project& project, const std::string& directory) } out << (project.sources.empty() ? "" : "\n ") << "],\n"; + // Before the chain, because a stage's "style" is a name from this list. + if (!project.styles.empty()) { + out << " \"styles\": ["; + for (std::size_t i = 0; i < project.styles.size(); ++i) { + const auto& style = project.styles[i]; + out << (i == 0 ? "\n" : ",\n"); + out << " {\n \"kind\": " << quote(style.kind) << ",\n"; + out << " \"name\": " << quote(style.name) << ",\n"; + out << " \"params\": "; + writeParams(out, styleSpecs(style.kind), style.values, directory); + out << "\n }"; + } + out << "\n ],\n"; + } + out << " \"chain\": ["; for (std::size_t i = 0; i < project.stages.size(); ++i) { const auto& stage = project.stages[i]; @@ -363,45 +468,12 @@ std::string projectToJson(const Project& project, const std::string& directory) // says otherwise, so an ordinary project stays as uncluttered as it // was before a stage could be switched off at all. if (!stage.enabled) out << " \"enabled\": false,\n"; - out << " \"params\": {"; - - const auto& specs = stage.effect->params(); - bool first = true; - for (const auto& spec : specs) { - out << (first ? "\n" : ",\n") << " " << quote(spec.key) << ": "; - first = false; - switch (spec.type) { - case ParamSpec::Type::Bool: - out << (paramBool(stage.values, spec.key, spec.defaultValue != 0.0) ? "true" - : "false"); - break; - case ParamSpec::Type::Int: - out << paramInt(stage.values, spec.key, static_cast(spec.defaultValue)); - break; - case ParamSpec::Type::Double: - out << number(paramDouble(stage.values, spec.key, spec.defaultValue), - spec.digits); - break; - case ParamSpec::Type::Choice: - 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. - const auto path = paramText(stage.values, spec.key, ""); - out << quote(path.empty() ? path : relativeTo(path, directory)); - break; - } - } - } - out << (specs.empty() ? "}" : "\n }") << "\n }"; + // Likewise only when there is one: a stage that stands on its own + // values says nothing about styles at all. + if (!stage.style.empty()) out << " \"style\": " << quote(stage.style) << ",\n"; + out << " \"params\": "; + writeParams(out, stage.effect->params(), stage.values, directory); + out << "\n }"; } out << (project.stages.empty() ? "" : "\n ") << "]\n"; out << "}\n"; @@ -457,6 +529,33 @@ Project projectFromJson(const std::string& text, const std::string& directory) { } } + // Read before the chain, so that a stage's "style" can be checked against + // the pool as it is read. + if (const auto* styles = root.member("styles")) { + for (const auto& item : styles->items) { + const auto* kind = item.member("kind"); + const auto* name = item.member("name"); + if (kind == nullptr || kind->kind != Json::Kind::String) continue; + if (name == nullptr || name->kind != Json::Kind::String || name->text.empty()) continue; + + // A kind no effect in this build claims carries no params, so + // there would be nothing to keep; a repeated name would make + // "style": "Title" ambiguous, so the first one wins. + const auto specs = styleSpecs(kind->text); + if (specs.empty()) continue; + if (findStyle(project.styles, kind->text, name->text) != nullptr) continue; + + Style style; + style.kind = kind->text; + style.name = name->text; + style.values = styleDefaults(style.kind); + if (const auto* params = item.member("params")) { + readParams(specs, *params, directory, style.values); + } + project.styles.push_back(std::move(style)); + } + } + const auto* chain = root.member("chain"); if (chain == nullptr || chain->kind != Json::Kind::Array || chain->items.empty()) { throw EffectError("the project has no effect chain"); @@ -479,48 +578,17 @@ Project projectFromJson(const std::string& text, const std::string& directory) { // declare is dropped, and an old project simply keeps the defaults for // params that did not exist when it was written. if (const auto* params = item.member("params")) { - for (const auto& spec : stage.effect->params()) { - const auto* value = params->member(spec.key); - if (value == nullptr) continue; - switch (spec.type) { - case ParamSpec::Type::Bool: - if (value->kind == Json::Kind::Bool) stage.values[spec.key] = value->boolean; - break; - case ParamSpec::Type::Int: - if (value->kind == Json::Kind::Number) { - stage.values[spec.key] = static_cast(std::llround(value->number)); - } - break; - 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::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) { - stage.values[spec.key] = choice; - } - } - break; - } - } + readParams(stage.effect->params(), *params, directory, stage.values); + } + + // A style is kept only when the file actually holds one of that name + // for this effect's pool: a stage pointing at a style that is not + // there falls back to the values it wrote beside it, which are the + // ones it last rendered with. + if (const auto* style = item.member("style"); + style != nullptr && style->kind == Json::Kind::String && + findStyle(project.styles, stage.effect->styleKind(), style->text) != nullptr) { + stage.style = style->text; } project.stages.push_back(std::move(stage)); } diff --git a/src/core/project.hpp b/src/core/project.hpp index 5034a45..ffb1749 100644 --- a/src/core/project.hpp +++ b/src/core/project.hpp @@ -8,6 +8,7 @@ #include "core/canvas.hpp" #include "core/chain.hpp" #include "core/params.hpp" +#include "core/style.hpp" namespace effector { @@ -17,6 +18,10 @@ namespace effector { struct Project { std::vector sources; // absolute paths std::vector stages; + // The named looks the stages draw their formatting from. Kept beside the + // chain rather than inside it because that is the whole point of a style: + // several stages share one, and editing it moves all of them at once. + StyleSet styles; int width = 1920; int height = 1080; // The named format the canvas came from, empty for a custom pixel size. @@ -36,7 +41,9 @@ std::string projectToJson(const Project& project, const std::string& directory = // Throws EffectError on malformed JSON, an unknown effect or a missing chain. // Relative source paths are resolved against `directory`; params the effect -// does not declare are dropped, and missing ones keep the effect's default. +// does not declare are dropped, and missing ones keep the effect's default. A +// style of an unknown kind, or a stage naming a style the file does not hold, +// is dropped the same way: the stage keeps its own values and still renders. Project projectFromJson(const std::string& text, const std::string& directory = {}); // Both throw EffectError: the file is the user's, so failures are reported diff --git a/src/core/style.cpp b/src/core/style.cpp new file mode 100644 index 0000000..3380ffa --- /dev/null +++ b/src/core/style.cpp @@ -0,0 +1,39 @@ +#include "core/style.hpp" + +#include + +#include "core/registry.hpp" + +namespace effector { + +std::vector styleSpecs(const std::string& kind) { + std::vector specs; + if (kind.empty()) return specs; + for (const auto& effect : effects()) { + if (effect->styleKind() != kind) continue; + for (const auto& spec : effect->params()) { + if (!spec.inStyle) continue; + const bool known = std::any_of(specs.begin(), specs.end(), [&](const ParamSpec& seen) { + return seen.key == spec.key; + }); + if (!known) specs.push_back(spec); + } + } + return specs; +} + +ParamValues styleDefaults(const std::string& kind) { + ParamValues values; + for (const auto& spec : styleSpecs(kind)) values[spec.key] = paramDefault(spec); + return values; +} + +const Style* findStyle(const StyleSet& styles, const std::string& kind, const std::string& name) { + if (kind.empty() || name.empty()) return nullptr; + for (const auto& style : styles) { + if (style.kind == kind && style.name == name) return &style; + } + return nullptr; +} + +} // namespace effector diff --git a/src/core/style.hpp b/src/core/style.hpp new file mode 100644 index 0000000..cb9eabe --- /dev/null +++ b/src/core/style.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "core/params.hpp" + +namespace effector { + +// A named set of formatting values, shared by every stage that follows it. +// +// A style carries only the params an effect marked styled() -- the look, never +// the placement -- so several text stages can share one typography while each +// keeps its own words and its own corner of the canvas. Changing the style is +// the point: it changes every stage pointing at it at once. +struct Style { + std::string kind; // the pool, as Effect::styleKind() names it + std::string name; // unique within a kind, never empty + ParamValues values; // the styled params only +}; + +using StyleSet = std::vector