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:
+20
-3
@@ -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);
|
||||
|
||||
+14
-1
@@ -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<const ChainStage*> enabledStages(const std::vector<ChainStage>& 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<ChainStage>& stages, const std::vector<Image>& 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
|
||||
|
||||
+1
-19
@@ -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<long>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const = 0;
|
||||
|
||||
|
||||
@@ -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<long>(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<long>(value)) return *v;
|
||||
|
||||
@@ -24,9 +24,17 @@ struct ParamSpec {
|
||||
int digits = 2;
|
||||
std::string defaultChoice;
|
||||
std::vector<std::string> 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<long, double, bool, std::string>;
|
||||
using ParamValues = std::map<std::string, ParamValue>;
|
||||
|
||||
// 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);
|
||||
|
||||
+149
-81
@@ -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<ParamSpec>& 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<long>(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<ParamSpec>& 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<long>(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<long>(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<long>(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));
|
||||
}
|
||||
|
||||
@@ -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<std::string> sources; // absolute paths
|
||||
std::vector<ChainStage> 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
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "core/style.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "core/registry.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
std::vector<ParamSpec> styleSpecs(const std::string& kind) {
|
||||
std::vector<ParamSpec> 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
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<Style>;
|
||||
|
||||
// The styled specs of a kind: every registered effect answering that kind
|
||||
// contributes its styled params, the first declaration of a key winning. This
|
||||
// is to a style what Effect::params() is to a stage -- the only thing the
|
||||
// interface and the project file know about what a style holds -- so an empty
|
||||
// or unknown kind gives an empty list.
|
||||
std::vector<ParamSpec> styleSpecs(const std::string& kind);
|
||||
|
||||
// A fresh set of values for a style of that kind: every styled param at the
|
||||
// default its effect declares, the way Effect::defaults() fills a stage.
|
||||
ParamValues styleDefaults(const std::string& kind);
|
||||
|
||||
// The style of that name in that pool, or nullptr. A stage naming a style that
|
||||
// is not there simply stands on its own values.
|
||||
const Style* findStyle(const StyleSet& styles, const std::string& kind, const std::string& name);
|
||||
|
||||
} // namespace effector
|
||||
Reference in New Issue
Block a user