in ,

Experiments in Constraint-based Graphic Design, Hacker News

Experiments in Constraint-based Graphic Design, Hacker News

[” or “]       [” or “]      

   (Dec)   · 22 min read        – shared on                   Hacker News,              Lobsters [aligned_middle(row)forrowinrows] and              [ # all elements are the same size same_size([iforrowinrowsforiinrow.elements]               ************************** Standard GUI-based graphic design tools only support. a limited “snap to guides” style of positioning, have a basic object grouping system, and implement primitive functionality for aligning or distributing objects. They don’t have a way of remembering constraints and relationships between objects, and they Don’t have ways of defining and reusing abstractions. I’ve been dissatisfied with existing tools for design, in particular for creating figures and diagrams, so I’ve been working on a new system called Basalt that matches the way I think: in terms of relationships and abstractions. (******************************

Basalt is implemented as a domain-specific language (DSL), and it’s quite different from GUI-based design tools like Illustrator and Keynote. It’s also pretty different from libraries / languages ​​likeD3.js,TikZ, anddiagrams (****************************. At its core, Basalt is based on constraints: the designer specifies figures in terms of relationships, which compile down to constraints that are solved automatically using an SMT solver to produce the final output. This allows the designer to specify drawings in terms of relationships like “These objects are distributed horizontally, with a 1: 2: 3 ratio of space between them. ”Constraints are also a key aspect of how Basalt supports abstraction, because constraints compose nicely. (****************************** [List[Element] **************************

I’ve been experimenting with this concept, off and on, for the last couple years. Basalt is far from complete, but the exploration has yielded some interesting results already. The prototype is usable enough that I made all the figures in my latest researchpaperand

If you want to read about the core ideas behind Basalt, take a look at thephilosophy section. If you want to hear about my experience Using Basalt to design real figures, skip ahead to thecase studiessection. If you want to see how gradient descent can be used to solve figures, go to thegradient descentsection. (******************************

Basalt’s programming model is as follows. Designers write programs that produce figures described in terms of relationships. These relationships are compiled down to constraints, which are then solved automatically. (******************************

Basalt is a DSL embedded in a general-purpose programming language, so it inherits support for functional abstraction, classes, and so on. Its constraint-based approach is key to supporting abstractions that compose nicely. Specifying drawings in terms of relationships (********************************************

Basalt’s programming model allows drawings to be (specified) in terms of relationships between objects. The standard GUI-based tools for graphic design Don’t have support for this. They have a limited “snap to guides” style of positioning, plus a basic object grouping system and basic functionality for aligning or distributing objects. They don’t encode figures in terms of primitive objects and constraints; instead, actions like aligning objects imperatively update positions, which is what the representation stores. CAD tools have support forconstraints [], but they aren’t meant for graphic design. (******************************

Consider a figure with the following description. “There is a light gray canvas. In the center is a blue circle with a diameter that is half the canvas width. Circumscribed in the circle is a red square. ”The picture looks like this:

******************************************

************************************** (Without using constraints) *******************************************, code to generate such a figure in Basalt looks like this:

width=height=(**********************************************************************************************************************************************  bg=Rectangle (0, 0, width, height,     style=Style ({Property.fill: RGB (0xe0e0e0)}))

********************************************** (circ=Circle (x=) , y=(*************************************************************************************************************************************************, radius=(********************************************************************************************************************************************************,    style=Style ({Property.stroke: RGB (0x****************************************************************************************************************************** ff), Property.fill_opacity: 0}))

********************************************** (rect=Rectangle (x=) **************************************************************************************************************************************************** [

border,image] ********************************************************************************************************************************************, y=(*****************************************************************************************************************************************************. (******************************************************************************************************************************************, width=. (**************************************************************************************************************************************************************, height=[List[Element] ,    style=Style ({Property.stroke: RGB (0xff, Property.fill_opacity: 0})) g=Group ([bg, circ, rect]) c=Canvas (g, width=width, height=height)

The designer has to figure out where everything goes, manually computing the anchor points and size. Instead, with constraints (**************************************, the designer can write Down the relationship between the shapes, and the tool will solve the figure: (******************************

width=height=(**********************************************************************************************************************************************  bg=Rectangle (0, 0, width, height, style=Style ({Property.fill: RGB (0xe0e0e0)})) circ=Circle (style=Style ({Property.stroke: RGB (0x********************************************************************************************************************************** ff), Property.fill_opacity: 0})) rect=Rectangle (style=Style ({Property.stroke: RGB (0xff, Property.fill_opacity: 0}))  # The Group constructor takes two arguments: the first is a list of objects, # and the second lists additional constraints, equations that relate the # objects to one another  g=Group ([bg, circ, rect], [  # circle is centered circ.bounds.center==bg.bounds.center, # circle diameter is 1/2 of canvas width 2*circ.radius==width/2, # rectangle is centered on circle rect.bounds.center==circ.bounds.center, # rectangle is a square rect.width==rect.height, # rectangle is circumscribed rect.width==circ.radius*2**0.5])  c=Canvas (g, width=width, height=height)

Every primitive shape knows how to calculate its own bounds based on its internal attributes: for example, a circle’s bounds are to , and a group’s bounds are defined in terms of min / max of the individual elements ’bounds. Attributes, as well as bounds, are allowed to be symbolic expressions: e.g. in the code above, the circle is not given a concrete center or radius, so these attributes are each automatically initialized to a fresh (Variable () (********************************************************, and then the bounds depend on these variables. They are only assigned a concrete value when the constraint solver runs. (******************************

Basalt's constraint-based approach allows the designer tothink in terms of relationshipsand

express those in the specification of the drawing itself, rather than having them be implicit and be manually solved by the designer.

The figure above could feasibly be drawn in Illustrator, though it might require taking out a pencil and paper to calculate positions of objects. Manually solving implicit constraints that are in the designer’s mind but not encoded in the tool, which is what designers currently do with existing programs, is painful and not scalable.

What if the figure design were changed slightly, for example the circle was to be

********************************** in the rectangle ? With Illustrator, it requires recomputing all the positions by hand; With Basalt, the change is one line of code: (****************************** [” or “] ****************
- # rectangle is circumscribed - rect.width==circ.radius * 2 ** 0.5

# rectangle is inscribed rect.width==circ.radius * 2

************************************************ [

# rectangle is centered on circle rect.bounds.center==circ.bounds.center, # rectangle is a square rect.width==rect.height, # rectangle is circumscribed rect.width==circ.radius*2**0.5 ]

****************************************************

For a simple figure, making such a change by hand might be feasible, but what if the figure had hundreds of objects? With Illustrator, it would get out of hand to manually position all the precisely objects where they need to go based on the constraints in the designer’s mind. In Basalt, the constraint solver does the difficult job of determining exact positions for objects, and it scales well (for example, this figure has hundreds of objects). (Supporting abstraction)

Constraints lead to a natural way of supporting abstraction, another key aspect necessary for designing sophisticated figures. Sub-components can be specified in terms of their parts and internal constraints. When these components are instantiated for use in a top-level figure (or a higher-level component), the constraints can simply be merged together.

Suppose we wanted to have an abstraction for a circumscribed square and then have four of them, arranged in a 2×2 grid, that fill the canvas. First, we can define the abstraction, a group consisting of a circle and rectangle, with some internal constraints: [” or “] ****************

def [      # all elements are the same size    same_size([iforrowinrowsforiinrow.elements] **************  circumscribed_square  [” or “]  ():    
circ[” or “] **************************** () Circle) *****************************

(style

=

Style ([aligned_middle(row)forrowinrows] {{         Property[] ****************************

stroke

: (RGB[” or “]

[

List[Element] ff),        Property[] **************************** (fill_opacity [ # all elements are the same size same_size([iforrowinrowsforiinrow.elements]) ****************************

: (0    ))    rect [boxed] [” or “] ****************************

Rectangle

(style

=

Style ([

aligned_middle(row)forrowinrows] {{         Property[] ****************************

stroke

: (RGB[” or “]

0xff [

List[Element]),        Property[] **************************** (fill_opacity [ # all elements are the same size same_size([iforrowinrowsforiinrow.elements]) ****************************

: (0    ))    return

Group (

([

circ,rect],([ # rectangle is centered on circle rect.bounds.center==circ.bounds.center, # rectangle is a square rect.width==rect.height, # rectangle is circumscribed rect.width==circ.radius*2**0.5 ])(************************************** (****************************************************

Then, we can instantiate it multiple times and add the constraint that they are arranged in a 2x2 grid and are all the same size: (****************************** [” or “] ****************

[” or “] c1 [

# all elements are the same size same_size([iforrowinrowsforiinrow.elements] ************** [List[Element] ******************************** [” or “] circumscribed_square()c2

[” or “] ******************************

circumscribed_square

()c3

[” or “] ******************************

circumscribed_square

()c4

[” or “] ******************************

circumscribed_square

()

g[” or “] **************************** Group) *****************************

([c1,c2,c3,c4],[ # arranged like# c1 c2# c3 c4c1.bounds.right_edge==c2.bounds.left_edge, c3.bounds.right_edge==c4.bounds.left_edge, c1.bounds.bottom_edge==c3.bounds.top_edge, # and all the same sizec1.bounds.width==c2.bounds.width, c2.bounds.width==c3.bounds.width, c3.bounds.width==c4.bounds.width,] **************************** (************************************** (****************************************************

Finally, we can express the constraint that the figure fills the canvas:[” or “] ****************

width [

List[Element] ******************************** [” or “] (***************************** height [boxed] [” or “] **************************** (****************************** bg [” or “] [” or “] ****************************

Rectangle

(********************** (0

,[

List[Element] ****************** [List[Element], (************************ (width) ****************************** [border,image] ,height [boxed] [ # all elements are the same size same_size([iforrowinrowsforiinrow.elements], **************************** style

=********************** Style({

(Property)

[ # all elements are the same size same_size([iforrowinrowsforiinrow.elements] ************ ([] fill [” or “] : (RGB) **************************** (

0xe0e0e0 [

aligned_middle(row)forrowinrows] [boxed] ************************************top [boxed] [” or “] **************************** Group) *****************************

([

bg,g],

[

# figure is centeredg.bounds.center==bg.bounds.center, # and fills the canvasg.bounds.height==bg.bounds.height] [List[Element] ******************[” or “] **************************** [aligned_middle(row)forrowinrows] (Canvas) *****************************

(top

,[” or “] width [boxed] ( (************************ (width) ****************************** [

border,image] ,height [boxed] [” or “] **************************** height) *****************************

)(************************************** (****************************************************

When rendered, this code produces the following figure:

****************************************************** [

border,image] ******************** Constraints as an assembly language

Constraints aren’t necessarily what end-users should write directly, but constraints make for a nice assembly language: it’s easy to express higher-level geometric concepts in terms of Basalt's constraints. For example, expressing that a set of objects are top-aligned is as simple as this: (****************************** [” or “] ****************

def [

# all elements are the same size same_size([iforrowinrowsforiinrow.elements] ************** aligned_top [” or “] ([List[Element] ************ (elements) :    top [boxed] [” or “] ****************************

****************************

()************************** # some unknown value

******************** (return) **************************** (Set) ([i.bounds.top==topforiinelements]) (************************************** (****************************************************

Other concepts, like objects being horizontally or vertically distributed, Being the same width or height, or being inset inside another object, can also be compiled down to constraints. Basalt provides some higher-level geometric primitives, and users can define their own. Adversarial turtle figure

Here is a figure I made for a paper(Figure 5), recreated in Basalt:

********************************************************** (******************************

The figure shows a number of images, along with a classification of those images given by the border color, laid out in a grid. The BasaltAdversarial turtle figurecodefor this figure defines two abstractions that are composed to make the figure: (****************************** [” or “] ****************

def [

# all elements are the same size same_size([iforrowinrowsforiinrow.elements] ************** bordered_image [” or “] ([List[Element] ************ (source(************************:

str [List[Element], ****************************** [” or “] border_color(**************************, (border_radius)

:     image

=**************************** (Image [distributed_horizontally(row,spacing)forrowinrows]) ****************************

(source[boxed]     

border

(Rectangle************************ () style [boxed] [” or “] ****************************** ()

******************************({*********************** (Property)

************************** [” or “] fill [

i.bounds.top==topforiinelements] ****************** [ # all elements are the same size same_size([iforrowinrowsforiinrow.elements]:******************** (border_color) ****************************

}))    return

Group (

([border,image],

************************ [

List[Element] ****************************

def [aligned_middle(row)forrowinrows]

grid [

inset(image,border,border_radius,border_radius) ] ****************** (

(

elements************************:List[” or “] ],) ************************ (spacing) ****************************

:    rows

[” or “] **************************** [

# all elements are the same size same_size([iforrowinrowsforiinrow.elements]     return

Group (

(

rows,

[

# all elements are the same size same_size([iforrowinrowsforiinrow.elements],         # elements in rows are distributed horizontally******************** (Set) **************************** ([List[Element]), [border,image]          # and aligned vertically******************** (Set) **************************** ([ # all elements are the same size same_size([iforrowinrowsforiinrow.elements]),         

# rows are distributed vertically

******************** (Set) **************************** ([ # all elements are the same size same_size([iforrowinrowsforiinrow.elements]),        

# and aligned horizontally

aligned_center************************ ()

rows [ # all elements are the same size same_size([iforrowinrowsforiinrow.elements] ********************************     ) [boxed] (************************************** (****************************************************

This figure is simple enough that it would be possible to draw it in a traditional tool like Illustrator, but there are benefits to drawing it programmatically beyond avoiding the annoyance of manually laying out the figure in a GUI-based tool. The paper has 5 figures in this style, with varying images and sizes: using code allowed us to parameterize over these and do the work of designing the figure just once. Furthermore, the figures were meant to go in a paper, a fairly restricted format: for example, the width of our figures was fixed. We needed to decide on figure parameters, such as grid dimensions, border size, and image size, that would make the figures readable, and having the code generate the figures made it easy to explore this parameter space. (******************************

The original figures in the paper were actually made without Basalt, because Basalt didn’t exist at the time. Instead, I wrote

Python codethat directly painted pixels of an output image. The Basalt codeis much nicer. (Robust ML logo) *******************************************

My brother [boxed] ********************************** Ashay **************************** and I designed the logo forRobust MLStarting with the general ideas of shields and neural networks, Ashay drew a couple sketches on paper: (******************************

**************************************************************** () ******************************

Next, I sketched the logo in Illustrator. Even after we had the basic idea for the logo, it took a ton of iteration to figure out the details, including choosing the number of layers in the neural network, the number of nodes in each layer, and the spacing between the nodes in a given layer. Certain changes were easy, like tweaking colors using Illustrator’s recolor artwork feature. Other changes were extremely painful; for example, adding a node required a lot of manual labor, because it required moving moving nodes and lines as well as creating and positioning a new node and many new lines. Cumulatively, over dozens of iterations of the logo, I spent a couple hours just moving shapes around in Illustrator.

Since then, I have re-made the logo with Basalt, where exploring a parameter space is much easier with the help of the live preview tool. (******************************

Here is what the final output looks like: (******************************

******************************************************************

Notary architecture figure (******************************************

This is a figure made inTikZ

, from a draft of a recent paper: (******************************

Circumscribed square

Here's theNotary architecture figure in TikZcodefor the TikZ figure above. It uses lots of hard-coded positions, with commands like draw [boxed] (-1, -2.5) rectangle ( (5,4)

. It was difficult to get the figure to look particularly pretty using TikZ. For the published version of the

Circumscribed square

The figure defines and makes use of a number of new abstractions built on top of Basalt's primitives, including: (******************************

  • (Component) - a box, with text centered inside
  • What do you think?

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    GIPHY App Key not set. Please check settings

    Facebook Stock Got Hammered Because the FTC Plans App Crackdown, Crypto Coins News

    Facebook Stock Got Hammered Because the FTC Plans App Crackdown, Crypto Coins News

    jeremycw / httpserver.h, Hacker News

    jeremycw / httpserver.h, Hacker News