Skip to contents

You can read and check a character file using mascot_read_character() and mascot_check_character(). Both functions are experimental, and their arguments, results and diagnostics may change in a later release.

Saving and registering characters and palettes are planned for a later release. The mascot studio does not take custom characters yet, but you can preview your own character offline with mascot_preview(), also experimental, and use it directly in Shiny apps.

Find the example character

mascot ships with a sample character recipe, study_bot.json. Start by copying it to a temporary file in tempdir() so you can inspect and modify it without touching package files:

path <- system.file("examples", "characters", "study_bot.json", package = "mascot")
copy <- tempfile(fileext = ".json")
file.copy(path, copy)
#> [1] TRUE

Read and check the file

Call mascot_read_character() with the file path. It reads the JSON file, checks every field against the character format rules, and returns an opaque, checked character object:

bot <- mascot_read_character(copy)
bot
#> study_bot: Study Bot (visorfolk)
#> Moods: idle, thinking, happy, worried, celebrating
#> Gestures: rock, nod, wave, greet
#> Detail overrides: micro
#> From: /tmp/RtmpVf6X2c/file22442d270012.json

Printing the object shows a concise summary: the character identifier, display name, family, supported moods, gestures, detail overrides, and the source path.

A checked character object cannot be edited: package functions refuse an object that has been changed. Always edit the source JSON or plain list, then read or check it again.

Inside the character format

study_bot.json holds a single JSON object, and the checker tests every field against the format rules:

  • format_version: The integer 1. It says which version of the file format the file uses.
  • id: A unique lowercase identifier ("study_bot"). Built-in character ids ("tessa", "sela", "knurl", "neer") are reserved and refused.
  • name: The human-readable name shown in titles and summaries ("Study Bot").
  • description: One sentence about the character.
  • family: The character family ("visorfolk"). Every character names one, and a custom character also gives the family’s definition in family_definition: its id, name, description and optional defaults, such as gestures and palettes the character takes unless it defines its own.
  • colours: Named colour roles and their default hex values (body, outline, screen, feature, and accent).
  • ground: Where the character stands: its centre height (centre_height: 60).
  • outline: The width of the outline and the colour role it uses.
  • parts: The pieces the character is built from (in this file head, antenna and badge_disc). A part can sit inside another part, and each part holds drawn items, such as the shell and visor shapes inside head.
  • anchors: Named points on parts for attaching accessories or badges.
  • face: Which part carries the face (part), the face kit ("screen"), where it sits (at), a clip, and the eyes and mouth settings.
  • moods: Configurations for the five core moods that every character must support: idle, thinking, happy, worried, and celebrating.
  • gestures: Named motion tracks. Every character must define moving wave and nod gestures: wave must move at least one non-root part (such as a hand or antenna), while nod may move any channel, including root. In this file wave swings the antenna and nod tilts the head.
  • palettes: Alternative colour collections (such as "sea_glass").
  • contrast: Accessibility rules that verify contrast between foreground and background colour roles.
  • lod: Level-of-detail rules for smaller sizes, such as hiding fine details like badge_disc in the micro detail band.

study_bot has no requires field, and your character needs none either: every version-1 mascot draws the eight version-1 capabilities (parts, paints, clip, outline, glow, face_kit, tracks and lod). requires is for capabilities added after version 1; a name this version of mascot does not know stops the check with an error that names it. Older files that list the eight names keep loading.

Drawing coordinates are design units in a 300 by 300 frame. The point where the character stands is (0, 0), 150 units from the left edge and 270 from the top, and y grows downwards: a part above the ground has a negative y.

Keys for other tools

Editors and other tools can add their own information to a character file, and mascot keeps it without reading it:

  • A top-level "$schema" string, so an editor can link the file to a schema.
  • Any key that starts with x_ in an object whose fields the format fixes: the character itself, a part, an item, a mood, a gesture, the face and so on. Its value can be any JSON value, and it stays in the file, as written, when the character is checked and saved. It counts towards the file’s size limits. In an object whose keys are ids or names, such as colours, moods or palettes, a key that starts with x_ is an ordinary id.
  • A "note" string in any of those objects, as a comment.

The start of such a file, shortened:

{
  "$schema": "https://example.org/my-editor/mascot-character-v1.json",
  "format_version": 1,
  "id": "sparky_bot",
  "x_my_editor": {"last_opened": "2026-10-23", "grid": 10},
  "parts": [
    {"id": "head", "x_my_editor": {"locked": true}, "items": []}
  ]
}

What you can draw

Changing a colour is only the start. Each part holds a list of drawn items, and there are nine kinds of item. Together they can draw any silhouette:

Item What it draws
circle, ellipse, rect, round_rect The basic shapes. A circle or ellipse has a centre and radii; a rect or round_rect has its top-left corner, a width and a height. A round_rect has rounded corners.
polygon, hexagon A regular shape with 3 to 12 equal sides (a hexagon always has 6), around a centre, with optional rounded corners.
polyline A line through a list of points, joined by straight segments. Good for whiskers, antennae, cracks and hair strands.
arc A piece of a circle between two angles (in radians). Good for smiles, eyebrows, rings, or a spanner head with a gap.
path Free-form outlines written as SVG path data (M, L, Q, C, Z): straight lines and smooth curves. This is the one for organic shapes, and the built-in characters use path items for their curved bodies.

So the nine kinds are building blocks, not a limit on the variety of characters: a leafy creature, a fox or a robot are all drawn by combining items, with path doing the shaping. Items take a fill or a stroke from your colour roles, can be clipped, and can be hidden at small sizes (see lod). Here are three items as they would appear in a part:

{"shape": "path", "d": "M-30 0Q0 -40 30 0Z", "fill": "body", "outline": true}
{"shape": "polyline", "points": [[-10, 0], [0, 10], [10, 0]], "stroke": "outline", "width": 2}
{"shape": "arc", "x": 0, "y": 0, "r": 20, "start": 0.5, "end": 5.78, "stroke": "outline", "width": 3}

The format reserves some capabilities, and the checker refuses them. Examples are morphing between shapes, particles, blending, outfits and accessories that any character can wear, and connecting items to each other. A character in format version 1 is drawn from the items above and animated with gestures (movement tracks on its parts).

Make a small edit

To customise your character, read the JSON into R using jsonlite, edit the fields, and write it back:

data <- jsonlite::read_json(copy)

# Change the identifier and display name
data$id <- "sparky_bot"
data$name <- "Sparky Bot"

# Change the primary body colour from blue to warm coral
data$colours$body <- "#ff6b4a"

# Save the modified JSON back to disk
jsonlite::write_json(data, copy, auto_unbox = TRUE, pretty = TRUE)

# Read and check the edited character
sparky <- mascot_read_character(copy)
sparky
#> sparky_bot: Sparky Bot (visorfolk)
#> Moods: idle, thinking, happy, worried, celebrating
#> Gestures: rock, nod, wave, greet
#> Detail overrides: micro
#> From: /tmp/RtmpVf6X2c/file22442d270012.json

mascot_read_character() validates the modified file and returns the updated character object.

Break it on purpose

The checker stops at the first problem and raises an error of class mascot_error_character (which also inherits from mascot_error). The error carries these fields:

  • code: A stable problem identifier (such as "version" or "input_type").
  • path: The JSON path where the error occurred (for example $.format_version).
  • value: The invalid value that was rejected.
  • fix: Plain-words advice on how to fix the problem. For a value out of range it gives the range or the choices, for a drawing budget the count and the limit, and for a character too big for its frame the part that reaches furthest outside it.

Change format_version to an unsupported number to see the diagnostic:

data$format_version <- 2
jsonlite::write_json(data, copy, auto_unbox = TRUE, pretty = TRUE)

err <- tryCatch(
  mascot_read_character(copy),
  mascot_error_character = function(e) e
)

class(err)
#> [1] "mascot_error_character" "mascot_error"           "rlang_error"           
#> [4] "error"                  "condition"
err$code
#> [1] "version"
err$path
#> [1] "$.format_version"
err$value
#> [1] 2
err$fix
#> [1] "This file is format version 2; this mascot version supports 1. Update the package, or use a version-1 file."
conditionMessage(err)
#> [1] "\033[1m\033[22m$.format_version: This file is format version 2; this mascot version\nsupports 1. Update the package, or use a version-1 file."

The message names the format version this mascot supports: "This file is format version 2; this mascot version supports 1. Update the package, or use a version-1 file."

Preview and use in an app

Reset the format version to 1 before using the character:

data$format_version <- 1
jsonlite::write_json(data, copy, auto_unbox = TRUE, pretty = TRUE)
sparky <- mascot_read_character(copy)

# Generate an offline comparison preview across moods and sizes
preview <- mascot_preview(character = sparky)

Assigning mascot_preview(character = sparky) to an object generates an offline preview page comparing moods, sizes, and light and dark backgrounds without opening an interactive viewer.

To use your character in a Shiny application, pass the checked object directly to the character argument of mascot(). You can also style your character’s own colour roles using mascot_theme():

library(shiny)
library(mascot) # nolint: undesirable_function_linter.

ui <- fluidPage(
  mascot(
    "helper",
    character = sparky,
    theme = mascot_theme(light = list(screen = "#051b20"))
  )
)

mascot_theme() checks colour role names against the selected character when the mascot renders, so custom role names such as screen work as well.

What you can rely on

The format is experimental and its future direction is open. These points hold either way:

  • Additions stay in version 1: new optional keys, wider ranges and new categories keep the format at version 1, so existing files keep working.
  • Clear errors on older versions: a file that uses an addition lists it in requires; an older version of mascot then stops with an error naming it, instead of drawing it wrongly. The version-1 capabilities never need listing.
  • Room for other tools: $schema and x_ keys are kept and never read, so an editor can store its own information in a character file.
  • Breaking changes mean version 2: only a change that would break existing files introduces format version 2. Retiring an older format version is announced in NEWS at least one release ahead.
  • Predictable validation: the checker only becomes stricter in a new format version, except for documented security or resource-safety fixes, which come with a diagnostic and migration guidance.
  • No silent migrations: character files are never modified or upgraded silently.