CLUI is a constraint-based 2D graphics library for Common Lisp, built atop OpenGL.
In CLUI, we define shape-classes and then define shape-instances based off those classes. Here, I define a shape-class made of a square and an image.
(defshape
(shape-from-defshape 'basic-square :x -50 :y 0 :width 100 :colour '(0 0 1 1))
(shape-from-defshape 'basic-image :x 50 :y 0 :image-path "assets/images/lispLogo.png")
:name "example-shape")
And then I define an instance of it, naming the instance ‘my-shape.
(shape-instance 'example-shape :x 400 :y 300 :instance-name 'my-shape)
I’ve set the x and y position of the shape to be 400 pixels to the right and 300 up. The values passed as shape-properties can be dynamic, as seen here when I set the x and y to match the mouse’s x and y.
(shape-instance 'example-shape :x (mouse-x) :y (mouse-y) :instance-name 'my-shape)
The shape’s position is dynamically updated to match the mouse’s position, even though we never passed a function as the value of the shape’s positional properties. The code passed can be procedurally inspected and transformed into a function, as is the case with this shape, which then becomes the property of that shape.
I’ve changed the properties of the shape-instance while the program is running, though I can also modify the definition of a class during runtime. Here I change the square’s default colour and set the default rotation of the image to be based off the current time.
(defshape (shape-from-defshape 'basic-square :x -50 :y 0 :width 100 :colour '(1 0 1 1)) (shape-from-defshape 'basic-image :x 50 :y 0 :image-path "assets/images/lispLogo.png" :rot (* 100 (get-real-time-seconds))) :name "example-shape")
Shapes are closed under the operation of shape definition. This means you can define shapes out of previously defined shapes.
(defshape
(shape-from-defshape 'example-shape :x 0 :y -100)
(shape-from-defshape 'example-shape :x 0 :y 100)
:name "example-combi-shape")
(shape-instance 'example-combi-shape :x (mouse-x) :y (mouse-y) :instance-name 'my-shape)
I defined ‘example-combi-shape as two ‘example-shapes stacked atop each other, and then I defined the shape-instance with the instance-name ‘my-shape, reusing the name, as an instance of ‘example-combi-shape. This redefines the instance ‘my-shape to be of a different class, during runtime.
When you instance a shape, you’re actually storing the code of that shape’s definition in a hash-table which gets looked up and evaluated dynamically. Each property is stored as code, and by doing this, dynamic properties can be re-evaluated if needed to match a changing environment. It’s delayed evaluation. Most languages, Common Lisp included, use applicative-order evaluation, wherein the arguments to a function are evaluated before the function is applied to those arguments. I’ve setup code to dynamically create and store lambda-expressions based off the supplied code for shape-instance definitions, using macros. By doing this, I can store the code passed to calculate the return-value of a property, instead of having the language calculate the return-value at the time of shape-definition and return that one static value. This is why I can define a shape’s position as the mouse’s x or y position, and it automatically updates itself to match the current mouse’s position, instead of setting itself to be what the mouse’s position was at the time of definition.
Anyway.
With an instance of ‘example-combi-shape in existence, I’ll redefine ‘example-shape’s default rotation for the square.
(defshape
(shape-from-defshape 'basic-square :x -50 :y 0 :width 100 :colour '(1 0 1 1) :rot (* -100 (get-real-time-seconds)))
(shape-from-defshape 'basic-image :x 50 :y 0 :image-path "assets/images/lispLogo.png" :rot (* 100 (get-real-time-seconds)))
:name "example-shape")
As soon as the change to ‘example-shape’s class definition is changed, the change propagates to all classes that inherit from it, and all instances update to use the new definition in realtime, without restarting or re-instantiating anything. This is another advantage of storing definitions in lambda-expressions, when the definition updates, everything referencing it doesn’t have to change data within itself, since it doesn’t have a copy of the data but rather just a reference to the stored definition.
Shapes can have their properties defined in terms of other shapes’ properties.
Here I define a circle whose radius is proportionate to my-shape’s y position, and whose x-position is the inverse of my-shape’s x.
(shape-instance 'basic-circle :x (- *window-width* (get-instance-property 'my-shape :x)) :y (half *window-height*) :radius (change-range (get-instance-property 'my-shape :y) 0 *window-height* 10 500))
Since the code of any given shape’s definition is stored, we can reference it in other shape definitions. I’ve also setup a serial-property-list form, that lets you define a shape’s properties in terms of itself, as long as you only refer to properties if they’ve been previously defined in that list. As an example, you can define a shape’s x position as the mouse’s x position, then it’s y position as it’s x position. On shape-definition, I create a lexical scope wherein I bind all the properties of the shape in a serial let* form, then I transform the given code to sit inside that scope.
To really drive home that shapes can be dynamically defined in terms of other shapes, this “default-menu” shape instances and positions the four rects inside itself, whose widths and heights are tied to the sin and cos of the current-time. The menu-shape uses the width/height information of the rects to calculate their positions, sets those positions and then defines its own dimensions by interpreting the definitions of the rects.
Here are a few shape classes that come with CLUI, though you can define your own:
- Circle
- Square
- Rect
- Image
- Line
- Bezier-Line
- Button
- Textbox
- Checkbox
- Dialog-box
- Progress-bar
Since CLUI is dynamic, things like textboxes can adjust themselves automatically. This textbox’s width and height parameters were tied to the mouse’s position.
Common Lisp is made primarily of two things: atoms and cons-cells. You can combine two atoms into a cons-cell by calling #’cons on them. Consing a cons with another atom (or cons cell) lets you make lists. Lisp itself is short for LISt Processing. All useful Lisp code is made of lists, thus a Lisp program is simply a list. This is very, very useful.
I setup a way to parse the contents of a textbox into a list of symbols that I then transform into a lisp-expression, that can be evaluated. It was a bit of a hassle to keep tabbing back-and-forth between Emacs and the CLUI window to run small bits of code, so I setup textboxes to function as consoles that evaluate the given text as Lisp code in the global scope, when I press a certain key-combination.
Here’s a quick demonstration.
I’ve also setup textboxes to be able to evaluate malformed code, and return the appropriate error as text in the textbox, instead of halting the application.
An application made with CLUI can manually or procedurally define and modify classes and their definitions, from within the application, during runtime. Not only that, but all of Common Lisp is exposed to the end-user, and thus all of CLUI’s code. You can incrementally redefine, extend and recompile CLUI as it’s running from within itself.