Introduction
This book is the primary reference for the Altia BluePrint language, ABP. It describes each construct of the language, what it means, and the rules a document must follow to be accepted.
ABP is a declarative language for describing a human–machine interface (HMI). A BluePrint document is a tree of UI components: the properties on each, the events each one responds to, and the actions those events run. Property values are expressions, and they are reactive: when a value an expression reads changes, the expression is evaluated again and the screen follows.
The surface syntax is YAML with three deliberate restrictions, and expressions are written in a subset of JavaScript. Drawing, layout, animation and custom GPU materials are part of the language; I/O and heavy computation are left to the host application, which reaches the design through Data objects and external functions.
Scope of this reference
This reference covers the language as implemented by the Altia BluePrint toolchain distributed with the Altia BluePrint extension, version 1.0. It describes:
- the lexical and structural rules of a document (chapters 1–4);
- the expression language, name resolution and evaluation model (chapters 5–7);
- events and actions (chapters 8–9);
- every built-in type and its properties (chapters 10–15);
- how a host application integrates with a design, and how designs are checked and run (chapters 16–17).
It is not a tutorial. Readers new to BluePrint should start with the worked examples throughout chapters 10–15 and the recipes in Appendix E.
Conventions
The words must, must not, should and may are used in their usual normative sense. A document that breaks a must rule is not a valid BluePrint document. Most such rules are enforced by blueprint --check and reported with a diagnostic (Appendix C). A rule the checker does not yet enforce is marked Not diagnosed: a design that breaks it checks clean but may not behave as intended. Where the language accepts a construct silently by design, the text says so explicitly.
intro.rulesRules are labeled with identifiers such as lex.indent.spaces. Each identifier is a stable link target; select one to copy a link to that rule.
Code examples are ABP unless labeled otherwise. Grammar productions use the notation described in chapter 1.
Notes give background or rationale. They are informative, not normative.
Warnings describe constructs that are accepted but do something other than what an author would likely expect.
Reserved callouts describe names that the language accepts but that have no effect in this version. They are listed so that the set of names is closed. Documents should not use them; their behavior may be defined in a future version.
Notation
Grammar
Syntax is summarized in grammar boxes. The notation is:
| Notation | Meaning |
|---|---|
Name | A non-terminal production, defined elsewhere |
"text" | The exact characters text |
x? | Zero or one x |
x* | Zero or more x |
x+ | One or more x |
a | b | Either a or b |
( … ) | Grouping |
[a-z] | Any character in the range |
INDENT / EOL | Leading spaces / the end of a line |
… | Any characters up to the end of the line |
// text | An annotation inside a grammar box; not part of the syntax |
Grammar boxes are a summary. Where prose and a grammar box disagree, the prose is authoritative.
Examples
Examples are complete documents or fragments of one. A fragment may read names declared elsewhere in a design (a Data object such as App or Vehicle, a Font, a theme object) and assumes those declarations exist; an example that shows two forms of the same thing side by side is two alternatives, not one document. Comments in examples explain what a line does. Examples that show an error or a pitfall are marked with a warning bar and a comment beginning # WRONG.
x: parent.x + 10 # WRONG -- child coordinates are already parent-relative
Lexical structure
An ABP document is a UTF-8 text file, conventionally with the extension .abp. Its syntax is a restricted form of YAML: every non-blank line is a mapping entry of the form key: value, and hierarchy is expressed by indentation.
Relationship to YAML
lex.yaml.restrictABP is YAML with three constructs removed. A document must not use:
- block sequences (
- item): child components are nested by indentation, never listed; - anchors and aliases (
&name,*name): reuse is expressed with type definitions (§4.2); - tags (
!type): the type of an object is given by its key.
lex.yaml.valuesUnlike YAML, a value is never interpreted as a YAML scalar. Every value is an expression. In particular an unquoted word is a name reference, not a string.
Lines and indentation
lex.indent.spacesIndentation is made of spaces. Tab characters must not be used for indentation.
lex.indent.nestingA line indented further than the entry above it is a child of that entry. A line at the same indentation is a sibling. The conventional width is two spaces per level. Within one body every sibling must use the same indentation; a line indented by a different amount is attached to a different parent.
lex.blankBlank lines and lines that hold only a comment are ignored.
Comments
lex.comment.hashAt the document level a comment starts with # outside a string literal and runs to the end of the line. It may stand on its own line or follow an entry.
lex.comment.exprInside an expression body — including every line of a | block scalar — the text is JavaScript and comments are // line comments. A # inside an expression body, outside a string literal, is a syntax error: App.s = "a # b" is fine.
# a document comment
Data: App
count: 0
Text: label # a trailing comment
text: "count: " + App.count
@press:
code: |
// a JavaScript comment: this line is inside the block
App.count = App.count + 1
Entries
Every entry is a key, a colon, and an optional value. The key's leading character determines what kind of entry it is; the value's leading character refines it.
lex.entry.shapesThe entry shapes are:
| Shape | Pattern | Meaning | See |
|---|---|---|---|
| Instance declaration | Type: name | Declare an instance of Type named name | §4.1 |
| Type definition | Type: $NewType | Define a new type derived from Type | §4.2 |
| Property assignment | name: <expr> | Assign a property of the enclosing object | §4.3 |
| Array by elements | name: + children | An array property, one named element per line | §4.4 |
| Import | import: "path" | Load another document | §3.4 |
| Event handler | @event: [<cond>] | Handle an event, optionally gated | §8 |
| Action | action: <value> | A step inside an event handler | §9 |
| Animation binding | &name: <expr> | Drive a baked 3D animation (Scene only) | §15.2 |
lex.key.caseA key that begins with an uppercase letter declares a component; a key that begins with a lowercase letter assigns a property. An uppercase key that names no type is an error (decl.unknown-kind). Property keys should be camelCase (maxChars, blurRadius), as every built-in property is. A snake_case key such as elide_text is accepted silently as a user property (decl.prop.user) and matches no built-in property.
lex.key.scopeA :: in key position names a type exported by an aliased import: Ui::Card: cardOne. In value position :: always names an enumeration value (§5.5).
Values
lex.value.onelineA value written after key: ends at the end of that line. It cannot continue onto the next line; this applies to every value, including array literals.
points: [[0, 0], [10, 0], # WRONG -- the value stops
[10, 10], [0, 10]] # at the end of the line
lex.value.blockA value of | starts a block scalar. Every following line indented further than the key belongs to the block, and the lines are read together as one expression body. A block is how a long value, multi-statement code, or GLSL source is written.
points: [[0, 0], [10, 0], [10, 10], [0, 10]] # one line
points: | # or a block
[[0, 0], [10, 0],
[10, 10], [0, 10]]
lex.value.stringsString values must be quoted, with double or single quotes. text: "Hello" is a string; text: Hello reads the object or property named Hello. Numbers must not be quoted: width: "100" is accepted silently as a string, not a number. Not diagnosed
Identifiers
lex.ident.cIdentifiers follow C rules: a letter or underscore, followed by letters, digits and underscores. A declaration name must also contain at least one letter. 1Foo, Foo Bar, Foo-Bar, _ and _9 are not valid names.
lex.ident.digitA declaration name must not start with a digit. This includes the element names of an array written by elements, so 1: 0 is invalid.
lex.ident.caseThe casing of an instance name is unconstrained. The first letter of a type name must be uppercase. Underscores and digits before that letter are allowed, as long as the name does not start with a digit: _Button and _9Gauge are type names, while _button and _ are not.
Reserved names
lex.reserved.listThe following names must not be used as component, property or array-element names. Using one is diagnosed decl.reserved-name, which also covers redeclaring intrinsic properties such as name. Array-element names are not yet fully checked: type, parent, data, strings, images, views, motion and as are accepted there without a diagnostic. Not diagnosed
| Category | Names | Reason |
|---|---|---|
| Declaration keys | import Root as export type parent | Structural keywords |
| Call namespaces | ext Math String Array | Heads of call expressions, not addressable objects |
| Domain names | data strings images views motion | Reserved; see scope.res.byname for their use as a prefix |
| JavaScript keywords | every ECMAScript keyword (if, for, let, return, …) | Conflict with expression syntax |
| Built-in member | length | x.length is always the length built-in |
lex.reserved.nsThe names this, parent, src, root and target have fixed meanings during name resolution (§6.1). Declaring something named this or parent is an error. src, root and target must not be used as names either Not diagnosed: they are accepted, but a declaration with one of them hides the namespace and breaks resolution.
Documents and projects
Top-level structure
doc.top.orderA document's top level holds its imports first, followed by its declarations. Not diagnosed
doc.top.norootThere is no root wrapper. The top-level declarations are themselves the children of the design root. A Root: declaration must not be used; it is not a wrapper. Not diagnosed
doc.top.displayContent that is drawn maps onto a Display. A document that draws declares one or more displays at the top level.
doc.top.handlersEvent handlers are allowed at the top level. A handler at column 0 belongs to the design root; this is the usual place for an @init that starts timers.
import: "imports/theme.abp"
Display: main
width: 1280
height: 480
Font: TitleFont
path: "fonts/NotoSans-Bold.ttf"
pointSize: 32
Text: title
text: "Hello"
font: TitleFont
The project file
A design that spans several files is described by a project file named abp.project.yaml in the project folder. Its entry field names the entry document, relative to the project file. The project file is ordinary YAML.
name: Text-Showcase
entry: main.abp
sourceRoots:
- imports
assets: res
| Field | Meaning | Status |
|---|---|---|
name | Project display name | Reserved |
entry | The entry document, relative to the project file | Used |
sourceRoots | Directories to search for imports | Reserved |
assets | Directory for asset paths | Reserved |
doc.project.entryOf the project fields, only entry is read. name, sourceRoots and assets are accepted, but nothing reads name, imports do not search sourceRoots and asset paths do not resolve against assets.
doc.project.singleA single .abp file with no project file is also a complete design; it is its own entry document.
Path resolution
| Path | Resolves relative to |
|---|---|
An import: path | The importing file's directory |
An import: path beginning @/ | The entry document's directory |
| Any absolute path | Used as written |
Asset paths: Image.img, Font.path, Texture.source, Scene.scene | The entry document's directory |
Imports and modules
A document loaded through import: is a module. Each import: line names one module, optionally followed by an alias; nothing but a comment may follow either (import.parse). The alias qualifies type names (Ui::Card), so it is written as a type name is: an upper-case first letter, then letters, digits and underscores.
import: "imports/widgets.abp" -> Ui # aliased
import: "imports/theme.abp" # unaliased
doc.import.aliasAn aliased import exposes the module's types under the alias, as qualified names: Ui::Card, Ui::CardLabel.
doc.import.mergeAn unaliased import merges the module's type names into the importing document's namespace: Heading: title.
doc.import.onceA module imported through several paths (a diamond) loads exactly once.
doc.import.deriveImported types may be used as bases: Ui::CardLabel: $Badge defines a local type derived from an imported one.
doc.import.transitiveImports are not transitive. A document sees the types of the modules it imports itself, not those of the modules they import. If main.abp uses a type from theme.abp that only buttons.abp imports, main.abp must import theme.abp as well.
doc.import.parseA module with a syntax error keeps its last good types. The error is reported at its location (doc.parse) and on the line that imports the module (import.module-parse). Until the module parses again, importers see the types from its last good parse, or none if it never parsed.
doc.import.liveEditing a type definition updates every instance of it. Children that an instance declares itself are kept.
import: "imports/widgets.abp" -> Ui
import: "imports/theme.abp"
Display: main
width: 1280
height: 480
Heading: title # unqualified: from the unaliased theme.abp
text: "Import Showcase"
Ui::Card: cardOne # qualified: from the aliased widgets.abp
x: 100
y: 60
Ui::CardLabel: label # authored child beside the type's own content
text: "Alpha"
A module provides types only
doc.module.instancesInstances declared at the top level of a module — a Data, Motion, Font, Timer, Display, or named Component — exist only when that module is previewed on its own. A design that imports the module never creates them, and each such declaration is flagged in the module (import.module-instance).
A type in the module that refers to one of those instances by name therefore finds nothing where the type is used, and the instance in the importing design is flagged expr.stamped-unresolved. There are two correct ways to write it. The first is preferred, because it does not depend on every importer declaring the same name.
Make the instance part of the type:
# imports/lib.abp
Component: $Anim
pos: 0
Motion: slide # a child, created with every instance
Step:
Tween:
out: target.pos
to: 100
duration: 300
@init:
start: slide
# main.abp
import: "imports/lib.abp"
Anim: a1
Or have the importing design declare the name the type expects:
# imports/lib.abp
Component: $Anim
pos: 0
@init:
start: slide # resolved where the type is used
# main.abp
import: "imports/lib.abp"
Motion: slide # the name the type expects
Step:
Tween:
out: target.pos
to: 100
duration: 300
Anim: a1
Declarations
Instance declarations
decl.instance.formType: name creates one object of Type named name. Its body, indented one level, holds the object's properties, event handlers and children.
decl.instance.orderWithin a body, properties must precede child declarations. Not diagnosed
decl.instance.anonThe name may be omitted. An anonymous instance cannot be referenced by name; it can be reached only through this and parent.
decl.instance.zorderDeclaration order is draw order: a later sibling draws on top of an earlier one. It is also the order of items in a layout (§12.9), and the order presses are offered in: the topmost object under the pointer gets the first chance (ev.pointer.overlap).
Image: myImage # instance: Image type, named myImage
img: "res/logo.png" # property assignment
Component: $MyButton # type definition: new type MyButton
label: "click me" # default property value
MyButton: okButton # instance of the user-defined type
label: "OK" # override the default
Ui::Card: cardOne # instance of Card from the import aliased Ui
Type definitions
decl.typedef.formBase: $Name defines a new type Name derived from Base. The body has the same form as an instance body. Once defined, Name: instanceName creates instances of it.
decl.typedef.apiProperties declared on the definition are the type's public interface; their values are defaults that an instance may override. Children of the definition are its structure: every instance gets its own copy of them, and that copy can be reached from outside the instance (decl.children.access). Prefer properties as the way other objects use a type: they are what the type promises to keep, while its children are only how it is built today.
decl.typedef.rootlevelA type definition must be at the top level of a document; it must not be nested inside another component.
decl.typedef.nameThe first letter of the type name must be uppercase (leading underscores are allowed), and the name must not reuse a built-in type name: Component: $Text is rejected.
decl.typedef.baseAny component type may be a base, including a user-defined type and an imported type.
decl.typedef.dollarThe $ prefix is reserved for type-definition values. width: $foo is invalid.
Component: $LabeledIcon
iconName: ""
labelText: ""
Image: icon
img: iconName
x: 0
y: 20
Text: label
text: labelText
x: 0
y: 0
LabeledIcon: warning
iconName: "res/warn.png"
labelText: "Warning"
x: 40
y: 60
Reaching into an instance
decl.children.accessAn instance's children can be read and written from outside the instance by name, at any depth. In the example above, warning.icon.x reads the Image's x, and code: warning.label.text = "Alert" changes the Text. Through a nested instance the chain simply continues: panel.row.warning.icon.x.
decl.children.writeA write to a property that has an expression lasts only until that expression is evaluated again. After code: warning.label.text = "Alert", a change to warning.labelText re-evaluates text: labelText, and the Text shows the new labelText. That includes a change made earlier in the same action: code: warning.labelText = "Caution"; warning.label.text = "Alert" ends showing "Caution", because the re-evaluation runs after the action. This is true of every write; it matters most to a writer outside the type, who does not see the expression. For a value that must stay as written, write the type's property (warning.labelText) instead.
decl.children.viapropA chain that continues through a property holding an instance reaches only that instance's properties, not its children. With ref: warning, ref.labelText reads "Warning", but ref.label.text reads undefined, and code: ref.icon.x = 5 changes nothing. A Motion's out is such a chain, through target; that form is not diagnosed either, but the run logs a warning when it starts (motion.target.declared). To reach a child's value this way, declare a property on the type for it. Not diagnosed
decl.children.overrideAn instance declaration overrides the type's properties only; it cannot replace a child. A child declared in an instance's body with the name of one of the type's children is a duplicate name, an error (decl.duplicate-name, decl.names.unique), and that child is not created. The same applies to a derived type that declares a child with the name of one of its base type's children. To vary a child per instance, declare a property on the type and have the child read it; to change it at run time, write it from a code: action (decl.children.access).
LabeledIcon: other
labelText: "Other"
Text: label # WRONG -- decl.duplicate-name: the type already has a label
text: "Changed"
LabeledIcon: other
labelText: "Changed" # the type's label reads labelText
decl.children.namesA child that shares its name with a property its parent inherits, or with a built-in property its parent writes, cannot be reached by that name: the property answers first, so inst.k reads the property and inst.k.n is unresolved. The reverse holds for a component's own name, which answers before its properties: an instance named like one of its own type's properties (Icon: glyph where $Icon declares glyph) hides that property inside the instance. Both are reported as a warning (decl.shadows-property, decl.names.shadow). Give children names that no property uses.
decl.children.outThe reverse does not hold: a definition cannot reach whoever instantiates it. Inside a definition, parent means the definition's own structure, so a chain such as parent.parent.value written to reach the instance's owner resolves to nothing. Declare a property on the type and let the owner bind it at the instance declaration (§6.3).
Properties
decl.prop.assignname: expression binds a property of the enclosing object to an expression. The binding is reactive (§7.1).
decl.prop.userAssigning a name the object's type does not define declares a user property on that object. A user property is an ordinary reactive value, readable and writable like a built-in one. This is how a Data object holds its fields and how a user type declares its parameters.
Component: gauge
sweepAngle: 240 # user property -- nothing built in is called this
rotate: sweepAngle * 0.5
decl.prop.misspellProperty names are not validated against the type. A misspelled built-in property such as widht: 10, or color: "#fff" (the color properties are fg and bg), is accepted silently as a user property and has no effect. By contrast, unknown type, event and action names are rejected.
decl.prop.declaredAn optional built-in property that an object leaves unset (x, y, width, …) is not declared on that object. A chain that reaches it from elsewhere (panel.x) is unresolved, and a Motion cannot drive it. Write the property, at its default, on any object whose property is read by chain or animated.
Arrays written by elements
decl.array.formA property whose value is a nested block is an array, one element per line. This is the only meaning a nested block under a property has; it never declares an object.
borders: [0, 1, 2, 3] # flat form
borders: # element form -- the same four elements
l: 0
b: 1
r: 2
t: 3
decl.array.indexDeclaration order is index order, top down. Both forms address the same storage: borders[0] and borders.l are the same element.
decl.array.sizeA declared array has a fixed number of elements, the number declared. A write of a whole array writes the elements that fit: a longer source is cut, and a shorter one leaves the old elements after it. A value that is not an array is ignored. An empty literal [] declares no elements, so it is not a declared array: the property holds an ordinary array value, and a write of a whole array of any length replaces it, as it does for items: 0.
decl.array.namesElement names are chosen by the author. They are labels and access paths, not schema fields, and nothing checks them against the property. The positional meaning is fixed by the property; for borders it is left, bottom, right, top regardless of the names used.
decl.array.exprElement values are ordinary reactive expressions. An element with children of its own is a nested array, to any depth.
Name uniqueness and shadowing
decl.names.uniqueTwo siblings must not share a name (decl.duplicate-name), and a child must not share its parent's name (decl.parent-name). This applies to array elements too: two elements named l are an error, not an overwrite. It also applies to the children an instance gets from its type: a child declared under an instance, or under a derived type, with the name of one of those children is a duplicate (decl.children.override).
decl.names.shadowA child must not share a name with a property declared in the same scope; that is an error (decl.duplicate-name). An instance or child should not share a name with a property of its type or of its parent — one the type declares or inherits, or a built-in property the type writes: a component answers its own name before its properties, so the name hides the property, or is itself hidden by it. That is reported as a warning (decl.shadows-property).
Component: $Icon
glyph: "?"
Text: t
text: parent.glyph # finds the child below, not the property
Icon: glyph # WRONG -- instance named like its type's property
Event and action names (tick, stop, next, key, …) are not properties and are safe as child names.
Expressions
The right-hand side of every property assignment, event condition and action is an expression in ABP's subset of JavaScript. It follows current ECMAScript rather than one edition, and includes features such as ?? and the at, includes, padStart and trimStart methods. Where a result differs from ECMAScript, §5.6 says so. This chapter defines the subset.
Literals
| Kind | Examples |
|---|---|
| Integer | 42 0xFF8800 (a negative number such as -3 is unary minus applied to a literal) |
| Float | 0.5 1.0 |
| Boolean | true false |
| Null and infinity | null Infinity |
| String | "double" 'single' |
| Template string | `rpm: ${Vehicle.rpm}` |
| Array | [1, 2, 3] [[0.0, 0.0], [1.0, 1.0]] |
expr.lit.templateA template string is always a string, even when it consists of a single ${…}.
expr.lit.noobjectThere are no object literals: {a: 1} is refused.
Operators
expr.op.setThe following operators are supported, with ECMAScript precedence and associativity (except ??, expr.op.nullish), highest first:
| Operators | Kind |
|---|---|
. [ ] ( ) | Member access, subscript, call |
++ -- (postfix) | Increment / decrement |
! ~ + - typeof ++ -- (prefix) | Unary |
* / % | Multiplicative |
+ - | Additive; + concatenates when either side is a string |
<< >> >>> | Shift |
< > <= >= | Relational |
== != | Equality |
& | Bitwise AND |
^ | Bitwise XOR |
| | Bitwise OR |
&& | Logical AND |
|| | Logical OR |
?? | Nullish coalescing: evaluates to the right side only when the left is null or undefined |
? : | Conditional |
= += -= *= /= | Assignment |
expr.op.nullish?? binds more loosely than ||. Unlike ECMAScript, ABP accepts ?? mixed with || or && without parentheses; add parentheses to make the grouping clear.
expr.truthyA value is truthy unless it is false, 0, -0, NaN, "", null or undefined, as in ECMAScript. Every other number, such as 0.5 or Infinity, every object, and every array, including [], is truthy. !, &&, ||, ? :, @if, event conditions, visible and enabled use this rule.
expr.op.equalityThere is no strict equality. === parses but is refused (expr.generate, “Unsupported expression construct”); !== is a parse error (expr.parse). Use == and !=.
expr.op.compoundThe only compound assignments are += -= *= /=. Every other compound form (%= **= &= |= ^= <<= >>= >>>= &&= ||= ??=) is a parse error.
expr.op.writesAssigning a property or array element, and ++/-- on one, is a write. Writes are legal only in a code: action (§7.2). Assigning a let local is not a write and is allowed in any expression body.
Statements and declarations
expr.stmt.setAny expression body may contain let and const declarations (const is not enforced, expr.diff.const) and the statements if/else, for, while, do, switch, break and return. Statements are separated by newlines or semicolons. return ends the body with its value, and no statement after it runs. In a code: action it ends that action; the handler's next action still runs.
@complete:
code: |
let next = App.count + 1;
App.count = next > 9 ? 0 : next;
expr.stmt.blockStatements may be grouped in braces. A braced block at the top of a body gives the same value as its statements without the braces, so a short body fits on one line.
total: { let s = 0; for (let i = 0; i < 5; i++) { s = s + i; } s } # gives 10, the same as these statements in a | block
expr.stmt.valueA body gives the value of the last statement that ran, as in JavaScript. An if, a loop or a switch gives the value of the last statement that ran inside it, or undefined when none did, never its condition or a case test. A value with no statements at all, empty or only a comment, is null, which reads as 0.
size: { if (D.n > 2) { "many" } else { "few" } } # gives "many" or "few"
last: { for (let i = 0; i < 3; i++) { i * 10 } } # gives 20, the last pass's value
Calls
expr.call.kindsExactly three kinds of callee exist. A call that is none of them — a user function, or an unknown built-in or action name — is refused when the design loads (expr.generate, “Unknown or unsupported function call”).
Built-in functions
expr.call.builtinBuilt-ins come from a closed table. They write nothing, so they are legal in any expression body. All of them except Math.random are pure: the same arguments always give the same result.
| Receiver | Members |
|---|---|
Math functions | abs ceil floor round trunc sign min max random sin cos tan asin acos atan atan2 sqrt cbrt pow hypot exp log log2 log10 |
Math constants | PI E LN2 LN10 LOG2E LOG10E SQRT2 SQRT1_2 |
| String methods | at concat includes indexOf lastIndexOf slice substring startsWith endsWith padStart padEnd repeat trim trimStart trimEnd toUpperCase toLowerCase charAt |
| Array methods | at concat includes indexOf slice join |
| Number methods | toFixed(digits) |
| Length | s.length, arr.length |
expr.call.utf8String indices and lengths count bytes of UTF-8, not UTF-16 code units. Case mapping applies to ASCII letters only.
expr.call.tofixedtoFixed clamps digits to 0–15 and rounds the decimal as written, so 1.005.toFixed(2) is "1.01".
Action calls
expr.call.actionAn action call applies a component verb to a receiver: myTimer.start(), group.nested.pause(). Action calls act, so they are legal only in a code: action. The verbs are those of chapter 9.
expr.call.motionA Motion verb takes the object to animate as its first argument: slide.start(panel). Without one the call does nothing.
expr.call.arityArgument counts are not checked. Extra arguments are ignored (myTimer.start(5) starts the timer); a missing argument reads undefined.
External functions
expr.call.extAn external function is implemented by the host and called through the reserved ext namespace, in exactly the form ext.name(…). It is never a method on an object and never nested (ext.a.b() is refused). External calls are legal only in a code: action.
expr.call.extruntimeExternal names are resolved at run time, so an unknown name is never refused at load. A call that no host answers reads undefined. See §16.2.
Enumeration values
expr.enum.formAn enumeration value is written EnumType::Value as one contiguous token, with no spaces around ::. Value names are capitalized: Justify::Center, ShapeType::Ellipse, Button::Right.
expr.enum.dotThe dot form Justify.Center is not an enumeration value. A . chain resolves through scope; :: resolves through the enumeration tables; the two never mix. An enumeration value must carry its type prefix: flexDirection: Row reads a name Row.
All enumerations are listed in Appendix B.
Differences from JavaScript
expr.diff.listThe following parse and run, but give a result that is not the ECMAScript result. None is diagnosed, so write around each one.
expr.diff.constconst is not enforced; it behaves as let. const c = 1; c = 2;, and also c += 1 and c++, assign 2. Not diagnosed
expr.diff.realA real keeps 45 significant bits, about 13 decimal digits; ECMAScript keeps 53. 0.1 + 0.2 is 0.29999999999998295, and a whole number above 245 (35184372088832) loses its low digits: 1e15 + 1 == 1e15 is true. A bitwise operator reads the rounded value, so 1e21 | 0 is -570425344. Not diagnosed
expr.diff.compare== and != compare reals with a relative tolerance of 2-44. < and > compare exactly, and <= and >= are < or > or the tolerant ==. So 0.1 + 0.2 == 0.3 and 0.1 + 0.2 < 0.3 are both true, and 0.1 + 0.2 > 0.3 is false. Integers in the 32-bit range compare exactly, Infinity equals only itself, and NaN equals nothing. Do not test a computed real for equality; compare with a margin: Math.abs(a - b) < 0.001. Not diagnosed
expr.diff.arrayeq== does not convert an array to a primitive. [1] == 1, ["1"] == 1 and [1, 2] == "1,2" are false. Compare an element, or join() first. Not diagnosed
expr.diff.numlitOnly decimal and 0x number literals are read. 0b101 reads 0 and 1_000 reads 1; 0o17 is not a number. Not diagnosed
expr.diff.templateA template string drops its escape sequences: `a\nb` is "ab" and `\u0041` is "". Quoted strings decode escapes, so join one in: a + "\n" + b. Not diagnosed
expr.diff.nulA NUL character ends a string: "a\u0000b".length is 1. Not diagnosed
expr.diff.strlimitThe string limit of 65535 bytes applies to some operations only. repeat, padStart, padEnd and concat give undefined past it; + and template strings do not stop there. Keep strings small. Not diagnosed
expr.diff.indexA fractional index is truncated toward zero: [1, 2, 3][1.5] is 2 and "abc"[1.5] is "b", where ECMAScript gives undefined. Round a computed index: list[Math.floor(n / 2)]. Not diagnosed
expr.diff.trailingcommaA trailing comma in an array literal adds three null elements: [1, 2,].length is 5 and [1, 2,].join("|") is "1|2|||". With one element, [1,] is a parse error (expr.parse). Write no comma after the last element. Not diagnosed
Unsupported constructs
expr.unsupported.listThe following are not part of the subset:
| Construct | Diagnostic | Alternative |
|---|---|---|
var | expr.parse | let or const |
** | expr.parse | Repeated multiplication or Math.pow |
?. optional chaining | expr.parse | Not needed: a hop through a non-object yields undefined instead of throwing |
=== | expr.generate | == |
!== | expr.parse | != |
Compound assignments other than += -= *= /= | expr.parse | Write the assignment in full: a = a % b |
continue | expr.resolve (read as an identifier) | Restructure the loop |
| Object literals, comma expressions | expr.generate | Arrays; separate statements |
| Function expressions and function declarations | expr.generate | Move the work to the host (§16.2) |
Arrow functions, new, void, in, instanceof, try/catch, regular expressions | expr.parse | Move the work to the host (§16.2) |
Classes, import/export, promises, DOM | Not supported | Move the work to the host |
Examples
width: 42 # numeric literal
text: "rpm: " + Vehicle.rpm # string concatenation
visible: App.ready && App.count > 0 # boolean compound
font: App.useBold ? BoldFont : BodyFont # conditional selecting an object
corners: [[0.1, 0.0], [0.9, 0.0], [1.0, 1.0], [0.0, 1.0]]
justify: Justify::Center # enumeration value
Names and scope
Reserved namespaces
| Prefix | Resolves to | Example |
|---|---|---|
this | The object that owns the expression | this.width |
parent | The parent of this | parent.height |
src | The source object of the event being dispatched (§8.3) | src.deltaX |
root | The design root | root.userProp |
target | Inside a Motion only: the object a run animates (§13.3) | target.x |
scope.ns.linksEvery object also has the properties parent, sibling and child: its parent, its next sibling under that parent, and its first child. They are read like any other property (card.child.x) and are computed from the design's structure. Intrinsic Protected A write to them is ignored (eval.writes.readonly).
scope.ns.conventionthis and parent are reserved names. src and root are conventions of name resolution rather than keywords: src is a property the event exposes while it dispatches, and root is the name of the design root. They must not be reused as names (lex.reserved.ns).
Resolving names
scope.res.walkAn unprefixed identifier is resolved through scope: first the owning component's own properties and children, then each ancestor's properties and children, nearest first. This is how a child reads a property its parent declares.
scope.res.chainOnly the first name of a chain walks scope. A name after a . is a member of the object before it and is looked up on that object alone: this.x, parent.x and card.label.x never fall back to an ancestor's x.
scope.res.unsetA built-in property that an object leaves unset is not declared on it, so a chain that reads it (warningLamp.alpha with no alpha: on warningLamp) is reported unresolved, and reads the default at run time, while a write or a Motion through it changes nothing. Write the property, at its default, on any object whose property is read, written or animated through a chain (decl.prop.declared). A bare name is not answered by an unset built-in either: enabled == false inside a type that leaves enabled unset walks past the instance to the nearest ancestor that declares enabled (scope.res.walk), so a type that reads its own built-in by bare name must declare it.
scope.res.unresolvedIf a chain names nothing it is unresolved and is diagnosed expr.resolve. At run time, reading it yields nothing and writing it changes nothing, with one exception: a chain ending in a built-in property the object leaves unset reads that property's default (alpha reads 255, x reads 0).
scope.res.bynameObjects are addressed by their declared names: Vehicle.speed. Domain prefixes such as data. or strings. must not be used. They are accepted silently, and data.speed resolves against the first top-level object of that kind, which is rarely the one intended. Not diagnosed
The type boundary
scope.type.parentInside a type definition, parent stops at the type boundary: it means the definition's own structure, not the instantiator.
scope.type.deferredSome chains in a type body cannot be judged where they are written, because they depend on the instance: parent.x when the definition does not declare x, and a member of a property the type declares (model.value with model: 0). These are not reported in the body. Each instance resolves its own copy, and a chain the instance's site cannot satisfy is reported on the instance declaration as expr.stamped-unresolved; the message quotes the chain and names the segment where resolution stopped (parent.zz.qq names zz).
scope.type.onceA chain binds once. A segment that names nothing is dead for the life of that binding; it does not start working later. Rewriting a chain as a subscript (parent["x"]) does not help and breaks the binding.
The correct pattern gives the type its own property and lets the owner bind it, one hop, at the instance declaration:
Component: $Slider
value: 0 # the type's own, drawn by its children
Shape: fill
points: [[0, 9], [200 * parent.value, 15]]
Component: $Row
value: 0
Slider: sl
value: parent.value # the owner binds it -- always valid
Scope inside handlers
scope.handler.transparentEvent handlers and actions are scope-transparent. Inside a condition or an action body, this is the component that owns the handler and parent is that component's parent. The event's own payload is reached through src.
Evaluation model
Reactive bindings
eval.reactive.defaultProperty bindings are reactive. When a value changes, every property expression that reads it is evaluated again, and anything that reads those properties follows in turn. A change to a Non-reactive property is the exception: it re-evaluates nothing (eval.propclass).
Data: Vehicle
speed: 0
Text: speedReadout
text: Vehicle.speed + " km/h" # re-evaluates whenever Vehicle.speed changes
eval.handlers.ondemandEvent handlers are the exception. A handler's condition is evaluated when its trigger fires, and an action body when it runs; neither is re-evaluated when something it reads changes. A condition therefore sees state as of the moment of dispatch. The one reactive trigger is @if (§8.2).
eval.reactive.frameProperties are updated in one pass per frame. A write changes the property written at once. The properties computed from it are evaluated again in the next update pass, not at the moment of the write.
The figure shows the parts of a frame and where each kind of write is picked up; the rules below refer to it.
eval.handlers.readsA property computed from a value written since the last update pass keeps its old value until the next update pass. A code: statement that reads it after the write gets the old value, and so does a later handler in the same part of the frame, such as a second @tick. A Motion that the handler starts reads the old value too: its @start handler, and the guards, to: and from: of its first Step (§13.1). Compute from the value you wrote, not from the property computed from it.
Data: A
s: 0
scaled: s * 10
copy: 0
Shape: writer
kind: ShapeType::Rect
points: [[0, 0], [200, 100]]
@press:
code: |
A.s = 2
A.copy = A.scaled // 0: scaled is not evaluated again until the next update pass
A.copy = A.s * 10 // 20: computed from the value written
eval.handlers.framesWhen the writes of a handler reach the properties computed from them depends on when the handler runs:
@initwhen the design starts,@tick, and the@completeof aTimeror aMotionrun before the update pass of their frame. Their writes reach the computed properties in the same frame.@ifhandlers run at the end of the update pass, after every other property is updated. Their writes reach the computed properties in the next frame. Each further@ifin a chain adds one frame: an@ifthat writesA.s, and a second@if: A.s > 1that writesB.t, update a property computed fromB.ttwo frames after the first handler runs.- Pointer handlers such as
@pressrun when the frame handles input, after its update pass. Their writes reach the computed properties in the next frame.
eval.reactive.drawEach frame ends with a draw. A property that a handler writes is drawn at the next draw, so a write from an @if or pointer handler is drawn in the same frame. A computed property is drawn after the update pass that evaluates it, so a property computed from that write is drawn one frame later.
Property classes
eval.propclass.listEvery built-in property belongs to one or more of these classes. The property tables mark each class with its badge; a property with no badge is Reactive.
| Class | Badge | Meaning |
|---|---|---|
| Reactive | none | Authored. A code: action may write it, and so may a host where host.data.write allows. A change re-evaluates every expression that reads it (eval.reactive.default) |
| Read-only | Read-only | Fixed by its authored value for the life of the design. An expression in it is evaluated when its object uses it, such as an operation's to when its Step starts, and not when something it reads changes. A write is ignored |
| Protected | Protected | Written only by the runtime, such as a Timer's remaining. Readable everywhere; a write is ignored. Reactive unless it is also Non-reactive |
| Non-reactive | Non-reactive | A change re-evaluates nothing. An expression that reads it sees the current value the next time it is evaluated for another reason. Read it from a handler or @tick, which evaluate when they run |
| Intrinsic | Intrinsic | Supplied by the runtime from the design's structure, such as parent. It has no authored value and is not stored |
eval.propclass.combineClasses combine. The pointer payload and a Motion's target, time and step are Protected and Non-reactive; a Timer's active and remaining and a Motion's running are Protected and reactive; parent, sibling and child are Intrinsic and Protected.
Text: countdown
text: "left " + beat.remaining # Protected, reactive: follows the Timer
Text: progress
text: "at " + puck.slide.time # WRONG -- Non-reactive: stays at its first value
@tick:
code: this.text = "at " + puck.slide.time # read it where it is evaluated
Writes belong to code actions
eval.writes.ruleAn expression that writes state is a procedure, not a value. It is legal only as the body of a code: action. In any other position — a property value, an event condition, a log: body — it is refused (expr.side-effects).
eval.writes.whatWrites are: =, the compound assignments and ++/-- applied to a property or array element; action calls; and external ext.* calls. Reads are unrestricted, and assigning a let local is not a write.
Data: state
count: 0
Text: readout
text: "count: " + state.count # value: a pure read
@press:
code: state.count += 1 # procedure: the write is allowed here
text: state.count += 1 # WRONG -- write in a value slot
eval.writes.readonlyA write to a Read-only or Protected property (eval.propclass) is ignored. While the design runs, the first such write to each property logs 'beat.active' is read-only; the write is ignored, once per property per run. --check and the editor report the write as expr.read-only-write (warning) when they can tell which property it reaches. A host write is refused with read-only (host.data.write). A Motion operation whose out or orient names such a property drives nothing, and the run logs motion: `out` names read-only 'remaining'; it is not driven.
Run mode
eval.run.handlersEvent handlers dispatch only while the design is running. Rendering and property propagation remain live when it is stopped, so an editor shows a live design whose handlers are silent.
eval.run.init@init is delivered to the whole tree when the design starts, and again on every restart. It is not a construction-time hook.
Events
Handler syntax
ev.syntax.condAn event handler is an @-prefixed key whose optional value is a condition. The handler runs only if the condition is truthy when the trigger fires. Its children are actions.
ev.syntax.caseEvent names are camelCase and must be spelled exactly as listed: @press, @doubleTap. @Press and on-press are not event names.
ev.syntax.whereA handler may be placed on any component body and at the top level. It must not be placed under a property, or nested inside another handler or action.
Component: powerButton
@press:
log: "pressed"
@release: App.armed # condition-gated handler
code: App.power = !App.power
Triggers
ev.triggers.closedThe set of event names is closed. Any other name is rejected.
| Trigger | Fires | Status |
|---|---|---|
init | When the design starts, and on every restart | Active |
start complete | A Timer (re)starting and expiring (§10.9); a Motion run beginning and ending, delivered to the object the run drives (§13.4) | Active |
enable disable | On the object whose enabled property changes value, by binding, by code:, or by the enable/disable actions. Writing the value it already has fires nothing. They never fire on a Motion or a Display | Active |
tick | Every frame, on the object that declares it | Active |
if | Reactively, whenever its condition re-evaluates to a truthy value | Active |
press hold release click tap doubleTap enter leave hover hoverX hoverY drag dragX dragY | Pointer input (§8.3) | Active |
show hide move moveX moveY next prev first last select unselect key | Never | Reserved |
The reserved triggers are accepted but never fire, so a handler on one is silently inert. To react to a value, put the expression in the property that consumes it, or use @if.
ev.triggers.noneThere is no wildcard trigger, and there are no swipe or fling triggers. A swipe is a condition over the release payload (@release: src.xVelocity < -200); a fling is a Motion started from that handler with the release velocity (§13.8).
The @if trigger
ev.if.reactiveThe condition of @if is re-evaluated whenever a value it reads changes, and the handler runs each time that re-evaluation is truthy. It is the way to run an action when a value changes, as opposed to merely displaying the value.
ev.if.cond@if must be given a condition; without one it never fires. Not diagnosed
ev.if.initial@if does not fire for the state at startup, only when a value it reads changes. To act on the initial state as well, handle @init too.
Data: Vehicle
speed: 0
Text: warning
text: "SLOW DOWN"
visible: false
@if: Vehicle.speed > 120
code: visible = true
A condition that always reads true, such as @if: nav.tab >= 0, runs on every change of the values it reads.
The @tick trigger
ev.tick.frame@tick fires once per rendered frame. It ignores any Timer period. To run something once per period, handle the timer's @complete.
Pointer triggers
ev.pointer.semanticsThe pointer triggers behave as follows:
| Trigger | Fires when |
|---|---|
press | A button goes down over the object |
release | Every button-up of the button that armed the gesture, with the final payload |
click | Press then release without a drag, of any duration |
tap | Press then release within 200 ms |
doubleTap | A tap whose release comes within 1000 ms of the previous tap's release. It replaces that second tap: no @tap fires for it |
hold | A press lasts 500 ms without dragging |
enter leave | The pointer enters or leaves the object |
hover hoverX hoverY | The pointer moves inside the object while no press on that object is being tracked; the axis variants only when that axis moved in the delivery |
drag dragX dragY | The pointer moves while pressed, past a 2 px dead zone; the axis variants only when that axis moved |
ev.pointer.viewablePointer triggers fire only on viewable objects. On a logical object such as a Timer or Data they never fire.
ev.pointer.nosuppressclick, tap and doubleTap do not suppress release; both fire. A doubleTap does replace the tap it completes.
ev.pointer.timingGesture timing is fixed and cannot be changed from ABP: tap 200 ms, double-tap 1000 ms, hold 500 ms, drag dead zone 2 px.
ev.pointer.buttonButton identity is part of the payload, not the trigger. Every button arms the gesture machine; a condition selects a button with src.button == Button::Right. The values are Button::None, Button::Left, Button::Middle and Button::Right.
ev.pointer.disabledA component whose enabled property is false is transparent to pointer input and stops receiving @tick.
ev.pointer.effectsInside a buffered, blurred or masked component, a child takes pointer input only inside the box (view.effects.size), not where the box clips it. A mask's stencil shape is not consulted.
ev.pointer.overlapWhen objects overlap, a press goes to the topmost object under the pointer that has a pointer trigger. If that object has press, hold, release, click, tap, doubleTap or drag (any axis form), it takes the press, and the drag and release that follow, and nothing beneath it receives them. An object with only enter, leave or hover triggers, or with no pointer triggers, lets the press through to the next object down. Pointer movement is not blocked: objects beneath still get enter, leave and hover unless a press on the object above is in progress. An object takes input over its box (the object and its children, cut by clip windows and by the box of a buffered, blurred or masked component, ev.pointer.effects), not over its painted shape. An object that is hidden (visible: false, or a hidden ancestor) or not enabled is skipped. alpha does not count: an object at alpha: 0 still takes presses, so bind enabled: alpha > 0 on anything that fades out.
The src payload
ev.payload.propsDuring a pointer dispatch, src exposes:
| Property | Meaning |
|---|---|
x y | Protected Non-reactive Current pointer position |
startX startY | Protected Non-reactive Position at press |
lastX lastY | Protected Non-reactive Position at the previous delivery |
deltaX deltaY | Protected Non-reactive Movement since the previous delivery |
xVelocity yVelocity | Protected Non-reactive Velocity in pixels per second |
startTime currentTime | Protected Non-reactive Timestamps of the press and of this delivery |
button | Protected Non-reactive A Button value |
ev.payload.lifetimeThe payload is valid only during a pointer dispatch: src is cleared when the handler returns. In other handlers src is a different source — the Motion in a run's @start/@complete, the handler's own event object in @if, which has no pointer fields, and nothing in @tick or a timer's events. src itself is Protected Non-reactive.
ev.payload.nonreactivePayload properties are Non-reactive (eval.propclass). An expression that reads them is not re-evaluated as the pointer moves; read them in the handler.
ev.payload.velocityOn @release the velocity is the drag's final sample. A flick keeps its last real sample; a pause before letting go reads zero.
Data: Nav
page: 0
Shape: ball
kind: ShapeType::Ellipse
points: [[0, 0], [40, 40]]
x: 0 # declared, so the handler can move it
y: 0
@drag:
code: |
x = x + src.deltaX
y = y + src.deltaY
@release: src.xVelocity < -200 # a leftward swipe
code: Nav.page = Nav.page + 1
Actions
Action syntax
act.syntax.bareActions are bare lowercase keys inside an event handler. They are not prefixed: !Log: and !Code: are not actions.
act.syntax.oneEvery action takes exactly one value, beginning on the same line. code and log evaluate their value; every other action takes the object it acts on. An action must not have children. Not diagnosed
act.syntax.seqA handler's actions run in order, top to bottom.
act.syntax.silentAn action whose value names nothing is diagnosed expr.resolve. An action whose value names an object it cannot act on — start on a Data object, for example — does nothing, without a diagnostic.
@init:
start: blink # start the Timer named blink
log: "screen up" # log a string
Action reference
act.list.closedThe set of action names is closed. Any other name is rejected.
| Action | Value | Effect | Status |
|---|---|---|---|
code | An expression or a | block | Runs it. The only place state may be written (§7.2) | Active |
log | An expression | Logs its value | Active |
start pause resume stop reset restart complete | A Timer or a Motion | Timer control (§10.9); Motion run control on the handler's owner (§13.4). On any other object they do nothing | Active |
enable disable | Any object | Writes the target's enabled property, which fires @enable/@disable. A disabled component is input-transparent and stops ticking | Active |
show hide select unselect next prev first last input anim call set | — | None | Reserved |
The reserved actions are accepted but do nothing. In particular, set: does not assign a property — write code: obj.prop = value — and show:/hide: do not toggle visibility — bind visible to an expression or write it from a code: action.
Built-in types
The set of types that may be declared is small and closed. Every type in this chapter either draws (a viewable, with the common surface of chapter 11) or provides a resource or behavior (a logical type).
Overview
| Type | Kind | Purpose | See |
|---|---|---|---|
Display | Logical, top level only | A screen | §10.2 |
Font | Logical | A typeface at a size | §10.3 |
Component | Viewable | Grouping; base for user types | §10.4 |
Image | Viewable | PNG or SVG picture | §10.5 |
Text | Viewable | Single- or multi-line text | §10.6 |
Shape | Viewable | Geometric primitives | §10.7 |
Stamp | Viewable | Draws another component by reference | §10.8 |
Timer | Logical | Periodic counter | §10.9 |
Data | Logical | Named values; the host interface | §10.10 |
Layout | Logical | Flexbox arrangement | §12 |
Motion | Logical | Reusable animation program | §13 |
Shader Material Texture | Logical | Custom GLSL drawing | §14 |
Scene | Viewable | Pre-baked 3D scene | §15 |
type.scopedSome type names belong only inside a particular parent: Step, Set, Tween, Path, Spring and Wait inside a Motion (§13), and MatVar inside a Material (§14.3). They must not be declared elsewhere, where they do nothing. Not diagnosed
type.userIn addition, any type the document defines with $Name (§4.2) or imports may be declared.
type.reservedThe type names View, DeckView, ModelView, StringTheme and ImageTheme are accepted but not implemented: the view types render their children as a plain Component and ignore their own properties, and the theme types do nothing. Event and Action cannot be declared (decl.event-syntax); events and actions are written with the @ and bare-key forms. Container and Region are not types (decl.unknown-kind).
Object, Root, State, StateAssignment, Gesture, GestureConfig, InputEvent and MotionOp are internal type names. They are accepted without a diagnostic but have no authorable meaning, and must not be used. Not diagnosed
Display
Declares one screen.
type.display.rootA Display must be declared at the top level. Declaring one inside a component or a type definition is an error.
| Property | Type | Default | Meaning |
|---|---|---|---|
width | real | — | Screen width in pixels |
height | real | — | Screen height in pixels |
bg | color | engine default | Fill shown wherever nothing is drawn |
type.display.mapTop-level content maps onto a display through its display: property (§11.2). Content with no display: maps onto the first display declared.
Display: main
width: 1280
height: 480
bg: "#0E130F"
Font
A typeface resource: one face file at one point size. Not viewable; it has no children. A Text refers to it through font:.
type.font.rootDeclare a Font only at the root level of a document, and give it a name no other Font in the design uses, imported files included.
| Property | Type | Default | Meaning |
|---|---|---|---|
path | string | — | Face file, relative to the entry document's directory. The file determines weight and style |
pointSize | unsigned | 12 | Size in points |
bold | bool | false | Stored; no effect in this version. Use a bold face file |
italic | bool | false | Stored; no effect in this version. Use an italic face file |
stroke | real | 0 | Stroke width in pixels, 0–100; 0 draws no stroke |
Font: TitleFont
path: "fonts/NotoSans-Bold.ttf"
pointSize: 32
type.font.strokeA Text using a font with a non-zero stroke draws the stroke in its fg and the glyph fill in its bg. With no bg set anywhere above it, the fill is white.
A Text with no font: renders in a built-in default face.
Component
type.component.defComponent is the base viewable container. It groups children and carries the whole common surface (chapter 11), but draws nothing of its own. It is the usual base for user-defined types.
Component: card
x: 10
y: 10
width: 200
height: 100
clip: true
Image: backdrop
img: "res/card.png"
Text: label
text: "Hello"
x: 12
y: 12
Image
Displays a picture: a PNG raster or an SVG. An Image may have children; they draw on top, parent-relative.
| Property | Type | Default | Meaning |
|---|---|---|---|
img | string | — | Picture path, relative to the entry document's directory: a .png or a .svg |
scaling | enum | Scaling::Scale | How the picture fills the width by height box (below) |
borders | array | 0 | Nine-slice insets in pixels: [left, bottom, right, top], or one number for all four edges. All zero disables nine-slice |
Sizing a picture
type.image.boxwidth/height are the picture's box, and scaling picks how the picture fills it:
scaling | The picture is drawn |
|---|---|
Scaling::Scale (default) | width by height, each axis on its own |
Scaling::None | at its natural size |
Scaling::Aspect | at the largest uniform size that fits the box |
Scaling::X | width wide; the height keeps the aspect ratio |
Scaling::Y | height tall; the width keeps the aspect ratio |
type.image.naturalAn axis with no size (unset, or below 1) keeps the picture's natural size, so an Image with no width/height draws its picture as the file has it.
type.image.centerA picture that does not match its box is centered in it. X and Y can overflow the box on the free axis; clip: true crops to the box.
type.image.resizeA nine-slice (borders non-zero) fits the same way, with its corners held while the edges and center stretch. In a layout slot the slot is the box (§12.7). scaling fits the picture only; scale (§11.3) scales the whole component, children included, after the fit.
Image: panel # nine-slice: corners hold, center stretches
img: "res/panel.png"
width: 400
height: 240
borders: [8, 8, 8, 8]
Image: hero # a 400 x 300 picture
img: "res/hero.png"
width: 200
height: 200
scaling: Scaling::Aspect # drawn 200 x 150, centered in the box
Grayscale PNGs are coverage masks
type.image.grayscaleA PNG with no color channels loads as an alpha raster and is drawn in the component's fg. In a gray-only file the gray value is the coverage, white being fully covered; in a gray-plus-alpha file the alpha channel is the coverage. Color PNGs draw as they are.
type.image.grayfgA mask image without its own fg draws in the nearest ancestor's fg, or black if none is set; bg plays no part. Bind fg to a theme value and one anti-aliased asset re-tints for every theme.
Data: Theme
onPrimary: "#FFFFFF"
Image: check
img: "res/mask/check.png" # grayscale: coverage only
fg: Theme.onPrimary # the color it draws in
SVG pictures
type.image.svgsizeAn .svg draws as anti-aliased vector art at the size the SVG declares (its width/height, else its viewBox). The sizing rules are those of a PNG: width/height with scaling, scale and a layout slot all scale it, and it stays crisp.
type.image.svgcolorfg replaces every stroke color and bg every fill color, including an fg inherited from an ancestor. Unset, the SVG draws its own colors.
type.image.svgbordersborders is ignored on an SVG, with one warning.
type.image.svgonlyOnly Image shows SVGs. A Texture source, or a texture named by a Scene's .a3d, must be a bitmap; an SVG there is reported and samples nothing.
type.image.svgfeaturesUnsupported SVG features are dropped with a warning naming them: <text>, <image>, <use>, stroke-dasharray, markers, and theme-object theming. Some of them change the picture:
<style>andclass: a shape styled only through a class fills black. Put the style on the element instead, asstyle="fill:…".clip-path: the shape draws unclipped.visibility: the element draws. Hide it withdisplay="none".spreadMethod="reflect"and"repeat": the gradient pads.- A colour written as
rgba(),hsl(),hsla()orcurrentColor, or a name that does not resolve, draws grey (#808080). Write it as#hexorrgb(), withfill-opacityorstroke-opacityfor transparency.
Gradients keep at most 16 stops; more collapse to the first and last.
type.image.svglimitsTwo SVG effects are approximated. A stroke under a non-uniform scale draws at one width, the average of the two scales, where SVG draws it wider along the stretched axis. That covers a transform such as scale(3, 1) inside the SVG, and an Image fitted to a box of another aspect ratio. Group opacity applies to each child of the group on its own, so overlapping children show through each other instead of fading as one.
Data: Theme
onSurface: "#E6EDF3"
Image: wifi
img: "res/wifi.svg"
fg: Theme.onSurface # optional: recolors the strokes
Text
Renders a string, as a single line or as a wrapping multi-line block.
| Property | Type | Default | Meaning |
|---|---|---|---|
text | string | — | The string. A number renders as its string form |
lines | unsigned | 1 | 1 single line; 0 multi-line, unlimited; greater than 1 multi-line (the number is not a cap) |
font | object | default face | A Font |
justify | enum | Justify::None | Horizontal placement in the width box: ::None ::Left ::Right ::Center |
maxChars | unsigned | — | Character limit: truncation width (single line) or wrap width (multi-line). Unset, the limit is width in pixels; with no width either, there is no limit |
truncate | enum | Truncate::None | Single-line overflow: ::None ::Clip ::ElideEnd ::ElideStart ::ElideCenter |
elideText | string | … | Elision marker; its first character replaces the default |
wrap | bool | — | Multi-line: wrap long lines at maxChars characters, else at width pixels |
wrapMode | enum | Wrap::Normal | ::Normal, or ::NewLineOnly (break only at \n) |
align | enum | — | Align::Top ::Center ::Bottom. Stored; no effect in this version |
wrapText | string | — | Wrap marker. Stored; no effect in this version |
type.text.justifyJustify::None, the default, starts the text at x with each character at its own offset, and keeps the offset the last mode gave it: changing the font (stroke, point size or face) doesn't move the text. Justify::Left puts the first character's ink, stroke included, on the box's left edge. Outside a layout, Center and Right place the text within width (type.text.layout).
type.text.linesSingle-line text keeps an embedded \n on one line. Multi-line text breaks at every \n regardless of wrap. Truncation applies to single-line text only; wrapping to multi-line text only.
type.text.colorText color is the inherited fg (§11.3). There is no Text-specific color property. With a stroked font, fg is the stroke color and bg the fill color (type.font.stroke).
type.text.measureText is not measured. A Text's width and height are the box the author sets, not the size of the string, and no property reports the measured extent. To center a label, give it a width and justify: Justify::Center.
type.text.baseliney is the bottom of the line box, not the baseline; the baseline sits one font descent above it. pointSize is in points of 4⁄3 px, so glyphs are a third taller in pixels than the number suggests.
type.text.layoutInside a layout, a Text that wraps or truncates at its width (no maxChars) follows the box the layout gives it: when the box is narrower, multi-line text wraps and single-line text clips or elides at the box. width remains the maximum; a wider box leaves the text at width, and a short string keeps its natural width. A Text with no limit, or a maxChars limit, does not change. The glyphs are never scaled to a grown box. justify has no effect on a Text placed by a layout: the text starts at the box's left edge. To centre a Text in a layout, centre it with its container's justifyContent.
Text: readout
text: Vehicle.speed
font: BigFont
width: 280
justify: Justify::Right # right-anchored: digits grow leftward
Text: body
text: "the quick brown fox jumps over the lazy dog"
lines: 0
wrap: true
width: 240
Text: crumb
text: "a/very/long/path/to/something"
truncate: Truncate::ElideCenter
maxChars: 16
elideText: "~"
Truncate::Page and Truncate::Scale exist in the enumeration but have no effect in this version.
Shape
Every geometric primitive is one type, Shape. kind selects the primitive and points is its geometry. A Shape may have children; they draw on top, parent-relative.
| Property | Type | Default | Meaning |
|---|---|---|---|
kind | enum | ShapeType::Rect | ::Rect ::Line ::Polyline ::Ellipse ::Polygon ::Spline ::ClosedSpline ::Arc |
points | array | [] | [x, y] pairs, 16-bit signed (−32768…32767). A Polygon or Polyline vertex may be [x, y, r] with its own corner radius in whole pixels |
filled | bool | true | Fill the interior; only closed kinds enclose an area |
outlineWidth | unsigned | 0 | Outline thickness in pixels; 0 draws none |
svg | bool | true | Anti-alias every edge; false draws hard edges |
radius | real | 0 | Rect, Polygon, Polyline: rounds every vertex with a circular fillet of this many pixels |
startAngle | real | 0 | Arc: start, degrees counterclockwise from +X |
sweep | real | 360 | Arc: degrees swept; positive counterclockwise, negative clockwise; clamped to a full turn |
inner | real | 0 | Arc: inner radius as a fraction 0–1 of the outer |
type.shape.kindnottypeThe selector is kind, not type. type is a built-in property of every object that reports its class.
Points by kind
kind | Points | Meaning |
|---|---|---|
Line | 2 | A straight segment |
Polyline | 2 or more | Connected open segments |
Rect | 2 | Opposite corners |
Ellipse | 2 | Corners of the bounding box |
Polygon | 3 or more | A closed figure |
Spline | 3 or more | An open curve |
ClosedSpline | 3 or more | A closed curve |
Arc | 2 | Bounding box as for Ellipse, cut by startAngle, sweep and inner |
type.shape.keepA list longer than the kind reads is kept, not truncated, so changing kind never loses geometry. Likewise startAngle, sweep and inner are kept on every kind and drawn only by Arc.
type.shape.splineCurves are uniform cubic B-splines: they pass through the first and last control points and are pulled toward the ones between. Explicit Bézier handles are not supported.
type.shape.sizewidth and height do not affect a Shape's geometry; points is the geometry. They keep their common meaning and still drive clip and blur.
Color
type.shape.fgbgOn a Shape, fg is the outline color and bg is the fill color. A Shape whose fg and bg match reads as a plain filled figure; give it a distinct fg and an outlineWidth to show an outline.
Shape: triangle
kind: ShapeType::Polygon
points: [[0, 0], [100, 0], [50, 80]]
bg: "#204060" # fill
fg: "#80c0ff" # outline
outlineWidth: 2
Shape: divider
kind: ShapeType::Line
points: [[0, 0], [200, 0]]
fg: "#404040"
outlineWidth: 1
Shape: dial # points written by elements
kind: ShapeType::Ellipse
points:
bl: [0, 0]
tr: [120, 120]
bg: "#101010"
Anti-aliasing
type.shape.svgsvg: true anti-aliases every edge; svg: false draws the same figure with hard edges. Hard edges suit a large flat Rect such as a backdrop, whose axis-aligned edges gain nothing from anti-aliasing.
type.shape.svgmaterialA Shape drawn through a material always draws hard-edged, whatever svg says; writing svg: true on one logs a warning. A corners warp applies either way.
Rounded corners
type.shape.filletEach rounded vertex becomes a circular arc tangent to both of its edges. Two arcs sharing an edge never use more than that edge, so an oversized radius produces a pill, never an inverted corner. A vertex's own [x, y, r] overrides radius; r of 0 keeps that vertex sharp.
type.shape.rectradiusA Rect takes radius only. Its two points are opposite corners, so an [x, y, r] there rounds nothing and is reported as a warning (shape.rect-vertex-radius). For per-corner rounding, use a four-point Polygon.
type.shape.polylineendsThe end points of a Polyline never round.
type.shape.outlineA sharp Rect's outline is inset when the Rect is unrotated or rotated by a multiple of 90°; at any other angle it is centered on the edge. A rounded Rect's outline is centered on the rounded edge, as a Polygon's is. A wide outline therefore sits half its width further out once the Rect is rounded.
Shape: card # a rounded card
kind: ShapeType::Rect
points: [[0, 0], [200, 120]]
radius: 12
bg: "#1E3A5F"
fg: "#5FB0FF"
outlineWidth: 2
Shape: tab # top corners only
kind: ShapeType::Polygon
points: [[0, 0], [120, 0], [120, 40, 10], [0, 40, 10]]
Arcs
type.shape.arcdefWith default properties an Arc is the ellipse of its box. sweep takes a slice of it and inner hollows the slice: inner: 0 is a wedge to the center, 0.7 a ring segment. Angles follow the rotate convention, counterclockwise from 3 o'clock.
type.shape.arcseamA full ring (a 360° sweep with an inner) fills correctly, but its outline shows a seam at startAngle where the path closes through the ring. Leave outlineWidth at 0 on a full ring, or draw it as two halves.
Data: Fleet
a: 38
b: 24
c: 17
total: Fleet.a + Fleet.b + Fleet.c
Shape: $Seg
kind: ShapeType::Arc
points: [[0, 0], [400, 400]]
inner: 0.7 # 0 here makes a pie
fg: "#0B0F14" # outline in the background color separates segments
outlineWidth: 2
x: 200
y: 160
Seg: s1
bg: "#4F8FE0"
startAngle: 90 # 12 o'clock
sweep: -360 * Fleet.a / Fleet.total # negative: clockwise
Seg: s2
bg: "#3DBE8A"
startAngle: s1.startAngle + s1.sweep
sweep: -360 * Fleet.b / Fleet.total
Seg: s3
bg: "#F2B441"
startAngle: s2.startAngle + s2.sweep
sweep: -360 * Fleet.c / Fleet.totalStamp
type.stamp.defA Stamp draws another component's graphics at its own position, by reference. Nothing is duplicated: one set of objects is drawn twice. This distinguishes a stamp from an instance of a user type, which copies its content.
| Property | Type | Default | Meaning |
|---|---|---|---|
stamp | object | — | The component to draw. Anything that is not a resolvable component draws nothing |
type.stamp.refThe target is referenced, never moved or owned. It keeps drawing where it lives, and deleting the stamp leaves it untouched.
type.stamp.childrenA stamp's own children draw on top of the stamped content.
type.stamp.selfA target that would draw the stamp inside itself — the stamp or any ancestor of it — is refused, and the previous target remains.
In all other respects a stamp is an ordinary viewable, with the common surface, events, user properties and children.
Component: box # the original, drawn where it lives
width: 120
height: 80
Image: a
img: "res/tile.png"
Stamp: boxAgain # the same objects, drawn again here
stamp: box
x: 450
y: 120
Timer
A periodic counter. It draws nothing and has no children.
| Property | Type | Default | Meaning |
|---|---|---|---|
period | unsigned | 100 | Milliseconds |
repeats | bool | false | Reset and run again on expiry |
active | bool | false | Protected Whether it is running |
remaining | unsigned | 0 | Protected Milliseconds left |
type.timer.startA timer does nothing until it is started, with the start action or timer.start() — often from its own @init.
type.timer.complete@complete is the period event: it fires on expiry, and with repeats: true the timer then resets and runs again. @start fires when it starts or restarts, and again each time a repeating timer begins another period.
type.timer.verbsThe actions start pause resume stop reset restart and complete control a timer.
A timer also accepts @tick, but @tick fires every frame and ignores period. It is not the “timer elapsed” event.
Timer: blink
period: 500
repeats: true
@init:
start: blink
@complete:
code: warningLamp.alpha = (warningLamp.alpha == 255) ? 64 : 255
Image: warningLamp
img: "res/warn.png"
alpha: 255 # declared, so the timer can read and write it
Data
type.data.defA Data is a hierarchical container of named values — the state the UI binds to. It draws nothing and has no events. Its fields are user properties; its children are nested Data objects. Fields are referenced through the object's declared name.
Data: Vehicle
rpm: 0
speed: 0
cruise: false
Text: speedReadout
text: Vehicle.speed + " km/h"
type.data.writesFields are written from a code: action (code: Vehicle.cruise = true), by a Motion, or by the host (§16.1).
Common viewable properties
Every viewable type — Component, Image, Text, Shape, Stamp and Scene, and every user type derived from them — has the properties in this chapter.
Geometry
view.coord.systemAll geometry is in pixels. The origin is at the bottom left: +X points right and +Y points up, so a higher y is higher on screen. An object's (x, y) places its own bottom-left corner.
view.coord.relativeChild coordinates are relative to the parent. A child at x: 0 sits at its parent's left edge and moves with the parent. Writing x: parent.x + 10 on a child counts the parent's offset twice; write x: 10.
| Property | Type | Meaning |
|---|---|---|
x | real | Bottom-left X, relative to the parent. Truncated to whole pixels when drawn |
y | real | Bottom-left Y, relative to the parent, +Y up. Truncated to whole pixels when drawn |
width | real | Extent along +X |
height | real | Extent along +Y |
clip | bool | Clip the object and its children to the clip window |
clipRect | array | The clip window [x, y, w, h], from the object's own origin |
view.clip.windowclip switches clipping on and off; clipRect selects the rectangle. The window follows the object wherever it moves or scales. Without a clipRect, or with one of fewer than four values, the window is the width × height box; with no width/height either, clip: true clips nothing.
view.clip.zeroA clip window of zero width shows nothing of the object or its children.
Component: card
width: 200
height: 100
clip: true
clipRect: [0, 0, 120, 100] # left 120 px visible, children clipped with it
view.geometry.layoutInside a layout, x, y, width and height stop applying, and so do rotate, scale and corners: a laid-out component takes its position and size from the arrangement (chapter 12).
Display mapping
| Property | Type | Meaning |
|---|---|---|
display | object | The Display a top-level component renders on. Unset, the first display declared |
view.display.noneContent mapped to no resolvable display is never drawn.
Appearance
| Property | Type | Default | Meaning |
|---|---|---|---|
visible | bool | true | Draw the object and its children. A hidden object takes no pointer input (ev.pointer.overlap) |
enabled | bool | true | False makes the object transparent to input and stops its @tick; a change of value fires @enable/@disable |
alpha | 0–255 | 255 | Opacity; inherited by children. It does not affect input: an object at alpha 0 still takes presses (ev.pointer.overlap) |
fg | color | inherited | Foreground: text color, solid fill color, Shape outline, grayscale-PNG tint, SVG stroke |
bg | color | inherited | Background: Shape fill, SVG fill, stroked-font glyph fill |
scale | array | [1.0, 1.0] | Scale factors [sx, sy] about anchor; [-1, 1] mirrors. Anything but an array of two counts as [1, 1] |
rotate | real | 0 | Degrees, counterclockwise, about anchor |
anchor | array | [0.5, 0.5] | Pivot [ax, ay] as a fraction of the object's authored bounds |
corners | array | — | Non-affine quad warp: four normalized [x, y] pairs in BL, BR, TR, TL order. Unset or invalid is affine |
blur | bool | false | Render the subtree through a blur |
blurRadius | array | [0, 0] | [rx, ry] in pixels, 0–255 |
buffer | bool | false | Cache the subtree as an image |
mask | object | — | A component whose coverage stencils this one |
maskMode | enum | MaskMode::Normal | ::Normal ::Invert ::None |
maskHideSource | bool | false | Stop the mask source drawing on its own |
material | object | — | A Material to draw through (§14) |
z | signed | — | Stored; no effect. Draw order is sibling declaration order |
Anchor
view.anchor.boundsanchor is a UV position over the object's authored bounds. For a Component the bounds are the union of its children's bounds and its own box from its origin to (width, height), so the origin is always inside them. A buffered, blurred or masked component whose effect box (view.effects.size) comes from its width and height or from a layout has that box as its bounds; one whose box follows its contents keeps its children's bounds. A component that holds one counts those bounds. While the contents fit inside the box, switching the effect on or off leaves the pivot where it is; content the box clips is outside the bounds while the effect is on, so the pivot moves with the toggle. One anchor serves both scaling and rotation.
view.anchor.outsideValues outside 0–1 place the pivot outside the bounds; this is how an object orbits or sweeps about an external point. Where the pivot must be exact, set width/height so the bounds are deterministic.
Data: App
dimmed: true
Component: spotlight
x: 100
y: 100
width: 200 # the blurred box
height: 120
rotate: 10 # about the bottom edge's middle, blur on or off
anchor: [0.5, 0]
blur: App.dimmed
blurRadius: [6, 6]
fg: "#66CCFF"
Buffer, blur and mask
view.buffer.sizebuffer caches the subtree as an image. It looks the same as drawing directly, and content changes update the cache automatically.
view.effects.sizeA buffered, blurred or masked component draws inside a box, sized like any component with a size of its own. The box comes from, in order: a layout (the box a parent layout gives it, or its own Layout's flexWidth and flexHeight); then its width and height, when both are set; otherwise its contents. Contents are measured in the component's own coordinates, so its rotate and scale turn and scale the box with them and never change its size. The box follows changes to any of them, and content outside it is clipped. A box that follows its contents starts at the component's origin, so content left of or below it is clipped too.
view.mask.defmask names another component whose alpha coverage stencils this one: only the parts the source covers are drawn. Any viewable may be a source. Only the overlap with this component's own box counts. The source keeps its own place, may mask several components, and the mask follows it when it changes or moves. mask: false, or no mask at all, leaves a plain component that blur or buffer can use, so a binding such as mask: on ? stencil : false switches the mask off and on; a mask that names no valid component, or maskMode: MaskMode::None, keeps the mask and draws the content unmasked.
view.mask.modeMaskMode::Normal shows content where the source is opaque; ::Invert where it is transparent; ::None disables the coverage.
view.mask.hidemaskHideSource: true stops the source drawing on its own, making it a coverage-only stencil. This is the only way to hide a stencil: visible: false on the source makes it stencil nothing. The setting on any component the source masks hides the source for all of them, so set and clear it on the same component.
view.effects.exclusivebuffer, blur and mask are mutually exclusive on one component. If more than one is present when the component first renders, the priority is mask > blur > buffer. Afterwards, whichever is applied first holds the component, and the others take effect only once it is cleared; clearing falls through to the highest-priority remaining effect.
Color values
view.color.formsA color may be written in any of these forms:
| Form | Examples |
|---|---|
| Hex string | "#FF8800" "FF8800"; in the eight-digit form "#00FF8800" the leading byte is ignored, and opacity comes from alpha |
| Integer | 0xFF8800 |
| Array of 0–255 integers | [255, 136, 0] |
| Array of 0.0–1.0 reals | [1.0, 0.53, 0.0] |
Layout participation
| Property | Type | Meaning |
|---|---|---|
layout | object | The Layout this component binds. Unset, children keep their own x/y |
view.layout.bothBinding a layout makes a component arrange its children. If the component is itself inside a laid-out parent, the same record also tells the parent how to place it (§12.1).
Layout
A Layout is a flexbox arrangement resource. It is not viewable and has no children. Assigning one to a viewable's layout: property makes that component arrange its children, which stop honoring their own x/y.
Layout: toolbar
flexDirection: FlexDirection::Row
justifyContent: FlexJustify::SpaceBetween
alignItems: FlexAlign::Center
padding: 12
flexWidth: 600
flexHeight: 80
Component: bar
x: 100
y: 380
layout: toolbar
Image: back
img: "res/back.png"
Image: home
img: "res/home.png"
One record, three roles
layout.roles.defA Layout carries three groups of properties at once. Which apply depends on where the bound component sits:
| Group | Answers | Applies when |
|---|---|---|
| Container | How do I arrange my children? | The component has children to lay out |
| Item | How does my parent place me? | The component is inside a laid-out parent |
| Own box | How big am I? | Always |
A record may carry any subset. A child that needs only item properties binds a record carrying only those.
Container properties
| Property | Type | Values |
|---|---|---|
flexDirection | enum | FlexDirection::Row (default), ::RowReverse ::Column ::ColumnReverse |
justifyContent | enum | Main axis: FlexJustify::FlexStart (default), ::Center ::FlexEnd ::SpaceBetween ::SpaceAround |
alignItems | enum | Cross axis: FlexAlign::Stretch (default), ::FlexStart ::Center ::FlexEnd ::Baseline |
alignContent | enum | Wrapped lines: FlexAlign::Stretch (default), ::FlexStart ::Center ::FlexEnd ::SpaceBetween ::SpaceAround |
flexWrap | enum | FlexWrap::NoWrap (default), ::Wrap ::WrapReverse |
layoutDirection | enum | FlexDir::Inherit (default), ::LTR ::RTL (RTL flips main-start) |
children | object | A Layout used as the children template (§12.6) |
padding | dim | All edges; per edge paddingLeft paddingTop paddingRight paddingBottom |
autoSizeWidth | bool | Shrink-wrap width to content plus padding. Default false |
autoSizeHeight | bool | Shrink-wrap height to content plus padding. Default false |
Item properties
| Property | Type | Meaning |
|---|---|---|
flexGrow | real | Share of leftover main-axis space to absorb. Default 0 |
flexShrink | real | Share of overflow to give up. Default 0: items do not shrink unless asked |
display | enum | FlexDisplay::Flex (default) participates; ::None takes the item out of the flow (§12.8) |
alignSelf | enum | Overrides the parent's alignItems; FlexAlign::Auto (default) defers |
margin | dim | All edges; per edge marginLeft marginTop marginRight marginBottom |
positionType | enum | FlexPositionType::Relative (default) offsets from the flow; ::Absolute removes the item from it |
positionLeft positionTop | dim | Offsets used with positionType |
aspectRatio | real | Width ÷ height; derives one axis from the other |
positionRight and positionBottom are accepted but not honored. Pin with positionLeft/positionTop.
Own-box properties and dimensions
| Property | Type | Meaning |
|---|---|---|
flexWidth flexHeight | dim | The component's size in the layout |
flexMinWidth flexMinHeight | dim | Lower clamps |
flexMaxWidth flexMaxHeight | dim | Upper clamps |
layout.dim.formsA dimension (dim) takes one of four forms:
| Form | Example | Meaning |
|---|---|---|
| Number | flexWidth: 100 | Pixels |
"<n>px" | "100px" | Pixels |
"<n>%" | "50%" | Percent of the parent's dimension |
"auto" | "auto" | Size from content |
On a margin, "auto" takes the free space on that edge instead: marginLeft: "auto" pushes the item to the far end, and auto on both sides of an axis centres it. margin: "auto" sets all four edges, and an edge given its own value (marginLeft: 8) keeps it.
layout.noshorthandThere is no multi-value shorthand: padding: "10 20" is malformed. Use the per-edge properties.
layout.clearRemoving a layout property returns that member to its default. An empty value (nothing after the colon, or "") clears the property.
layout.rejectA malformed or out-of-range value — "12qx", a quoted enumeration value such as flexDirection: "Row", a negative flexGrow, aspectRatio: 0, alignItems: FlexAlign::SpaceBetween or FlexAlign::Auto — is rejected silently: the previous value stands and no diagnostic is reported.
layout.margin.autoAn auto margin on the cross axis (top or bottom in a row, left or right in a column) does not work beside the default alignItems: FlexAlign::Stretch: the item is not laid out. Give the item alignSelf: FlexAlign::FlexStart, or give the parent an alignItems other than Stretch. That includes margin: "auto", which sets both cross-axis edges.
Layout: Lcentered
margin: "auto"
alignSelf: FlexAlign::FlexStart
Sizing inside a layout
layout.sizing.flexA laid-out component's own width/height do not size it in the flow. Its size comes from its content, from flexWidth/flexHeight, or from flexGrow. The outermost laid-out component keeps its x/y for placement; every component inside it takes both position and size from the layout.
layout.sharedA Layout is a shared resource. One record may drive many components; each arranges its own children independently.
Children template
layout.template.defA container's layout may name, in children:, another Layout that every direct child starts from. A child that binds its own layout: overrides the template member by member; members neither sets take their defaults.
layout.template.directThe template reaches direct children only. Children added or moved in later pick it up automatically. Clearing children:, or deleting the template, returns the children to their own layouts over the defaults.
Layout: cardRow
flexDirection: FlexDirection::Row
flexWidth: 600 # a fixed row, so there is space to grow into
flexHeight: 120
children: cardItem # every card starts from this
Layout: cardItem
margin: 4
flexGrow: 1
Layout: heroCard # one card that differs
flexGrow: 3 # margin still comes from cardItem
Component: shelf
layout: cardRow
Image: card1
img: "res/a.png"
Image: card2 # heroCard's grow, cardItem's margin
img: "res/b.png"
layout: heroCard
Image: card3
img: "res/c.png"
Content follows the box
layout.content.imageWhen a layout gives an Image a box larger than its picture, its scaling (type.image.box) decides how the picture uses it: Scaling::Scale, the default, fills the box, and a nine-slice keeps its borders and stretches its center. On an axis its Layout leaves unset, an Image lays out at its own size: its width or height, otherwise its picture's, a nine-slice included. To fill the row's height, give it flexHeight: "100%".
layout.content.textText is the exception: its box grows but its glyphs keep their natural size. A box narrower than the text wraps or elides a Text that has a width limit (§10.6).
Removing an item from the flow
layout.none.itemAn item bound to display: FlexDisplay::None stops occupying a slot: siblings space as if it were absent, and a content-sized container stops counting it. The item keeps rendering where the layout last placed it (or at its authored x/y if it was never laid out). To stop it rendering, also set visible: false.
layout.none.containerOn a container, FlexDisplay::None freezes that layout's computation until it returns to ::Flex.
visible: false on a laid-out component hides its pixels, but the item keeps its slot and gaps. Use FlexDisplay::None when the space should collapse.
Order
layout.order.declItems are laid out in declaration order, which is also draw order. Moving a child's declaration reorders the arrangement, and each item's margins travel with it.
Layout: bar
flexDirection: FlexDirection::Row
alignItems: FlexAlign::Center
padding: 8
flexWidth: 800
flexHeight: 64
Layout: grow
flexGrow: 1
Layout: spaced
marginLeft: 8
Component: statusBar
x: 0
y: 416
layout: bar
Image: logo
img: "res/logo.png"
Text: title
text: App.screenName
layout: grow # absorbs the leftover width
Image: battery
img: "res/battery.png"
layout: spacedMotion
A Motion is a reusable animation: a program of sequential Steps, each holding operations that run concurrently, applied to a target object when a handler starts it. A Motion says what happens to which properties of its target, not which object the target is; one Motion serves any number of objects, and each application is an independent run.
Program structure
Motion: $SlideIn
Step:
Set:
out: target.x
to: -400
Set:
out: target.alpha
to: 0
Step:
Tween:
out: target.x
to: 0
duration: 450
ease: Ease::OutCubic
Tween:
out: target.alpha
to: 255
duration: 250
delay: 100
Step:
Wait:
duration: 200
SlideIn: slideIn
Component: panel
x: 0 # declared, so the Motion can drive it
alpha: 255
@init:
start: slideIn
motion.step.seqSteps run in order. A Step ends when none of its operations still holds it open; the next Step then starts. No operation writes past its own Step, so a Spring that should keep settling while something else happens belongs in the same Step as that thing.
motion.guardSteps and operations have no names. Their value position is an optional guard, evaluated each time the Step or operation arms: a false Step guard skips the whole Step; a false operation guard skips that operation while its siblings run. An empty value always runs. enabled: false on a Step or operation skips it in the same way. A guard reads values as they are when it arms, and so do an operation's to: and from:. A Motion started by a handler arms its first Step at once, so these read a property computed from that handler's writes before it is updated (§7.1). A false guard skips its Step or operation for the whole run, and to: and from: keep the old value for the whole run. Have them read the value the handler writes instead. A later Step arms after the update and reads the new value.
Motion properties
| Property | Type | Meaning |
|---|---|---|
repeat | signed | Extra iterations of the whole program. Any negative value repeats forever |
repeatType | enum | RepeatType::Loop (default) restarts; ::Reverse alternates direction and plays curves backward; ::Mirror swaps start and end and plays curves forward |
repeatDelay | unsigned | Milliseconds between iterations |
running | unsigned | Protected Number of live runs |
time | unsigned | Protected Non-reactive Milliseconds into the current run |
step | unsigned | Protected Non-reactive Current Step index |
motion.props.positiontime and step are the current run's position. With several runs live they have no single value outside a run, so they are Non-reactive: a binding that reads them is not re-evaluated as the run moves. Read them in a handler or @tick.
motion.userpropsUser properties are allowed on a Motion and are how a handler passes a value in (fling.v = src.xVelocity, then velocity: v in an operation). An operation's own property names come first in its scope, so a user property must not share a name with an operation property. Not diagnosed
The target
motion.target.ownerThe target of a run is the owner of the handler that starts it. start: slideIn inside panel's handler runs the Motion on panel. A handler on a child animates the child, not the component around it.
motion.target.nameInside an operation, target names the run's target: out: target.x is this run's target's x. target is Protected Non-reactive.
motion.target.explicitTo animate another object, name it in an action call: code: slideIn.start(panel), or code: grow.start(parent) from a child's handler. Alternatively, have that object watch state with @if and start the Motion on itself.
motion.target.declaredA Motion drives only properties the target declares. An operation whose out names an undeclared property writes nothing — built-ins such as x, alpha and scale included — and the run logs motion: `out` names no property when it starts. Declare each driven property on the target, at its default. The same happens when out names a child of the target (out: target.icon.x): a chain through target reaches the target's properties only (decl.children.viaprop). To animate a child, start the Motion on the child itself, or drive a property of the target that the child reads.
motion.target.elementout can name one element of an array property, by index or by name: out: target.scale[0], out: target.pair.a. On a Set, Tween or Spring, an array named whole (out: target.scale) or a list of references (out: [target.x, target.y]) drives every element on one clock.
Verbs and events
| Verb | Effect on a run |
|---|---|
start | Starts a run. On an object already running this Motion, restarts it in place from wherever its properties are now; a Spring in flight keeps its velocity |
pause / resume | Suspend and continue the run |
stop | Ends the run, leaving properties where they are |
reset | Writes every operation that has started back to its start value (its from, else the value when it began) and ends the run without @complete. Where several operations drive one property, the last in program order wins |
complete | Lands every started operation on its end value, fires @complete and ends the run. Steps not yet reached never run |
restart | A reset followed by a start: values revert to their start values, and a Spring loses its velocity |
enable / disable | Write the Motion's own enabled property. disable ends every live run without @complete, and start is ignored while the Motion is disabled. No @enable/@disable event fires on a Motion |
motion.events@start and @complete fire on the target once per run, with src being the Motion.
Operations
| Operation | Effect |
|---|---|
Set | Writes to into out the instant its Step starts |
Tween | Interpolates out from its current value (or from) to to over duration, on an easing curve |
Path | Drives one or more properties through a list of waypoints (§13.7) |
Spring | Damped-oscillator approach to to, or a fling projected from velocity (§13.8) |
Wait | Holds its Step open for duration, or while until is false |
motion.ops.propsOperation properties are listed below. A property that does not apply to its operation is accepted without a diagnostic and ignored. Every operation property is Read-only (eval.propclass): the operation reads it when it uses it, to when its Step starts and until or held while it runs, not when something it reads changes.
| Property | Applies to | Meaning |
|---|---|---|
out | Set Tween Path Spring | Read-only The driven property, e.g. target.x. On a Set, Tween or Spring, a whole array (target.scale) or a list ([target.x, target.y]) drives every element. On a Path, a list: [target.x, target.y] |
from | Tween Spring | Read-only Start value; an array gives each element its own. Default: the property's current value, so an interrupted run continues rather than snapping |
to | Set Tween Spring | Read-only End value, read when the Step starts. With several elements, an array is per element and a number applies to all. Optional on a Spring |
duration | Tween Path Spring Wait | Read-only Milliseconds. Defaults: Tween 300, Path 300, Spring 800, Wait 0. On a Path, duration mode is the default; speed, drive or decay overrides it |
delay | All | Read-only Milliseconds after the Step starts before this operation begins |
ease | Tween Path | Read-only An Ease value or a points curve (§13.6) |
round | Set Tween Path Spring | Read-only How a real result lands in an integer property: Round::None (default) ::Round ::Up ::Down; per dimension on a Path |
repeat repeatType repeatDelay | Tween Path Spring Wait | Read-only As on Motion, per operation. A speed-mode Path repeats only while it cruises at speed; a driven (drive) or coasting (decay) Path does not repeat |
until | Wait | Read-only Hold while false. Set until or duration, not both |
bounce | Spring | Read-only 0 is critically damped, up to 0.95 very bouncy. Default 0.25 |
stiffness damping mass | Spring | Read-only Physics form; writing any one selects it. Defaults 100, 10, 1 |
velocity | Spring | Read-only Initial velocity in units per second; an array is per element |
power snap min max | Spring | Read-only Where a spring with no to settles. min and max also clamp an authored to |
motion.ops.exclusiveSome properties are alternatives and must not be combined: until with duration on a Wait, and points with shape, or duration with speed, on a Path. If both of a pair are set, until wins over duration; shape, when it names any object, wins over points; and speed (or drive or decay) wins over duration, selecting speed mode. Not diagnosed
Easing
motion.ease.namedThe named curves are Ease::Linear (default), Ease::Hold (a step at the end), and thirty curves formed from In, Out or InOut followed by Sine Quad Cubic Quart Quint Exp Circle Back Elastic or Bounce — for example Ease::InOutSine. The Back and Elastic families overshoot the 0–1 range; Bounce stays within it.
motion.ease.pointsAny other shape is given as an array of points: values at evenly spaced times ([0, 0.05, 0.5, 0.95, 1] is an S-curve; [0, 0.6, 1.08, 1.02, 1] overshoots and settles), or [t, value] pairs at chosen times ([[0, 0], [0.2, 0.7], [1, 1]]). One curve per operation. The run lands where the curve ends: a curve ending at 0.7 leaves a Tween at 70% of the way to to.
Ease::None and Ease::Bezier exist in the enumeration but are not for authoring.
Path
A Path moves an n-tuple of properties through waypoints on one clock. out is an array of references, one per dimension. The route is either points, an array of tuples of the same width, or a drawn Shape named by shape.
Shape: route
kind: ShapeType::Polyline
points: [[0, 0], [120, 40], [200, 0], [260, 120]]
visible: false
Motion: $Follow
Step:
Path:
out: [target.x, target.y]
shape: target.guide
duration: 900
ease: Ease::InOutSine
orient: target.rotate
Follow: follow
Shape: car
kind: ShapeType::Rect
points: [[0, 0], [28, 20]]
width: 28 # bounds, so the anchor point rides the route
height: 20
x: 0 # declared, so the Path can drive them
y: 0
rotate: 0
guide: route
@click:
start: follow
motion.path.startA Path starts from where the target is. Every entry in points is a destination; the first leg approaches the first one. Nothing jumps.
motion.path.easeOne ease applies across the whole traversal, never per segment: the target passes through waypoints at speed and eases only at the ends. Back curves extrapolate along the final segment and settle back.
motion.path.shapeA Path takes points or shape, not both (motion.ops.exclusive). With shape, the Shape's points are the route and its x/y place it, so the guide and the target must share a parent. A closed kind closes the route; a Rect rides its box (rounded, if it has a radius); an Arc guide is the sector's whole perimeter. A curved guide — Ellipse, Spline, ClosedSpline, Arc, or any kind with a corner radius — is followed along its drawn curve at a steady pace. Inside a layout the route is the guide as drawn on screen. Editing the guide's kind during a run opens or closes the route immediately. Not diagnosed
motion.path.scalarA one-dimensional Path (out: target.alpha, points: [255, 40, 255]) is a multi-keyframe animation of one property.
Duration mode and speed mode
duration: | speed: | |
|---|---|---|
| Guaranteed | Arrival time | Speed in px/s |
| Emergent | Speed per segment | Arrival time |
times | Arrival time (0–1) of each point, one per point; default evenly spaced | Not used |
ease | Progress over time across the route | Shapes the accel/brake ramps only |
| Uneven segments | Long segments are faster | Every segment at the same pace |
| Route changes mid-run | Snaps to the new geometry at the same progress | Continues at the same distance along the current segment |
motion.path.timestimes does not apply to a curved guide.
| Speed-mode property | Meaning |
|---|---|
speed | Read-only Cruise speed in px/s, or initial speed when decay is set. speed, drive and decay each select speed mode |
accel | Read-only Pixels from rest to cruise, shaped by ease |
brake | Read-only Pixels of deceleration into the end, shaped by ease run backward. A route shorter than accel + brake never reaches cruise |
decay | Read-only Time constant in ms: speed decays exponentially and the target stops where it stops, or at the end of the route |
drive | Read-only [dx, dy]: a pointer displacement projected onto the route, so a drag moves the target along the path only. Followed only while held is true |
held | Read-only While true, drive supplies the motion and the run never completes. When it turns false, a Path with decay coasts from the measured speed; without decay it continues at speed, or stops if there is none |
join | Read-only Join::Start (default) heads for the first waypoint and runs the whole route; Join::Nearest heads for the nearest point on the route. Default Nearest when drive is set |
orient | Read-only A property to receive the heading in degrees; the target's art faces +X. Two-dimensional Paths only |
anchor | Read-only Which point of the target rides the path, as a fraction of its bounds. Default: the target's own anchor |
motion.path.anchorThe route runs through the target's anchor point, which needs the target's bounds: give a Shape or Image on a Path a width and height. Author a draggable knob with its anchor point on its track and the first grab moves nothing.
motion.path.heldBind held to a Motion user property with a different name (held: grip); held: held reads the operation's own held.
Spring
motion.spring.formsA Spring approaches its end as a damped oscillator and holds its Step until it comes to rest. It is tuned either by duration and bounce (at rest at duration), or by stiffness, damping and mass, which take over as soon as any one is written. In the physics form the spring rests when its motion dies out, up to 10 s.
motion.spring.velocityA spring restarted in flight keeps the velocity it had, so a target whose goal moves mid-spring turns smoothly. Otherwise velocity defaults to zero.
Motion: $Snap
Step:
Spring:
out: target.x
to: target.home
duration: 500
bounce: 0.3
motion.spring.flingFlings. A Spring with a velocity and no to heads for from + power × velocity (power default 0.8), rounded to the snap grid if set. The end, projected or authored, is clamped into min…max. min/max are where the spring settles, not walls it bounces off. A drag release's src.xVelocity is in pixels per second, which suits a spring that drives x.
Motion: fling
v: 0
Step:
Spring:
out: target.x
velocity: v
snap: 120
min: 0
max: 480
duration: 700
bounce: 0
Shape: knob
kind: ShapeType::Ellipse
points: [[0, 0], [40, 40]]
x: 0 # declared, so the Spring can drive it
@release:
code: fling.v = src.xVelocity
start: fling
For a fling along a route, use a Path in speed mode with decay.
Materials
Materials give viewables custom GLSL drawing. Every viewable draws through one of the engine's built-in GLSL programs; a Material replaces that program with a Shader the author writes, and supplies values for its uniforms. A viewable uses a material through its material: property. The same GLSL compiles in Altia Design (Custom 2D Materials), on a DeepScreen target, and in the BluePrint runtime.
| Type | What it is | Referenced from |
|---|---|---|
Shader | A vertex + fragment pair (GLSL ES 3.00) and its program | Material.shader |
Material | A shader, a blend mode, and variable values | Any viewable's material: |
MatVar | One uniform's value; a child of a Material | Its name is the uniform it sets |
Texture | What a sampler2D reads: a picture or a buffer | A MatVar.value |
mat.toplevelShader, Material and Texture are top-level resources. MatVar belongs under a Material; anywhere else it is accepted and does nothing (mat.matvar.unused). None of the four is viewable, and none has events or actions.
Shader: tint
shaderType: ShaderType::Image
fragment: |
#version 300 es
precision highp float;
uniform vec4 ALTIA_u_colorFg;
uniform sampler2D ALTIA_u_texture;
uniform float ALTIA_u_alpha;
uniform vec4 u_filterColor; // NAME "Filter" DEFAULT "1.0 1.0 1.0 1.0"
in vec3 v_texCoord;
out vec4 fragColor;
void main() {
vec2 uv = v_texCoord.xy / v_texCoord.z;
fragColor = texture(ALTIA_u_texture, uv) * ALTIA_u_colorFg.a * u_filterColor;
}
Material: warm
shader: tint
MatVar: u_filterColor
value: [1.0, 0.8, 0.6, 1.0]
Image: photo
img: "res/photo.png"
material: warm
Shader
| Property | Type | Meaning |
|---|---|---|
shaderType | enum | The family; default ShaderType::Custom |
vertex | string | Vertex stage source, written as a | block |
fragment | string | Fragment stage source, written as a | block |
mat.shader.familyFor every family except Custom, a stage left out is supplied, byte for byte, from Altia Design's built-in shader of that family, so a fragment-only shader is the common case. Custom supplies nothing. Because Custom is the default, a fragment-only shader must name its family or it fails to compile with “missing vertex stage”. Not diagnosed by blueprint --check; the editor reports the failure when the shader compiles.
shaderType | Default stages | Typical user |
|---|---|---|
ShaderType::Vector | Vector 2D | Shape |
ShaderType::Image | Image 2D | Image; the blit of a buffer or blur component |
ShaderType::Text | Text 2D | Text glyphs |
ShaderType::Mask | Mask 2D | A masked component's composite |
ShaderType::Custom | None | Author supplies both stages |
mat.shader.glslBoth stages are GLSL ES 3.00: they begin with #version 300 es, use in/out rather than attribute/varying, and the fragment stage declares out vec4 fragColor. The default stages require an ES 3 context; on an ES 2 fallback they fail to compile and the failure is reported through the normal status path.
mat.shader.blockShader source should be written as a | block scalar. A quoted one-line string loses line numbers in diagnostics.
Engine inputs
mat.engine.reservedUniforms whose names contain ALTIA_ belong to the engine and are set per draw. A shader declares the ones it reads. A uniform the family does not bind reads as zero.
| Family | Fragment inputs | Engine uniforms (ALTIA_u_*) |
|---|---|---|
| Vector | v_texCoord | colorFg colorBg pass |
| Image | v_texCoord | colorFg colorBg texture texWidth texHeight alpha |
| Text | v_texCoord v_texCoord2 | colorFg colorBg texture pass alpha texWidth texHeight |
| Mask | v_texCoord v_texCoord2 | colorFg colorBg texture texture2 texWidth texHeight alphaOperator masked |
Every family's vertex stage takes ALTIA_u_pmvMatrix and ALTIA_u_objXForm; the Vector vertex stage also takes ALTIA_u_lineWidth and ALTIA_u_pass.
| Name | Type | Value |
|---|---|---|
ALTIA_u_pmvMatrix | mat4 | Canvas projection and view. Vertex stage: gl_Position = (ALTIA_u_pmvMatrix * ALTIA_u_objXForm) * vec4(ALTIA_a_position, 0.0, 1.0) |
ALTIA_u_objXForm | mat4 | The object's own transform (scale, rotate, anchor, corners). Omit it and the object stops following its transform |
ALTIA_u_colorFg | vec4 | The viewable's fg; .a is its effective alpha (0–1, inherited alpha included) |
ALTIA_u_colorBg | vec4 | The viewable's bg |
ALTIA_u_pass | float | Vector: 1.0 on the fill pass, 0.0 on the outline or line pass. Text: 1.0 on the fill draw of a stroked font (type.font.stroke), otherwise 0.0 |
ALTIA_u_lineWidth | float | Shape outlineWidth in pixels. 1.0 means the thin-line path; wider offsets vertices along ALTIA_a_prev, ALTIA_a_next and ALTIA_a_side |
ALTIA_u_texture | sampler2D | Image: the picture (premultiplied). Text: the glyph raster. Mask: the masked content. Buffered or blurred component: the cached surface |
ALTIA_u_texture2 | sampler2D | Mask: the mask source's rendering; its alpha is the coverage |
ALTIA_u_texWidth ALTIA_u_texHeight | float | Size of ALTIA_u_texture in pixels |
ALTIA_u_alpha | float | A format flag, not opacity: 1.0 when the texture is alpha-only (8-bit picture, glyphs), 0.0 when it carries color |
ALTIA_u_alphaOperator | float | 1.0 for MaskMode::Normal, −1.0 for ::Invert; coverage = (op - 1.0) / -2.0 + op * mask.a |
ALTIA_u_masked | float | 1.0 while a mask is applied |
mat.engine.projectivev_texCoord (vec3) is projective: divide by .z before sampling (v_texCoord.xy / v_texCoord.z). It spans 0–1 over the object's own surface. v_texCoord2 is, for Text, UV relative to the whole string's box and, for Mask, UV into ALTIA_u_texture2. For Vector, v_texCoord is meaningful on the fill pass only.
mat.engine.premultPictures in the BluePrint runtime are premultiplied: ALTIA_u_texture's rgb is already scaled by its alpha. Multiply by ALTIA_u_colorFg.a to honor the object's alpha.
Variables
mat.var.defEvery uniform in either stage whose name does not contain ALTIA_ is a variable that a Material may set. Its type is its GLSL type. Its default is the value in a DEFAULT "…" comment on the same line — up to 16 numbers separated by spaces or commas — else zero. A NAME "Label" comment is accepted and ignored.
uniform vec4 u_tint; // NAME "Tint" DEFAULT "1.0 1.0 1.0 1.0"
uniform float u_phase; // DEFAULT "0.5"
uniform bool u_flip; // an int variable: 0 or 1
uniform float u_taps[4]; // one variable named u_taps
uniform sampler2D u_overlay;
mat.var.scanVariables are found by scanning declarations:
- One declaration per line; a
uniformsplit across lines is not seen. layout(…)and precision qualifiers are skipped; interface blocks are not variables.- Several declarators on one line each become a variable; an array declarator binds by its base name (
u_taps[4]→u_taps). - The first stage to declare a name wins, vertex before fragment.
- Limits: 50-character identifiers (longer is truncated; a
MatVarname over 50 characters is refused) and 2048-character lines (longer is cut). Stay within Altia Design's limit of 256 uniforms; the BluePrint runtime does not enforce it.
| GLSL type | MatVar.value shape |
|---|---|
int uint bool | One number (bool: 0 or 1) |
float | One number |
vec2 vec3 vec4 | Array of 2, 3 or 4 reals |
mat3 mat4 | Array of 9 or 16 reals, column-major |
sampler2D | A Texture reference |
samplerCube | Not settable in this version |
ivec* bvec* uvec* mat2 | Unsupported: flagged in the hover, never written |
Compilation lifecycle
mat.life.compileA shader compiles when a Material first names it. After that it recompiles only on request. Editing a stage marks the shader changed since the last compile, and the running program keeps drawing the previous GLSL.
mat.life.recompileIn the editor, a single shader is recompiled from its hover (“Recompile this shader”) or the code action on its Shader: line; every changed or failed shader of a document from the context menu (“Recompile Shaders”) or the Altia BluePrint status menu. Stopping or restarting the design recompiles every bound shader.
mat.life.failOn a compile failure the previous program stays live if there was one; otherwise the shader's users draw with the engine's built-in program. The compiler log is reported on the offending GLSL line inside the block, or on the Shader: key when the error has no line or is in a default stage.
mat.life.deleteA material that names a deleted shader draws its users with the built-in program until it is pointed at another shader.
| Hover status | Meaning |
|---|---|
| not compiled yet | No material has bound it |
| current | The live program matches the sources |
| changed since the last compile | Edited; the old program is still drawing |
| FAILED (with log) | The last compile failed |
Material
| Property | Type | Meaning |
|---|---|---|
shader | Shader | The program to draw through. Unset, unresolved or not a Shader: users draw with the built-in program |
blendMode | enum | Unset uses the family default (below) |
mat.material.shareOne material may serve any number of viewables. Two materials on one shader hold independent values over a shared program. A material declares a MatVar only for variables it sets; every other uniform keeps the shader's default.
BlendMode | GL blend | Default for |
|---|---|---|
SourceAlpha | SRC_ALPHA, ONE_MINUS_SRC_ALPHA | Vector, Text, Mask, Custom |
Overwrite | Blending off | |
Multiply | DST_COLOR, ONE_MINUS_SRC_ALPHA | |
Screen | ONE, ONE_MINUS_SRC_COLOR | |
Additive | SRC_ALPHA, ONE | |
PremultipliedAlpha | ONE, ONE_MINUS_SRC_ALPHA | Image |
mat.material.blendSet blendMode when a shader's output convention differs from its family's default. The built-in Vector, Text and Mask stages emit straight alpha; the Image stage emits premultiplied color. Setting BlendMode::SourceAlpha on a picture's material darkens its edges.
MatVar
mat.matvar.nameA MatVar's name is the uniform it sets, and value is what the uniform receives. There is no type: property; the type is the uniform's.
mat.matvar.shapeA value whose shape does not fit the uniform is refused with a warning, and the uniform keeps what it had: its default, or the last value that fit. A value that resolves to nothing clears the override.
mat.matvar.animatevalue is an ordinary reactive property: an expression over a Data field re-pushes the uniform on every change. Drive the field with a Motion for eased animation.
mat.matvar.unusedA MatVar naming a uniform the current shader lacks is kept and shown as unused. A MatVar outside a Material is accepted and does nothing.
Material: sweep
shader: scanline
MatVar: u_phase
value: anim.tri / 20.0 # re-pushed as it changes
MatVar: u_tint
value: [0.2, 0.9, 0.4, 1.0]
MatVar: u_overlay
value: overlayTex # a Texture, for a sampler2D
Texture
| Property | Type | Default | Meaning |
|---|---|---|---|
source | string or object | — | A picture path, resolved as Image.img; or a buffer: true component, sampled live |
filter | enum | TextureFilter::Linear | ::Linear or ::None (nearest) |
wrap | enum | TextureWrap::Clamp | ::Clamp ::Repeat ::Mirror: sampling outside 0–1 |
mipmap | bool | false | Build mipmaps; a buffer's mipmaps are rebuilt on every redraw |
mat.tex.uploadNothing is uploaded when a Texture is declared; it becomes a GL texture the first time a MatVar binds it.
mat.tex.sharedPictures are shared: a picture shown by an Image and sampled by a shader is one GL texture, and two Textures on one file with the same sampling are one texture. A different filter, wrap or mipmap gets its own copy.
mat.tex.bufferA buffered component is sampled as it redraws. An unbuffered component source is unresolved and samples black until it gains buffer: true; the texture is the buffer's box (view.effects.size). Blur output cannot be a texture source.
mat.tex.unitsUser samplers start at texture unit 2; units 0 and 1 belong to ALTIA_u_texture and ALTIA_u_texture2.
Texture: overlay
source: "res/icon_circle.png"
wrap: TextureWrap::Repeat
Texture: faceTex
source: gaugeFace # a component declared buffer: true
Where a material applies
| Viewable | Draws through the material | Family |
|---|---|---|
Image | The picture | Image |
Text | The glyph run | Text |
Shape | Fill pass and outline pass (ALTIA_u_pass) | Vector |
Component with buffer or blur | The cached surface's blit | Image |
Component with mask | The mask composite | Mask |
Plain Component | Nothing | — |
Stamp | The stamped objects carry their own materials | The source's |
mat.use.familyA shader of the wrong family compiles but draws wrongly; for example, an Image-family shader on a Shape samples a texture the shape does not have. The blur pass itself cannot be customized; a material on a blurred component customizes only the blit.
Data: anim
tri: 0
Timer: clock
period: 33
repeats: true
@init:
start: clock
@complete:
code: anim.tri = anim.tri >= 20 ? 0 : anim.tri + 1
Shader: vecBand
shaderType: ShaderType::Vector
fragment: |
#version 300 es
precision highp float;
uniform vec4 ALTIA_u_colorFg;
uniform vec4 ALTIA_u_colorBg;
uniform highp float ALTIA_u_pass;
uniform float u_phase; // DEFAULT "0.5"
in vec3 v_texCoord;
out vec4 fragColor;
void main() {
vec2 uv = v_texCoord.xy / v_texCoord.z;
float band = 1.0 - step(0.1, abs(uv.y - u_phase));
fragColor = mix(ALTIA_u_colorBg, ALTIA_u_colorFg, band);
}
Material: bandAnim
shader: vecBand
MatVar: u_phase
value: anim.tri / 20.0
Shape: gauge
kind: ShapeType::Rect
points: [[0, 0], [150, 100]]
bg: "#1E3A5F"
fg: "#5FB0FF"
material: bandAnimThis example continues the previous one: bandAnim is the material defined there.
Component: band
y: 140 # clear of the previous example's gauge
buffer: true
width: 150
height: 100
Shape: fill
kind: ShapeType::Rect
points: [[0, 0], [150, 100]]
bg: "#1E3A5F"
fg: "#5FB0FF"
material: bandAnim
Texture: bufTex
source: band
Shader: vecSample
shaderType: ShaderType::Vector
fragment: |
#version 300 es
precision highp float;
uniform vec4 ALTIA_u_colorFg;
uniform vec4 ALTIA_u_colorBg;
uniform highp float ALTIA_u_pass;
uniform sampler2D u_src;
in vec3 v_texCoord;
out vec4 fragColor;
void main() {
vec2 uv = v_texCoord.xy / v_texCoord.z;
fragColor = texture(u_src, uv);
}
Material: bufSample
shader: vecSample
blendMode: BlendMode::PremultipliedAlpha # the buffer is premultiplied
MatVar: u_src
value: bufTex
Shape: mirror
kind: ShapeType::Rect
points: [[0, 0], [150, 100]]
x: 200
y: 140
material: bufSample # follows band frame for frame
A picture sampled through a Vector shader may appear vertically flipped because of texture row order.
Limitations
- An ES 3 context is required for the default stages.
- Transform (
Xform) variables,samplerCubevalues,mat2, integer and boolean vectors, multi-pass materials and 3D materials are not supported. - Programs are not cached across a stop/restart; every bound shader recompiles.
- Only
buffer: truecomponents can be texture sources; blur output cannot. - Uniform declarations spanning lines are not scanned.
Scenes
A Scene renders a pre-baked .a3d 3D scene into its component box. It is viewable and may have children, which draw on top, parent-relative.
Properties
| Property | Type | Default | Meaning |
|---|---|---|---|
scene | string | — | Path to the .a3d, relative to the entry document's directory |
camera | unsigned | 0 | Index into the file's cameras. Reactive: the view cuts when the expression changes |
antialias | enum | AntiAlias::None | ::None ::Taa ::Msaa ::Ssaa; one mode at a time |
taaWarmup | unsigned | 2 | Warm-up frames for Taa, clamped 1–8 |
msaaSamples | unsigned | 4 | Samples per pixel for Msaa, clamped 2–16 |
ssaaScale | real | 2.0 | Render scale for Ssaa |
Animation bindings
scene.anim.declThe animations baked into an .a3d are not properties of the type. The author declares each one used, with the & sigil and the animation's name; the property's value is the animation's position.
scene.anim.position&name: expression is a position, not a play command. The scene has no clock of its own: the expression bound to the animation is the animation over time. Typically it reads a Data field stepped by a Timer or tweened by a Motion.
scene.anim.globalAnimation names are global. Two scenes binding the same name drive each other's timelines, and the last value written wins.
scene.anim.collideAn animation binding must not share its name with a property of the same Scene: &fade and a property fade collide, and the clash is reported as decl.duplicate-name.
scene.anim.onlyAn & key is accepted on any type but implemented only by Scene; elsewhere it logs “animation binding '&name' is not supported on this object” at run time. It should not be used outside a Scene.
Data: anim
fade: 0
move: anim.fade * 2 # follows fade
on: anim.fade < 50 # first camera for the first half of the cycle
Timer: clock
period: 33
repeats: true
@init:
start: clock
@complete:
code: anim.fade = anim.fade >= 100 ? 0 : anim.fade + 1
Scene: simple
scene: "res/no_tex.a3d"
camera: anim.on ? 0 : 1
width: 600
height: 480
&node_move: anim.move # the position of the timeline of that name
&blue_fade: anim.fade
Host integration
ABP is not intended to express I/O, search or heavy computation. Those belong in the host application, which reaches the design in two ways: through Data properties, and through external functions the design calls.
The Data interface
host.data.reachA host reads, writes and watches properties by dotted path (Vehicle.rpm). How far a path reaches depends on the API. In an application linking HMI::API, a path resolves Data properties only. Through the altia-blueprint-client library, against a design blueprint plays, a path resolves anything in the design: components and their properties as well as Data. Either API can walk the whole design from the root, child by child.
host.data.evalAn expression evaluated as a string reaches any property in the design and may write it, as a code: action would. That includes eval through the client library, and the expression console, Evaluate Expression, the Debug Console, Watch and hover in Visual Studio Code. String evaluation is available only through blueprint: the language server and the client library. Embedded targets have none.
host.data.writeA host write is accepted only while the design is running, and only by a writable property authored as a literal or constant expression. A refused write reports its reason: not running, read-only, or dynamic expression for a bound property such as speed: rpm / 30. An accepted write propagates exactly like a code: write. A whole array written to a declared array keeps the declared size (decl.array.size), and the reply and the watchers carry the value kept, not the value sent. Through the client library the same rules apply to any property a path resolves, not only Data.
host.data.watchOnly properties on Data objects trigger watch callbacks when they change. Over the client API and the language server, changes are collected and sent once per frame, so a property written several times within one frame reports only its last value.
host.data.framesA host write reaches the properties computed from it in the next frame. A host that writes a value and then reads a property computed from it must let the design process one frame first, plus one frame for each @if handler that the change passes through (§7.1).
External functions
host.ext.registerA host makes a function callable as ext.name(…) by registering it under that name. An application registers functions with HMI::API::registerFunction, or through the altia-blueprint-client library. In Visual Studio Code, the Altia BluePrint extension loads a JavaScript module named externals.js, looked for first beside the entry document and then at the root of the workspace folder; each exported name is callable. The ABP is the same in every case.
// externals.js -- each exported name is callable as ext.NAME(...)
module.exports = {
formatSpeed(mps) { return (mps * 3.6).toFixed(1) + " km/h"; },
greet(name) { return "Hello, " + name + "!"; },
};
host.ext.patternExternal calls are legal only in a code: action. A property cannot read an external directly (text: ext.formatSpeed(27.8) is refused with expr.side-effects). The pattern is always: a handler calls the function and writes a Data property; the display reads the property reactively.
Data: state
speed: ""
greeting: ""
presses: 0
Component: driver
@init:
code: |
state.speed = ext.formatSpeed(27.8)
state.greeting = ext.greet("Altia")
Text: speedOut
text: state.speed
Shape: regreetBtn
kind: ShapeType::Rect
points: [[0, 0], [220, 44]]
@press:
code: |
state.presses += 1
state.greeting = ext.greet('press ' + state.presses)
host.ext.unknownWhich host answers a name is decided at call time, so a misspelled name is not an error anywhere; the call reads undefined. ABP names and host registrations must match exactly.
host.ext.syncCalls are synchronous and hold the frame while the host answers. Every call goes out to the host by name. A host outside the engine (the VS Code extension, or an application using the client library) has one second to answer; a call past that deadline reads undefined. An asynchronous implementation is still waited on. An application using the client library is asked only while it waits on one of its own requests. When blueprint plays the design with --wait-for-api, that is every call: @init runs inside the application's initialize() and each frame inside its process(). On a design that runs on its own, a call made between the application's requests, such as one in an @init that ran before the application connected, is not sent to the application: it goes to externals.js when the design runs in VS Code, and otherwise reads undefined.
host.ext.slowFor slow work, the call should start the work and return at once. The host then writes the result to a literal Data field that an @if handler watches; or, if the host only answers calls, a repeating Timer polls a second function for the result.
Checking and running designs
The blueprint command-line program checks and runs designs. In a project prepared by the Altia BluePrint extension for Visual Studio Code (command Setup AI and Tools…), it is installed in .altia/tools/blueprint/ (blueprint.exe on Windows). The README in .altia/tools/ describes every command, and blueprint --help lists every option. A program of your own can drive a running design through the client API; the C++ and JavaScript clients are in .altia/client/.
Checking
.altia/tools/blueprint/blueprint --check main.abp
tool.check.scope--check is given the design's entry document, or a folder whose abp.project.yaml names it. It checks every file of the design, each module in the context of the files that import it, without running anything.
tool.check.outputStandard output carries the diagnostics, one per line, and nothing else; a clean design prints nothing. --format json selects machine-readable output.
error main.abp:12:9 [expr.resolve] Unresolved identifier: foo.bar
| Exit code | Meaning |
|---|---|
0 | No errors (warnings allowed) |
1 | One or more errors |
2 | The design could not be loaded, or the command line was wrong |
Check a design from its entry document. Checking a module as a separate file checks it outside the design that imports it, and reports errors the design does not have.
Running and capturing
.altia/tools/blueprint/blueprint --play main.abp # run in a window
.altia/tools/blueprint/blueprint --play main.abp --capture shot.png # write the display to a PNG
tool.play.errorsA design with errors still plays. A document with a syntax error contributes nothing, because --play reads each file once and has no earlier parse to keep (doc.import.parse). When that document is the entry, nothing is left to play: --play and --capture report the syntax error and exit with code 1.
Inspecting values
The blueprint-lsp.js helper in .altia/tools/ evaluates expressions and reads data against a design without a language-server client:
node .altia/tools/blueprint-lsp.js eval main.abp "Vehicle.speed * 2"
node .altia/tools/blueprint-lsp.js data main.abp Vehicle
node .altia/tools/blueprint-lsp.js capture main.abp shot.png # as authored, without running
eval evaluates the expression at the design root and can read any property, not only Data. It answers ok: false for an expression whose chain names nothing, naming the segment that failed (unresolved: veh.nope (at 'nope')). It also refuses what a design load refuses with expr.generate, such as a call to an unknown function (expr.call.kinds) or ===, with the same message followed by the refused text: Unknown or unsupported function call 'start(tick)'. Such an expression is refused whole: nothing in it runs, not even a statement before the refused part, and a refused part in a branch that is not taken still refuses it. In a data answer each requested path carries its own ok; a path that names no Data element answers ok: false, “not a data element”, without failing the others. set runs the design and writes one Data value, refusing a bound property with its reason. A property holding undefined answers undefined.
tool.inspect.diffTwo eval results differ from a design. eval reads 1.2.3, which a design refuses (expr.parse), as a number near 0.3. String literals that eval writes inside an array read empty in the design afterward, although the answer shows them: an array written into a property not declared as an array (D.x = ["a", "b"]), an array written into one element of an array, and a string written into an element of a computed array (items: [1, 2].concat([3]), then D.items[0] = "a"). A plain string (D.label = "a") and a whole array written into a property declared as an array (D.tags = ["a", "b"]) keep their text.
Grammar summary
This appendix collects the productions given throughout the reference. It is informative; the chapters are authoritative.
Enumerations
Every enumeration value is written Enum::Value. Defaults depend on the property and are given in each property table, not by position here.
| Enumeration | Values | Used by |
|---|---|---|
ShapeType | Rect, Line, Polyline, Ellipse, Polygon, Spline, ClosedSpline, Arc | Shape.kind |
Justify | None, Left, Right, Center | Text.justify |
Truncate | None, Clip, ElideEnd, ElideStart, ElideCenter; reserved: Page, Scale | Text.truncate |
Wrap | Normal, NewLineOnly | Text.wrapMode |
Align | Top, Center, Bottom (no effect in this version) | Text.align |
Button | None, Left, Middle, Right | src.button |
MaskMode | Normal, Invert, None | maskMode |
FlexDirection | Row, RowReverse, Column, ColumnReverse | flexDirection |
FlexJustify | FlexStart, Center, FlexEnd, SpaceBetween, SpaceAround | justifyContent |
FlexAlign | Stretch, FlexStart, Center, FlexEnd, Baseline, SpaceBetween, SpaceAround, Auto | alignItems alignContent alignSelf (each accepts its own subset) |
FlexWrap | NoWrap, Wrap, WrapReverse | flexWrap |
FlexDir | Inherit, LTR, RTL | layoutDirection |
FlexDisplay | Flex, None | Layout display |
FlexPositionType | Relative, Absolute | positionType |
RepeatType | Loop, Reverse, Mirror | Motion and operation repeatType |
Ease | Linear, Hold, and In/Out/InOut × Sine, Quad, Cubic, Quart, Quint, Exp, Circle, Back, Elastic, Bounce; reserved: None, Bezier | ease |
Round | None, Round, Up, Down | Operation round |
Join | Start, Nearest | Path join |
ShaderType | Custom, Vector, Image, Mask, Text | Shader.shaderType |
BlendMode | SourceAlpha, Overwrite, Multiply, Screen, Additive, PremultipliedAlpha | Material.blendMode |
TextureFilter | Linear, None | Texture.filter |
TextureWrap | Clamp, Repeat, Mirror | Texture.wrap |
AntiAlias | None, Taa, Msaa, Ssaa | Scene.antialias |
Common diagnostics
Diagnostics are reported as severity file:line:column [code] message. Diagnostics are errors unless marked as warnings. This table lists every code blueprint --check reports, and decl.override-revert. The editor also reports shader.compile for a shader that failed to compile and shader.stale for one changed since its last compile (§14.1).
| Code | Reported when | See |
|---|---|---|
doc.parse | A document has a syntax error | §2 |
import.parse | An import: line is malformed: it needs a quoted path, optionally followed by -> Alias, and nothing else; or its alias is not a type name | doc.imports |
import.unresolved | An import path cannot be resolved, or names a folder | doc.paths |
import.cycle | A module imports itself, directly or through other modules | doc.imports |
import.module-parse | An imported module has a syntax error; reported on the import line | doc.import.parse |
import.module-instance | A module declares top-level instances, which importers never create (warning) | doc.module.instances |
decl.unknown-kind | An uppercase key names no type. A warning instead when the type's module has a syntax error, since its types are unavailable until it parses | lex.key.case |
decl.unknown-event | An @ key names no trigger | ev.triggers.closed |
decl.unknown-action | A key inside a handler names no action | act.list.closed |
decl.event-syntax | Event: or Action: is declared directly | type.reserved |
decl.bad-parent | A declaration is placed where it cannot go: inside a property, inside an event handler, or a Display below the top level | decl.array.form, ev.syntax.where, type.display.root |
decl.value-and-children | A property has both a value and child elements | decl.array.form |
decl.bad-name | A declaration name is not a plain identifier (quoted, or starting with a digit), or an & key has no animation name | lex.identifiers |
decl.duplicate-name | A name is already declared in this scope | decl.names.unique |
decl.parent-name | A child reuses its parent's name | decl.names.unique |
decl.reserved-name | A reserved name is used as a component, property or array-element name, or an intrinsic property such as type is redeclared | lex.reserved.list |
decl.shadows-property | A child or instance shares a name with a property it would hide (warning) | decl.names.shadow |
decl.typedef-name | A type definition's name is not a valid type name | decl.typedef.name |
decl.typedef-reserved | A type definition reuses a built-in type name | decl.typedef.name |
decl.typedef-not-root | A type definition is not at the top level | decl.typedef.rootlevel |
decl.typedef-duplicate | A type is defined twice | decl.typedef |
decl.typedef-cycle | A type is used inside its own definition | decl.typedef |
decl.type-ambiguous | A type name matches more than one imported module; the first import wins | doc.import.merge |
decl.override-revert | While a design is being edited, an override of an imported type's property is deleted from an instance, but the type's own value cannot be restored, because the module has changed that property since it was last read or the module no longer resolves. Until the module's changes are picked up, the instance has no value for the property. Reported on the instance's declaration (warning). Appears only during live editing, never from blueprint --check | doc.import.live |
expr.parse | An expression has a syntax error, or uses a construct outside the subset | expr.unsupported |
expr.generate | An expression uses an unsupported construct or calls an unknown function | expr.unsupported |
expr.resolve | A name or chain resolves to nothing | scope.res.unresolved |
expr.stamped-unresolved | A chain in a type body cannot be satisfied where the type is instantiated | scope.type.deferred |
expr.side-effects | A write, action call or external call appears outside a code: action | eval.writes.rule |
expr.cycle | A property depends on itself through a chain of references; it evaluates to undefined | eval.reactive |
expr.read-only-write | A code: write targets a read-only property, such as a Timer's active or a Motion's running, or parent; the write is ignored (warning) | eval.writes.readonly |
shape.rect-vertex-radius | A Rect point carries a per-vertex radius (warning) | type.shape.rectradius |
Accepted without a diagnostic
The following are accepted without a diagnostic. They are the most common causes of a design that checks clean but does not behave as intended.
| Construct | Effect | Rule |
|---|---|---|
| A misspelled property name | Becomes a user property; does nothing | decl.prop.misspell |
| A reserved trigger or action | Never fires / does nothing | §8.2, §9.2 |
| An action naming an object it cannot act on | Does nothing | act.syntax.silent |
A domain prefix such as data. | Resolves against the first top-level object of that kind | scope.res.byname |
An @if with no condition | Never fires | ev.if.cond |
| Any rule marked Not diagnosed | Accepted; the design may not behave as intended | The rule itself |
| A Motion driving an undeclared property | Writes nothing; the run logs a warning when it starts | motion.target.declared |
A child named after a property that holds an instance (ref.label.text) | Reads undefined; a write changes nothing | decl.children.viaprop |
| An operation property its operation does not use | Ignored | motion.ops.props |
| A malformed or out-of-range layout value | Previous value stands | layout.reject |
An auto margin on the cross axis beside alignItems: FlexAlign::Stretch | The item is not laid out | layout.margin.auto |
An unknown ext.* name | Reads undefined at run time | host.ext.unknown |
A MatVar outside a Material | Does nothing | mat.matvar.unused |
visible: false on a mask source | Masks out everything | view.mask.hide |
Reserved features
The names below are accepted by this version but have no effect. They are reserved for future versions and should not be used.
| Category | Names |
|---|---|
| Types | View DeckView ModelView StringTheme ImageTheme |
| Triggers | show hide move moveX moveY next prev first last select unselect key |
| Actions | show hide select unselect next prev first last input anim call set |
| Properties | Font.bold Font.italic Text.align Text.wrapText z positionRight positionBottom |
| Enumeration values | Truncate::Page Truncate::Scale Ease::None Ease::Bezier |
| Project fields | name sourceRoots assets |
Recipes
Informative examples of common patterns.
Pressable button as a reusable type
Component: $PrimaryButton
labelText: ""
tone: "#333344"
@press:
code: tone = "#5566AA"
@release:
code: tone = "#333344"
Shape: face
kind: ShapeType::Rect
points: [[0, 0], [160, 48]]
bg: tone
fg: tone
Text: lbl
text: labelText
justify: Justify::Center
width: 160
x: 0
y: 8
PrimaryButton: okButton
labelText: "OK"
x: 100
y: 60
A plain Component draws nothing, so the press feedback needs something that draws: the face Shape fills with the type's tone. justify centers within the Text's own width.
Gauge needle driven by data
Data: Vehicle
rpm: 0 # 0..8000
Image: face
img: "res/tach_face.png"
x: 170
y: 90
Image: needle
img: "res/needle.png" # same size as the face, pointing straight up
x: 0
y: 0
width: 300 # the art's size: an Image needs it for anchor
height: 300
anchor: [0.5, 0.5]
rotate: 135 - Vehicle.rpm / 8000 * 270
The needle art is the same size as the face and points straight up, so placing it at x: 0, y: 0 lines the two pictures up and the center anchor is the dial's pivot. Positive rotation is counterclockwise, so the angle starts at 135° (pointing down and to the left at 0 rpm) and decreases, sweeping the needle clockwise through 270° to −135° at 8000 rpm.
State-dependent imagery
Data: App
level: 0 # 0..4
Image: gauge
img: "res/level_" + App.level + ".png"
The path re-evaluates whenever App.level changes; an image must exist for every reachable value.
Gauge fill swept by a mask
Data: Gauge
pct: 0 # 0..100
# A coverage-only rectangle that slides across the bar.
# At pct 0 it sits entirely left of the bar; at 100 it covers it exactly.
Shape: fillStencil
kind: ShapeType::Rect
points: [[0, 0], [200, 24]]
bg: "#ffffff"
x: -161 + 200 * (Gauge.pct / 100) # -161, not -160: no 1-px sliver at 0%
y: 80
Component: fuelBar
x: 40
y: 80
mask: fillStencil
maskHideSource: true # the stencil shapes the mask but never draws
Image: fillArt
img: "res/bar_fill.png" # the full fill artwork, masked
x: 0
y: 0
width: 200
height: 24
For an arc gauge, rotate the stencil instead: author a square wedge image whose pivot is its center, place that center on the dial pivot so it turns about that center, and bind rotate. Keep the stencil and the masked component under the same parent so the overlap arithmetic stays readable; the stencil is positioned in its own parent's frame.
Animated show/hide driven by state
Data: nav
tab: 0
Motion: $Reveal
Step:
Tween:
out: target.alpha
to: target.shown ? 255 : 0
duration: 350
ease: Ease::InOutSine
Tween:
out: target.y
to: target.shown ? 80 : 40
duration: 350
ease: Ease::OutCubic
Reveal: reveal
Component: $Page
shown: false
alpha: 0
y: 40
visible: alpha > 0
enabled: alpha > 0
@init:
start: reveal # @if does not fire for the initial state
@if: nav.tab >= 0
start: reveal
Shape: face
kind: ShapeType::Rect
points: [[0, 0], [400, 200]]
radius: 12
bg: "#1E3A5F"
Page: home
shown: nav.tab == 0
Text: heading
text: "Home"
x: 24
y: 150
Page: settings
shown: nav.tab == 1
Text: heading
text: "Settings"
x: 24
y: 150
Shape: switcher
kind: ShapeType::Rect
points: [[0, 0], [120, 40]]
x: 440
y: 80
bg: "#5FB0FF"
@click:
code: nav.tab = nav.tab == 0 ? 1 : 0
Each page watches state with @if and starts the Motion on itself, and @init covers the starting state, which @if does not fire for (ev.if.initial). Because to is read when the Step starts and from defaults to the current value, switching pages mid-transition continues smoothly from wherever each property is now. Visibility and input follow the animated alpha: at 0 the page is hidden and disabled, so it cannot be clicked. The bindings are what do it; alpha alone does not stop input (ev.pointer.overlap).
Wipe by animating the clip window
Motion: $Wipe
Step:
Tween:
out: target.clipRect[2]
to: 200
duration: 400
ease: Ease::OutCubic
Wipe: wipe
Component: card
width: 200
height: 100
clip: true
clipRect: [0, 0, 0, 100]
@init:
start: wipe
Image: art
img: "res/card.png"
width: 200
height: 100
The Tween drives the window's width, the third element, from nothing to the full card (motion.target.element). To wipe vertically, drive the fourth element, target.clipRect[3], to the card's height, 100; because +Y is up, the card is revealed from the bottom up. To wipe from the right, two elements move together: tween a Data value reveal.w and write clipRect: [200 - reveal.w, 0, reveal.w, 100].
Shared modules
# imports/theme.abp -- types only
Text: $Heading
font: HeadingFont # resolved where the type is used
fg: "#FFFFFF"
# main.abp
import: "imports/theme.abp"
Display: main
width: 1280
height: 480
Font: HeadingFont
path: "fonts/NotoSans-Bold.ttf"
pointSize: 32
Heading: title
text: "My Design"
x: 40
y: 420
The Font lives in main.abp because a design never creates a module's own instances (doc.module.instances).
Glossary
- ABP
- Altia BluePrint, the language this reference describes.
- Action
- A step inside an event handler, written as a bare key (chapter 9).
- Design
- An entry document together with every module it imports.
- Entry document
- The document named by
abp.project.yaml'sentry, or a lone.abpfile: the top of the design's tree. - External function
- A host-implemented function called from a
code:action asext.name(…). - Instance
- One object of a type, created by a declaration.
- Item
- A component positioned by an enclosing layout.
- Logical type
- A type without graphic content:
Timer,Data,Font,Layout,Motion,Display,Shader,Material,Texture. - Main axis / cross axis
- The direction set by
flexDirection, and the one perpendicular to it. - Module
- A document loaded through
import:. A module provides types only. - Reactive
- Re-evaluated automatically when a value it reads changes; the default for property bindings.
- Run
- One application of a
Motionto one target. - Run mode
- The state in which event handlers dispatch.
- User property
- A property an author declares by assigning a name the type does not define.
- User type
- A type defined with
Base: $Name. - Viewable
- A type that takes part in drawing —
Component,Image,Text,Shape,Stamp,Sceneand types derived from them — and has the common surface of chapter 11. A plainComponentdraws nothing itself but is still viewable.