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)
endBounding 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/2on 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:
centerorc- centered at pointleftorlft- to the left of pointrightorrt- to the right of pointtop- above pointbottomorbot- below pointupperleftorulft- upper-left of pointupperrightorurt- upper-right of pointlowerleftorllft- lower-left of pointlowerrightorlrt- 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)
endLabel 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")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")