Appearance
Text, Prose Styling & Transitions
Printing and notifying, inline and block prose styling, and the scene/zone transition effects.
UI
(img: filename)
(img: filename, transition)
(img: filename, align)
(img: filename, transition, align)
Displays an image. The two optional positional args are identified by their value: transition names and alignment names don't overlap, so order doesn't matter.
Transitions: fade dissolve slide-left slide-right slide-up slide-down fade-left fade-right fade-up fade-down zoom flicker pulse rumble shudder blur instant (see Transitions)
Alignments: left right center inline
- Without an alignment, the image pushes to the
imagezone (standard background/scene image behavior). - With an alignment, the image pushes to the current zone, enabling inline and floating images in text passages.
Accepts a filename relative to assets/images/ or assets/video/. Extension is optional if unambiguous. Video files loop automatically.
ana
(img: bar_evening.jpg) // image zone
(img: bar_evening.jpg, fade) // image zone with transition
(img: portrait.jpg, right) // current zone, floated right
(img: portrait.jpg, dissolve, left) // current zone, transition + float
(img: icon.png, inline) // current zone, baseline heightAlignment values:
| Value | CSS effect | Use case |
|---|---|---|
left | float: left with right margin | Portrait with text wrapping right |
right | float: right with left margin | Portrait with text wrapping left |
center | display: block; margin: auto | Centered image break |
inline | display: inline; height: 1.5em | Icon sized to sit on the text baseline |
Float images use max-width: 40% by default. Override .ana-img-align-left, .ana-img-align-right, etc. in styles/game.css.
ana
// Float example in a text zone
@zone(text)
(img: old_bill.jpg, right)
Old Bill is a man of few words. He wipes the bar without making
eye contact. Years of this work have made him patient.Tooltip: add a tip, "text" pair for a hover/focus tooltip on the image; see Tooltips.
ana
(img: map.png, inline, tip, "The old quarter")(portrait: slot, character, expression)
Loads a character portrait into a semantic slot. slot is character (player character) or npc. The layout and CSS determine which sidebar each slot maps to; the passage does not specify a physical position.
ana
(portrait: character, player, neutral)
(portrait: npc, bartender, suspicious)Portrait assets are resolved from assets/images/{character}/{expression}.jpg (or .png). The file extension is optional when the filename is unambiguous: if only one file in assets/images/{character}/ matches the expression name, the engine resolves it automatically.
(audio: track, behavior, channel: channelName)
Controls audio playback. The behavior argument defaults to "play" when omitted.
Valid behaviors: play, loop, fade-in, fade-out, stop.
| Behavior | Effect |
|---|---|
play | Plays the track once from the beginning (default when omitted) |
loop | Plays the track on repeat until stopped |
fade-in | Starts playing and fades volume in |
fade-out | Fades volume out and stops the track |
stop | Stops the track immediately |
The channel: kwarg selects which audio channel to play on and defaults to "sfx".
Built-in channels: bgm, sfx, ambiance, voice. Each channel plays one track at a time; a new track on the same channel replaces the current one. Custom channels can be declared in GameInit with (audio-channel:).
ana
(audio: jazz_loop, fade-in, channel: bgm)
(audio: door_creak, play) // defaults to sfx
(audio: door_creak) // behavior defaults to "play"
(audio: jazz_loop, fade-out, channel: bgm)
(audio: rain, loop, channel: ambiance)
(audio: jazz_loop, stop, channel: bgm)(audio-channel: name) (GameInit only)
Declares a custom audio channel beyond the four built-in ones. Call in GameInit.
ana
(audio-channel: "radio")
(audio-channel: "phone_call")(game-new:) (title screen only)
Triggers the new-game flow: runs @system(init), then navigates to the passage declared by (start:). Used inside the action linked from the "New Game" button on the title screen.
(start: PassageName) (GameInit only)
Declares which passage the engine navigates to after GameInit completes.
ana
(start: MainStreet)(notify: "message")
Shows a brief notification that auto-dismisses. Use for feedback on actions: item pickup, gold change, passage of time. Does not block passage flow.
ana
(notify: "You gained 50 gold.")
(notify: "Day advanced to Tuesday.")
(notify: "The lock clicks open.")(print: expression)
Evaluates any expression and renders its value as prose in the current zone. Useful when you need to display the result of a macro call that returns a value ((get:), (tier:), (shop-price:), and so on) without assigning it to a temp variable first.
(print:) pushes to whatever zone is currently active (respects @zone() directives). Null and undefined values are silently suppressed.
ana
(print: (get: $npc, _id, "name")) // NPC name by dynamic id
(print: (tier: _id)) // relationship tier label
(print: (shop-price: "bar_shop", "beer")) // item buy price as prose
(print: $npc.bartender.name) // same as inline interpolationWhen to use (print:) vs inline interpolation:
| Situation | Use |
|---|---|
| Known static variable | $npc.bartender.name (cleaner inline) |
| Dynamic id in a loop | (print: (get: $npc, _id, "name")) |
| Nested macro result | (print: (tier: _id)) |
| Expression with math/logic | (print: (random: 1, 6)) |
(meter: $variable, $maxVariable)
(meter: $variable, $maxVariable, "extra-class")
(meter: $variable, $maxVariable, nolabel)
(meter: $variable, $maxVariable, "extra-class", nolabel)
Renders an HTML progress bar with the current and max values overlaid as text in the center of the bar. Useful in sidebar character panels and HUD areas.
The optional "extra-class" string adds a second CSS class to the meter container, letting you style multiple meters differently without modifying the base .meter rule. The bare symbol nolabel suppresses the number overlay. Both are optional and can appear in either order.
ana
(meter: $player.health, $player.maxHealth) // label shown
(meter: $player.health, $player.maxHealth, nolabel) // bar only
(meter: $player.health, $player.maxHealth, "health-bar") // custom class
(meter: $player.health, $player.maxHealth, "health-bar", nolabel) // bothStyle the custom class in styles/game.css or with (css:):
css
.meter-fill { background: #9a7d5a; } /* base fill color */
.health-bar .meter-fill { background: #c05050; } /* red health bar */
.stamina-bar .meter-fill { background: #50a050; } /* green stamina bar */
.xp-bar .meter-fill { background: #5080c0; } /* blue XP bar */Base CSS classes: .meter (container, 22px tall), .meter-fill (fill), .meter-label (centered overlay text).
(layout: LayoutName)
Changes to a different layout template. Zones that exist in both the old and new layout keep their current content: only zones unique to the old layout are removed, and zones unique to the new layout are created empty. So switching from a layout with text/options/sidebar_npc to one with just text/options drops the sidebar but leaves text and options untouched.
ana
(layout: Layout_Standard)
(layout: Layout_Cinematic)
(layout: Layout_Phone)Tooltips
Two forms, both rendering the same .ana-tip-wrap / .ana-tip-content DOM and shown on hover and keyboard focus. Style via --ana-tooltip-* custom properties.
tip, "text" kwarg (links & images)
For a plain text tooltip, add a tip, "..." pair to (link:), (link-if:), (link-once:), or (img:).
ana
(link: "the locket", tip, "A tarnished silver locket.")[(goto: LocketScene)]
(img: map.png, inline, tip, "The old quarter")(tooltip:) / (tip:)
For a rich tooltip (multiple lines, images), wrap the visible trigger in (tooltip:) and put the tooltip content in a nested (tip:) block. The trigger can be prose, a (link:), or an (img:); the tip can contain prose and images (images are auto-sized to the tooltip width).
ana
(tooltip:)[
(link: "the strange potion")[(goto: PotionScene)]
(tip:)[
(img: potion_red.png)
**Unidentified potion.** It smells of cloves and something metallic.
]
](tip:) is only meaningful inside a (tooltip:) body. Note that, like other absolutely-positioned overlays, a tooltip can be clipped by a scrolling ancestor zone, so prefer them on inline prose and short links.
Prose Styling
Macros and inline syntax for styling text: headings, alignment, fonts, and rich formatting.
(div: "class", ...)[body]
(span: "class", ...)[body]
(p: "class", ...)[body]
Generic HTML element wrappers. Each positional arg is a CSS class name; multiple classes are joined with a space on the element. All three support the bare no-arg form for unclassed structural grouping.
ana
// Single class
(div: "char-panel-name")[(proper: $player.name)]
// Multiple classes
(div: "char-panel-stat", "stat-active")[
(div: "char-panel-stat-label")[Health]
(meter: $player.health, $player.maxHealth)
]
// Nested loop — works naturally
(div: "char-panel-traits")[
(each: _trait in $player.traits)[
(span: "char-panel-trait")[_trait]
]
]
// Bare form — structural grouping only
(div:)[
(span: "label")[Str]
(span: "value")[$skill.strength]
]
// Styled paragraph
(p: "cc-hint")[Name is required to continue.]The body supports full macro execution: (if:), (each:), (print:), variable interpolation, nested (div:) / (span:) / (p:), and any other block macro.
These replace the (html: "<div class='...'>" + content + "</div>") pattern. The main difference: the body executes as Ana markup, so you get proper macro support rather than string concatenation.
(br:) / (br: count)
Inserts one or more HTML line breaks (<br>). Ana groups consecutive non-blank lines into a single paragraph and starts a new paragraph at each blank line; (br:) is how you force a manual line break within the current paragraph (e.g. for poetry or dialogue formatting) without ending it.
Optional count arg sets how many breaks to insert (1–10, default 1).
ana
Opening remarks:
(br:)
Good evening.
The crate contains:
(br: 2)
Nothing.Markdown inline formatting
Recognized in any prose line inside a passage body or block:
| Syntax | Renders as | HTML |
|---|---|---|
**text** | Bold | <strong>text</strong> |
*text* | Italic | <em>text</em> |
~~text~~ | Strikethrough | <s>text</s> |
==text== | Highlight/mark | <mark>text</mark> |
`text` | Inline code | <code>text</code> |
--- (alone on a line) | Horizontal rule | <hr> |
Limitation: Macros cannot appear inside a formatting span using this method; the grammar captures them as literal text. Use (text-style:) when you need macros inside formatted content.
Inline code is verbatim: unlike the other spans, `text` is never interpolated. $variables and _temp names print exactly as written, so authors can show example Ana syntax (e.g. `(set: $player.gold to 10)`) on the page.
ana
The blade is **truly terrible**.
*He doesn't look at you.*
~~Wrong choice.~~ You reconsider.
==Critical hit!==
Use `(set: $player.gold to 10)` to set a value.
---Fenced code blocks
A run of lines wrapped in triple backticks renders as a <pre><code> block, block-level like a heading or (table:) (it flushes any open paragraph and stands alone):
| Syntax | Renders as | HTML |
|---|---|---|
```\ncode\n``` | Code block | <pre><code>code</code></pre> |
```lang\ncode\n``` | Code block with language class | <pre><code class="language-lang">code</code></pre> |
Content is captured completely verbatim: no inline formatting, no inline macros, no $variable/_temp interpolation. The optional language tag right after the opening fence becomes a language-<tag> class on the <code> element (for syntax-highlighting CSS/JS you supply).
```ana
(set: $player.hp to 10)
(add: $player.hp, 5)
```Bullet and numbered lists
A run of consecutive lines starting with -, *, or N. (a number followed by a period) becomes a list. The first item's marker decides the list type: -/* produce <ul>, N. produces <ol>:
| Syntax | Renders as | HTML |
|---|---|---|
- item / * item | Bullet list | <ul><li>item</li>...</ul> |
1. item | Numbered list | <ol><li>item</li>...</ol> |
| Indented item under another | Nested sublist | <li>...<ul>/<ol>...</li> |
List items support the same inline content as a prose line: bold/italic/strike/highlight/code spans, $variable interpolation, _temp names, and inline macros all work inside them. Indenting a line further than the item above nests it as a sublist of that item, to any depth.
ana
- Beer
- **Iron Knife**
- Roll result: (dice: 1, 6)
- Town
- Tavern
- Market
1. Pick a tab
2. Click an itemLists are block-level: they flush any open paragraph and stand on their own.
(text-style: "name", ...)[body]
Applies one or more named styles to block content. Unlike markdown shorthand, the body supports full macro execution: (if:), (each:), inline macros, and variable interpolation.
ana
(text-style: "bold")[You have (count: $inv, "beer") beers left.]
(text-style: "italic")[He says nothing.]
(text-style: "bold", "shudder")[DANGER!]
(text-style: "fade-in-out")[The spirit drifts through the wall.]All supported styles:
| Category | Style names |
|---|---|
| Basic | none bold italic underline double-underline wavy-underline |
| Strike | strike double-strike wavy-strike |
| Script | superscript subscript mark |
| Transform | mirror upside-down tall flat condense expand |
| Textured | outline shadow emboss smear |
| Motion | blink shudder rumble sway buoy fidget fade-in-out |
| Blur | blur blurrier |
CSS classes are .ana-ts-{style}. Override any in styles/game.css or with (css:).
Unknown style names: If an unrecognized style name is passed, the macro applies no visible style for that name (the CSS class .ana-ts-unknown-name is added but has no rules). No error is thrown and no warning is logged; the body still renders normally.
(text-align: "type")[body]
Wraps block content in a <div> with the specified text alignment. Valid values: "left", "center", "right", "justify".
ana
(text-align: "center")[
Chapter Three
]
(text-align: "right")[— The End](h1:) / (h2:) / (h3:) / (h4:) / (h5:) / (h6:)
Renders a semantic heading element. Both forms are supported:
ana
// Simple arg — text only
(h2: "Act Two — The Docks")
// Block body — allows prose interpolation and inline macros
(h2:)[Act $world.actNumber — $world.actTitle]
(h3:)[(if: $world.crisisLevel > 5)[★ URGENT: ]The Mission]Default heading styles use --ana-font and --ana-text. Override .zone h2 { ... } etc. in styles/game.css.
(font: "family")[body]
Renders block content in the specified CSS font-family string.
ana
(font: "Georgia")[The letter reads as follows...]
(font: "'Courier New', monospace")[>> ACCESS GRANTED]
(font: "'IM Fell English', serif")[The ancient inscription says...]Use any CSS font-family syntax including fallbacks and quotes. The font name is sanitized to prevent HTML injection.
(text-opacity: value)[body]
Sets the opacity of block content. 0 = invisible, 1 = fully opaque. Values are clamped to the 0–1 range.
ana
(text-opacity: 0.5)[This appears at half opacity.]
(text-opacity: 0.2)[A ghostly whisper...](text-color: "color")[body]
Sets the text color of block content. Accepts any CSS color value: named colors or hex codes.
ana
(text-color: "crimson")[Danger!]
(text-color: "#4a9")[A calming green.]
(text-color: "rgba(255,200,0,0.85)")[Amber glow.](text-size: factor)[body]
Sets the font size relative to the current inherited size, in em units. 1 = normal, 1.5 = 50% larger, 0.75 = smaller. Clamped to the range 0.1–10.
ana
(text-size: 2)[Big announcement!]
(text-size: 0.75)[A footnote.]
(text-size: 1.25)[Slightly emphasized.](text-rotate: degrees)[body]
(text-rotate-x: degrees)[body]
(text-rotate-y: degrees)[body]
Rotates block content by the given number of degrees around the Z, X, or Y axis respectively. Uses CSS transform: rotate*(Ndeg) with display: inline-block.
ana
(text-rotate: 45)[Tilted at 45°]
(text-rotate: 180)[upside down and backwards]
(text-rotate-x: 45)[Tilting away from you]
(text-rotate-y: 180)[Mirror-flipped horizontally]Note: (text-rotate: 180) rotates on the Z axis, so the result is upside-down AND left-right mirrored (same as CSS rotate(180deg)). For upside-down only, combine with (text-style: "upside-down").
(transition: "name")[body]
(text-transition: "name")[body]
(text-transition: "name", durationMs)[body]
Applies a one-shot CSS entry animation to a block of prose content when it renders. (transition:) is the shorter alias; both are identical.
All shared transition names from Transitions are valid: fade, dissolve, slide-up, slide-left, zoom, pulse, rumble, shudder, blur, etc.
The optional second arg on (text-transition:) overrides the duration in milliseconds.
ana
// A paragraph that fades in as it appears
(transition: "fade")[The letter slides under the door.]
// Named entry direction
(transition: "slide-up")[New information has surfaced.]
// Per-call duration override — rumble faster than the default 0.6s
(transition: "rumble", 300)[Your name is now $player.name.]
// Combined with (after:) for delayed dramatic entry
(after: 1.5s)[
(transition: "dissolve")[A figure steps inside.]
]
// Works with macros inside the body — the body executes normally
(transition: "pulse")[
You now have (count: $inv, "gold_coin") gold coins.
]
**This is different from CTC text transitions** (set via `(default-transition: text, ...)`). CTC animations apply to the entire `text` zone during navigation. `(transition:)` applies to a specific block anywhere in any zone, independently of navigation.
## Transitions {#transitions}
Ana uses one shared set of CSS animation names across scene changes, zone updates, image reveals, and inline block content. The same name `"slide-up"` works on a `(goto:)`, an `(img:)`, and a `(transition:)` block.
### Shared transition names
| Name | Effect |
|------|--------|
| `fade` | Fade in from transparent |
| `dissolve` | Fade in while resolving from a blur |
| `slide-left` | Enter from the left |
| `slide-right` | Enter from the right |
| `slide-up` | Enter from below |
| `slide-down` | Enter from above |
| `fade-left` | Subtle fade + leftward drift |
| `fade-right` | Subtle fade + rightward drift |
| `fade-up` | Subtle fade + upward float |
| `fade-down` | Subtle fade + downward float |
| `zoom` | Scale from 85% to 100% |
| `flicker` | Strobe opacity then settle |
| `pulse` | Scale overshoot then settle |
| `rumble` | Horizontal shake |
| `shudder` | Multi-axis shake |
| `blur` | Resolve from heavy blur |
| `instant` | Appears immediately (no animation) |
### Scene and zone transitions
Passed as an optional positional arg to `(goto:)` or `(update:)`:(link: "Enter")[(goto: BarScene, "fade")] (link: "Enter")[(goto: BarScene, "slide-up")] (link: "Ask")[(update: Bar_Rumors, "dissolve")]
ana
### Image transitions
Passed as an optional positional arg to `(img:)`. Identifiable by value: transition names and alignment names (`left`, `right`, `center`, `inline`) don't overlap:(img: bar_evening.jpg, "fade") (img: bar_evening.jpg, "zoom") (img: bar_evening.jpg, "dissolve", left) // transition + alignment
ana
### `(default-transition: key, "value")`
### `(default-transition: key, "value", durationMs)`
Sets an engine-wide transition default. Call once per key, one line per key is cleanest. An optional number as the third positional arg sets the default duration in milliseconds for that tier. Keys: `goto`, `update`, `img`, `text`. Set a value to `none` to disable.// In GameInit — one call per key (default-transition: goto, "fade", 400) (default-transition: update, "dissolve", 300) (default-transition: img, "fade", 600) (default-transition: text, "dissolve", 800) (default-transition: audio, "fade-in") // ← no duration, see note below
ana
Each duration sets a per-key CSS variable (`--ana-goto-dur`, `--ana-update-dur`, `--ana-img-dur`, `--ana-tt-default-dur`) that falls back to the global `--ana-transition-dur` (`0.35s`) when unset.
**`audio` does not accept a duration arg.** Audio fade duration is a per-call positional on `(audio:)` itself, not a global default, because different tracks naturally fade at different speeds. The `(default-transition: audio, "fade-in")` call only sets the *behavior* used when none is specified:// Audio duration is always per-call: (audio: pub_music, bgm, "fade-in", 2000) // 2s fade for music (audio: door_creak, sfx, "play") // no fade, instant
### Text transitions (CTC, click to continue)
These three transitions animate the entire `text` zone when a passage loads. The `options` zone is hidden until the animation completes; clicking the text zone skips to the end.
| Name | Effect |
|------|--------|
| `typewriter` | Characters revealed one at a time (30 ms/char) |
| `dissolve` | All paragraphs fade in simultaneously |
| `line` | One paragraph at a time, 400 ms apart |(default-transition: text, "typewriter") (default-transition: text, "dissolve", 600) // with duration override
ana
CTC transitions are zone-level: they always apply to the whole text zone. For transitions on individual blocks, use `(text-transition:)` / `(transition:)` instead (see below and [Prose Styling](/reference/text-and-styling#prose-styling)).
### `(text-transition: "name")[body]`
### `(text-transition: "name", durationMs)[body]`
### `(transition: "name")[body]` *(alias)*
One-shot CSS entry animation on a **specific block of content**, independent of navigation. All shared transition names are valid. The optional second arg overrides the duration in ms; the global default is `0.6s` (or whatever `(default-transition: text, "...", N)` has set).(transition: "fade")[The letter slides under the door.]
(transition: "slide-up")[A new lead has appeared.]
// Per-call duration override (transition: "rumble", 300)[Your name is now $player.name.]
// Combined with (after:) for delayed entry effects (after: 1.5s)[ (transition: "dissolve")[A figure steps inside.] ]
ana
| Situation | Use |
|-----------|-----|
| Entire text zone reveals on navigation | CTC: `(default-transition: text, ...)` |
| One paragraph or phrase animates in | `(transition:)` |
| Delayed reveal with drama | `(after: Ns)[(transition: "slide-up")[...]]` |
| Stat change result shown with impact | `(transition: "pulse")[You gained +10 persuasion!]` |
### CSS custom properties
| Property | Default | Controls |
|----------|---------|----------|
| `--ana-transition-dur` | `0.35s` | Scene and zone animation duration |
| `--ana-tt-default-dur` | `0.6s` | Default `(text-transition:)` / `(transition:)` duration |
Override in `styles/game.css`, or set the text default in GameInit:(default-transition: text, "dissolve", 300) // sets --ana-tt-default-dur to 300ms