Pictures

Pictures

Pictures are containers for grouping paths. They support clipping and can be combined.

Creating a Picture

local h = require("hobby")

local pic = h.picture()
    :add(path1)
    :add(path2)
    :add(path3)

Picture Methods

add(path)

Adds a path to the picture.

pic:add(circle)
pic:add(square)

addpicture(other)

Adds all paths from another picture.

local combined = h.picture()
    :addpicture(pic1)
    :addpicture(pic2)

clip(path)

Sets a clipping boundary. All paths in the picture will be clipped to this shape.

pic:clip(clipPath)

clippath()

Returns the current clipping path, or nil if none is set.

local cp = pic:clippath()

paths()

Returns all paths as a Lua table.

local all_paths = pic:paths()
for i, p in ipairs(all_paths) do
    print("Path " .. i)
end

Bounding Box

Pictures have the same bounding box properties as paths. These correspond to MetaPost’s llcorner, urcorner, etc.

  • If the picture has a clip path, the clip path’s bounds are returned (matching MetaPost behavior where the visible area determines the bounding box).
  • Otherwise, the bounds of all contained paths are aggregated, with stroke width taken into account (±strokeWidth/2 on each side).
local pic = h.picture()
    :add(circle)
    :add(topbar)
    :add(bottombar)

local ll = pic.llcorner   -- lower-left  (minx, miny)
local ur = pic.urcorner   -- upper-right (maxx, maxy)
local c  = pic.center     -- center of bounding box

-- Draw the bounding box as a path
local frame = pic:bbox():stroke("red"):evenly()

This is particularly useful for constructing clip paths relative to the drawn content, as shown in the Euro sign example where the clip path is built from the picture’s bounding box corners.

Labels

Pictures support text labels similar to MetaPost’s label and dotlabel commands.

label(text, point, anchor[, options])

Adds a text label at the given position with the specified anchor point. An optional table can set color and fontsize.

pic:label("A", h.point(0, 0), "llft")    -- lower-left of point
pic:label("B", h.point(100, 0), "lrt")   -- lower-right of point
pic:label("C", h.point(50, 86), "top")   -- above point

-- With color and font size
pic:label("D", h.point(50, 0), "bot", {
    color = h.color("navy"),
    fontsize = 8,
})

Anchor values:

  • center or c - centered at point
  • left or lft - to the left of point
  • right or rt - to the right of point
  • top - above point
  • bottom or bot - below point
  • upperleft or ulft - upper-left of point
  • upperright or urt - upper-right of point
  • lowerleft or llft - lower-left of point
  • lowerright or lrt - lower-right of point

dotlabel(text, point, anchor[, color | options])

Adds a label with a dot at the reference point. The fourth argument can be a color (for backward compatibility) or an options table with color and fontsize.

-- Simple form with color
pic:dotlabel("P", h.point(50, 50), "rt", h.color("blue"))

-- With options table
pic:dotlabel("Q", h.point(80, 50), "rt", {
    color = h.color("red"),
    fontsize = 6,
})

labels()

Returns all labels in the picture as a Lua table.

local all_labels = pic:labels()
for i, lbl in ipairs(all_labels) do
    print(lbl.text, lbl.fontsize)
end

Label Properties

Labels have the following properties and methods:

Property/Method Description
.text The label text
.fontsize Font size
:position() Returns position point
:setfontsize(size) Set font size
:color() Returns color
:setcolor(color) Set color

converttopaths(face)

Converts all labels to glyph outline paths using a loaded font. The labels are replaced by filled paths representing the actual glyph shapes. This ensures the text looks the same everywhere, without relying on system fonts.

Without calling converttopaths, labels are rendered as SVG <text> elements.

local face = h.loadfont("path/to/font.ttf")

local pic = h.picture()
pic:label("A", h.point(0, 0), "llft")
pic:dotlabel("B", h.point(100, 0), "lrt", h.color("blue"))

-- Convert text labels to glyph paths
pic:converttopaths(face)

Loading Fonts

Use h.loadfont(path) to load a TrueType or OpenType font file:

local face = h.loadfont("/System/Library/Fonts/Supplemental/Arial.ttf")

The returned font face is passed to converttopaths(). Text shaping (kerning, ligatures) is handled automatically via the textshape library.

Adding Pictures to SVG

Use addpicture() on the SVG builder:

h.svg()
    :addpicture(pic)
    :write("output.svg")

Clipping Example

local h = require("hobby")

-- Create a circular clipping boundary
local clipCircle = h.fullcircle()
    :scaled(60)
    :shifted(50, 50)

-- Create paths to be clipped
local line1 = h.path()
    :moveto(h.point(0, 0))
    :lineto(h.point(100, 100))
    :stroke("red")
    :strokewidth(3)
    :build()

local line2 = h.path()
    :moveto(h.point(0, 100))
    :lineto(h.point(100, 0))
    :stroke("blue")
    :strokewidth(3)
    :build()

local square = h.unitsquare()
    :scaled(80)
    :shifted(10, 10)
    :stroke("green")
    :strokewidth(2)

-- Create picture with clipping
local pic = h.picture()
    :add(line1)
    :add(line2)
    :add(square)
    :clip(clipCircle)

-- Show clipping boundary (dashed)
local outline = h.fullcircle()
    :scaled(60)
    :shifted(50, 50)
    :evenly()
    :stroke("gray")

-- Output
h.svg()
    :padding(10)
    :addpicture(pic)
    :add(outline)
    :write("clip.svg")

Clipping example

Combining Pictures

local h = require("hobby")

-- First picture: circles
local circles = h.picture()
    :add(h.fullcircle():scaled(30):shifted(30, 30):stroke("red"))
    :add(h.fullcircle():scaled(20):shifted(30, 30):stroke("blue"))

-- Second picture: square
local squares = h.picture()
    :add(h.unitsquare():scaled(20):shifted(60, 20):stroke("green"))

-- Combine everything
local combined = h.picture()
    :addpicture(circles)
    :addpicture(squares)
    :add(h.fullcircle():scaled(10):shifted(80, 30):fill("purple"))

h.svg()
    :padding(10)
    :addpicture(combined)
    :write("combined.svg")