Skip to content

Foundations: Elements, Scenes & Projects

Composer is one mechanism reused at three altitudes, not three bespoke systems: an Element (composed of primitives) is placed into a Scene; Scenes are sequenced into a Project, the unification of what are separately "Template" and "Campaign" today. Each altitude is the same shape: an ordered set of instances of the level below, plus its own field manifest. This page owns the altitudes' mechanics — what's spatial, what's temporal, how things size and time themselves, and how far a reference can reach. Overlays, Identity, Versions & Schema Versioning, and Brand Scoping & Brand Style each own one part of the schema this page leaves to them.

Decided: one manifest mechanism at every altitude

An Element's field manifest is generated the exact same way a template's is today, not a second, Composer-specific exposure mechanism grown alongside the one that already exists. Every altitude follows the identical pattern for the same reason (Home → Design principles): one way to expose a configurable surface, reused at every level.

Matthew's own working reference point for how this should feel: an After Effects Motion Graphics Template (MOGRT). An author builds an Element's internal structure once; everyone downstream works only with the exposed controls, never the layers underneath. A rough analogy, not a technical mapping — MOGRTs don't share Composer's fork/rebase or tier model — but it's the closest existing mental model for what "configure without reaching inside" is meant to feel like.

Elements are spatial, Scenes are temporal

An Element and a Scene are similar in mechanism but not the same kind of thing. A layer, whether inside an Element or inside a Scene, is the engine's spatial unit, positioned and composed through the motion graph (Resolver Engine → The Motion Graph). A Scene — and an Overlay — is the sequencing unit instead: an ordered or time-ranged placement with no layout relationship to its siblings at all. That difference has a concrete consequence: an Element never defines its own fixed size, and never defines its own duration. Its definition carries relative or intrinsic sizing only; the actual width, height, position, and duration for a given placement are all instance data, assigned by whatever contains it, set through setInstanceLayout (The Change Model), never baked into the Element's own version.

Default size yes, min/max clamp no

An Element may, when appropriate, optionally declare a default size per aspect ratio — a suggested starting value only, never a constraint. The ratio set those keys come from is the Project's, declared through setDocumentProp at Project altitude, since a Project is what renders and therefore what decides which ratios exist. An Element authored against one Project's ratios and placed into a Project with a different set simply has no default for the ratios it doesn't know, which is the same "no default declared" case the rest of this paragraph covers. It seeds a freshly placed instance with a reasonable box before the placing Scene author adjusts it; once an instance's own setInstanceLayout value is set, that's what resolves at render, and an Element that's genuinely content-sized in every dimension has no need to declare a default at all.

A min/max clamp was dropped as a concept entirely rather than kept: composing a clamp with the engine's sizing-collapse fallback chain (Resolver Engine → Coordinate System & Sizing → Intrinsic size) would need a second algorithm alongside it, for a case this brief has no real need for yet — the "don't unnecessarily complicate things" law in practice.

An Element instance is a layer, and a container

Within a Scene or Overlay, an Element instance is treated as a layer in its own right, so it can be pinned to another Element instance exactly the way any two layers can. There is no such thing as a bare primitive placed directly into a Scene: for every primitive or component a separate, automatically generated Element exists that contains that one thing alone (The Change Model → The wrapper Element mechanism), so everything a Scene or Overlay's layer list holds is an Element instance, trivial or fully authored. The one rule that has to hold, to avoid infinite recursion: an Element can never contain another Element.

Because an Element instance is a layer, it is also a container in exactly the sense a Scene is: its own internal layers position, pin, and flow against its box the same way a Scene's top-level layers position against the Scene's canvas. Container is a role any layer with children plays, recursively, not a Scene-specific concept, and an Element's own motion, if it has any, composes into its internals the same way a Scene's enter and exit motion composes into everything inside it. A layer with no animation of its own simply inherits the whole of whatever its container is doing. The engine's container rules — container-relative targetFraction sizing against the containing box, overflow: show | hide, and "a layer's own mode and unit choice is its complete response to its container resizing" (Resolver Engine → Coordinate System & Sizing → Container-relative sizing) — apply to an Element instance's box as one more container, with nothing left to decide at the Element level when an instance is placed at a size other than the one it was authored against.

This is Composer's largest unreconciled ask of the engine

An Element instance being both a layer in its Scene's graph and an encapsulated container root for its own internals is not designed on the engine side. flowBox is the closest existing precedent and still leaves four real gaps — see The Resolver Engine Dependency, which holds the detail.

Decided: an Element's layer list is a Scene's layer list

For all practical purposes the same shape as a Scene's layer list: the engine's schemaVersion: 2 layer type union — one text type (the point/box consolidation, designed but not yet built), image, video, lottie, shape, sticker, component, product, audio, plus the three new types that carry no intrinsic size of their own (flowBox, pinPoint, background) — with no Element type admitted per the no-nesting rule. Not a new document shape designed from scratch — the engine's, reused.

Scenes are temporal, Projects are sequential

A Scene instance within a Project follows the identical mechanism as an Element instance within a Scene, with two genuine departures, not oversights: a Scene already owns its own duration, and a Project has no spatial dimension for a Scene to be sized or positioned against. A Scene instance is a placement of one frozen, immutable Scene version plus whatever field overrides this particular Project has set on it, via the same setInstanceField Change type — the identical single mechanism, reused a third time.

  • No setInstanceLayout equivalent, and no instance-assigned duration. An Element carries no authored size or duration precisely because it has no identity outside a placement; a Scene already has both, today, independent of any Project, and unifying Template and Campaign into Project doesn't change that. A Scene instance's only placement data is whichever of its exposed fields the Project author has configured.
  • Sequence position is list order, not an authored field, the same convention the engine's flowBox members use for main-axis position — which is why reordering has its own Change type rather than a remove-and-add (The Change Model). Overlays don't participate in sequencing at all and so get their own Change types (Overlays).
  • Scenes generate their own field manifest the same way Elements do — via setManifestBinding on the Scene's own manifest, which any composing rung may emit — and the Project's instance of that Scene configures those exposed fields via setInstanceField.

Decided, kept deliberately simple: automatic passthrough upward

The Project re-exposes a Scene's whole exposed surface upward to campaign permission automatically, by default — not a field-by-field curated subset. Each field keeps the stricter of its declared and inherited editTier, so passthrough can't loosen a gate an Element's author set (Permissions & Enforcement). A passed-through field is keyed by (instanceId, fieldKey), not by field key alone, so a Project holding the same Scene version twice — a reused intro — exposes each instance's fields separately and a Project author can configure or lock one without touching the other. A Project author choosing which of a Scene's already-exposed fields to further expose would be a real, separate curation step with its own UI and its own failure mode (a field silently not making it up because nobody re-exposed it), and nothing about this design needs that yet.

No wrapper mechanism is needed at this altitude. A Scene is already always the atomic, addressable sequencing unit, never a bare primitive that could show up unwrapped in a Project's list; the only other citizen of a Project's list is an Overlay, already its own first-class instance type.

Element timing

An Element never carries an authored duration, the same way it never carries an authored fixed size. An Element instance placed into a Scene gets an actual duration, defaulting to the Scene's own length — the same category of instance data setInstanceLayout already assigns for position and size, carried alongside it rather than through a separate mechanism.

A layer's start and end within an Element are relative to the Element's own start, not the Scene's. Each is either a plain number — an absolute offset that holds regardless of what the instance's resolved duration later turns out to be — or { value, unit: "targetFraction" }, a fraction of the instance's resolved duration that stretches or compresses with it: the engine's spatial unit reused on the time axis, scoped to an implicit target, the instance's own duration this time rather than its box (an ask of the engine — The Resolver Engine Dependency). Omitting both is the common case: the layer spans the instance's full duration, matching how a layer with no authored time range behaves today.

Authored as Under a longer or shorter instance Cost
Plain number (absolute offset) Holds its authored length regardless Can leave time unaccounted for if the instance ends up shorter than the layer's authored end
{ value, unit: "targetFraction" } Stretches or compresses with the instance An entrance or exit changes speed when the instance's duration changes

Neither is universally correct, which is why both are offered rather than the schema forcing one. This only matters for a layer that isn't full-span — an entrance, an exit, a timed hold.

Duration never needs an intrinsic-value fallback the way size does: it is instance-assigned, never derived from a layer's content, so the circular-dependency and empty-container problems the engine's sizing rules solve have no temporal equivalent. And composing an instance's local time against its container's global time never needs a graph the way spatial pin/flow chains do: because an Element can never contain another Element, this composition is always exactly one level — a single shift, and for a fraction-based layer a single scale, from the Scene's frame clock into the instance's local time. There is no time equivalent of the engine's chain depth cap, because there is no chain.

Layer ids and reference scope

A layer's id is scoped to whichever altitude document it lives in, an Element or a Scene, not globally unique across the whole Project — the concrete instance of the general rule The Change Model states for every instance id: unique and stable within its own scope.

pin and flow references only ever resolve within that same scope. A layer inside an Element can only pinTo or flowIn another layer inside that same Element; a Scene or Overlay's top-level layers — always Element instances — can only reference each other, never reach inside one of those instances. This falls directly out of the resolver's shape: it resolves exactly one altitude's base plus Change[] pair at a time and never recurses into a referenced Element or Scene live, so an Element's internal resolution and a Scene's top-level resolution happen in genuinely separate invocations with no shared execution context in which a cross-boundary reference could even be attempted. The same structural reason is why one Scene's layers can never reference another Scene's: each Scene is its own separately resolved document, exactly like each Element is.

Where the empty-container rule runs for Composer

The engine owns intrinsic size, the two sizing-collapse cases, and the empty-container rule — any container, at any depth, must have at least one descendant that resolves to a real intrinsic size, checked on every mutation, extended to a background's of list (Resolver Engine → Coordinate System & Sizing → The empty-container rule). Composer's part is where "every mutation" lands: a removeElementInstance can strip a Scene, or an Element's internals, down to nothing intrinsic after the fact, so saving a fork's diff is rejected if it would leave a container in that state, naming the Change responsible (The Change Model). An Element whose internals are only flowBox / pinPoint / background layers fails the rule exactly as an empty flowBox would.

Risks

  • The no-nesting rule. Whether "an Element can never contain another Element" turns out to block a real composition need once real Element authoring is attempted at scale.
  • Per-ratio defaults against real variability. An Element that arranges a variable number of products is authored with reasonable defaults per aspect ratio, since each ratio's canvas dimensions are known at authoring time. That doesn't guarantee those defaults are accurate for every real BSR case; the mechanism is settled, its calibration isn't.
  • Fixing a value against campaign edits takes a deliberate act, and the default is the other way. Passthrough exposes every field a Scene exposed, so "fixed versus editable" for a campaign is decided at Scene altitude by default. A Project author can lock one — by declaring a higher editTier on the passed-through field, which the max(declared, inherited) clamp honours (Permissions & Enforcement) — but nothing prompts them to, and the curated manifest at Scene altitude versus automatic passthrough one level up is a deliberate asymmetry. This is the inverse shape of the BSR incident, and worth watching once real usage happens.
  • Composer surfaces a schema need the engine doesn't yet cover — see The Resolver Engine Dependency for the list so far.

Open questions

  • Flowing or spanning several Element instances at Scene level. A flowBox and a background are layer types, not Elements, and a Scene's top level holds only Element instances; a wrapper Element containing only a flowBox fails the empty-container rule, and an instance cannot flowIn a layer inside another Element by the reference-scope rule. As written, therefore, one Element instance cannot be flowed against another and a background can only span layers inside one Element — which means BSR's large background block works only within a single Element. Two candidate resolutions, neither decided: admit flowBox, pinPoint, and background as first-class Scene-level layers alongside Element instances (exempting the non-visual container types from the wrapper rule and widening the reference-scope rule to match), or accept the single-Element limit explicitly. The former is the more consistent with the engine's own model of a Scene.
  • Time-intrinsic primitives under an instance-assigned duration. video, audio, and lottie have a natural length; an Element holding a 3-second video placed as a 10-second instance has undefined behaviour from 3 to 10 seconds (hold, loop, cut), and a targetFraction time range on such a layer implies retiming this page never mentions. Needs a stated rule.
  • Whether the unified Project relocates the old Template/Campaign ambiguity onto where the tier boundaries sit rather than dissolving it. Project as the single unit is decided and load-bearing (its own table family, its own versioning, campaign permission as the bottom rung of one scale); what's open is whether that one scale's boundaries turn out to be the same argument in new clothes (Permissions & Enforcement).
  • Property-level interaction between Elements — one Element's content driving a change in a sibling Element's content, not just its position — has no designed mechanism. Worth keeping the model open to adding it later without a rebuild; explicitly not a requirement for the foundational release.
  • A name for the common "versioned, forkable thing" spanning an Element instance, a Scene instance, and a campaign fork of a Project. Tentative working name: Composer Item, not locked. "Composition" was considered and rejected outright — it collides with an existing, unrelated retail-studio use of the word. Fine to keep bikeshedding later; blocks nothing.