Constrain
Constraint-Based Animated Figures for Web Pages
Andrew C. Myers
Constrain is a JavaScript framework designed for adding diagrams to web pages and web-based presentations. It has some advantages over alternative ways to generate diagrams:
- Constrain supports animated figures in which displayed objects move and change their appearance smoothly or discontinuously. To make animations smooth, it uses a fast solver based on backpropagation, with each animation frame's solution used as a starting point for solving the next one.
- Figures can be interactive: the reader can change their appearance.
- Constrain is based on declaring constraints that are automatically solved. This makes it easy to produce precise diagrams, and easier to build and modify complex diagrams.
- Constrain draws figures onto HTML canvases. Diagrams are resolution-independent and as crisp as possible on all displays.
- Constrain includes an integration for the Reveal.js web-based presentation framework, so you can use it for presentations too.
- Constrain supports PDF and Postscript output, so it can be used to generate figures for static documents.
Constrain is written in JavaScript (ECMAScript 6) and integrates into web pages in most modern browsers. It can be obtained at its GitHub repository.
This document is a reference manual for Constrain. It is written using Constrain, so it is also an example of its use. The reference manual is a work in progress, so not all features are covered yet and many features deserve more explanation. There are many features that are not explicitly documented here. These features should be considered very much subject to change.
Heydon and Nelson wrote in their introduction to the Juno 2 constraint-based drawing editor in 1994, “thirty years after Sketchpad, the sad fact is that constraints have so far been more promising than useful” [1]. It has now been almost sixty years since Sketchpad [2], and there have been several other constraint-based graphical systems, such as Garnet [3], but Constrain does seem to make constraints useful for graphics.
Overview
Here is a simple Constrain diagram, generated by the code on the right (if the code appears about the diagram, your device may be too narrow to display this manual properly; try rotating your device or changing to a wider display):
All Constrain resources are accessed via the global Constrain
object. For example,
the class Figure
can be named as Constrain.Figure
. For brevity,
the Constrain.
qualifier is often omitted in this document.
A figure is created by instantiating the class Figure
with the identifier
attribute of the canvas in which the figure is to appear, or with the canvas object itself.
In the example above, the canvas is named "fig1"
, so this attribute is passed
to the Figure
constructor.
The code of this figure, like other code seen in this document, can be edited by clicking on it.
Clicking elsewhere on the page will cause the figure to be regenerated with the edited code.
Here's something you can try right now: make the circle smaller by
clicking on the code above and adding the code .setW(60)
at line 7.
Typically, the methods of Figure
are used to define graphical objects to appear
in the figure, constraints to govern their size and placement, and possibly animation frame
information to specify how the graphical objects and constraints change over time. In
the example above, these methods are invoked implicitly on the new Figure
object
because of the with
statement; it turns out to be convenient for use with Constrain.
The square
and circle
methods create graphical objects to be displayed
in the figure. Both of them inherit the figure's default line width of 3; the circle overrides
the fill color to be light blue by calling setFillStyle
. The second method call on
the circle introduces constraints to position the circle at the lower right corner of the square.
Note that graphical objects support cascaded method calls; the result of setFillStyle
is simply the circle itself.
All dimensions are specified in Constrain using CSS pixels, which are nominally 1/96 of an inch, though their exact size may depend on the display device. Even font sizes are specified in pixels rather than, say, points (1/72 or 1/72.27 inch). Even though dimensions are specified in CSS pixels, rendering is done at the full resolution of the display device.
Graphical objects
Constrain supports a number of useful graphical objects,
which support a common API
for controlling their appearance,
in class Constrain.Graphic
. The Figure
class provides
convenience methods for creating these objects, although they can also be
created by invoking the constructors of the corresponding classes. Constrain
can be extended with new user-defined graphical objects, using the full power
of HTML canvases to produce desired graphical effects.
Graphical objects are drawn in the order in which they are declared, so objects created later may cover those created earlier. In this respect, figures are not fully declarative.
Shapes
rectangle([fillStyle], [strokeStyle], [lineWidth])
A rectangle object (class Rectangle
). The default fill style, stroke style,
and line width of the figure may optionally be overridden by providing these parameters.
square([fillStyle], [strokeStyle], [lineWidth])
A square (class Square
), which is just a rectangle
constrained to have equal dimensions. The default fill style, stroke style, and
line width of the figure may optionally be overridden by providing these
parameters.
ellipse([fillStyle], [strokeStyle], [lineWidth])
An axis-aligned ellipse (class Ellipse
). The default fill
style, stroke style, and line width of the figure may optionally be overridden
by providing these parameters.
circle([fillStyle], [strokeStyle], [lineWidth])
A circle object (class Circle
), which is just an ellipse
constrained to have equal dimensions. The default fill style, stroke style, and
line width of the figure may optionally be overridden by providing these
parameters.
polygon(...points)
A closed polygon connecting the given points (class Polygon
). For
example, we can draw an equilateral triangle by constraining the three sides to have the same
length.
closedCurve(...points)
A smooth closed curve that goes near the specified control points (class ClosedCurve
).
Control points can be repeated to introduce sharp corners.
Lines
line([start], [end], [strokeStyle], [lineWidth])
Creates a straight line whose appearance can be overridden with optional
parameters (class Line
). The line goes from the point start
to the point end
, which if not specified are initialized to new points.
The start and end of the line can be accessed using its start()
and
end()
methods, and constraints can be applied to them.
A line can also be given arrowheads.
horzLine([start], [end], [strokeStyle], [lineWidth])
Creates a straight horizontal line that goes from left to right (class HorzLine
).
vertLine([start], [end], [strokeStyle], [lineWidth])
Creates a straight vertical line that goes from the top to the bottom (class VertLine
).
connector(...objects)
Connectors (class Connector
) are often the easiest way to draw a line. A connector is a smooth
curve connecting the specified list of objects. Typically, the goal is to
connect the objects at the beginning and end of the list, with the intermediate
objects (often Point
s) serving as control points to guide the
shape of the curve. Connectors act like Bezier splines near their endpoints but
like B-splines (which are smoother) in the middle.
Connectors connect to the objects at the beginning and end of the path using
one of two algorithms, set using the setConnectionStyle()
method.
Its argument may be one of the following:
"magnet"
: attach to one of several magnet points defined by the object"intersection"
: attach to the point where the path would intersect the object if connecting to the center.
The default connection style is initially "magnet"
but can be
changed by calling the setConnectionStyle()
on the figure
itself.
Text
Text is not a graphical object but can be part of a graphical object. Where text is
expected, an ordinary string may be used. Alternatively, the method text()
may be applied to a string or a sequence of strings to construct a text object. Whitespace
in those strings is interpreted as a place where line breaks are allowed.
Constrain uses a variant of the TeX formatting algorithm by default, in which line breaks are chosen to minimize the sum of the cubes of excess space on each line. If it is not possible to lay out text in the available space, Constrain will choose the formatting that maximizes the amount of text that does fit.
label(text, [fontSize], [fontName], [fillStyle])
Creates a graphical object that is a simple line of text. The text may either be a string
or a text object created using text()
.
textFrame(text, [fillStyle])
Creates a graphical object that is a box containing text. The text is automatically reformatted to fit within the box. Note that several other graphical objects, such as rectangles and circles, support adding text with similar formatting capability.
text(...)
Create a text object from a sequence of strings. Each string may contain whitespace to indicate potential line breaks.
br()
A text object for a line break.
subscript(text)
superscript(text)
A text object that places its text in subscript or superscript position while reducing the font size.
whitespace()
A text object for whitespace that is an allowed position for a line break.
negspace(w)
A text object that moves the position leftward by w
pixels.
numericText(expr, precision)
Create a text object containing the value of the expression expr
,
which can be solved for. The optional parameter precision
specifies
the number of digits of precision, which is 2 by default.
Interactive objects
handle(style)
A handle (class Handle
) is an object that can be moved using the mouse,
affecting any constraints that depend on its position. The following example constrains
distances to dynamically circumscribe a triangle;
you can drag the red diamond to change the figure.
advanceButton()
A button that can be used to advance the current state of the animation.
Unrendered objects
Some figure elements act like graphical objects in most respects but do not result in any rendering. These elements are useful for positioning other objects.
point()
Creates a point (class Point
) at an unspecified location. Its
location is solved for.
point(x, y)
Creates a point (class Point
) at the location (x,y). This is
equivalent to point().at(x,y)
box()
A box (class Box
) is an axis-aligned rectangle that has no
rendering. In other respects it acts like a graphical object; for example, it
has a width and height.
hspace(w, [unit])
Horizontal space. It has width w
but no height constraint. An
optional unit parameter can be provided. It can be any graphical object, in which
case it stands for the width of that object. Other options are "canvas", which
signifies the width of the canvas, and "em", which stands for the current font size.
vspace(h, [unit])
Vertical space. It has height h
but no width constraint. Units
work as for hspace()
but signify heights rather than widths.
DOMElement(id)
A box with position and the size of the DOM element with the specified id
attribute (class DOMElementBox
). This feature helps overlay
graphics onto regular web page content, as demonstrated in
the title of this document.
canvasRect()
A box exactly filling the canvas the figure is drawn in, which is useful for positioning objects relative to the canvas. See also
margin(), margin(d)
A box representing the canvas margin, which is the full box returned by
canvasRect()
, but inset either explicitly by d
pixels
or, if the margin width is not specified, by the default canvas line width so
that objects aligned against the margin do not overflow outside the canvas.
group(...objects)
A box containing a set of objects. The box is just large enough to surround all the objects.
User-defined graphical objects
The class UserDefined
provides a template for building new graphical objects.
By default, a user-defined object is drawn as a plain gray rectangle, but much fancier
effects can be achieved. For an example, see the fireworks in this demo presentation.
It is the responsibility of the user to define a subclass of UserDefined
, in
which the following methods may be overridden:
draw(context, frame, time, x0, y0, x1, y1)
This method defines how the user-defined object appears. Draw a user-defined
object in the graphical context context
. The current animation
frame is frame
and the time (0–1) within that frame is
time
. The numeric parameters x0
, y0
,
x1
, y1
define the box that nominally contains the
object. Rendering may in general rely on other expressions that may be solved
for using constraints. The method Constrain.evaluate
may be used
variables()
Constrain solves for variables that it knows are needed by graphical objects,
and avoids solving for variables that aren't needed for rendering. However, the
draw()
method might need additional variables. To ensure they are
solved for, the variables()
method may be overridden to return a
larger set of variables than the default. The expression super.variables()
may be used to obtain the default set of variables, and that set can then be added to
appropriately.
Figure control
Figure control methods
Figure objects offer some methods for controlling the figure as a whole.
start()
Requests the figure to start rendering when it is ready.
Figures do not automatically appear; for example, this document
ends with a loop over the list of all figures (Constrain.Figures
),
telling each one to start:
stop()
Clears the figure and moves it back to an unready status.
destroy()
Destroys the figure and removes it from the list of figures (Constrain.Figures
).
freeze()
Add a new stage to the figure, freezing all previously created objects in place. See Staged solving for more explanation.
saveStyle()
Save current style settings. Saved styles are pushed onto a stack.
Further style changes can be made and then undone using restoreStyle()
.
restoreStyle()
Restore the most recently saved style settings, removing them from the stack of saved styles. It is an error to call this method when there is no currently saved style.
setStyle(name, value)
Set the named style parameter to have the given value. The various figure
style settings that are inheritable have corresponding style parameters.
For example, calling figure.setFillStyle(f)
is the same as calling
figure.setStyle('fillStyle', f)
.
getStyle(name)
Return the value of the named style parameter.
hasStyle(name)
Report whether the named style parameter is defined for the figure.
setZoom(zoom)
By default the coordinate system of the figure is the same as the HTML
coordinate system, with 1 HTML pixel per horizontal or vertical unit. (Note
that HTML pixels typically do not correspond to actual screen pixels.) The
setZoom()
method can be used to change the number of HTML pixels
per figure pixel to the specified value. Changing the zoom affects everything
that is rendered, including font sizes and line widths. For example, if you
want a figure pixel to correspond to a PostScript/PDF point, you should set a zoom
of 1 1/3 ≅ 1.333. To make it correspond to a TeX point, the zoom value should
be 96/72.27 ≅ 1.328.
thisFigure()
with (new Constrain.Figure("figure1")) { const figure1 = thisFigure() ... }
This method simply returns the figure object itself, a functionality that
is handy when used in conjunction with the with
statement.
enableSubstitution(b)
By default, Constrain tries to accelerate solving by simplifying the system of constraints.
For hard constraints, it may use substitution to achieve this simplification. Substitution
usually speeds up solving but in some cases can make it harder for the minimization algorithm
to escape local minima, by overconstraining related variables. This method allows substitution
to be prevented, by passing false
as an argument.
Other figure control operations
Constrain.fullWindowCanvas(canvas)
Makes the specified canvas automatically resize to fill the window.
Constrain.autoResize()
This function causes figures to automatically resize to their containing canvases.
Constrain.setupTouchListeners()
By default, touch events are not tracked by figures, which means that handles will not work correctly on tablets and phones. Calling this method enables tracking of touch events. They are not turned on by default because handling touch events seems to require interposing on touch events at the level of the entire window.
Constrain.getFigureByName(name)
Find and return the figure with the specified name, or return undefined
if no such figure
currently exists.
Default styles
Graphical objects can set their own style, but if no style is specified, the style is inherited from the figure. The following figure methods support defining the default styles for the entire figure, taking the same arguments as the corresponding methods on graphical objects.
setFillStyle(style)
setStrokeStyle(style)
setTextStyle(style)
setFontSize(s)
setFontName(n)
setLineWidth(w)
setLineDash(d)
setConnectionStyle(s)
See also the setStyle()
method, which can also be used to set
these styles under corresponding names: fillStyle
,
strokeStyle
, etc.
Style parameters
The following table is a mostly exhaustive list of style parameters built into Constrain. More may be added by user code.
Name | Effect |
---|---|
arrowSize | The size of the arrow, if any, at the start of a line or connector. |
baseline | Vertical offset of text on a line, in points |
connectionStyle | The style of connecting objects, either “intersection” or ”magnet”. |
endArrow | The style of arrow at the end of a line or connector. |
fillStyle | The color or gradient used to fill shape interiors |
fontName | The name of the font family, e.g., “Times” |
fontSize | The size of the font, in pixels |
fontStyle | The style of the font, e.g., “italic”, “bold” |
inset | Inset of text within a containing object |
justification | The style of contained text justification (left, right, justify) |
layoutAlgorithm | Text layout algorithm (TeX or greedy) |
lineDash | The pattern of dashes for lines. An empty array [] signifies no dashes. |
lineSpacing | The spacing between lines in multiline text, as a multiple of font size. |
lineWidth | The width of lines or connectors, in pixels |
scriptSize | Scaling of superscript or subscript text |
startArrow | The style of arrow at the start of a line or connector. |
strokeStyle | The color used to stroke shape borders |
subscriptOffset | Offset of subscript text (as fraction of point size) |
superscriptOffset | Offset of superscript text (as fraction of point size) |
textStyle | The color used for text |
verticalAlign | Vertical alignment of contained text |
Constraints
The appearance of a figure is governed by a set of constraints. Some graphical objects automatically add their own constraints to the set. Additional constraints can be added by methods on the figure object, on graphical objects, and on other figure elements.
Figure constraint methods
align(horizontal, vertical, ...objs)
Aligns the objects in the list objs
according to the alignment specification
given by the strings horizontal
and vertical
, and returns the
associated array of constraints added to achieve this alignment. Objects may be
passed either as a single array of objects or as a variable-length list of arguments.
The horizontal alignment specification can be any of the following:
"none"
: No horizontal alignment"left"
: Align left sides"right"
: Align right sides"center"
: Align horizontal centers"target"
: Align the target locations to have the same horizontal position."left right"
: Align both left and right sides, forcing the objects to have the same width."abut"
: Make objects abut each other directly, left-to-right. Space may be inserted between the objects using the methodhspace()
."distribute"
: Equalize the horizontal separation between the objects in the list.
The vertical alignment specification is the same as the horizontal specification, except
that top
and bottom
replace left
and right
.
The abbreviations "L"
, "R"
, "T"
, and "B"
may be used as shorthands for "left"
, "right"
, etc. Further,
"LR"
and "RL"
are shorthands for "left right"
and similarly with
"TB"
and "BT"
.
direction(g1, g2, dir)
Adds and returns a constraint that the direction from object g1 to object g2 is the specified direction. Directions may be specified in degrees as on a compass, with 0 meaning up, 90 meaning to the right, 180 to the bottom, and so on. They may also be specified as a compass point, such as "N", "NE", "SSW", and so on. Compass points are case-insensitive.
equal(...exprs)
Adds and returns a constraint that the scalar expressions in the list
exprs
are all equal to each other.
geq(...e)
Adds and returns a constraint that each expression in the list e
is greater than or equal to
those later in the list.
nearZero(e)
Adds and returns a constraint that expression e
is as close to zero as possible.
positive(e)
Adds and returns a constraint that the expression e
is greater than or equal to zero.
leq(...e)
Adds and returns a constraint that each expression in the list e
is less than or equal to the next.
pin(...o)
Adds and returns a constraint that all the objects in the list o
are located at the
same position.
collinear(p0, p1, p2, ...)
Adds and returns a constraint that the specified points all lie in a straight line.
constraintGroup(...constraints)
Adds and returns a constraint that enforces all of the supplied constraints.
keepInside(obj1, obj2)
Constrain object obj1
to remain inside the bounding box of obj2
.
sameSize(obj1, obj2, ...)
Constrain objects obj1
, obj2
, etc. to have the same width and height.
Figure element constraint methods
The following methods of graphical objects and figure elements more generally can be used to constrain their positioning and size.
setX(x)
Constrain the x coordinate of this element to be x
and
return this element.
setY(y)
Constrain the x coordinate of this element to be x
and
return this element.
at(x,y), at(p), at(obj)
Constrain both the x and y coordinates of this element to be equal to the provided coordinates or the provided point or object center.
setW(w)
Constrain the width of this element to be w
and
return this element.
setH(h)
Constrain the height of this element to be w
and
return this element.
atSameSize(g)
Constrain the size and position of this element to be the same as graphic
g
.
toRight(g), toLeft(g), above(g), below(g), fromDirection(g, dir)
Constrain the position of this element to be in the specified direction relative
to element g
. Directions may be specified as in the direction()
function, as either a compass point or in degrees.
placeUnder(obj), placeOver(obj)
These method do not affect the positioning of graphical objects, but they affect the order in which objects are rendered. They respectively cause the receiver object to be rendered immediately before or immediately after the specified object. Note that by default, graphical objects are rendered in exactly the order they are created, so this method allows objects to be defined out of order with respect to the rendering order.
Costs
In general, the constraint solver cannot solve every constraint exactly. It works by trying to minimize a total cost over all constraints. Every constraint has an associated cost multiplier that is equal to 1 by default. Normally, this multiplier does not need to be changed, but it can be useful to change the cost of some constraints to make them softer or harder constraints relative to other constraints. Constraints with a cost multiplier that is at least 1 are assumed to be hard constraints than can be solved exactly. The solver may optimize the solving process under that assumption; so, if the assumption is violated, it is likely to find a suboptimal solution. A cost multiplier strictly less than 1 means the constraint is treated as a soft constraint that cannot be assumed to be satisfied exactly.
changeCost(c)
This method can be invoked on Constraint
objects to multiply the current cost by
the factor c
. This method can help get the solver out of local
minima, for example by using leq
or geq
with a high cost to pull
variables and points forcibly to the correct side of a diagram, even when other constraints
opppose that motion.
Expressions
Some constraints are built out of expressions. The following expressions are
supported by the constraint solver. Expressions may have either a scalar
value (a number) or an array value. For example, as an expression, a point
evaluates to a two-element array, and any graphical object evaluates similarly
to its center position. Many operations can be performed either on scalars or
arrays. The function Constrain.evaluate()
may be used to obtain the
current value of an expression, although usually this value is not needed.
plus(a, b)
The sum of a
and b
, which may be scalars or arrays.
minus(a, b)
The difference of a
and b
, which may be scalars or arrays.
times(a, b)
The product of a
and b
, at least one of which must be a
scalar.
divide(a, b)
Dividing a
by b
. The divisor must be a scalar.
abs(a)
The absolute value of a
.
sqrt(a)
The square root of a
.
sq(a)
The square of a
.
sin(a), cos(a)
The sine or cosine of the argument in radians.
distance(p1, p2)
The Euclidean distance between points p1
and p2
.
average(a, b, ...)
The average of the arguments, which may be arrays of the same length, so it is possible to compute the average of two or more points.
max(a, b, ...)
The maximum of the arguments, which must all be scalars.
min(a, b, ...)
The minimum of the arguments, which must all be scalars.
conditional(cond, epos, eneg)
If the value of the conditional expression is positive, the value of this
expression is the value of epos
. Otherwise, it is eneg
.
For best behavior, epos
and eneg
should have the
same value when cond
is zero.
linear(frame, a, b)
An expression (class Linear
) whose value is the value of expression a
before the
specified frame, and the value of b
after that frame. During the
frame, it linearly interpolates over time between a
and
b
.
smooth(frame, a, b)
An expression (class Smooth
) whose value is the value of expression a
before the
specified frame, and the value of b
after that frame. During the
frame, its value interpolates over time between a
and
b
, using a cubic spline so that the rate of change at the
endpoints is zero. Expressions a
and b
may compute scalars
or, as in the following example, arrays.
variable(name)
Creates and returns a fresh variable that can be solved for. Its name is based
on the name
parameter. Normally this method does not need to be
used directly.
hint(e, v)
Add a hint to the solver that the value v
is a good initial guess
for the solved value of the expression e
. The value must be a
scalar computed at the time of figure construction. Usually such hints are
not necessary; the main use is in situations where the solver needs to be
biased toward one of multiple solutions, or away from local minima that are not
solutions. The call returns the expression e
.
ptToLineDist(p0, p1, p2)
The signed distance from point p0
to the line p1 → p2
,
where p1
and p2
are
both points. The distance is negative if
the point is on the left side of the line and positive if on the right side.
relative(x, y, a, b)
The position (x,y) in a coordinate system in which point a is the origin and point b is (1,0). Arguments x and y should be scalars.
Other expressions
new Global(f, name)
The class Global
represents an expression whose value may change but which is
not affected by any variables that are solved for. Its value is provided by a function f
passed to the constructor. The function is expected to return a number or array of numbers.
This class is used to interface Constrain diagrams with external data sources such as the user
interface. The optional argument name
can be used to give the global a name, for
debugging purposes.
new DebugExpr(e)
Wrapping an expression in a DebugExpr
object causes the value of
that expression to be logged to the console whenever it is evaluated or
differentiated.
Positions
Graphical object positioning methods
Graphical objects provide methods that return expressions for scalars and points relating to their position in the figure.
x()
The horizontal position of the center of the object, as a canvas coordinate.
y()
The vertical position of the center of the object, as a canvas coordinate. Recall that y coordinates start from zero at the top of the canvas and increase downward.
x0()
The horizontal position of the left edge.
x1()
The horizontal position of the right edge.
y0()
The vertical position of the top edge.
y1()
The vertical position of the bottom edge.
width(), w()
The width of the object.
height(), h()
The height of the object.
center()
The center point of the object.
ul(), ll(), ur(), lr()
These four methods each return a point corresponding to the one of the four corners of the object: upper left, lower left, upper right, and lower right.
uc(), lc(), cr(), cl()
These methods each return a point that is the center of one of the edges of the bounding box of the object.
target()
This method returns a point somewhere in the object. It is used for
connectors between objects and when "target"
alignment is chosen.
By default, the object target is the object's center, but it can be useful to
override this method.
toTop(d), toBottom(d), toRight(d), toLeft(d)
These methods can be used to nudge the position of a point in the
specified direction by distance d
, returning the new resulting point.
When applied to a graphical object, the starting position is respectively the
result of uc()
, lc()
, cr()
, or
cl()
. It is possible for the nudge distance to be negative, which is useful
for nudging a point into the interior of a graphical object.
inset(d)
Returns a box element whose bounding box is the same as the bounding box of this
object, except moved inward by distance d
.
expand(d)
Returns a box element whose bounding box is the same as the bounding box of this
object, except moved outward by distance d
.
Line positioning methods
start(), end()
These methods return the points corresponding to the start and end of the line.
setStart(p), setEnd(p)
These methods constrain the start or end of the line to be at the specified point p
Style
Graphical objects offer some standard methods for controlling their appearance, and some object have more specialized methods for this purpose.
Graphical object style methods
The following methods return the graphical object while also changing its style in the specified way.
setFillStyle(s)
Set the fill style of the object. If this method is not used, then the object gets its fill style from the constructor call, or from the figure itself if the not provided.
setStrokeStyle(s)
Set the stroke style of the object. If this method is not used, then the object gets its stroke style from the constructor call, or from the figure itself if the not provided.
setLineWidth(w)
Set the line width for drawing the boundary of the object. If this method is not used, then the object gets its stroke style from the constructor call, or from the figure itself if the not provided.
setLineDash(dash)
Set the line dash specification for the boundary of the object. The argument is an array specifying the lengths of the dash segments.
addText(text)
Graphical objects generally have the ability to hold some text, which is specified
by calling the addText
method. The argument should be either a string
or a text object. After text has been added to a graphical object, the following
methods can be used to affect how it appears.
setTextStyle(s)
Set the fill style with which contained text will be rendered.
setLineSpacing(s)
Set the spacing of lines of text, as a multiplier applied to the font size. By default the line spacing is 1.3.
setInset(d)
Set the amount by which text is inset inside the boundary of the containing graphical object.
setJustification(j)
The justification of text can be either "left"
(the default) or
"center"
.
setVerticalAlign(pos)
The vertical positioning of text can be either "top"
(the default),
"center"
, or "bottom"
.
setFontSize(s), setFontName(f)
These methods act like the corresponding methods on labels.
setLayoutAlgorithm(algorithm)
This method can be used to change the layout algorithm.
Currently there are two choices: "TeX"
(the default) and
"greedy"
. The latter is faster but chooses line breaks greedily
and tends to find uglier layouts. Constrain does not do automatic hyphenation
ala TeX.
Rectangle and Polygon methods
setCornerRadius(r)
This method rounds the corners of the shape to the radius r
.
Line and Connector methods
setStartArrow(style), setEndArrow(style)
By default, lines and connectors do not have arrowheads or other terminators. These methods allow overriding this behavior to add some kind of terminator. The legal choices ofstyle
are "arrow", "bullet", and "curved".
setArrowSize(s)
This method sets the scale of the arrowhead if there is any.
addLabel(label, position, [offset], [margin])
Add a label to a connector (or line), positioned somewhere along the connector.
The label may be either text, a text object, or a graphical object.
The position is a number from 0 to 1, representing how far along the connector
the label is placed; by default, it is set fo 0.5.
The optional parameter offset
specifies how far to the port
side of the connector the label is placed; by default, it is the currrent
font size. The optional parameter margin
specifies a margin around
which the label will clip the connector, allowing labels to be placed on top of
lines and connectors.
Text objects
A text object can be created by the method
text
of Figure
objects. A text
is not a graphical object; to be useful, it needs to be added as the text
associated with a graphical object and typically, some of the following methods
are used to control its appearance.
Animation
A figure has one or more frames that can differ in what objects and constraints are operative during each frame.
Figure methods for animation
By default, a figure has a single frame, but new frames can be added, creating
an animation that can be controlled using an AdvanceButton
or by other
programmatic means.
addFrame(...names)
Adds one or more frames to the figure. Names may optionally be supplied for the frames.
reset()
Resets this figure back to its first frame.
advance()
Advances this figure to the next frame, if any. Returns true if there is a next frame to go to.
rewind()
Rewinds this figure to the previous frame, if any. Returns true if there is a previous frame to go to.
setRepeat(b)
Set whether the figure resets to the first frame when it advances past the last frame.
setFadeColor(color)
For repeating figures, there is a period in which the figure fades out to a color. By default it is the same as the background color of the canvas, which usually the most effective choice, but this method can be used to change the fade color.
setAnimatedSolving(b)
The constraint solver does not show its solving process by default.
If this method is called with true
, the steps the solver takes
are shown as an animation. This can be useful for visualization or debugging.
Animating graphical objects
By default, graphical objects and constraints exist for the entire duration of a figure.
However, objects and constraints can be wrapped in frame filters that make them exist
or be visible only some of the time. Convenience methods in Figure
make these
easy to use.
drawAfter(frame, ...objs)
Draw objects objs
only starting from the specified frame.
However, the objects still exist in the figure.
drawBefore(frame, ...objs)
Draw objects objs
only before the specified frame.
However, the objects still exist in the figure.
drawBetween(frame1, frame2, ...objs)
Draw objects objs
only starting from frame frame1
and
before frame frame2
. However, the objects still exist in the
figure.
after(frame, ...objs_constraints)
Objects or constraints objs_constraints
are only present in the figure
starting from frame frame1
. Note that introducing new constraints
can cause objects to jump discontinuously at frame boundaries, so the method
drawAfter
may be a better choice.
before(frame, ...objs_constraints)
Objects or constraints objs_constraints
are only present in the figure
before frame frame1
.
betweenFrames(frame1, frame2, ...objs_constraints)
Objects and constraints objs_constraints
are only present in the figure
after frame frame1
and before frame frame2
.
Frame methods
Frame objects returned by addFrame()
support methods for controlling
animations.
setLength(ms)
By default, a frame has zero duration; it is simply rendered once. By giving the frame
a duration in milliseconds, the frame renders repeatedly for the given duration, with
the value of certain expressions (e.g., Linear
and Smooth
)
varying continuously over the duration.
setAutoAdvance(b)
If invoked on a frame with true
, the frame automatically advances
to the following frame once its duration is over.
Staged solving
For complex diagrams, it may be too hard for the solver to find a stable solution with all the constraints and variables at once. Often this limitation can be worked around by breaking solving into a series of stages. Stages are also useful if you want to fix part of the diagram so that its solution is unaffected by other parts. In each stage, some set of the variables are solved for. Variables associated with later stages have no value until that stage is reached; variables in earlier stages have already been solved and their values cannot change.
To introduce stages to a diagram, the method freeze()
advances the figure to the next stage. Any constraints and variables created
will be associated with the (new) current stage. Calling freeze()
effectively freezes all previously created objects in place, with their positions
determined only by the constraints created up to this point.
For example, the following code declares a square with no constraints on its size or position, but solving the subsequent constraints on the circle does not move the square even though the square is unconstrained.
Note that stages have nothing to do with animation frames; the same stages exist for all frames. Stages simply define the order in which solving will be done for any frame.
The Constrain Solver
Constrain acts like a constraint solver, but it is actually based on minimizing loss. This choice makes it more robust in the fact of constraints that cannot be solved, and means that it is also possible to add minimization goals to the problem it is solving.
Constraints are converted into loss terms whose value is always nonnegative and
is zero when the constraint is solved. The constraints are then converted into
a minimization problem that is passed to the well-known BFGS algorithm. If the set
of constraints can be decomposed into independently solvable subsets, this is done
automatically. In addition, each call to freeze()
partitions the
set of variables and constraints into a series of stages whose constraints are decomposed
into independendent components that are solved independently.
Loss terms have an associated cost multiplier; when the cost multiplier is less than one, the solver treats the constraint as a soft constraint whose loss may not be reducible to zero. Hard constraints, with loss at least one, allow the solver to optimize by performing equational manipulations before trying to minimize loss.
Once a solution is found, any new instance of the minimization problem is passed the previous solution values, along with the approximate inverse Hessians computed during the BFGS algorithm, as an initial condition for the new solver run. This reuse of the previous solution is critical for allowing new solutions to be found quickly during animated sequences, reducing the number of loss-function evaluations by orders of magnitude in practice.
Debugging Tips
Sometimes you create a set of constraints and the solver just seems to refuse to find the right solution, or it is not consistent about finding the right solution. Automation is great when it works but frustrating when it doesn't! A few tips may help resolve the problem.
One possibility is that your constraints are actually unsolvable, so the solver is doing the best it can by balancing multiple unsolved constraints. Another possibility is that the solver is getting stuck in a local minimum.
Debugging unsolvable constraints
- Try commenting out constraints until you figure out which constraints are causing trouble. Then add constraints back in gradually.
-
If you're trying to understand what is happening with some variable or expression,
wrap it in a
DebugExpr
, which will cause its value during solving to be logged to the console.
Escaping local minima
- Keep constraints simple. It's always better to express constraints in a
simple way rather than adding complexity for the solver. For example, if you are expressing
horizontal or vertical distance constraints, it's better to write constraints directly on
the coordinates rather than using
distance()
. If you useequal()
to set several things equal to each other, make sure the simplest expression comes first in the list, since it is the one that will be set equal to the rest. - Bounding constraints. Constraints that keep the solution reasonable are often
handy. Methods like
keepInside
andbetween
are useful for this task, as are the humbleleq()
andgeq
. - Hints. The solver works by changing the values of variables to minimize
loss, which is the difference between a perfect solution and the current solution.
A perfectly solved constraint has zero cost and causes zero loss.
The initial values of variables can make a big difference in which solution is found
and how fast. Variables can be given a hinted value that defines the initial conditions
of the constraint solver, using the
setHint()
method. - Staging. The figure's
freeze()
can be used to break solving into multiple stages, simplifying the solver's task in each stage. However, constraints appearing in later stages have no effect on how earlier stages are solved. - Changing constraint costs. If some constraints are being violated — especially
bounding constraints — try increasing the cost of violating them using the
changeCost
method. Increasing cost will cause the solver to prioritize solving those constraints. An order-of-magnitude increase in cost in a constraint is usually sufficient to prevent other constraints from overriding it. -
User-defined objects. For complex diagrams, it will be easier for the constraint solver,
and sometimes just as convenient for the user, to describe some of the diagram as a user-defined
rendering task rather than encoding it into constraints. You can subclass
Constrain.UserDefined
to specify how to render a complex graphical object, as described above.
Generating printable diagrams
The Constrain.PS
and Constrain.PDF
subpackages (in constrain-ps.js and constrain-pdf.js)
enable Constrain to generate figures in printable non-HTML
format. They are useful for generating figures for papers, so
you can use the same code to generate both the HTML version of the figure and
the printed version.
Interactive objects in the figure, such as handles and buttons, are not rendered.
The function Constrain.PS.print
can be called on a figure to
generate PostScript output. For PDF output, the class
Constrain.PDF.PrintJob
allows more control.
To make it easier to generate printable output on demand, the classes
Constrain.PS.PrintButton
and Constrain.PDF.PrintButton
define buttons (interactive objects)
that causes the appopriate kind of output to be generated for the figure in which it
appears. Thus, the example figure below is printable using the gray buttons.
In the rendering, dimensions are expanded by 33% so that all pixel
dimensions become point dimensions. The figure therefore becomes
correspondingly larger, although since PostScript and PDF are resolution-independent
page descriptions, the generated figures are easily rescaled. Figure method setZoom
can also be used to make the coordinates agree.
Support for PDF depends on the jsPDF package, so HTML documents using PDF should include this package, with a line like the following:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.1/jspdf.umd.min.js"></script>
Embedding fonts
If you use fonts outside the standard set of PostScript fonts, they will not be automatically embedded
into the generated file. There are various ways to make this happen. One option
is to use Adobe Acrobat to re-export the PDF output with fonts embedded. Another option is to make
the font data available to the PDF renderer using its addFont
and
addFontFile
methods, which are supported both by the Constrain PrintJob
and
PrintButton
methods. TTF font files can be re-encoded
into JavaScript using a publicly available web service. The file fig2c.html in the examples directory demonstrates how
to embed fonts directly using a JavaScript-encoded font file.
Integrating with MathJax
If you want complex mathematics in your diagram, it can be useful to embed MathJax.
The module constrain-mathjax.js extends the Figure
class with
an additional method to create MathJax objects. (A simple example)
mathJax(formula, [displayMath])
Create a graphical object that shows the specified formula. The optional displayMath
parameter controls whether MathJax considers it to be “display math” in TeX's nomenclature. The size
of the rendered object is determined by the figure's current font size.
Graphs
Constrain also includes support for automatic graph layout, in the module
constrain-graph.js. The class Constrain.Graph
represents a graph consisting of nodes and edges. It has various methods for building
and controlling the automatic layout of the graph.
Graph methods
addNode(obj)
Add the graphical object obj
as a node in the graph.
edge(obj1, obj2)
Add an undirected edge from graph node obj1
to graph node obj2
.
dedge(obj1, obj2)
Add a directed edge from graph node obj1
to graph node obj2
.
Graph properties
The layout of a graph is controlled by various graph properties whose default values can be overridden.
sparsity
This parameter controls how widely separated graph nodes are.
cost
Graphs are laid out using soft constraints whose importance is scaled relative to ordinary constraints
by the cost
property of the graph.
gravity
Nodes in a directed graph experience a force like gravity that tends to push edges to align with it.
The strength of this force is controlled by the property gravity
.
repulsion
Nodes in the graph exert a repulsive force on nearby nodes to discourage overlap. The
property repulsion
controls the strength of the repulsive force.
branchSpread
When a node is connected to multiple other nodes, a torque is introduced on the edges to
those nodes, to try to equalize the angles between the edges. The strength of the torque
is controlled by the branchSpread
property.
horizontalLayout
This property controls the direction of gravity. By default, it is false, meaning downward gravity. If true, gravity pulls to the right.
Animated Trees
The module constrain-trees.js includes support for automatically
laying out trees via the class AnimatedTree
. These trees can be
animated to support changes to the tree structure over multiple frames. A tree
is not a graphical object, but it coordinates placement and animation of
the graphical objects that depicts its nodes and edges. Changes to the configuration
of a tree can be tied to animation frames.
Tree nodes are not graphical objects, but are tied to graphical objects that represent them. They have the following two methods:
getValue()
Return the value from which this node was constructed.
graphic()
Return the graphical object representing this node.
Tree objects are created by the following Figure method:
tree(style, root, ...children)
Trees are created using the tree
method. The nodes of the tree are
specified using values that are mapped to graphical objects according to the specified
tree style.
If the provided tree style is null
, the current figure style
is used instead. There are four style parameters, which can be bound to different
functions or provided as methods of the style objects:
drawNode(figure, value)
The tree is built out of graphical objects, which are created from the values
provided in root
and children
, using the
drawNode
method. It should return the graphical object
representing the node.
drawEdge(figure, n1, n2)
This method creates graphical objects representing an edge from node
n1
to node n2
.
decorateRoot(figure, n)
This method optionally creates and returns a graphical object
decorating the root node n
of the tree.
glue(figure)
This method returns an expression representing the amount of glue space to insert on the sides of the tree.
Tree objects offer the following methods for controlling their appearance over time.
view(frame)
The view method returns a TreeView
object, which represents
the configuration of the tree in the specified frame. The tree can be
conveniently manipulated using the TreeView
with the following
methods. It is important to perform tree operations in frame order, because
when an operation is performed on a tree, it is assumed to not require any
changes between the last operation and the current one.
rootGraphic()
Return the graphical object that corresponds to the root node of the tree in this frame. Its position may be animated during the frame.
rootPosition()
Return a graphical object representing the final position of the root node at the end of this frame.
findNode(v)
Return a tree node with the value v.
findGraphic(v)
Return a tree node with the value v.
swapNodeWithParent(value)
Swap the node with the specified value with its parent node.
rotateNodeWithParent(value)
Rotate the node with the specified value up to the position of its parent node while keeping the rest of the nodes in the same in-order traversal order.
addLeaf(value, parentValue, position)
Create a new leaf node containing the value at the given position in the children of the given parent node. Children at that position or later have their position increased. If position is omitted, the new leaf is added as the last child.
removeLeaf(value)
Remove the leaf node with the specified value in this frame.
spliceNode(value)
Remove the single-child node identified by value, connecting its parent node to its single child.
replaceLeaf(value1, value2)
Remove the node with value value1
with a new node
with value value2
.
Reveal.js integration
To use Constrain with Reveal.js, the script constrain-reveal.js should be included in the presentation. It relies on some hooks being added to Reveal, so you must also use the version of Reveal.js that comes with Constrain.
Acknowledgments
Constrain was designed and implemented by Andrew Myers. Its design draws on several previous systems, notably the Juno-2 constraint-based drawing editor by Allan Heydon and Greg Nelson, which Myers helped implement.
References
- Allan Heydon and Greg Nelson. The Juno-2 constraint-based drawing editor. HP Labs Technical Report SRC-RR-131A, Dec. 1994.
- Ivan E. Sutherland. Sketchpad: A man–machine graphical communication system. Simulation 2.5 (1964): R-3.
- Brad A. Myers, Dario A. Giuse, Roger B. Dannenberg, David S. Kosbie, Edward Pervin, Andrew Mickish, Brad Vander Zanden, and Philippe Marchal. Garnet: Comprehensive support for graphical, highly-interactive user interfaces. IEEE Computer 23(11):71–85, 1990.