Documentation
Feature reference for Eido's declarative image language.
Drawing
Shapes
Everything in Eido starts with shapes. You describe a shape as a map — a collection of key-value pairs that says what the shape is, where it goes, and how it looks. Eido takes care of drawing it.
Here are the basic building blocks:
Rectangle
{:node/type :shape/rect
:rect/xy [50 50] ;; top-left corner position
:rect/size [200 100] ;; width and height
:style/fill [:color/rgb 0 128 255]}Add rounded corners with :rect/corner-radius:
{:node/type :shape/rect
:rect/xy [50 50]
:rect/size [200 100]
:rect/corner-radius 16
:style/fill [:color/rgb 0 128 255]}Circle
{:node/type :shape/circle
:circle/center [200 200] ;; center point
:circle/radius 80 ;; radius in pixels
:style/stroke {:color [:color/rgb 0 0 0] :width 2}}Ellipse
{:node/type :shape/ellipse
:ellipse/center [200 200]
:ellipse/rx 120 ;; horizontal radius
:ellipse/ry 60 ;; vertical radius
:style/fill [:color/rgb 200 50 50]}Arc
A partial ellipse — like a pie slice or an open curve:
{:node/type :shape/arc
:arc/center [200 200]
:arc/rx 80 :arc/ry 80
:arc/start 0 :arc/extent 270 ;; degrees
:arc/mode :pie ;; :open, :chord, or :pie
:style/fill [:color/rgb 255 200 50]}Line
{:node/type :shape/line
:line/from [50 50]
:line/to [350 250]
:style/stroke {:color [:color/rgb 0 0 0] :width 2}}Path
For anything that isn't a basic shape, use a path. Paths are sequences of drawing commands — move to a point, draw a line, draw a curve, and close the shape. This is how you create arbitrary freeform shapes:
{:node/type :shape/path
:path/commands [[:move-to [100 200]] ;; pick up the pen
[:line-to [200 50]] ;; draw a straight line
[:curve-to [250 0] [300 100] ;; cubic bezier curve
[300 200]] ;; (two control points + end)
[:quad-to [250 250] ;; quadratic bezier curve
[200 200]] ;; (one control point + end)
[:close]] ;; connect back to start
:style/fill [:color/rgb 255 200 50]}Convenience helpers
The eido.scene namespace provides shortcuts for common shapes:
(require '[eido.scene :as scene])
(scene/regular-polygon [200 200] 80 6) ;; hexagon
(scene/star [200 200] 80 35 5) ;; 5-pointed star
(scene/triangle [100 200] [200 50] [300 200])
(scene/smooth-path [[50 200] [150 50] [250 200] [350 50]]) ;; smooth curve through pointsText
Text in Eido is not rasterized pixels — it's converted to vector paths, just like any other shape. That means text works with everything: gradient fills, strokes, transforms, clipping, even 3D extrusion.
Simple text
{:node/type :shape/text
:text/content "Hello"
:text/font {:font/family "Serif" :font/size 48 :font/weight :bold}
:text/origin [50 100] ;; baseline-left anchor
:text/align :center ;; :left (default), :center, :right
:style/fill [:color/rgb 0 0 0]}Per-glyph control
Style each character independently — great for rainbow text, animated reveals, or creative typography:
{:node/type :shape/text-glyphs
:text/content "COLOR"
:text/font {:font/family "SansSerif" :font/size 64}
:text/origin [50 100]
:text/glyphs [{:glyph/index 0 :style/fill [:color/rgb 255 0 0]}
{:glyph/index 1 :style/fill [:color/rgb 0 255 0]}]
:style/fill [:color/rgb 100 100 100]} ;; default for unlisted glyphsText on a path
Make text follow any curve:
{:node/type :shape/text-on-path
:text/content "ALONG A CURVE"
:text/font {:font/family "SansSerif" :font/size 24}
:text/path [[:move-to [50 200]]
[:curve-to [150 50] [350 50] [450 200]]]
:text/offset 10 ;; start distance along path
:text/spacing 1 ;; extra inter-glyph spacing
:style/fill [:color/rgb 0 0 0]}Fonts reference system fonts by name. Java's built-in fonts — "Serif", "SansSerif", "Monospaced" — work on every system.
Styling
Colors
Eido understands several color formats. Use whichever feels natural — they all work everywhere:
[:color/rgb 255 0 0] ;; red, green, blue (0-255)
[:color/rgba 255 0 0 0.5] ;; same, with transparency (0-1)
[:color/hsl 0 1.0 0.5] ;; hue (0-360), saturation, lightness
[:color/hsb 0 1.0 1.0] ;; hue, saturation, brightness
[:color/hex "#FF0000"] ;; hex notation
[:color/name "coral"] ;; 148 CSS named colorsAll formats work directly in style maps — just drop them in:
{:style/fill [:color/hsl 200 0.9 0.5]}
{:style/fill [:color/hex "#FF6B35"]}
{:style/fill [:color/name "tomato"]}Color manipulation
The eido.color namespace provides functions for adjusting colors — lighten, darken, blend, shift hue:
(require '[eido.color :as color])
(color/lighten [:color/rgb 255 0 0] 0.2) ;; lighter red
(color/darken [:color/rgb 255 0 0] 0.2) ;; darker red
(color/saturate [:color/rgb 150 100 100] 0.3) ;; more vivid
(color/rotate-hue [:color/rgb 255 0 0] 120) ;; shift hue → green
(color/lerp color-a color-b 0.5) ;; blend two colorsStrokes
Strokes are the outlines around shapes. You can control the width, the shape of line endings (caps), how corners look (joins), and add dashed patterns:
;; Rounded line endings and beveled corners
{:style/stroke {:color [:color/rgb 0 0 0]
:width 4
:cap :round ;; :butt, :round, or :square
:join :bevel}} ;; :miter, :round, or :bevel
;; Dashed lines
{:style/stroke {:color [:color/rgb 0 0 0]
:width 2
:dash [10 5]}} ;; alternating dash and gap lengthsA shape can have both a fill and a stroke — the stroke is drawn on top.
Gradients
Instead of a flat color, fill a shape with a smooth color transition. Eido supports two kinds:
Linear gradient
Colors transition along a line from one point to another:
{:style/fill {:gradient/type :linear
:gradient/from [0 0] ;; start point
:gradient/to [200 0] ;; end point
:gradient/stops [[0.0 [:color/rgb 255 0 0]] ;; red at start
[1.0 [:color/rgb 0 0 255]]]}}Radial gradient
Colors radiate outward from a center point:
{:style/fill {:gradient/type :radial
:gradient/center [100 100]
:gradient/radius 100
:gradient/stops [[0.0 [:color/name "white"]]
[1.0 [:color/name "black"]]]}}Add as many color stops as you want for multi-color transitions. Any color format works in stops.
Pattern Fills
Beyond solid colors and gradients, Eido supports texture-like fills that give shapes a hand-crafted look:
Hatching
Parallel lines drawn across a shape — like pen-and-ink cross-hatching:
{:style/fill {:fill/type :hatch
:hatch/angle 45 ;; line angle in degrees
:hatch/spacing 4 ;; distance between lines
:hatch/stroke-width 1
:hatch/color [:color/rgb 0 0 0]}}Stippling
Random dots packed inside a shape — like pointillism:
{:style/fill {:fill/type :stipple
:stipple/density 0.6 ;; how packed (0-1)
:stipple/radius 1.0 ;; dot size
:stipple/seed 42 ;; for reproducibility
:stipple/color [:color/rgb 0 0 0]}}Custom tile patterns
Tile any collection of shapes as a repeating pattern:
{:style/fill {:fill/type :pattern
:pattern/size [20 20] ;; tile size
:pattern/nodes [...]}}Composition
Groups
Groups let you treat multiple shapes as one unit. Any style, transform, or effect applied to the group affects all its children. Styles inherit — children get the group's fill color unless they specify their own. Opacity multiplies through the tree.
{:node/type :group
:node/transform [[:transform/translate 200 200]]
:style/fill [:color/rgb 255 0 0]
:node/opacity 0.8
:group/children
[{:node/type :shape/circle ;; inherits red fill
:circle/center [0 0]
:circle/radius 80}
{:node/type :shape/rect
:rect/xy [-30 -30]
:rect/size [60 60]
:style/fill [:color/rgb 0 0 255] ;; overrides with blue
:node/opacity 0.5}]}Clipping
Clipping restricts a group's visible area to a shape — like looking through a window. Only the parts of the children that fall inside the clip shape are drawn:
{:node/type :group
:group/clip {:node/type :shape/circle
:circle/center [200 200]
:circle/radius 80}
:group/children
[{:node/type :shape/rect
:rect/xy [120 120]
:rect/size [160 160]
:style/fill [:color/rgb 255 0 0]}]}
;; Only the part of the rectangle inside the circle is visibleCompositing
Blend modes control how overlapping shapes combine visually — like layer blend modes in Photoshop:
{:node/type :group
:group/children [...]
:node/opacity 0.5
:composite/blend :screen} ;; :src-over, :multiply, :screen, etc.Transforms
Move, rotate, scale, and skew any shape or group. Transforms are applied in order — translate first, then rotate, then scale:
{:node/transform [[:transform/translate 100 50]
[:transform/rotate 0.785] ;; angle in radians
[:transform/scale 1.5 1.5]
[:transform/shear-x 0.3]]} ;; skewTransforms compose through the tree — a shape inside a translated group that is itself translated will move by the sum of both translations.
Generative
Scene Helpers
The eido.scene namespace provides functions that generate collections of shapes from a pattern — grids, radial layouts, distributions along a line:
Grid
Create a grid of shapes by providing columns, rows, and a function that receives the column and row:
(scene/grid 10 10
(fn [col row]
{:node/type :shape/circle
:circle/center [(+ 30 (* col 40)) (+ 30 (* row 40))]
:circle/radius 15
:style/fill [:color/rgb (* col 25) (* row 25) 128]}))Distribute along a line
(scene/distribute 8 [50 200] [750 200]
(fn [x y t] ;; t is progress 0 to 1
{:node/type :shape/circle
:circle/center [x y]
:circle/radius (+ 5 (* 20 t))
:style/fill [:color/rgb 0 0 0]}))Radial arrangement
(scene/radial 12 200 200 120 ;; 12 items around (200,200) radius 120
(fn [x y angle]
{:node/type :shape/circle
:circle/center [x y]
:circle/radius 15
:style/fill [:color/rgb 200 0 0]}))Contour Lines
Contour lines connect points of equal value — like elevation lines on a topographic map. Eido generates them from noise fields using the marching squares algorithm:
(require '[eido.contour :as contour])
{:node/type :contour
:contour/bounds [0 0 500 400]
:contour/opts {:thresholds [0.0 0.2 0.4]
:resolution 3
:noise-scale 0.012
:seed 42}
:style/stroke {:color [:color/rgb 100 150 100] :width 1}}Noise
Noise functions produce smooth, organic-looking randomness — like clouds, terrain, or flowing water. Unlike random numbers, nearby inputs give nearby outputs, creating natural gradients:
(require '[eido.noise :as noise])
(noise/perlin2d x y) ;; smooth 2D noise (-1 to 1)
(noise/perlin3d x y z) ;; 3D noise (use z as time for animation)
(noise/fbm noise/perlin2d x y ;; fractal noise — layered detail
{:octaves 4 :seed 42})Particles
Particle systems simulate many small objects (sparks, snowflakes, smoke) moving under physics forces. You configure the behavior as data — emitter position, lifetime, gravity, wind — and Eido simulates the result deterministically:
(require '[eido.particle :as particle])
;; Pre-compute 60 frames of fire particles
(let [frames (vec (particle/simulate
(particle/with-position particle/fire [200 350])
60 {:fps 30}))]
;; Each frame is a vector of shape nodes — compose freely
(eido/render
(anim/frames 60
(fn [t]
{:image/size [400 400]
:image/background [:color/rgb 20 15 10]
:image/nodes (nth frames (int (* t 59)))}))
{:output "fire.gif" :fps 30}))Built-in presets: particle/fire, particle/snow, particle/sparks, particle/confetti, particle/smoke, particle/fountain. Customize any preset with assoc/update.
Animation
Creating Animations
An animation in Eido is just a sequence of scenes — one per frame. There's no timeline, no keyframe system, no mutable state. You write a function that takes a progress value t (from 0 to 1) and returns a scene. Eido calls it once per frame:
(require '[eido.animate :as anim])
(def frames
(anim/frames 60 ;; 60 frames total
(fn [t] ;; t goes from 0.0 to 1.0
{:image/size [200 200]
:image/background [:color/rgb 30 30 40]
:image/nodes
[{:node/type :shape/circle
:circle/center [100 100]
:circle/radius (* 80 t) ;; grows over time
:style/fill [:color/hsl (* 360 t) 0.8 0.5]}]})))
;; Render as animated GIF
(eido/render frames {:output "grow.gif" :fps 30})Since frames are just data, you can manipulate them with all the usual tools — map, filter, concat — to build complex sequences from simple parts.
Easing & Helpers
Easing functions make motion feel natural. Instead of moving at a constant speed, things can accelerate, decelerate, bounce, or overshoot:
(anim/ease-in t) ;; slow start, fast finish
(anim/ease-out t) ;; fast start, slow finish
(anim/ease-in-out t) ;; slow start and finish
(anim/ease-in-cubic t) ;; more dramatic
(anim/ease-out-elastic t) ;; springy overshoot
(anim/ease-out-bounce t) ;; bouncing ballOther useful helpers:
(anim/ping-pong t) ;; oscillate: 0→1→0
(anim/cycle-n 3 t) ;; repeat 3 times
(anim/lerp 0 100 t) ;; interpolate between values
(anim/stagger 2 5 t 0.3) ;; offset timing per element3D
3D Scenes
Eido can render 3D objects by projecting them onto 2D. You set up a camera (perspective or isometric), define lights, and place 3D meshes in the scene. The result is a regular 2D scene with shaded polygons — no GPU required:
(require '[eido.scene3d :as s3d])
(let [proj (s3d/perspective
{:scale 100 :origin [200 200]
:yaw 0.5 :pitch -0.3 :distance 5})
light {:light/direction [1 1 0.5]
:light/ambient 0.2
:light/intensity 0.8}]
(s3d/sphere proj [0 0 0] 1.5
{:style {:style/fill [:color/rgb 100 150 255]}
:light light}))Available primitives: sphere, cube, cone, torus, cylinder. Load arbitrary meshes from OBJ files with eido.obj/load-obj.
Camera types
;; Perspective — objects shrink with distance
(s3d/perspective {:scale 100 :origin [200 200]
:yaw 0.3 :pitch -0.4 :distance 5})
;; Isometric — no perspective distortion
(s3d/isometric {:scale 40 :origin [200 200]})
;; Look-at — point camera at a target
(s3d/look-at {:eye [3 2 5] :target [0 0 0] :up [0 1 0]
:scale 100 :origin [200 200]})Output
Export
Everything goes through one function — eido/render. The output format is determined by the file extension:
;; Static images
(eido/render scene {:output "out.png"}) ;; PNG (default)
(eido/render scene {:output "out.svg"}) ;; SVG (vector)
(eido/render scene {:output "out.jpg" :quality 0.9})
;; Animations
(eido/render frames {:output "anim.gif" :fps 30})
(eido/render frames {:output "anim.svg" :fps 30}) ;; animated SVG
(eido/render frames {:output "frames/" :fps 30}) ;; PNG sequenceOptions
:scale 2 ;; 2x resolution (retina)
:dpi 300 ;; DPI metadata for print
:transparent-background true ;; no background fill
:loop false ;; GIF plays once (default: loops)Render without an output path to get a BufferedImage back for further processing, or use :format :svg to get an SVG string.
File Workflow
Scenes can be stored as .edn files and loaded for rendering:
(eido/render (eido/read-scene "my-scene.edn") {:output "out.png"})
;; Watch a file — auto-reload the preview on every save
(watch-file "my-scene.edn")
;; Watch an atom for live coding
(def my-scene (atom {...}))
(watch-scene my-scene)
;; tap> integration — render by tapping
(install-tap!)
(tap> {:image/size [200 200] :image/nodes [...]})Validation
Scenes are validated before rendering. If something is wrong, you get a clear error pointing to the exact problem:
(eido/validate {:image/size [800 600]
:image/background [:color/rgb 255 255 255]
:image/nodes [{:node/type :shape/rect}]})
;; => [{:path [:image/nodes 0]
;; :pred "missing required key :rect/xy" ...}]Invalid scenes throw ex-info with :errors in the exception data, so you always know what went wrong.