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 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):


Example figure

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 Points) 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:

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.

NameEffect
arrowSizeThe size of the arrow, if any, at the start of a line or connector.
baselineVertical offset of text on a line, in points
connectionStyleThe style of connecting objects, either “intersection” or ”magnet”.
endArrowThe style of arrow at the end of a line or connector.
fillStyleThe color or gradient used to fill shape interiors
fontNameThe name of the font family, e.g., “Times”
fontSizeThe size of the font, in pixels
fontStyleThe style of the font, e.g., “italic”, “bold”
insetInset of text within a containing object
justificationThe style of contained text justification (left, right, justify)
layoutAlgorithmText layout algorithm (TeX or greedy)
lineDashThe pattern of dashes for lines. An empty array [] signifies no dashes.
lineSpacingThe spacing between lines in multiline text, as a multiple of font size.
lineWidthThe width of lines or connectors, in pixels
scriptSizeScaling of superscript or subscript text
startArrowThe style of arrow at the start of a line or connector.
strokeStyleThe color used to stroke shape borders
subscriptOffsetOffset of subscript text (as fraction of point size)
superscriptOffsetOffset of superscript text (as fraction of point size)
textStyleThe color used for text
verticalAlignVertical 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:

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 of style 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

Escaping local minima

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

  1. Allan Heydon and Greg Nelson. The Juno-2 constraint-based drawing editor. HP Labs Technical Report SRC-RR-131A, Dec. 1994.
  2. Ivan E. Sutherland. Sketchpad: A man–machine graphical communication system. Simulation 2.5 (1964): R-3.
  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.