BluePrint Language Reference
Home

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.

Note

Notes give background or rationale. They are informative, not normative.

Warning

Warnings describe constructs that are accepted but do something other than what an author would likely expect.

Reserved

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.

© 2026 Altia, Inc. Text licensed under CC BY-ND 4.0; code examples under MIT. Altia and Altia BluePrint are trademarks of Altia, Inc.

Notation

Grammar

Syntax is summarized in grammar boxes. The notation is:

NotationMeaning
NameA 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 | bEither a or b
( … )Grouping
[a-z]Any character in the range
INDENT / EOLLeading spaces / the end of a line
…Any characters up to the end of the line
// textAn annotation inside a grammar box; not part of the syntax
SyntaxExample : Key ":" ( " " Value )? Comment? EOL

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.

SyntaxEntry : INDENT Key ":" ( " " EntryValue )? Comment? EOL Key : TypeKey | PropertyKey | EventKey | AnimationKey | "import" TypeKey : ( Identifier "::" )? TypeName TypeName : ( "_" ( "_" | [0-9] )* )? [A-Z] [A-Za-z0-9_]* PropertyKey : ( "_" ( "_" | [0-9] )* )? [a-z] [A-Za-z0-9_]* EventKey : "@" EventName AnimationKey: "&" Identifier EntryValue : InstanceName | "$" TypeName | Expression | BlockScalar | ImportSpec BlockScalar : "|" EOL ( MoreIndentedLine )+

lex.entry.shapesThe entry shapes are:

ShapePatternMeaningSee
Instance declarationType: nameDeclare an instance of Type named name§4.1
Type definitionType: $NewTypeDefine a new type derived from Type§4.2
Property assignmentname: <expr>Assign a property of the enclosing object§4.3
Array by elementsname: + childrenAn array property, one named element per line§4.4
Importimport: "path"Load another document§3.4
Event handler@event: [<cond>]Handle an event, optionally gated§8
Actionaction: <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

SyntaxIdentifier : [A-Za-z_] [A-Za-z0-9_]* // with at least one letter

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

CategoryNamesReason
Declaration keysimport Root as export type parentStructural keywords
Call namespacesext Math String ArrayHeads of call expressions, not addressable objects
Domain namesdata strings images views motionReserved; see scope.res.byname for their use as a prefix
JavaScript keywordsevery ECMAScript keyword (if, for, let, return, …)Conflict with expression syntax
Built-in memberlengthx.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

SyntaxDocument : Import* TopLevelItem* TopLevelItem : Declaration | TypeDefinition | EventHandler

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
FieldMeaningStatus
nameProject display nameReserved
entryThe entry document, relative to the project fileUsed
sourceRootsDirectories to search for importsReserved
assetsDirectory for asset pathsReserved

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

PathResolves relative to
An import: pathThe importing file's directory
An import: path beginning @/The entry document's directory
Any absolute pathUsed as written
Asset paths: Image.img, Font.path, Texture.source, Scene.sceneThe entry document's directory

Imports and modules

SyntaxImport : "import:" " " ImportSpec EOL ImportSpec : StringLiteral ( " -> " TypeName )?

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

SyntaxDeclaration : TypeKey ":" ( " " Identifier )? EOL Body? Body : ( PropertyEntry )* ( EventHandler | Declaration | AnimationBinding )* // one level deeper

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

SyntaxTypeDefinition : TypeKey ": $" TypeName EOL Body?

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

SyntaxPropertyEntry : PropertyKey ":" ( " " ( Expression | BlockScalar ) )? EOL ElementList?

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
Warning

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

SyntaxElementList : ( INDENT Identifier ":" ( " " Expression )? EOL ElementList? )+

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

KindExamples
Integer42 0xFF8800 (a negative number such as -3 is unary minus applied to a literal)
Float0.5 1.0
Booleantrue false
Null and infinitynull 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:

OperatorsKind
. [ ] ( )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.

ReceiverMembers
Math functionsabs ceil floor round trunc sign min max random sin cos tan asin acos atan atan2 sqrt cbrt pow hypot exp log log2 log10
Math constantsPI E LN2 LN10 LOG2E LOG10E SQRT2 SQRT1_2
String methodsat concat includes indexOf lastIndexOf slice substring startsWith endsWith padStart padEnd repeat trim trimStart trimEnd toUpperCase toLowerCase charAt
Array methodsat concat includes indexOf slice join
Number methodstoFixed(digits)
Lengths.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

SyntaxEnumValue : EnumName "::" ValueName // one token, no spaces

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:

ConstructDiagnosticAlternative
varexpr.parselet or const
**expr.parseRepeated multiplication or Math.pow
?. optional chainingexpr.parseNot needed: a hop through a non-object yields undefined instead of throwing
===expr.generate==
!==expr.parse!=
Compound assignments other than += -= *= /=expr.parseWrite the assignment in full: a = a % b
continueexpr.resolve (read as an identifier)Restructure the loop
Object literals, comma expressionsexpr.generateArrays; separate statements
Function expressions and function declarationsexpr.generateMove the work to the host (§16.2)
Arrow functions, new, void, in, instanceof, try/catch, regular expressionsexpr.parseMove the work to the host (§16.2)
Classes, import/export, promises, DOMNot supportedMove 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

PrefixResolves toExample
thisThe object that owns the expressionthis.width
parentThe parent of thisparent.height
srcThe source object of the event being dispatched (§8.3)src.deltaX
rootThe design rootroot.userProp
targetInside 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.

frame N frame N+1 tick phase @tick @complete @init (at start) update pass sources → computed then @if handlers screen pump pointer handlers then draw tick phase @tick @complete update pass sources → computed then @if handlers screen pump pointer handlers then draw host writes same frame @if writes next frame +1 per chained @if pointer writes

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:

  • @init when the design starts, @tick, and the @complete of a Timer or a Motion run before the update pass of their frame. Their writes reach the computed properties in the same frame.
  • @if handlers 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 @if in a chain adds one frame: an @if that writes A.s, and a second @if: A.s > 1 that writes B.t, update a property computed from B.t two frames after the first handler runs.
  • Pointer handlers such as @press run 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.

ClassBadgeMeaning
ReactivenoneAuthored. 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-onlyRead-onlyFixed 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
ProtectedProtectedWritten only by the runtime, such as a Timer's remaining. Readable everywhere; a write is ignored. Reactive unless it is also Non-reactive
Non-reactiveNon-reactiveA 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
IntrinsicIntrinsicSupplied 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

SyntaxEventHandler : "@" EventName ":" ( " " Condition )? EOL Action* // actions one level deeper; an empty handler is accepted Condition : Expression

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.

TriggerFiresStatus
initWhen the design starts, and on every restartActive
start completeA Timer (re)starting and expiring (§10.9); a Motion run beginning and ending, delivered to the object the run drives (§13.4)Active
enable disableOn 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 DisplayActive
tickEvery frame, on the object that declares itActive
ifReactively, whenever its condition re-evaluates to a truthy valueActive
press hold release click tap doubleTap enter leave hover hoverX hoverY drag dragX dragYPointer input (§8.3)Active
show hide move moveX moveY next prev first last select unselect keyNeverReserved
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:

TriggerFires when
pressA button goes down over the object
releaseEvery button-up of the button that armed the gesture, with the final payload
clickPress then release without a drag, of any duration
tapPress then release within 200 ms
doubleTapA tap whose release comes within 1000 ms of the previous tap's release. It replaces that second tap: no @tap fires for it
holdA press lasts 500 ms without dragging
enter leaveThe pointer enters or leaves the object
hover hoverX hoverYThe 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 dragYThe 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:

PropertyMeaning
x yProtected Non-reactive Current pointer position
startX startYProtected Non-reactive Position at press
lastX lastYProtected Non-reactive Position at the previous delivery
deltaX deltaYProtected Non-reactive Movement since the previous delivery
xVelocity yVelocityProtected Non-reactive Velocity in pixels per second
startTime currentTimeProtected Non-reactive Timestamps of the press and of this delivery
buttonProtected 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

SyntaxAction : ActionName ":" " " ( Expression | BlockScalar ) EOL

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.

ActionValueEffectStatus
codeAn expression or a | blockRuns it. The only place state may be written (§7.2)Active
logAn expressionLogs its valueActive
start pause resume stop reset restart completeA Timer or a MotionTimer control (§10.9); Motion run control on the handler's owner (§13.4). On any other object they do nothingActive
enable disableAny objectWrites the target's enabled property, which fires @enable/@disable. A disabled component is input-transparent and stops tickingActive
show hide select unselect next prev first last input anim call set—NoneReserved
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

TypeKindPurposeSee
DisplayLogical, top level onlyA screen§10.2
FontLogicalA typeface at a size§10.3
ComponentViewableGrouping; base for user types§10.4
ImageViewablePNG or SVG picture§10.5
TextViewableSingle- or multi-line text§10.6
ShapeViewableGeometric primitives§10.7
StampViewableDraws another component by reference§10.8
TimerLogicalPeriodic counter§10.9
DataLogicalNamed values; the host interface§10.10
LayoutLogicalFlexbox arrangement§12
MotionLogicalReusable animation program§13
Shader Material TextureLogicalCustom GLSL drawing§14
SceneViewablePre-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.

Reserved

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.

PropertyTypeDefaultMeaning
widthreal—Screen width in pixels
heightreal—Screen height in pixels
bgcolorengine defaultFill 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.

PropertyTypeDefaultMeaning
pathstring—Face file, relative to the entry document's directory. The file determines weight and style
pointSizeunsigned12Size in points
boldboolfalseStored; no effect in this version. Use a bold face file
italicboolfalseStored; no effect in this version. Use an italic face file
strokereal0Stroke 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.

PropertyTypeDefaultMeaning
imgstring—Picture path, relative to the entry document's directory: a .png or a .svg
scalingenumScaling::ScaleHow the picture fills the width by height box (below)
bordersarray0Nine-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:

scalingThe picture is drawn
Scaling::Scale (default)width by height, each axis on its own
Scaling::Noneat its natural size
Scaling::Aspectat the largest uniform size that fits the box
Scaling::Xwidth wide; the height keeps the aspect ratio
Scaling::Yheight 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> and class: a shape styled only through a class fills black. Put the style on the element instead, as style="fill:…".
  • clip-path: the shape draws unclipped.
  • visibility: the element draws. Hide it with display="none".
  • spreadMethod="reflect" and "repeat": the gradient pads.
  • A colour written as rgba(), hsl(), hsla() or currentColor, or a name that does not resolve, draws grey (#808080). Write it as #hex or rgb(), with fill-opacity or stroke-opacity for 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.

PropertyTypeDefaultMeaning
textstring—The string. A number renders as its string form
linesunsigned11 single line; 0 multi-line, unlimited; greater than 1 multi-line (the number is not a cap)
fontobjectdefault faceA Font
justifyenumJustify::NoneHorizontal placement in the width box: ::None ::Left ::Right ::Center
maxCharsunsigned—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
truncateenumTruncate::NoneSingle-line overflow: ::None ::Clip ::ElideEnd ::ElideStart ::ElideCenter
elideTextstring…Elision marker; its first character replaces the default
wrapbool—Multi-line: wrap long lines at maxChars characters, else at width pixels
wrapModeenumWrap::Normal::Normal, or ::NewLineOnly (break only at \n)
alignenum—Align::Top ::Center ::Bottom. Stored; no effect in this version
wrapTextstring—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: "~"
Reserved

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.

PropertyTypeDefaultMeaning
kindenumShapeType::Rect::Rect ::Line ::Polyline ::Ellipse ::Polygon ::Spline ::ClosedSpline ::Arc
pointsarray[][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
filledbooltrueFill the interior; only closed kinds enclose an area
outlineWidthunsigned0Outline thickness in pixels; 0 draws none
svgbooltrueAnti-alias every edge; false draws hard edges
radiusreal0Rect, Polygon, Polyline: rounds every vertex with a circular fillet of this many pixels
startAnglereal0Arc: start, degrees counterclockwise from +X
sweepreal360Arc: degrees swept; positive counterclockwise, negative clockwise; clamped to a full turn
innerreal0Arc: 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

kindPointsMeaning
Line2A straight segment
Polyline2 or moreConnected open segments
Rect2Opposite corners
Ellipse2Corners of the bounding box
Polygon3 or moreA closed figure
Spline3 or moreAn open curve
ClosedSpline3 or moreA closed curve
Arc2Bounding 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.

Example — a donut chart without trigonometry
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.total

Stamp

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.

PropertyTypeDefaultMeaning
stampobject—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.

PropertyTypeDefaultMeaning
periodunsigned100Milliseconds
repeatsboolfalseReset and run again on expiry
activeboolfalseProtected Whether it is running
remainingunsigned0Protected 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.

Warning

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.

+X+Y (0,0) parent parent (x: 80, y: 30) child x: 80 y: 30
PropertyTypeMeaning
xrealBottom-left X, relative to the parent. Truncated to whole pixels when drawn
yrealBottom-left Y, relative to the parent, +Y up. Truncated to whole pixels when drawn
widthrealExtent along +X
heightrealExtent along +Y
clipboolClip the object and its children to the clip window
clipRectarrayThe 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

PropertyTypeMeaning
displayobjectThe Display a top-level component renders on. Unset, the first display declared

view.display.noneContent mapped to no resolvable display is never drawn.

Appearance

PropertyTypeDefaultMeaning
visiblebooltrueDraw the object and its children. A hidden object takes no pointer input (ev.pointer.overlap)
enabledbooltrueFalse makes the object transparent to input and stops its @tick; a change of value fires @enable/@disable
alpha0–255255Opacity; inherited by children. It does not affect input: an object at alpha 0 still takes presses (ev.pointer.overlap)
fgcolorinheritedForeground: text color, solid fill color, Shape outline, grayscale-PNG tint, SVG stroke
bgcolorinheritedBackground: Shape fill, SVG fill, stroked-font glyph fill
scalearray[1.0, 1.0]Scale factors [sx, sy] about anchor; [-1, 1] mirrors. Anything but an array of two counts as [1, 1]
rotatereal0Degrees, counterclockwise, about anchor
anchorarray[0.5, 0.5]Pivot [ax, ay] as a fraction of the object's authored bounds
cornersarray—Non-affine quad warp: four normalized [x, y] pairs in BL, BR, TR, TL order. Unset or invalid is affine
blurboolfalseRender the subtree through a blur
blurRadiusarray[0, 0][rx, ry] in pixels, 0–255
bufferboolfalseCache the subtree as an image
maskobject—A component whose coverage stencils this one
maskModeenumMaskMode::Normal::Normal ::Invert ::None
maskHideSourceboolfalseStop the mask source drawing on its own
materialobject—A Material to draw through (§14)
zsigned—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:

FormExamples
Hex string"#FF8800" "FF8800"; in the eight-digit form "#00FF8800" the leading byte is ignored, and opacity comes from alpha
Integer0xFF8800
Array of 0–255 integers[255, 136, 0]
Array of 0.0–1.0 reals[1.0, 0.53, 0.0]

Layout participation

PropertyTypeMeaning
layoutobjectThe 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:

GroupAnswersApplies when
ContainerHow do I arrange my children?The component has children to lay out
ItemHow does my parent place me?The component is inside a laid-out parent
Own boxHow 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

PropertyTypeValues
flexDirectionenumFlexDirection::Row (default), ::RowReverse ::Column ::ColumnReverse
justifyContentenumMain axis: FlexJustify::FlexStart (default), ::Center ::FlexEnd ::SpaceBetween ::SpaceAround
alignItemsenumCross axis: FlexAlign::Stretch (default), ::FlexStart ::Center ::FlexEnd ::Baseline
alignContentenumWrapped lines: FlexAlign::Stretch (default), ::FlexStart ::Center ::FlexEnd ::SpaceBetween ::SpaceAround
flexWrapenumFlexWrap::NoWrap (default), ::Wrap ::WrapReverse
layoutDirectionenumFlexDir::Inherit (default), ::LTR ::RTL (RTL flips main-start)
childrenobjectA Layout used as the children template (§12.6)
paddingdimAll edges; per edge paddingLeft paddingTop paddingRight paddingBottom
autoSizeWidthboolShrink-wrap width to content plus padding. Default false
autoSizeHeightboolShrink-wrap height to content plus padding. Default false

Item properties

PropertyTypeMeaning
flexGrowrealShare of leftover main-axis space to absorb. Default 0
flexShrinkrealShare of overflow to give up. Default 0: items do not shrink unless asked
displayenumFlexDisplay::Flex (default) participates; ::None takes the item out of the flow (§12.8)
alignSelfenumOverrides the parent's alignItems; FlexAlign::Auto (default) defers
margindimAll edges; per edge marginLeft marginTop marginRight marginBottom
positionTypeenumFlexPositionType::Relative (default) offsets from the flow; ::Absolute removes the item from it
positionLeft positionTopdimOffsets used with positionType
aspectRatiorealWidth ÷ height; derives one axis from the other
Reserved

positionRight and positionBottom are accepted but not honored. Pin with positionLeft/positionTop.

Own-box properties and dimensions

PropertyTypeMeaning
flexWidth flexHeightdimThe component's size in the layout
flexMinWidth flexMinHeightdimLower clamps
flexMaxWidth flexMaxHeightdimUpper clamps

layout.dim.formsA dimension (dim) takes one of four forms:

FormExampleMeaning
NumberflexWidth: 100Pixels
"<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.

layout.hiddenvisible: 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.

Example — fixed ends, stretchy middle
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: spaced

Motion

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

SyntaxMotion : "Motion:" ( " " Identifier | " $" TypeName ) EOL PropertyEntry* Step+ Step : "Step:" ( " " Guard )? EOL PropertyEntry* Operation+ Operation : ( "Set" | "Tween" | "Path" | "Spring" | "Wait" ) ":" ( " " Guard )? EOL PropertyEntry* Guard : Expression
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

PropertyTypeMeaning
repeatsignedExtra iterations of the whole program. Any negative value repeats forever
repeatTypeenumRepeatType::Loop (default) restarts; ::Reverse alternates direction and plays curves backward; ::Mirror swaps start and end and plays curves forward
repeatDelayunsignedMilliseconds between iterations
runningunsignedProtected Number of live runs
timeunsignedProtected Non-reactive Milliseconds into the current run
stepunsignedProtected 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

VerbEffect on a run
startStarts 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 / resumeSuspend and continue the run
stopEnds the run, leaving properties where they are
resetWrites 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
completeLands every started operation on its end value, fires @complete and ends the run. Steps not yet reached never run
restartA reset followed by a start: values revert to their start values, and a Spring loses its velocity
enable / disableWrite 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

OperationEffect
SetWrites to into out the instant its Step starts
TweenInterpolates out from its current value (or from) to to over duration, on an easing curve
PathDrives one or more properties through a list of waypoints (§13.7)
SpringDamped-oscillator approach to to, or a fling projected from velocity (§13.8)
WaitHolds 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.

PropertyApplies toMeaning
outSet Tween Path SpringRead-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]
fromTween SpringRead-only Start value; an array gives each element its own. Default: the property's current value, so an interrupted run continues rather than snapping
toSet Tween SpringRead-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
durationTween Path Spring WaitRead-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
delayAllRead-only Milliseconds after the Step starts before this operation begins
easeTween PathRead-only An Ease value or a points curve (§13.6)
roundSet Tween Path SpringRead-only How a real result lands in an integer property: Round::None (default) ::Round ::Up ::Down; per dimension on a Path
repeat repeatType repeatDelayTween Path Spring WaitRead-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
untilWaitRead-only Hold while false. Set until or duration, not both
bounceSpringRead-only 0 is critically damped, up to 0.95 very bouncy. Default 0.25
stiffness damping massSpringRead-only Physics form; writing any one selects it. Defaults 100, 10, 1
velocitySpringRead-only Initial velocity in units per second; an array is per element
power snap min maxSpringRead-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.

Reserved

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:
GuaranteedArrival timeSpeed in px/s
EmergentSpeed per segmentArrival time
timesArrival time (0–1) of each point, one per point; default evenly spacedNot used
easeProgress over time across the routeShapes the accel/brake ramps only
Uneven segmentsLong segments are fasterEvery segment at the same pace
Route changes mid-runSnaps to the new geometry at the same progressContinues at the same distance along the current segment

motion.path.timestimes does not apply to a curved guide.

Speed-mode propertyMeaning
speedRead-only Cruise speed in px/s, or initial speed when decay is set. speed, drive and decay each select speed mode
accelRead-only Pixels from rest to cruise, shaped by ease
brakeRead-only Pixels of deceleration into the end, shaped by ease run backward. A route shorter than accel + brake never reaches cruise
decayRead-only Time constant in ms: speed decays exponentially and the target stops where it stops, or at the end of the route
driveRead-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
heldRead-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
joinRead-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
orientRead-only A property to receive the heading in degrees; the target's art faces +X. Two-dimensional Paths only
anchorRead-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.

TypeWhat it isReferenced from
ShaderA vertex + fragment pair (GLSL ES 3.00) and its programMaterial.shader
MaterialA shader, a blend mode, and variable valuesAny viewable's material:
MatVarOne uniform's value; a child of a MaterialIts name is the uniform it sets
TextureWhat a sampler2D reads: a picture or a bufferA 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

PropertyTypeMeaning
shaderTypeenumThe family; default ShaderType::Custom
vertexstringVertex stage source, written as a | block
fragmentstringFragment 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.

shaderTypeDefault stagesTypical user
ShaderType::VectorVector 2DShape
ShaderType::ImageImage 2DImage; the blit of a buffer or blur component
ShaderType::TextText 2DText glyphs
ShaderType::MaskMask 2DA masked component's composite
ShaderType::CustomNoneAuthor 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.

FamilyFragment inputsEngine uniforms (ALTIA_u_*)
Vectorv_texCoordcolorFg colorBg pass
Imagev_texCoordcolorFg colorBg texture texWidth texHeight alpha
Textv_texCoord v_texCoord2colorFg colorBg texture pass alpha texWidth texHeight
Maskv_texCoord v_texCoord2colorFg 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.

NameTypeValue
ALTIA_u_pmvMatrixmat4Canvas projection and view. Vertex stage: gl_Position = (ALTIA_u_pmvMatrix * ALTIA_u_objXForm) * vec4(ALTIA_a_position, 0.0, 1.0)
ALTIA_u_objXFormmat4The object's own transform (scale, rotate, anchor, corners). Omit it and the object stops following its transform
ALTIA_u_colorFgvec4The viewable's fg; .a is its effective alpha (0–1, inherited alpha included)
ALTIA_u_colorBgvec4The viewable's bg
ALTIA_u_passfloatVector: 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_lineWidthfloatShape 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_texturesampler2DImage: the picture (premultiplied). Text: the glyph raster. Mask: the masked content. Buffered or blurred component: the cached surface
ALTIA_u_texture2sampler2DMask: the mask source's rendering; its alpha is the coverage
ALTIA_u_texWidth ALTIA_u_texHeightfloatSize of ALTIA_u_texture in pixels
ALTIA_u_alphafloatA format flag, not opacity: 1.0 when the texture is alpha-only (8-bit picture, glyphs), 0.0 when it carries color
ALTIA_u_alphaOperatorfloat1.0 for MaskMode::Normal, −1.0 for ::Invert; coverage = (op - 1.0) / -2.0 + op * mask.a
ALTIA_u_maskedfloat1.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 uniform split 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 MatVar name 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 typeMatVar.value shape
int uint boolOne number (bool: 0 or 1)
floatOne number
vec2 vec3 vec4Array of 2, 3 or 4 reals
mat3 mat4Array of 9 or 16 reals, column-major
sampler2DA Texture reference
samplerCubeNot settable in this version
ivec* bvec* uvec* mat2Unsupported: 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 statusMeaning
not compiled yetNo material has bound it
currentThe live program matches the sources
changed since the last compileEdited; the old program is still drawing
FAILED (with log)The last compile failed

Material

PropertyTypeMeaning
shaderShaderThe program to draw through. Unset, unresolved or not a Shader: users draw with the built-in program
blendModeenumUnset 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.

BlendModeGL blendDefault for
SourceAlphaSRC_ALPHA, ONE_MINUS_SRC_ALPHAVector, Text, Mask, Custom
OverwriteBlending off
MultiplyDST_COLOR, ONE_MINUS_SRC_ALPHA
ScreenONE, ONE_MINUS_SRC_COLOR
AdditiveSRC_ALPHA, ONE
PremultipliedAlphaONE, ONE_MINUS_SRC_ALPHAImage

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

PropertyTypeDefaultMeaning
sourcestring or object—A picture path, resolved as Image.img; or a buffer: true component, sampled live
filterenumTextureFilter::Linear::Linear or ::None (nearest)
wrapenumTextureWrap::Clamp::Clamp ::Repeat ::Mirror: sampling outside 0–1
mipmapboolfalseBuild 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

ViewableDraws through the materialFamily
ImageThe pictureImage
TextThe glyph runText
ShapeFill pass and outline pass (ALTIA_u_pass)Vector
Component with buffer or blurThe cached surface's blitImage
Component with maskThe mask compositeMask
Plain ComponentNothing—
StampThe stamped objects carry their own materialsThe 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.

Example — an animated band on a shape
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: bandAnim
Example — sampling a buffered component

This 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, samplerCube values, 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: true components 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

PropertyTypeDefaultMeaning
scenestring—Path to the .a3d, relative to the entry document's directory
cameraunsigned0Index into the file's cameras. Reactive: the view cuts when the expression changes
antialiasenumAntiAlias::None::None ::Taa ::Msaa ::Ssaa; one mode at a time
taaWarmupunsigned2Warm-up frames for Taa, clamped 1–8
msaaSamplesunsigned4Samples per pixel for Msaa, clamped 2–16
ssaaScalereal2.0Render scale for Ssaa

Animation bindings

SyntaxAnimationBinding : "&" Identifier ":" " " Expression EOL

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 codeMeaning
0No errors (warnings allowed)
1One or more errors
2The design could not be loaded, or the command line was wrong
Note

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.

DocumentDocument : Import* TopLevelItem* TopLevelItem : Declaration | TypeDefinition | EventHandler Import : "import:" " " StringLiteral ( " -> " Identifier )? EOL
DeclarationsDeclaration : TypeKey ":" ( " " Identifier )? EOL Body? TypeDefinition : TypeKey ": $" TypeName EOL Body? Body : PropertyEntry* ( EventHandler | Declaration | AnimationBinding )* TypeKey : ( Identifier "::" )? TypeName TypeName : ( "_" ( "_" | [0-9] )* )? [A-Z] [A-Za-z0-9_]*
PropertiesPropertyEntry : PropertyKey ":" ( " " ( Expression | BlockScalar ) )? EOL ElementList? PropertyKey : ( "_" ( "_" | [0-9] )* )? [a-z] [A-Za-z0-9_]* ElementList : ( INDENT Identifier ":" ( " " Expression )? EOL ElementList? )+ BlockScalar : "|" EOL MoreIndentedLine+ AnimationBinding : "&" Identifier ":" " " Expression EOL
Events and actionsEventHandler : "@" EventName ":" ( " " Expression )? EOL Action* Action : ActionName ": " ( Expression | BlockScalar ) EOL EventName : "init" | "start" | "complete" | "enable" | "disable" | "tick" | "if" | "press" | "hold" | "release" | "click" | "tap" | "doubleTap" | "enter" | "leave" | "hover" | "hoverX" | "hoverY" | "drag" | "dragX" | "dragY" | ReservedEventName ActionName : "code" | "log" | "start" | "pause" | "resume" | "stop" | "reset" | "restart" | "complete" | "enable" | "disable" | ReservedActionName ReservedEventName : "show" | "hide" | "move" | "moveX" | "moveY" | "next" | "prev" | "first" | "last" | "select" | "unselect" | "key" ReservedActionName : "show" | "hide" | "select" | "unselect" | "next" | "prev" | "first" | "last" | "input" | "anim" | "call" | "set"
MotionMotion : "Motion:" ( " " Identifier | " $" TypeName ) EOL PropertyEntry* Step+ Step : "Step:" ( " " Expression )? EOL PropertyEntry* Operation+ Operation : ( "Set" | "Tween" | "Path" | "Spring" | "Wait" ) ":" ( " " Expression )? EOL PropertyEntry*
LexicalIdentifier : [A-Za-z_] [A-Za-z0-9_]* // with at least one letter EnumValue : Identifier "::" Identifier Comment : "#" … EOL // document level ExprComment : "//" … EOL // inside expression bodies

Enumerations

Every enumeration value is written Enum::Value. Defaults depend on the property and are given in each property table, not by position here.

EnumerationValuesUsed by
ShapeTypeRect, Line, Polyline, Ellipse, Polygon, Spline, ClosedSpline, ArcShape.kind
JustifyNone, Left, Right, CenterText.justify
TruncateNone, Clip, ElideEnd, ElideStart, ElideCenter; reserved: Page, ScaleText.truncate
WrapNormal, NewLineOnlyText.wrapMode
AlignTop, Center, Bottom (no effect in this version)Text.align
ButtonNone, Left, Middle, Rightsrc.button
MaskModeNormal, Invert, NonemaskMode
FlexDirectionRow, RowReverse, Column, ColumnReverseflexDirection
FlexJustifyFlexStart, Center, FlexEnd, SpaceBetween, SpaceAroundjustifyContent
FlexAlignStretch, FlexStart, Center, FlexEnd, Baseline, SpaceBetween, SpaceAround, AutoalignItems alignContent alignSelf (each accepts its own subset)
FlexWrapNoWrap, Wrap, WrapReverseflexWrap
FlexDirInherit, LTR, RTLlayoutDirection
FlexDisplayFlex, NoneLayout display
FlexPositionTypeRelative, AbsolutepositionType
RepeatTypeLoop, Reverse, MirrorMotion and operation repeatType
EaseLinear, Hold, and In/Out/InOut × Sine, Quad, Cubic, Quart, Quint, Exp, Circle, Back, Elastic, Bounce; reserved: None, Bezierease
RoundNone, Round, Up, DownOperation round
JoinStart, NearestPath join
ShaderTypeCustom, Vector, Image, Mask, TextShader.shaderType
BlendModeSourceAlpha, Overwrite, Multiply, Screen, Additive, PremultipliedAlphaMaterial.blendMode
TextureFilterLinear, NoneTexture.filter
TextureWrapClamp, Repeat, MirrorTexture.wrap
AntiAliasNone, Taa, Msaa, SsaaScene.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).

CodeReported whenSee
doc.parseA document has a syntax error§2
import.parseAn import: line is malformed: it needs a quoted path, optionally followed by -> Alias, and nothing else; or its alias is not a type namedoc.imports
import.unresolvedAn import path cannot be resolved, or names a folderdoc.paths
import.cycleA module imports itself, directly or through other modulesdoc.imports
import.module-parseAn imported module has a syntax error; reported on the import linedoc.import.parse
import.module-instanceA module declares top-level instances, which importers never create (warning)doc.module.instances
decl.unknown-kindAn uppercase key names no type. A warning instead when the type's module has a syntax error, since its types are unavailable until it parseslex.key.case
decl.unknown-eventAn @ key names no triggerev.triggers.closed
decl.unknown-actionA key inside a handler names no actionact.list.closed
decl.event-syntaxEvent: or Action: is declared directlytype.reserved
decl.bad-parentA declaration is placed where it cannot go: inside a property, inside an event handler, or a Display below the top leveldecl.array.form, ev.syntax.where, type.display.root
decl.value-and-childrenA property has both a value and child elementsdecl.array.form
decl.bad-nameA declaration name is not a plain identifier (quoted, or starting with a digit), or an & key has no animation namelex.identifiers
decl.duplicate-nameA name is already declared in this scopedecl.names.unique
decl.parent-nameA child reuses its parent's namedecl.names.unique
decl.reserved-nameA reserved name is used as a component, property or array-element name, or an intrinsic property such as type is redeclaredlex.reserved.list
decl.shadows-propertyA child or instance shares a name with a property it would hide (warning)decl.names.shadow
decl.typedef-nameA type definition's name is not a valid type namedecl.typedef.name
decl.typedef-reservedA type definition reuses a built-in type namedecl.typedef.name
decl.typedef-not-rootA type definition is not at the top leveldecl.typedef.rootlevel
decl.typedef-duplicateA type is defined twicedecl.typedef
decl.typedef-cycleA type is used inside its own definitiondecl.typedef
decl.type-ambiguousA type name matches more than one imported module; the first import winsdoc.import.merge
decl.override-revertWhile 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 --checkdoc.import.live
expr.parseAn expression has a syntax error, or uses a construct outside the subsetexpr.unsupported
expr.generateAn expression uses an unsupported construct or calls an unknown functionexpr.unsupported
expr.resolveA name or chain resolves to nothingscope.res.unresolved
expr.stamped-unresolvedA chain in a type body cannot be satisfied where the type is instantiatedscope.type.deferred
expr.side-effectsA write, action call or external call appears outside a code: actioneval.writes.rule
expr.cycleA property depends on itself through a chain of references; it evaluates to undefinedeval.reactive
expr.read-only-writeA 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-radiusA 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.

ConstructEffectRule
A misspelled property nameBecomes a user property; does nothingdecl.prop.misspell
A reserved trigger or actionNever fires / does nothing§8.2, §9.2
An action naming an object it cannot act onDoes nothingact.syntax.silent
A domain prefix such as data.Resolves against the first top-level object of that kindscope.res.byname
An @if with no conditionNever firesev.if.cond
Any rule marked Not diagnosedAccepted; the design may not behave as intendedThe rule itself
A Motion driving an undeclared propertyWrites nothing; the run logs a warning when it startsmotion.target.declared
A child named after a property that holds an instance (ref.label.text)Reads undefined; a write changes nothingdecl.children.viaprop
An operation property its operation does not useIgnoredmotion.ops.props
A malformed or out-of-range layout valuePrevious value standslayout.reject
An auto margin on the cross axis beside alignItems: FlexAlign::StretchThe item is not laid outlayout.margin.auto
An unknown ext.* nameReads undefined at run timehost.ext.unknown
A MatVar outside a MaterialDoes nothingmat.matvar.unused
visible: false on a mask sourceMasks out everythingview.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.

CategoryNames
TypesView DeckView ModelView StringTheme ImageTheme
Triggersshow hide move moveX moveY next prev first last select unselect key
Actionsshow hide select unselect next prev first last input anim call set
PropertiesFont.bold Font.italic Text.align Text.wrapText z positionRight positionBottom
Enumeration valuesTruncate::Page Truncate::Scale Ease::None Ease::Bezier
Project fieldsname 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's entry, or a lone .abp file: the top of the design's tree.
External function
A host-implemented function called from a code: action as ext.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 Motion to 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, Scene and types derived from them — and has the common surface of chapter 11. A plain Component draws nothing itself but is still viewable.