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;lightsadd directional/point/spot lights;hemisphereis a sky/ground ambient gradient. Setauto_lights = falsefor full manual control.reflections— image-based reflection/light strength (0off,1normal). Metals/glossy surfaces reflect it, and it adds ambient fill; it does not replacebackgroundorsky.tone_mapping—"none"|"aces"|"neutral". Output color remapping.exposure— non-negative camera exposure multiplier.1is normal; typical useful range is0.25..4.sky—{ top, horizon }. A gradient backdrop (overridesbackground).ground—{ show, plane, color, size, height, receive_shadow, opacity }. A solid floor (auto-sized to the scene), distinct from the wireframegrid; also catches shadows.fog—{ enabled, color, near, far }. Distance fade;near/farauto-fit the scene when 0, color defaults to the backdrop.shadows—{ enabled, softness, resolution, bias }. Turn shadows on, then opt a light in withcast_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.