Display

Stone's display system renders graphs, canvases, and animations. When you call show(), Stone spawns a local server and opens an HTML page in your browser with the rendered output.

There are two rendering systems: graph terminals for data plotting and canvas terminals for shape rendering. Both use a declarative pattern: create display records with configuration and data, then pass them to show().

// Graph terminals (data plotting)
import graph2d, graph3d, line, scatter, surface from plot
import show, vary, reveal from display

// Canvas terminals (shape rendering)
import canvas2d, canvas3d, show, vary, reveal from display
import circle, rect, group from draw2d
import box, sphere, cylinder from draw3d

vary/reveal originate from the display module. The plot module re-exports them for convenience, so import vary from plot also works.

The show() Function

Displays are first-class values. You create them, configure them, and explicitly show them:

import canvas2d, show from display
import circle from draw2d

view = canvas2d({
    title = "My Canvas",
    objects = [circle({ center = [0, 0], radius = 50 })]
})
show(view)

Multiple calls to show() create separate terminals:

show(canvas2d({ title = "View 1", objects = shapes_a }))
show(canvas2d({ title = "View 2", objects = shapes_b }))

Graph Terminals

Graph terminals render data plots (lines, scatter, bar charts, surfaces). Import from plot.

graph2d

import graph2d, line, scatter, bar, heatmap, contour, show from plot

plot = graph2d({
    title = "My Plot",
    x_label = "x",
    y_label = "y",
    plots = [
        line({ x = xs, y = ys }),
        scatter({ x = xs, y = ys })
    ]
})
show(plot)

Configuration fields: title, x_label, y_label, x_scale ("linear" or "log"), y_scale, x_min, x_max, y_min, y_max, grid, legend, domains.

2D plot constructors (all from plot):

line({ x = xs, y = ys })
line({ points = [[0, 1], [1, 2]] })
scatter({ x = xs, y = ys })
bar({ labels = ["A", "B", "C"], values = [10, 20, 30] })
heatmap({ z = data, x = x_vals, y = y_vals })
contour({ z = data })
area({ x = xs, y = ys })

Field plot grids are axis-first: use z[x][y] for heatmap, contour, and surface. If the source data is a row/column matrix or raster shaped matrix[row][col], import transpose from array, bind the plot grid as plot_z: array<num, 2> = transpose(matrix), and pass plot_z.

Style customization via nested style field:

line({ x = xs, y = ys, style = { color = "red", line_width = 3 } })
scatter({ x = xs, y = ys, style = { color = "blue", marker_size = 10 } })

graph3d

import graph3d, surface, line, scatter, sphere, box, show from plot

scene = graph3d({
    title = "Scene",
    background = "light",
    plots = [
        surface({ z = z_data, x = x_vals, y = y_vals }),
        line({ x = xs, y = ys, z = zs })
    ]
})
show(scene)

Coordinate system: Z is up. Coordinates are [x, y, z].

Configuration fields: title, background ("light" or "dark"), axes, grid, camera_x/y/z, target_x/y/z, domains.

3D plot constructors:

surface({ z = z_data, x = x_vals, y = y_vals })
line({ x = xs, y = ys, z = zs })
scatter({ x = xs, y = ys, z = zs })
sphere({ center = { x = 0, y = 0, z = 0 }, radius = 1.0 })
box({ center = { x = 2, y = 0, z = 0 }, size = { x = 1, y = 1, z = 1 } })
text({ position = { x = 0, y = 0, z = 2 }, text = "Label" })
axes({})

Canvas Terminals

Canvas terminals render geometric shapes (draw2d/draw3d). Use canvas for scene composition, use graphs for data plotting.

All canvas shapes use array coordinates: [x, y] for 2D, [x, y, z] for 3D.

canvas2d

import canvas2d, show from display
import circle, rect, polygon, text, group from draw2d

view = canvas2d({
    title = "My Canvas",
    origin = "center",
    width = 800,
    height = 600,
    objects = [
        circle({ center = [0, 0], radius = 50 }),
        rect({ center = [100, 100], width = 80, height = 60 })
    ]
})
show(view)

Configuration fields: title, width (800), height (600), background, origin ("top-left", "center", "bottom-left"), pixel_ratio, domains.

canvas3d

import canvas3d, show from display
import box, sphere, cylinder, group from draw3d

scene = canvas3d({
    title = "3D Scene",
    camera = { position = [8, 6, 4], target = [0, 0, 0] },
    grid = { plane = "xy", size = 20 },  // xy is the floor (Z is up); xz/yz are walls
    axes = true,
    objects = [
        box({ center = [0, 0, 0], size = [2, 2, 2] }),
        sphere({ center = [3, 0, 1], radius = 1 })
    ]
})
show(scene)

Coordinate system: Z is up.

Configuration fields: title, background, camera, environment, grid ({plane, size, divisions}), axes, antialias, domains.

Environment (environment)

The whole atmosphere of a 3D canvas — lighting and backdrop — is one environment bundle. Use a preset (studio(), outdoor(), sunset(), night()), or write the record for fine control. Its fields:

  • lighting{ auto_lights, ambient, hemisphere, lights }. auto_lights (default true) is the built-in ambient+key+fill setup; lights add directional/point/spot lights; hemisphere is a sky/ground ambient gradient. Set auto_lights = false for full manual control.
  • reflections — image-based reflection/light strength (0 off, 1 normal). Metals/glossy surfaces reflect it, and it adds ambient fill; it does not replace background or sky.
  • tone_mapping"none"|"aces"|"neutral". Output color remapping.
  • exposure — non-negative camera exposure multiplier. 1 is normal; typical useful range is 0.25..4.
  • sky{ top, horizon }. A gradient backdrop (overrides background).
  • ground{ show, plane, color, size, height, receive_shadow, opacity }. A solid floor (auto-sized to the scene), distinct from the wireframe grid; also catches shadows.
  • fog{ enabled, color, near, far }. Distance fade; near/far auto-fit the scene when 0, color defaults to the backdrop.
  • shadows{ enabled, softness, resolution, bias }. Turn shadows on, then opt a light in with cast_shadow (the automatic key light casts if none is set). Meshes cast and receive automatically; the ground only receives.
import canvas3d, outdoor, options, night, show from display

// A preset:
show(canvas3d({ environment = outdoor(), objects = [ sphere({ radius = 1, material = { metalness = 0.9 } }) ] }))

// Or fine control:
show(canvas3d({
    environment = {
        reflections = 1,
        tone_mapping = "neutral",
        exposure = 1,
        sky     = { top = "#1a3a6a", horizon = "#bcd4ee" },
        ground  = { show = true, color = "slate" },
        fog     = { enabled = true, color = "#bcd4ee" },
        shadows = { enabled = true, bias = -0.0005 },
        lighting = { auto_lights = false, lights = [{ type = "directional", direction = [-1, -1, -2], cast_shadow = true }] }
    },
    objects = [ sphere({ radius = 1, material = { metalness = 0.9, roughness = 0.15 } }) ]
}))

Both environment and camera accept options({...}, default) — a named, switchable set the viewer flips between live (environment = options({ day = outdoor(), night = night() }, "day")).

2D Shapes (draw2d)

All constructors take a single record argument. Style is set inline via an optional style field.

import circle, rect, ellipse, line, arc, polygon, polyline, path, text, group from draw2d

circle({ center = [x, y], radius = num })

rect({ center = [x, y], width = num, height = num, rotation = num? })

ellipse({ center = [x, y], rx = num, ry = num, rotation = num? })

line({ start = [x, y], end = [x, y] })

arc({ center = [x, y], radius = num, start_angle = num, end_angle = num })

polygon({ points = [[x, y], ...], closed = bool? })

polyline({ points = [[x, y], ...] })

text({ position = [x, y], text = string, font_size = num? })

path({ commands = [PathCmd, ...] }) where commands are move_to(x, y), line_to(x, y), quad_to(cx, cy, x, y), cubic_to(c1x, c1y, c2x, c2y, x, y), arc_to(rx, ry, rotation, large_arc, sweep, x, y), close_path.

2D Style

circle({
    center = [0, 0],
    radius = 50,
    style = { fill = "red", stroke = "slate", stroke_width = 2, fill_opacity = 0.8 }
})

Style properties: fill, fill_opacity, stroke, stroke_width, stroke_opacity, stroke_dash ([5, 3]), stroke_cap ("butt"/"round"/"square"), stroke_join ("miter"/"round"/"bevel").

Text shapes with no fill/stroke automatically use the theme foreground color.

Transform Functions (draw2d)

translate(shape, [dx, dy]), rotate(shape, angle, pivot?), scale(shape, factor, pivot?), mirror_x(shape, x?), mirror_y(shape, y?).

3D Shapes (draw3d)

All constructors take a single record argument. Material is set inline via an optional material field.

import box, sphere, cylinder, cone, torus, mesh, line, polyline, pointcloud, text, group from draw3d

box({ center = [x,y,z], size = [w,h,d], rotation = [rx,ry,rz]? })

sphere({ center = [x,y,z], radius = num })

cylinder({ start = [x,y,z], end = [x,y,z], radius = num }) -- uses start/end, NOT center/height

cone({ start = [x,y,z], end = [x,y,z], radius = num }) -- uses start/end, NOT center/height

torus({ center = [x,y,z], major_radius = num, minor_radius = num, axis = [x,y,z]? })

mesh({ vertices = [[x,y,z], ...], indices = [[a,b,c], ...] }) -- normals auto-computed if omitted, uvs optional. Indices can also be flat: [a, b, c, d, e, f, ...].

line({ start = [x,y,z], end = [x,y,z] })

polyline({ points = [[x,y,z], ...], closed = bool? })

pointcloud({ points = [[x,y,z], ...], point_size = num? })

text({ position = [x,y,z], text = string })

3D Material

sphere({
    center = [0, 0, 0],
    radius = 1,
    material = { color = "red", metalness = 0.3, roughness = 0.7, opacity = 0.9 }
})

Material properties: color, opacity, metalness (0-1), roughness (0-1), wireframe, emissive, side.

Shapes with no material color get a neutral theme-adaptive default.

Groups and Transforms

A group holds child shapes and applies a shared transform. Available in both draw2d and draw3d.

import group from draw3d

group({
    objects = [shape1, shape2],
    position = [x, y, z],
    rotation = [rx, ry, rz],
    scale = 1,
    anchor = [0, 0, 0]
})

2D groups use position = [x, y], rotation = angle (single number), scale = num, anchor = [x, y].

3D groups use position = [x, y, z], rotation = [rx, ry, rz] (Euler XYZ degrees), scale = num, anchor = [x, y, z].

Groups can nest. Each child group's transform is relative to its parent, creating hierarchies for articulated assemblies (robot arms, solar system orbits). The anchor field sets the pivot point for rotation and scale.

Panels (Synchronized Animation)

Multiple displays can share animation domains for synchronized playback:

import canvas2d, canvas3d, panel, show, vary from display
import circle from draw2d
import sphere from draw3d

t_vals = [0, 1, 2, 3, 4, 5]
radii_2d = [10, 15, 20, 25, 30, 35]
radii_3d = [1, 1.5, 2, 2.5, 3, 3.5]

view_2d = canvas2d({ title = "2D", objects = [circle({ radius = vary(radii_2d, "t") })] })
view_3d = canvas3d({ title = "3D", objects = [sphere({ radius = vary(radii_3d, "t") })] })

synced = panel({
    title = "Synced Views",
    displays = [view_2d, view_3d],
    domains = {
        t: { values = t_vals, playback = { duration = 2, mode = "loop", auto_play = true } }
    }
})
show(synced)

All displays in a panel share the same domain controls - when you move the slider or play the animation, all displays update together.

Color Palette

Stone provides theme-adaptive named colors that look good in both light and dark mode:

"blue", "red", "green", "orange", "purple", "teal", "pink", "amber", "indigo", "cyan", "rose", "slate", "black", "white", "foreground", "background".

Use palette names instead of hex codes. Explicit hex like "#e74c3c" passes through unchanged. Graph traces auto-cycle through the palette when no color is specified.

Animation (Domains, Vary, Reveal)

Animation uses domains defined in the display config. Domains create sliders and play controls that drive values through your shapes. Playback config controls transport behavior: duration is seconds per full sweep, mode is "once", "loop", or "bounce", and auto_play controls whether playback starts when the display appears. Domains auto_play by default; set auto_play = false for inspect-first displays.

scene = canvas3d({
    domains = {
        t = {
            values = range(0, 100),
            label = "Time",
            playback = { duration = 2, mode = "bounce", auto_play = true }
        },
        scenario = { values = ["base", "stress"] }
    },
    objects = [...]
})
show(scene)

vary()

vary(data, "domain_name") selects from an array by the domain's current index. The first dimension of data maps to the domain.

// Swap entire shapes per frame
shapes = [mesh1, mesh2, mesh3, ...]
view = canvas3d({ objects = [vary(shapes, "t")] })
show(view)

// Animate individual properties
animated_circle = circle({
    center = vary([[0, 0], [50, 50], [100, 0]], "t"),
    radius = 20
})
view = canvas2d({ objects = [animated_circle] })
show(view)

// Animate group transforms
arm_group = group({ objects = [arm], rotation = vary(angles, "joint") })
view = canvas3d({ objects = [arm_group] })
show(view)

Multi-domain: vary(data, {t = 0, scenario = 1}) maps each domain to a tensor dimension.

Animatable 2D fields: center, radius, rx, ry, width, height, rotation, position, scale, points.

Animatable 3D fields: center, radius, start, end, size, rotation, position, scale, points.

reveal()

reveal(data, "domain_name") shows elements 0 through the domain's current index, for progressive build-up.

// Progressively reveal shapes in a group
revealed = group({ objects = reveal(shape_array, "t") })
show(canvas3d({ objects = [revealed] }))

// Progressively reveal points in a point cloud
cloud = { type = "pointcloud", points = reveal(pts, "draw"), point_size = 3 }
show(canvas3d({ objects = [cloud] }))

Use reveal() on array properties within shapes, not on entire shapes arrays.

Animation Example

import canvas3d, show from display
import sin, cos, PI from math

n = 300
indices = range(0, n)
t[k] = indices[k] * 10 * PI / n
pts[k] = [cos(t[k]) * 2, sin(t[k]) * 2, t[k] / PI - 5]

cloud = {
    type = "pointcloud",
    points = reveal(pts, "draw"),
    point_size = 3,
    material = { color = "red" }
}

scene = canvas3d({
    camera = { position = [6, 4, 3], target = [0, 0, 0] },
    domains = {
        draw = {
            values = range(1, n + 1),
            label = "Build",
            playback = { duration = 3, mode = "loop", auto_play = true }
        }
    },
    objects = [cloud]
})
show(scene)

Library Functions Returning Displays

Since displays are first-class values, library functions can create and return them:

// helper.stn
import canvas2d from display
import circle from draw2d

export fn make_scatter_view(points, title) {
    dots[i] = circle({ center = points[i], radius = 3 })
    canvas2d({ title = title, objects = dots })
}

// main.stn
import make_scatter_view from helper
import show from display

show(make_scatter_view(my_points, "Data"))

This enables reusable visualization components and clean separation of concerns.