Skip to content
forked from vydd/sketch

This is my experimental fork of Sketch, a Common Lisp graphics framework by vydd.

License

Notifications You must be signed in to change notification settings

Kevinpgalligan/sketch

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sketch (Kevin’s fork)

This is an experimental fork of Sketch, the Common Lisp graphics framework by vydd. Not all of the features have been extensively tested, and the API may be subject to change. The additions so far include:

  • Bugfix: Polyline is janky for sharp angles (vydd#53).
  • Feature: defsketch now generates a function (run-sketchname &rest args) for running sketches. You can also do (run-sketch 'sketchname).
  • Bugfix: Vertices are batched when drawing large shapes (vydd#160).
  • Feature: Adds a perlin noise interface. You can call (noise &rest coords) inside the defsketch body to get a noise value. (noise-seed seed) sets the seed, and (noise-detail &key lod falloff) configures the character of the noise (based on the p5js noise API).
  • Feature: Mixin classes for sharing behaviour between sketches.
  • Feature: Lots of enhancements for the canvas data structure.
    • (canvas-paint-gray255 canvas x y amount) and (canvas-paint-rgba255 canvas x y r g b a) for more efficient painting.
    • (canvas-get-pixel canvas x y) gives you (values r g b a).
    • (with-drawing-to-canvas (canvas) &body body) allows you to draw directly on a canvas, rather than just painting individual pixels.
    • (make-canvas-from-image path &key x y w h) loads a canvas based on an image; all the keyword parameters must be supplied if you want to load a cropped part of the image.
  • Bugfix: Use FBO for drawing with copy-pixels, prevents flickering on some systems. This change was mostly developed by Gleefre. (It leads to visual artifacts on other systems, like the boundaries of triangles not being drawn correctly, so we probably need a flag to let the user pick how to handle drawing for copy-pixels). (vydd#75).
  • Feature: Record a video of a sketch using (record-sketch name output-path &key args frames seconds close-on-finish (fps 60) &allow-other-keys), requires ffmpeg. Most params are self-explanatory: name is the name of your sketch, as a symbol; args is a list of extra input arguments to (run-sketch ...); frames and seconds determine how long to record for before saving the video; close-on-finish determines whether to close the window after the video has been saved.
  • Feature: Improved interactive development. Adding the binding (restart-on-change t) to your sketch definition now means that the state of the sketch won’t be reset when you recompile the defsketch form. If you still want to tweak a particular parameter, then add the :tweakable t flag to it. Also, the config parameter (restart-on :key-here) lets you set a key binding to manually reset the sketch state; by default, it’s set to F1. For example, the example sketch below shows a circle moving across the screen. You can change the colour and radius parameters, recompile the defsketch, and the colour/radius of the circle will change without its position being reset. You can press the spacebar to manually trigger a reset.
(defsketch tweak
    ((x (truncate width 2))
        (y (truncate height 2))
        (colour +red+ :tweakable t)
        (radius 10 :tweakable t)
        (restart-on-change nil)
        (restart-on :space))
  (with-pen (make-pen :fill colour)
    (circle x y radius))
  (incf x (random 5))
  (incf y (random 5)))

Original README

http://quickdocs.org/badge/sketch.svg

Sketch is a Common Lisp environment for the creation of electronic art, visual design, game prototyping, game making, computer graphics, exploration of human-computer interaction and more. It is inspired by Processing Language and shares some of the API.

http://i.imgur.com/MNZUwz8.png

Installation

Sketch is available through Quicklisp, Common Lisp’s de facto package manager. From your REPL, run:

(ql:quickload :sketch)

To make Sketch run correctly, however, a few requirements must be met.

Requirements

Common Lisp Implementation

Sketch should be compatible with all major Common Lisp implementations and all major operating systems - more specifically, all CL implementations and operating systems that cl-sdl2 runs on. Incompatibility with any of these is considered a bug.

Sketch is known to work with:

  • CCL 1.11 on Mac OS X El Capitan
  • CCL 1.12.1 on MacOS 13.1 (steps)
  • CCL SVN 1.12.dev.r16617 on Arch Linux
  • CCL 1.11 on Windows 10 64bit
  • SBCL on Debian Unstable
  • SBCL 1.2.16 on Arch Linux
  • SBCL 1.3.1 on Linux Mint 17
  • SBCL 1.3.6 on Windows 10 64bit

Workarounds, or extra steps, may be required on some systems:

  • Arch Linux, issue.
  • OpenSuse, issue.
  • CCL on OSX: Make sure to use the 64-bit version of CCL (issue).
  • Quickload fails with libffi error, issue.

Sketch is known to not work with:

  • SBCL 1.2.15 on Mac OS X

Sketch can’t handle input and the window’s titlebar is black. These kinds of issues are a known problem with Mac OS X, because it needs its GUI threads to be main, and CCL is the only lisp that accounts for that out of the box. There are ways to counter this, but until a solution finds its way into this repository, SBCL on Mac OS X will stay on this list. In the meantime, use CCL.

If you test Sketch on other systems, please send a pull request to include your results.

Foreign dependencies

SDL2

SDL2 is currently Sketch’s only backend. It is a C library which you will need to download manually from libsdl webpage. Select the release compatible with your operating system, or compile from the source code.

SDL2 Image & SDL2 TTF

For loading image and font files, Sketch relies on SDL2 Image and SDL2 TTF, respectively, both part of the SDL project.

libffi

Some users have reported that libffi needed to be installed to make Sketch work.

OpenGL

Sketch requires graphics hardware and drivers with support for GL version 3.3.

Installing and running Sketch on Windows

Sketch works on both CCL and SBCL, but installing all prerequisites might not be as straightforward as it is on the other platforms.

Libraries

Download SDL2, SDL2_IMAGE and SDL2_TTF dlls from libsdl webpage and copy them somewhere Windows can find them - \Windows\System32 will work. When copying SDL2_TTF, make sure to copy all of the dlls provided in the archive, and not just the TTF one.

Now you will need to get a libffi dll. One of the ways of doing this is compiling from the source, but for a quick and easy solution, you can just find a trusted source and use their version. For example, if you are using Emacs on Windows, you can find libffi-6.dll in emacs\bin. Copy it to the same directory you copied sdl2 dlls to earlier.

GCC

To bootstrap cffi-libffi, you are going to need a C compiler, more specifically the one from the GNU Compiler Collection. Also, libffi headers and pkg-config are needed. Luckily, you can get all these things (and more) with MSYS2. Go to https://msys2.github.io and follow the instructions for installing the 64bit version.

From its console, install gcc, libffi headers and pkg-config by running pacman -S gcc libffi libffi-devel pkg-config.

Environment variables

From the Control Panel, open System properties, go to the Advanced tab, and click “Environment Variables…” - or click the Start button, start typing “environment” and select “Edit the system environment variables”.

Double click “Path” from the list of System variables and make sure that both your lisp implementation’s path (something like C:\Program Files\Steel Bank Common Lisp\1.3.6\) and MSYS path (probably C:\msys64\usr\bin) are listed. If not, click “New” and add them now.

If you needed to change anything here, restart the computer now.

SLIME

If you are using SLIME, you won’t be able to load or run Sketch if you start SWANK from emacs (by running M-x slime). Instead, you should open the Command Prompt (the regular one, not MSYS), start your lisp and eval (ql:quickload :swank) (swank:create-server). From Emacs, type M-x slime-connect, and finally, press enter twice (for localhost and port 4005).

If you did everything correctly, you should be able to (ql:quickload :sketch) and move on to the tutorial.

Running provided examples

To get a feel for what Sketch can do, and also to make sure that everything has been installed correctly, run the examples as follows.

CL-USER> (ql:quickload :sketch-examples)
CL-USER> (in-package sketch-examples)
SKETCH-EXAMPLES> (run-hello-world)
SKETCH-EXAMPLES> (run-sinewave)
SKETCH-EXAMPLES> (run-brownian)
SKETCH-EXAMPLES> (run-life) ; Click to toggle cells,
                            ; any key to toggle iteration
SKETCH-EXAMPLES> (run-input)
SKETCH-EXAMPLES> (run-stars)

Running example code from this page

In all the following examples, we’re going to assume that Sketch is loaded with (ql:quickload :sketch), and that we’re in package :TUTORIAL, which is set to use :SKETCH.

CL-USER> (ql:quickload :sketch)
CL-USER> (defpackage :tutorial (:use :cl :sketch))
CL-USER> (in-package :tutorial)
TUTORIAL> ;; ready

Tutorial

Defining sketches is done with the defsketch macro. This defines a function called `run-NAMEHERE` for starting your sketch. Alternatively, you can do `(run-sketch ‘NAMEHERE)`.

(defsketch tutorial ())
(run-tutorial)

If all goes well, this should give you an unremarkable gray window. From now on, assuming you’re using Emacs + SLIME, or a similarly capable environment, you can just re-evaluate (defsketch tutorial () <insert drawing code here>) and the sketch will be restarted without you having to close the window or make another instance of the class.

Shapes

Let’s draw something! Drawing code goes inside the body of defsketch.

(rect x y w h) draws a rectangle where x and y specify the top-left corner of the rectangle, and w and h are the width and height. By default, the origin (0, 0) is at the top-left corner of the drawing area, and the positive y direction is facing down.

(defsketch tutorial ()
  (rect 100 100 200 200))
(defsketch tutorial ()
  (dotimes (i 10)
    (rect 0 (* i 40) (* (+ i 1) 40) 40)))

Something to note: drawing code doesn’t need to go into a special function or method, or be explicitly binded to a sketch. defsketch is defined as (defsketch sketch-name bindings &body body): that body, and any functions it calls to, is your drawing code. We will get to bindings later.

Circles and ellipses are drawn with (circle x y r) and (ellipse cx cy rx ry):

(defsketch tutorial ()
  (circle 300 100 50)
  (ellipse 200 200 100 50))

Lines with (line x1 y1 x2 y2):

(defsketch tutorial ()
  (line 0 0 400 400)
  (line 400 0 0 400))

Lines with an arbitrary number of segments with polyline:

(defsketch tutorial ()
  (polyline 100 100 200 150 300 100
            200 200 100 100))

Arbitrary polygons can be drawn using (polygon x1 y1 x2 y2 ...), the winding rule (how the “inside parts” and “outside parts” are determined) is specified as a pen property (pens will be described in more detail later) and can be one of (:odd :nonzero :positive :negative :abs-geq-two). By default, it’s :nonzero.

(defsketch tutorial ()
  (with-pen (make-pen :fill +blue+ :winding-rule :odd)
    (polygon 100 100 200 150 300 100 200 200)))

To draw a regular polygon with n sides, call (ngon n cx cy rx ry &optional (angle 0)); cx and cy are the coordinates of the center of the shape, while rx and ry are height of an ellipse that the shape is inscribed inside.

(defsketch tutorial ()
  (dotimes (i 4)
    (ngon (+ i 3) (+ 50 (* i 100)) 200 20 20 (* i 20))))

Bezier curves with 4 control points are drawn with (bezier x1 y1 bx1 by1 bx2 by2 x2 y2); x1, y1, x2 and y2 determine the start and end points.

(defsketch tutorial ()
  (bezier 0 400 100 100 300 100 400 400))

The resolution of a curve can be controlled with the pen property :curve-steps, for example:

(defsketch tutorial ()
  (with-pen (make-pen :curve-steps 4 :stroke +white+)
    (bezier 0 400 100 100 300 100 400 400)))

Configuring your sketch

The first form in defsketch after the name of your sketch, and before the body, is a list of bindings that will be available in the sketch body. This is also where a number of configuration options can be set:

  • title (string): window title.
  • width and height (in pixels): window dimensions, 400 x 400 by default.
  • fullscreen (t or nil): whether window is fullscreen.
  • resizable (t or nil): whether window is resizable.
  • copy-pixels (t or nil): if true, the screen is not cleared before each drawing loop.
  • y-axis (:down or :up): :down by default. Determines both the location of the origin and the positive direction of the y-axis. :down means (0,0) is in the top-left corner and greater values of y move down the screen. :up means (0,0) is in the bottom-left corner and greater y values go up.
  • close-on (a keyword symbol denoting a key, or nil to disable): a shortcut for closing the sketch window, :escape by default. Set to nil to disable. The key names (e.g. :space, :g) are based on SDL2 scancodes, see here.
(defsketch tutorial
    ((radius 10)
     (resizable t)
     (width 200))
  (circle (/ width 2) (/ radius 2) radius))

Colors

In the previous examples, you may have noticed how to draw a shape with a fill color. Let’s now explore the color capabilities of Sketch in more detail. To draw a yellow background:

(defsketch tutorial ()
  (background +yellow+))

Predefined colors

There are constants for commonly used colors: +RED+, +GREEN+, +BLUE+, +YELLOW+, +MAGENTA+, +CYAN+, +ORANGE+ +WHITE+, and +BLACK+.

RGB, HSB, GRAY

You can create other colors using (rgb red green blue &optional (alpha 1.0)), (hsb hue saturation brightness &optional (alpha 1.0)) or (gray amount &optional (alpha 1.0)). The arguments to these functions are values from 0 to 1. (gray amount &optional (alpha 1.0)) is really just a convenient alias for (rgb amount amount amount &optional (alpha 1.0)).

More information:

This might be a good place to note that function names in Sketch use the American English spellings, like “gray” and “color”. It’s just a choice that needed to be made, in pursuit of uniformity and good style.

For a lighter yellow:

(defsketch tutorial ()
  (background (rgb 1 1 0.5)))

All color functions have an additional ALPHA parameter that determines the transparency.

RGB-255, HSB-360, GRAY-255

Sometimes it’s easier to think about color values in non-normalized ranges. That’s why Sketch offers RGB-255, HSB-360, and GRAY-255.

This is how these functions map to their normalized variants.

(rgb-255 r g b a)(rgb (/ r 255) (/ g 255) (/ b 255) (/ a 255))
(hsb-360 h s b a)(hsb (/ h 360) (/ s 100) (/ b 100) (/ a 255))
(gray-255 g a)(gray (/ g 255) (/ a 255))

HSB-360 uses different ranges, because hue is represented in degrees (0-360), and saturation and brightness are represented as percentages (0-100).

HEX-TO-COLOR

If you are used to working with colors in hex, like in CSS, you can use (hex-to-color string), where STRING is the color in one of the following formats: “4bc”, “#4bc”, “4bcdef”, or “#4bcdef”.

Generating colors

If you don’t care about fiddling with the exact values, but still need different colors, you can use one of the following functions.

(lerp-color (start-color end-color amount &key (mode :hsb)))

Lerp is a shorthand for linear interpolation. This function takes the starting color and the ending color, and returns the color between them, which is an AMOUNT away from the starting color. When AMOUNT equals zero, the returned color equals the starting color, and when AMOUNT equals one, the ending color is returned. Amounts between zero and one give colors that are “in-between”. These colors are calculated according to the specified MODE, which is :HSB by default, meaning that the resulting color’s hue is between the starting and ending hue, as is the case with its saturation and brightness. Any other keyword will result in RGB interpolation.

(defsketch lerp-test ((title "lerp-color") (width 400) (height 100))
  (dotimes (i 4)
    (with-pen (make-pen :fill (lerp-color +red+ +yellow+ (/ i 4)))
      (rect (* i 100) 0 100 100))))
(random-color (&optional (alpha 1.0)))

Returns a random color. You probably don’t want to use this, because many of the returned colors will be either too dark, or too light. You do get to choose the ALPHA value, though.

(defparameter *colors* (loop for i below 16 collect (random-color)))

(defsketch random-color-test ((title "random-color") (width 400) (height 100))
  (dotimes (x 8)
    (dotimes (y 2)
      (with-pen (make-pen :fill (elt *colors* (+ x (* y 8))))
        (rect (* x 50) (* y 50) 50 50)))))
(hash-color (n &optional (alpha 1.0)))

This is probably the function you’re looking for, if you just want to create a non-repeating set of colors quickly. It maps all numbers to “interesting” (not too dark, not too light) colors. You can use this for coloring procedurally generated objects, when prototyping and just trying to make things look different quickly, when making palettes, looking for “the right” color, and many other things.

(defsketch hash-color-test ((title "hash-color") (width 400) (height 100))
  (dotimes (i 128)
    (with-pen (make-pen :fill (hash-color i))
      (rect (* i (/ 400 128)) 0 (/ 400 128) 100))))

Color filters

Sometimes you have a color, and would like to transform it in some way. That’s what color filters are for.

Grayscale

To convert colors to grayscale, you can use color-filter-grayscale. Two modes of grayscale conversion are implemented:

  • :luminosity, the default, which is luminance-preserving
  • :average, which sets all color channels to their average
(defsketch grayscale-test ((title "grayscale") (width 400) (height 300))
  (dotimes (i 10)
    (let ((color (hash-color i)))
      (with-pen (make-pen :fill (color-filter-grayscale color))
        (rect (* i 40) 0 40 100))
      (with-pen (make-pen :fill color)
        (rect (* i 40) 100 40 100))
      (with-pen (make-pen :fill (color-filter-grayscale color :average))
        (rect (* i 40) 200 40 100)))))
Invert

To invert a color, use color-filter-invert:

(defsketch invert-test
    ((title "invert") (width 300) (height 300) (i 0))
  (background +white+)
  (incf i 0.01)
  (let ((color (rgb (abs (sin i)) (abs (cos i)) 0)))
    (with-pen (make-pen :fill color)
      (circle 100 150 50))
    (with-pen (make-pen :fill (color-filter-invert color))
      (circle 200 150 50))))
Rotate

Rotating a color in Sketch using color-filter-rotate sets the value of its red channel to the previous value of the green channel; green to blue, and blue to red. The operation is intended to be used in palette generation, because the rotated colors usually work pretty well together.

(defsketch rotate-test
    ((title "rotate") (width 300) (height 300)
     (i 0) (color (rgb 0.2 0.8 1.0)))
  (background +white+)
  (incf i 1)
  (when (zerop (mod i 60))
    (setf color (color-filter-rotate color)))
  (with-pen (make-pen :fill color)
    (rect 100 100 100 100)))
HSB

HSB stands for Hue/Saturation/Brightness. You can use color-filter-hsb to adjust hue, saturation and brightness of an existing color.

(defsketch hsb-test
    ((title "hsb") (width 400) (height 300) (color (rgb 0.2 0.5 0.6)))
  (dotimes (i 4)
    (with-pen (make-pen :fill (color-filter-hsb color :hue (* 0.1 (+ i 1))))
      (rect (* i 100) 0 100 100))
    (with-pen (make-pen :fill (color-filter-hsb color :saturation (* 0.1 (+ i 1))))
      (rect (* i 100) 100 100 100))
    (with-pen (make-pen :fill (color-filter-hsb color :brightness (* 0.1 (+ i 1))))
      (rect (* i 100) 200 100 100))))

Pens

Pens are used to draw shapes. If no pen is specified, the default pen sets :fill to white, :stroke to black, and weight to 1.

Creating and Using Pens

Say you want to draw a red square and a blue circle. You would need to use two different pens.

(defsketch pen-test
    ((title "pens"))
  (with-pen (make-pen :fill +red+)
    (rect 100 100 100 100)) ; this rect will be red
  (with-pen (make-pen :fill +blue+)
    (circle 315 315 50))) ; this rect will be blue
Fill/Stroke

The squares in the previous example were filled because we specified the :fill property in make-pen. If we wanted to just draw the outline of the square, we would use :stroke like this:

(defsketch outline-square
    ((title "Outline Square"))
  (with-pen (make-pen :stroke +red+)
    (rect 100 100 100 100)))
(defsketch fill-stroke
    ((title "Fill and Stroke"))
  (background +white+)
  (with-pen (make-pen :stroke (rgb .5 0 .6) :fill (rgb 0 .8 .8))
    (rect 50 50 100 75)
    (circle 300 220 100)))
Weight

We can also change the thickness of the lines and shapes that we draw by changing the pen :weight.

(defsketch weight-test
    ((title "Weight Test"))
  (dotimes (i 10)
    (with-pen (make-pen :stroke +white+ :weight (+ i 1)) ; pen weight can't be zero
      (line 50 (* i 20) 350 (* i 20)))))

Curve-steps

:curve-steps is used to change the smoothness (resolution) of curves like #'bezier.

(defsketch curve-test
   ((title "Curve-steps"))
  (dotimes (i 99)
    (with-pen (make-pen :stroke +red+ :curve-steps (+ i 1)) ; as curve-step increases, curve becomes "smoother"
      (bezier 0 400 100 100 300 100 400 400))))

Transforms

The transforms (translate dx dy), (rotate angle &optional (cx 0) (cy 0)) and (scale sx &optional sy (cx 0) (cy 0)) are available to change the view matrix that is applied to coordinates.

Macros (with-translate (dx dy) &body body), (with-rotate (angle &optional (cx 0) (cy 0)) &body body) and (with-scale (sx &optional sy (cx 0) (cy 0)) &body body) can be used to restore the view matrix after executing the body.

The current view can also be saved on a stack and restored with (push-matrix) and (pop-matrix), which are analogous to push() and pop() in p5.js. The macro (with-identity-matrix &body body) pushes the current view matrix onto the stack, sets the view matrix to the identity matrix, executes body, and then pops the view matrix. (with-current-matrix &body body) is the same, except it doesn’t change the view matrix after pushing it.

In this example, translation and rotation are used to draw a triangle in the centre of the screen, without explicitly defining the coordinates of the vertices.

(defsketch transform-test
    ((title "Transform test")
     (width 500)
     (height 500)
     (side 100)
     (y-offset (/ side (* 2 (tan (radians 60))))))
  (with-translate (250 250)
    (loop repeat 3
          do (line (- (* 1/2 side)) y-offset  (* 1/2 side) y-offset)
          do (rotate 120))))

This example draws a sequence of increasingly shrinking squares using scaling.

(defsketch transform-test
    ((width 400)
     (height 400)
     (title "Scale test"))
 (translate 100 100)
 (dotimes (x 5)
   (rect 0 0 100 100)
   (translate 150 0)
   (scale 1/2)))

Text

Use (text text-string x y &optional width height) to draw text, where x and y specify the top-left corner of the rectangle containing the text. width and height control the shape of the text box. There is support for changing the font.

(defsketch text-test
   ((title "Hello, world!"))
 (text title 0 0 100))

The font can be specified using (make-font &key face color size line-height align) and the with-font macro.

(defsketch text-test
   ((title (format nil "Hello, world!~%Next line"))
 (with-font (make-font :color +white+
                       :face (load-resource "/path/to/font.ttf")
                       :size 12
                       :line-height 1
                       :align :left)
   (text title 0 0 100)))

align can be :left, :center or :right, and determines whether the x & y coordinates correspond to the left end, centre, or right end of the text box. line-height determines the vertical space given to a line of text, scaled according to the font size, i.e. :line-height 1 leaves just enough space so that the text on two lines won’t overlap.

Images

First (load-resource filename ...) to load the image from a given file, then (draw image &key x y width height) to draw the image with its top-left corner at (x, y) and with the given width and height. If not provided, default (x,y) is (0,0) and width & height are taken from the image.

(defsketch image-test
   ((title "Hello, image!")
    (pic (load-resource "/path/to/img.png")))
  (draw pic :x 10 :y 10 :width 200 :height 200))

Note that load-resource automatically caches the resource when it is called inside a valid sketch environment (i.e. inside the defsketch’s body), so it is not inefficient to call it in every loop. It is important to release resources using sketch::free-resource; this is done automatically for resources in the sketch environment when the sketch window is closed. Finally, to avoid caching and to reload the resource every time, the parameter :force-reload-p can be passed to load-resource.

Images can be cropped using (crop image x y w h), where x and y indicate the top-left corner of the cropping rectangle (relative to the top-left corner of the image) and w and h indicate the width & height. Image flipping can be accomplished by using negative w and h values.

Input

Input is handled by defining implementations of the methods listed below. Currently, it is not possible to call drawing functions from these methods, though this can be worked around by saving the input somewhere and then doing the drawing from the sketch body, as demonstrated in the examples to follow.

  • (on-click instance x y), (on-middle-click x y) and (on-right-click x y) are called when there’s a left, middle or right click. x and y give the coordinates of the click.
  • (on-mouse-button button state x y) is called for left, middle and right mousebutton interactions. button can be one of :left, :middle and :right. state can be either :up or :down.
  • Depending on the value of button, this propagates to one of: (on-mouse-left state x y), (on-mouse-middle state x y), or (on-mouse-right state x y).
  • These methods, in turn, propagate to (on-mouse-left-up x y), (on-mouse-right-down x y), (on-mouse-right-down x y), …
  • (on-hover instance x y) is called when the mouse moves, x and y give its coordinates.
  • (on-text instance text) is called when a single character is entered, text is a string consisting of just this character.
  • (on-key instance key state) is called when a key is pressed. key is a keyword symbol denoting which key was pressed/released (like :space or :left; for now, the names are based on SDL2 scancodes, see here for the full list), and state is a keyword symbol denoting whether the key was pressed (:up) or released (:down).

In this example, we draw a new rectangle every time there is a click.

(defsketch input-test
   ((title "Hello, input")
    (rectangles nil))
 (loop for (x y) in rectangles
       do (rect x y 50 50)))
(defmethod on-click ((window input-test) x y)
  (with-slots (rectangles) window
    (push (list x y) rectangles)))

In this example, all keyboard text input is echoed to the screen.

(defsketch text-test
   ((title "Hello, input")
    (text-to-write nil))
  (loop for s in text-to-write
        do (text s 0 0 20 20)
        do (translate 20 0)))
(defmethod on-text ((window text-test) text)
  (with-slots (text-to-write) window
    (setf text-to-write (nconc text-to-write (list text)))))

Finally, here is an example where a pair of eyes follow the mouse (the pupils are restricted to a rectangle, it would look better if they were restricted to a circle).

(defsketch hover-test
   ((looking-at (list 0 0))
    (cx (/ width 2))
    (cy (/ height 2)))
  (let ((cx-1 (- cx 50))
        (cx-2 (+ cx 50))
        (mx (car looking-at))
        (my (cadr looking-at)))
    (with-pen (make-pen :fill +white+)
      (ellipse cx-1 cy 40 80)
      (ellipse cx-2 cy 40 80))
    (with-pen (make-pen :fill +black+)
      (flet ((move-towards (x1 x2)
               (let ((diff (- x2 x1)))
                 (+ x1 (if (< (abs diff) 10)
                           diff
                           (* (signum diff) 10))))))
        (circle (move-towards cx-1 mx) (move-towards cy my) 10)
        (circle (move-towards cx-2 mx) (move-towards cy my) 10)))))
(defmethod on-hover ((window hover-test) x y)
  (with-slots (looking-at) window
    (setf (car looking-at) x
          (cadr looking-at) y)))

See also: life.lisp.

Setup

The generic function (setup instance &key &allow-other-keys) is a hook that gets called once on every “restart” of the sketch. That is:

  • before the drawing code in the sketch body is called for the first time.
  • whenever the sketch is redefined.
  • every time an error occurs.

Note that any drawing that takes place within setup will be immediately covered by a gray background, unless (copy-pixels t) is added to defsketch.

Here is an example usage of setup from brownian.lisp.

(defmethod setup ((instance brownian) &key &allow-other-keys)
  (background (gray 1)))

Saving a picture

(save-png pathname) can be called within the body of defsketch to save a PNG of the currently running sketch. A keyboard shortcut could be set up to take screenshots, as follows.

(defsketch save-test
   ((should-save nil)
    (copy-pixels t))
  (rect (random width) (random height) 10 10)
  (when should-save
    (setf should-save nil)
    (save-png "/tmp/my-sketch.png")))
(defmethod on-text ((window save-test) text)
  (when (string= text "s")
    (setf (slot-value window 'should-save) t)))

Drawing with a canvas

(make-canvas width height) can be used to create a rectangular grid of pixels. The shape of the grid is defined by width and height.

(canvas-paint canvas color x y) sets the color of a pixel within the grid.

(canvas-lock canvas) freezes the appearance of the canvas. Any calls to (canvas-image canvas) will show an image of the canvas when canvas-lock was last called.

(canvas-unlock canvas) allows the image of the canvas to be modified again.

(draw canvas &key (x 0) (y 0) (width nil) (height nil) draws the canvas; by default, the original width and height of the canvas are used, but these can be overridden.

Example: stars.lisp.

Control flow

(stop-loop) from within a sketch body or within an event handler to disable the drawing loop.

(start-loop) to start the drawing loop again.

This can be used, for example, to draw a static sketch and then disable the drawing loop so as to not burn up your CPU. It can also be used to regenerate the sketch with each mouseclick.

Example: control-flow.lisp.

Noise

(noise &rest coords) generates a correlated random value in the range 0-1. Similar inputs give similar outputs, hence the term “correlated”. This can be useful for producing natural-looking textures and terrain, among other things. See Gradient noise.

(noise-seed seed) seeds the noise so that its output is reproducible, where `seed` is an integer.

(noise-detail &key lod falloff) determines the ‘character’ of the noise, based on the noiseDetail() function from p5js.

  • lod stands for Level Of Detail. It should be a positive integer value (default is 4), and determines how many noise values are generated and summed together to produce the final value. Lower values -> smoother and more efficient, higher values -> greater level of detail.
  • falloff should be between 0 and 1; its default value is 0.5. Each successive noise value in the sum is weighted by increasing powers of the falloff. A falloff of greater than 0.5 will possibly result in noise outputs of greater than 1.
  • Another way to change the appearance of noise is to scale the inputs. Making the inputs smaller and closer together will make the output smoother. Scaling them up will have the opposite effect. The output value can also be scaled, obviously so that it goes outside the range 0-1.

Example: noise.lisp.

Sharing behaviour between sketches

Let’s say you want all your sketches to have a black background, but you don’t want to have to reimplement that feature every time. The answer is to implement the feature in a class, and then pass the name of that class to defsketch through the :mixins option.

First, define a mixin class that implements the desired behaviour in the draw method (with the :before method specialiser, so that it doesn’t override the behaviour of your sketches):

(defclass black-background () ())
(defmethod draw :before ((instance black-background) &key &allow-other-keys)
  (background +black+))

Then add the :mixins option to your defsketch bindings:

(defsketch moon
    ((:mixins black-background))
     (width 200)
     (height 200))
  (circle 100 100 50))

The resulting sketch should have a black background. Initialization/setup logic for black-background, and sketches inheriting from it, could be put in an initialize-instance or setup method:

(defmethod initialize-instance :before ((instance black-background) &key &allow-other-keys)
  (format t "Initializing black background!"))
(defmethod setup :before ((instance black-background) &key &allow-other-keys)
  (format t "Setting up black background!"))

If the mixin class has state that you need to access in the body of your sketch, then use (sketch-slot-value 'slot-name).

Example:

(defclass mixin-with-state () ((name :initform "Ruth" :reader name)))
(defsketch slot-example
    ((:mixins mixin-with-state black-background))
  (with-font (make-font :color +white+)
    (text (format nil "Hi ~a!" (sketch-slot-value 'name)) 100 100)))

Made with Sketch

Outro

For everything else, read the code or ask vydd at #lispgames. Go make something pretty!

About

This is my experimental fork of Sketch, a Common Lisp graphics framework by vydd.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Common Lisp 100.0%