Skip to content

Image

A sprite. It can be a static picture, an animation or an item's icon, and it can have text drawn on top of it: a sign, a plaque, a labelled diagram.

{
  "Type": "Image",
  "TexturePath": "LooseSprites/Cursors_1_6",
  "TextureSourceRectangle": { "X": 0, "Y": 192, "Width": 48, "Height": 64 },
  "Scale": 2,
  "Alignment": "Center"
}

An image is sized by its sprite: TextureSourceRectangle × Scale. If that's wider than the space available it's scaled down to fit, with a warning. Text never widens it.

Image fields

Property Type Default Description
ItemId optional string A qualified item ID such as (O)24, whose icon is drawn. When set, TexturePath and TextureSourceRectangle are ignored. The item's name and description also fill in DisplayName and Description automatically, so ItemId alone gives you the sprite and a vanilla-style tooltip. It can still be animated: see Animating an item.
TextArea optional Rectangle the whole sprite Where text is drawn, in unscaled sprite pixels relative to TextureSourceRectangle's top-left, not to the texture. This is how you place a label inside a sign's recessed panel. The text block is centred vertically within this area.
TextScale optional number 1 The text's scale, independent of Scale, which sizes the sprite.
TextAlignment optional Left | Center | Right Center How each line of text is aligned within TextArea. Distinct from Alignment, which places the whole image on the page.
Rotation optional number 0 How much rotation is applied to the texture. Note: does not affect text!
Origin optional Vector2 { X: 0.0, Y: 0.0 } The pivot point the sprite rotates and scales around, in unscaled sprite pixels relative to TextureSourceRectangle's top-left. It changes what the sprite turns and grows about, never where it rests, so a still sprite at its own Scale looks identical at any value. Note: does not affect text!

Frames

Frames, HoverFrames and FrameDuration live on every element type, and that's where the field tables and the looping rules are. Image is the type that reads a frame's SourcePoint and Scale, so the sections below cover what a frame can do with a sprite that it can't do anywhere else.

When every frame's condition fails, the element falls back to drawing TextureSourceRectangle on its own. An animation that's entirely conditional therefore goes still rather than disappearing.

TextureSourceRectangle is required when animating

It's the measuring stick: it defines the element's size, while Frames defines what's drawn. Without it, the whole sprite sheet becomes the element. The one exception is ItemId, which brings a measuring stick of its own.

Point it at a frame you'd be happy to see

Because it's the fallback, TextureSourceRectangle should be a sprite that stands on its own. Aim it at a blank cell and a fully conditional animation renders as nothing.

{
  "Type": "Image",
  "TexturePath": "LooseSprites/GemBird",
  "TextureSourceRectangle": { "X": 0, "Y": 0, "Width": 32, "Height": 32 },
  "Scale": 3,
  "Alignment": "Center",
  "Frames": [
    { "Duration": 1000, "SourcePoint": { "X": 0, "Y": 0 } },
    { "Duration": 100, "SourcePoint": { "X": 32, "Y": 0 } },
    { "Duration": 100, "SourcePoint": { "X": 64, "Y": 0 } }
  ]
}

A candle that only flickers after dark. Both frames drop out during the day, leaving the unlit sprite the source rectangle points at:

{
  "Type": "Image",
  "TexturePath": "{{ModId}}/candle",
  "TextureSourceRectangle": { "X": 0, "Y": 0, "Width": 16, "Height": 16 },
  "Scale": 4,
  "Frames": [
    { "SourcePoint": { "X": 16, "Y": 0 }, "Condition": "TIME 1800 2600" },
    { "SourcePoint": { "X": 32, "Y": 0 }, "Condition": "TIME 1800 2600" }
  ]
}

Frame scale

Scale on a frame is the one thing that changes a sprite's size mid-animation. The element is measured once, at TextureSourceRectangle × the element's own Scale, and that measurement is what reserves space on the page and what the cursor is tested against. A frame at 1.2 draws twenty percent larger over the top of that reserved space rather than pushing the elements below it down.

It grows from Origin, which defaults to the sprite's top-left corner, so a scaled frame spreads right and down unless you move the pivot. Put Origin in the middle of the source rectangle (8, 8 for a 16×16 sprite) and the frame grows evenly in every direction instead. The pivot itself doesn't move as the frame scales, so a pulse stays put rather than creeping across the page.

A pulse needs no extra art at all, just the same cell drawn bigger for a moment. None of these frames moves anywhere in the sheet, so all three leave SourcePoint out:

{
  "Type": "Image",
  "TexturePath": "{{ModId}}/pulse",
  "TextureSourceRectangle": { "X": 0, "Y": 0, "Width": 16, "Height": 16 },
  "Scale": 4,
  "Origin": { "X": 8, "Y": 8 },
  "SpacingAfter": 24,
  "Frames": [
    { "Duration": 700 },
    { "Duration": 120, "Scale": 1.15 },
    { "Duration": 200 }
  ]
}

Frame offset

Offset moves what a frame draws without moving where the element lives. Like frame scale, the element is measured once and keeps that space and that hitbox, so an offset frame slides over its own bounds rather than pushing the elements below it around or dragging its clickable area along.

Two or three frames are enough for a bob, and none of them needs new art:

{
  "Type": "Image",
  "TexturePath": "{{ModId}}/lantern",
  "TextureSourceRectangle": { "X": 0, "Y": 0, "Width": 16, "Height": 16 },
  "Scale": 4,
  "Frames": [
    { "Duration": 500 },
    { "Duration": 500, "Offset": { "X": 0, "Y": -1 } }
  ]
}

At Scale: 4 that single unscaled pixel is four screen pixels, since Offset is a measurement on the sprite rather than a coordinate on the page. That's the opposite of Position, which is a coordinate and deliberately doesn't scale.

Paired with hover frames, one offset frame gives you art that lifts under the cursor and settles when it leaves:

"HoverFrames": [
  { "Offset": { "X": 0, "Y": -2 } }
]

Offset carries the text, Scale doesn't

A frame's Scale leaves any text on the image at its own size, since scaling reads as emphasis on the art. An offset moves the whole element, text included, because a label left standing where a sprite used to be reads as a bug rather than as an effect.

Offsets are rounded to whole screen pixels. A still sprite sits happily on a fractional position, but one that moves every tick shimmers there, so the rounding is deliberate rather than incidental.

Frame actions

A frame can run trigger actions at the moment it starts. Actions are dispatched every tick, so they keep time with the animation rather than with the slower interval conditions are checked on.

They run on every cycle, forever

A three-frame loop with an action on the middle frame runs it several times a second for as long as the page is open. Nothing rate-limits this. Either keep the actions harmless to repeat, the way hover actions have to be, or condition the frames so the loop stops or gets skipped.

Playing an animation once

There's no PlayOnce field, because the pieces already here compose into one. The last frame sets a flag, every frame is conditioned on that flag being unset, and the animation drops out rather than looping:

{
  "Type": "Image",
  "TexturePath": "{{ModId}}/seal",
  "TextureSourceRectangle": { "X": 0, "Y": 0, "Width": 16, "Height": 16 },
  "Scale": 4,
  "FrameDuration": 120,
  "Frames": [
    { "SourcePoint": { "X": 16, "Y": 0 }, "Condition": "!PeacefulEnd.Parchment_HasFlag sealPlayed" },
    { "SourcePoint": { "X": 32, "Y": 0 }, "Condition": "!PeacefulEnd.Parchment_HasFlag sealPlayed" },
    { "SourcePoint": { "X": 48, "Y": 0 }, "Condition": "!PeacefulEnd.Parchment_HasFlag sealPlayed", "Action": "PeacefulEnd.Parchment_SetFlag sealPlayed" }
  ]
}

Two things make this work, and both are easy to get wrong:

TextureSourceRectangle is what's left when it ends. Conditioning every frame out doesn't hold the last frame, it falls back to the element's own source rectangle. Point that at the resting pose and the animation plays once and settles. Point it anywhere else and the sprite changes into something unrelated the moment the flourish finishes.

The flag has to outlive the frame, not the save. A session flag is cleared when the book closes, so the animation plays again next time the reader opens it. A mail flag would make it play once ever, on every save.

The last frame is cut short

Actions fire as a frame starts, and the flag conditions the frames out within the same tick. The final frame therefore never gets its full Duration. It doesn't show, because what replaces it is the fallback sprite, but it's why you shouldn't put the pose you want to end on in the last frame rather than in TextureSourceRectangle.

Animating an item

ItemId animates the same way, with one difference: the item's own icon is the measuring stick that TextureSourceRectangle usually is, so you don't need one. Leave SourcePoint off every frame and the item's sprite is what each frame draws, leaving Duration, Scale and Condition to do the work.

A parsnip that gives a little pulse, and a bigger one on hover:

{
  "Type": "Image",
  "ItemId": "(O)24",
  "Scale": 4,
  "Origin": { "X": 8, "Y": 8 },
  "Alignment": "Center",
  "Frames": [
    { "Duration": 900 },
    { "Duration": 150, "Scale": 1.1 },
    { "Duration": 250 }
  ],
  "HoverFrames": [
    { "Duration": 150, "Scale": 1.2 },
    { "Duration": 150, "Scale": 1.05 }
  ]
}

Set Origin to the middle of the item's sprite (8, 8, since item icons are 16×16) so it grows in every direction rather than down and to the right.

A SourcePoint on an item frame is measured in its sheet

Nothing stops you giving one, and it's read as a coordinate in whichever sheet the item lives in, such as Maps/springobjects. Beware that an item can move within its sheet between game versions and a modded item's sheet isn't yours at all. Use TexturePath when you want to pick sprites yourself.

Hover frames

HoverFrames is a second frame list that takes over while the cursor is on the element. It's the animated counterpart to HoverTextureSourceRectangle, which swaps a single still.

Both lists are sized by TextureSourceRectangle, or by the item's icon when ItemId is used, so this changes what's drawn and never the element's layout. Everything a frame understands works in either list: Duration, Condition and Scale all behave the same, and both lists share the element's FrameDuration as their default.

{
  "Type": "Image",
  "TexturePath": "{{ModId}}/lantern",
  "TextureSourceRectangle": { "X": 0, "Y": 0, "Width": 16, "Height": 16 },
  "Scale": 4,
  "DisplayName": "Lantern",
  "Frames": [
    { "Duration": 600, "SourcePoint": { "X": 0, "Y": 0 } },
    { "Duration": 600, "SourcePoint": { "X": 16, "Y": 0 } }
  ],
  "HoverFrames": [
    { "Duration": 120, "SourcePoint": { "X": 0, "Y": 16 } },
    { "Duration": 120, "SourcePoint": { "X": 16, "Y": 16 } }
  ]
}

Leaving HoverFrames out means the normal animation simply keeps playing under the cursor, which is the behaviour every existing book already has.

An empty hover animation falls back rather than freezing. If every frame in HoverFrames is conditioned out, the element carries on with Frames instead of dropping to a still. The order of preference is HoverFrames, then Frames, then TextureSourceRectangle, so a hover animation can come and go with the game state without interrupting the idle loop.

HoverFrames alone makes an element hoverable. In a page's Background or Foreground, an element with nothing else to offer is transparent to the cursor. A hover animation counts as something to offer, the same way a HoverTextureSourceRectangle does, so it will be reachable without needing a tooltip or an action. Setting IgnoreCursor takes that back, and logs a warning that the hover frames will never play.

Both animations restart on the swap. The hover animation plays from its first frame when the cursor arrives, and the normal animation plays from its first frame when the cursor leaves. Each is a fresh cycle rather than one picked up wherever the other left it, so a one-shot reveal on hover works as written.

Elements without HoverFrames are untouched

The restart only happens when a hover animation actually took over. An element whose Frames keep playing under the cursor never stops, so it never jumps back to its first frame when the cursor moves away.

Text fields

Optional: leave Text out for a plain picture.

Property Type Default Description
Text optional string The text to draw. Wraps automatically to the available width, including breaking words that are too long for a line on their own. \n forces a line break. Can carry tokens, both Parchment's %Token% forms and the game's tokenizable strings.
FontType optional font type varies Which font to draw with. The default differs per element type.
TextColor optional color the book's default The text color.
ShadowColor optional color the game's shadow color The color of the drop shadow drawn behind the text, alpha included. Left off, the shadow follows TextColor's alpha instead. Ignored when FontType is SpriteText, which draws its own outline.

Sprite fields

Property Type Default Description
TexturePath optional string The asset name of the sprite sheet, such as Assets/PeacefulEnd.Parchment/panelFrame2 or a vanilla path like LooseSprites/Cursors. This is a game asset name, not a file path in your content pack. Load your PNG into the game's content with a Content Patcher Load patch first. See Loading your art.
TextureSourceRectangle optional Rectangle the whole texture The area of the sprite sheet to draw. Almost always needed: without it, the whole sheet is used, which for a multi-sprite sheet means a much larger element than you expect.
HoverTextureSourceRectangle optional Rectangle? An alternate source rectangle drawn while the cursor is over the element. Must be the same size as TextureSourceRectangle. The element's layout is measured from the normal rectangle, so a differently-sized hover sprite will overhang its own bounds. For a moving hover state on an Image, use HoverFrames instead.
TintColor optional color white A color multiplied into the sprite. Multiplying means it can only darken: red on grey art gives red, red on blue art gives near-black. Best on neutral or greyscale art.
SpriteEffects optional None | FlipHorizontally | FlipVertically None Mirrors the sprite within its own bounds. It does not move the element. If flipping appears to shift the art sideways, the sprite has uneven transparent padding inside its source rectangle. Combine with a comma: "FlipHorizontally, FlipVertically". Ignored by Panel and Button, which are nine-sliced.

Common fields

Scale on an Image is the sprite scale. Use TextScale for the text.

Property Type Default Description
Type required element type Which kind of element this is. Determines every other field below.
Id optional string An optional identifier for this element. Not used for navigation, purely for your own reference.
Alignment optional Left | Center | Right Left Where the element sits within its container's width. Only has an effect when the element is narrower than the space available (see Layout). In a placed list it anchors the element and Position is measured from there.
VerticalAlignment optional Top | Center | Bottom Top Where the element sits within its container's height. Only used in Background, Foreground, Underlay and Overlay, since a stacked element takes its vertical position from the elements above it (see Placed elements). Setting it on a stacked element logs an error and does nothing.
Scale optional number 1 The element's scale. Its meaning depends on the element type: sprite scale for Image, Panel, Banner, Button and Divider. Font scale for Title, Heading and Paragraph. See Units and scale.
SpacingAfter optional int 8 The gap between this element and the next one, in unscaled sprite pixels × Scale. Not applied after the last visible element, so a trailing gap can't appear at the bottom of a page or panel.
MarginLeft optional int 0 Space reserved to the element's left, in unscaled sprite pixels × Scale. This narrows the width the element measures against, so text wraps at the indented width rather than overflowing.
MarginRight optional int 0 Space reserved to the element's right, in unscaled sprite pixels × Scale.
Position optional Point { X: 0, Y: 0 } The element's position, in screen pixels. Only used in Background, Foreground, Underlay and Overlay, where elements are placed rather than stacked. Measured from wherever Alignment and VerticalAlignment anchor the element, so under the default Left and Top it's a plain coordinate (see Placed elements). Unlike every other spacing field this is not multiplied by Scale. Changing an element's scale resizes it in place rather than moving it.
Condition optional string A game state query. When it evaluates false the element is hidden, and elements below it close the gap. Re-checked several times a second while the book is open. Understands tokens.
Lifetime optional number Seconds the element stays up once shown. Setting it makes the element a timed one: hidden until a ShowElement action names it, so an Id is required.
FadeAfter optional number Seconds before it starts fading, reaching nothing at the end of Lifetime. Needs Lifetime, and must be below it.
IgnoreCursor optional bool false Lets the cursor pass straight through the element to whatever sits beneath it, so it can't be hovered or clicked. Its children and its own Background and Foreground are still reachable (see Passing the cursor through).
Action optional string A trigger action run when the element is clicked. When set, the element becomes interactive: any element type can have one, not just Button. Understands tokens.
Actions optional list of string Trigger actions run in order when the element is clicked. Combined with Action rather than replacing it, so an element with both runs Action first. Understands tokens.
Sound optional string bigSelect The cue played when the element is clicked. Only used when Action or Actions is set, and played once however many actions run. Set to null for a silent click.
HoverAction optional string A trigger action run when the cursor moves onto the element. Runs on entry, not while the cursor rests there. Sound doesn't apply. Understands tokens.
HoverActions optional list of string Trigger actions run in order when the cursor moves onto the element. Combined with HoverAction rather than replacing it, so an element with both runs HoverAction first. Understands tokens.
DisplayName optional string The bold title of the element's hover tooltip. Works in every list, including a page's Background and Foreground. Can carry tokens.
Description optional string The body of the element's hover tooltip. Works in every list, including a page's Background and Foreground, where setting it is also what makes an otherwise decorative element reachable by the cursor. Ignored when IgnoreCursor is set, which logs a warning. Can carry tokens.
Tags optional list of string Free-form markers other mods can read off whatever the cursor is over, such as Lookup Anything support. Tagging an element also makes it reachable by the cursor, since a tag is only useful on something that can be hovered. Ignored when IgnoreCursor is set. See Tags.
ParseTokenizableStrings optional bool true Whether the game's [Token] tokenizable strings are resolved in this element's Text, DisplayName, Description, Condition and actions. Set it to false where a square bracket is meant as the character itself.

Watch for transparent padding

An image's size is its source rectangle, not the pixels painted in it. If your sprite has empty rows at the top or bottom of its rectangle, the element reserves space for them and the picture looks oddly offset. Tighten the rectangle to the art. See Preparing your art.