table of contents

common lisp graphics

2023-05-21

we need the libraries sdl2, livesupport (to be able to continue if an error occurs in the main loop) and opengl for rendering.

using quicklisp

(ql:quickload :sdl2)
(ql:quickload :livesupport)
(ql:quickload :cl-opengl)

here im implementing an abstraction layer over opengl/sdl2 to be able to render things like math functions easily we need a generic way to represent objects that are renderable onto a window so we use the generic function render and implement it in the various classes

(defgeneric render (obj)
  (:documentation "a function to render the object in an opengl context"))

(defstruct rgb
  "rgb color"
  (r 0) (g 0) (b 0))

(defstruct point-2d ()
  x y (color (make-rgb :r 0 :b 0 :g 0)))

(defmethod render ((p point-2d))
  (gl:with-primitives :points
    (gl:vertex (point-2d-x p) (point-2d-y p))))

(defstruct line-2d
  point1 point2 (color (make-rgb :r 0 :b 0 :g 0)) (width 1))

(defmethod render ((line line-2d))
  (let ((point1 (line-2d-point1 line))
        (point2 (line-2d-point2 line))
        (width (line-2d-width line)))
    (gl:line-width width)
    (gl:with-primitives
        :lines
      (gl:vertex (point-2d-x point1) (point-2d-y point1))
      (gl:vertex (point-2d-x point2) (point-2d-y point2)))))

(defclass window ()
  ((draw-axis :initarg :draw-axis :accessor window-draw-axis)
   (renderables :initform '() :accessor window-renderables)
   (width :initarg :width :initform 750 :accessor window-width)
   (height :initarg :height :initform 750 :accessor window-height)))

(defmethod window-run ((my-window window))
  (sdl2:with-init (:everything)
    (format t "Using SDL Library Version: ~D.~D.~D~%"
            sdl2-ffi:+sdl-major-version+
            sdl2-ffi:+sdl-minor-version+
            sdl2-ffi:+sdl-patchlevel+)
    (sdl2:with-window
        (sdl-window
         :title "basic graphics"
         :flags '(:shown :opengl)
         :w (window-width my-window)
         :h (window-height my-window))
      (sdl2:with-gl-context (gl-context sdl-window)
        ;; init opengl
        (sdl2:gl-make-current sdl-window gl-context)

        ;; enter main loop
        (sdl2:with-event-loop (:method :poll)
          (:keyup (:keysym keysym)
                  (when (sdl2:scancode= (sdl2:scancode-value keysym) :scancode-q)
                    (sdl2:push-event :quit)))
          (:idle
           ()
           (livesupport:update-repl-link) ;; accept requests from slime/sly, for live programming
           (livesupport:continuable (window-render my-window))
           (sdl2:gl-swap-window sdl-window))
          (:quit () t))))))

(defmethod window-render ((win window))
  "render the renderables, using opengl"
  (gl:clear-color 255 255 255 1.0)
  (gl:clear :color-buffer)
  (loop for renderable in (window-renderables win)
        do (gl:matrix-mode :projection)
           (gl:load-identity) ;; reset matrix to default state
           ;; (gl:ortho -1 1 -1 1 -1 1) ;; normalize window space, not really needed, done by default
           (gl:viewport 0 0 (window-width win) (window-height win))
           (gl:matrix-mode :modelview)
           (with-slots (color) renderable
             (gl:color (/ (rgb-r color) 255) (/ (rgb-b color) 255) (/ (rgb-g color) 255)))
           (render renderable))
  (sdl2:delay 20))

(defmethod window-add-renderable ((win window) renderable)
  "add a renderable to renderables, a renderable is an object that has the generic function render"
  (push renderable (window-renderables win)))

(defstruct axis
  "a renderable, can hold other renderables which would be drawn locally (transformed), pos is the (point-2d) position in the window"
  (renderables '()) pos width height (color (make-rgb :r 0 :b 0 :g 0))
  ;; bounds of the axes
  (min-y -1) (max-y 1) (min-x -1) (max-x 1))

(defmethod axis-add-renderable ((a axis) renderable)
  "add a renderable to renderables, a renderable is an object that has the generic function render"
  (push renderable (axis-renderables a)))

(defmethod render ((a axis))
  (with-slots (min-y min-x max-y max-x width height) a
    ;; axes view matrix setup
    (gl:matrix-mode :projection)
    (gl:viewport (point-2d-x (axis-pos a))
                 (point-2d-y (axis-pos a))
                 width
                 height)
    ;; apply matirx to make the screenspace correct for original width/height
    (gl:ortho 0 width 0 height -1 1)
    ;; render ticks of both axes
    (let ((origin (make-point-2d
                   :x (map-num 0 0 width (map-num 0 min-x max-x 0 width) width)
                   :y (map-num 0 0 height (map-num 0 min-y max-y 0 height) height))))
      (loop for x from (point-2d-x origin) below width by 75
            do (render
                (make-line-2d
                 :point1 (make-point-2d
                          :x x :y (- (point-2d-y origin) 10))
                 :point2 (make-point-2d
                          :x x :y (- (point-2d-y origin) -10))
                 :width 3)))
      (loop for x from (point-2d-x origin) above 0 by 75
            do (render
                (make-line-2d
                 :point1 (make-point-2d
                          :x x :y (- (point-2d-y origin) 10))
                 :point2 (make-point-2d
                          :x x :y (- (point-2d-y origin) -10))
                 :width 3)))
      (loop for y from (point-2d-y origin) above 0 by 75
              do (render
                  (make-line-2d
                   :point1 (make-point-2d
                            :x (- (point-2d-x origin) 10) :y y)
                   :point2 (make-point-2d
                            :x (- (point-2d-x origin) -10) :y y)
                   :width 3)))
      (loop for y from (point-2d-y origin) below height by 75
              do (render
                  (make-line-2d
                   :point1 (make-point-2d
                            :x (- (point-2d-x origin) 10) :y y)
                   :point2 (make-point-2d
                            :x (- (point-2d-x origin) -10) :y y)
                   :width 3))))
    ;; reset matrix then apply another matrix so that the screenspace is correct for min/max-x/y
    (gl:load-identity)
    (gl:ortho min-x max-x min-y max-y -1 1)
    (gl:matrix-mode :modelview)
    ;; render axes
    (render
     (make-line-2d
      :point1 (make-point-2d
               :x min-x :y 0)
      :point2 (make-point-2d
               :x max-x :y 0)
      :width 3))
    (render
     (make-line-2d
      :point1 (make-point-2d
               :x 0 :y max-y)
      :point2 (make-point-2d
               :x 0 :y min-y)
      :width 3))
    ;; render children (renderables)
    (loop for renderable in (axis-renderables a)
          do (render renderable))))

(defstruct plot
  "lam is the lambda function to call with x (should return y)"
  lam
  ;; bounds of the plot (drawing bound of the plots)
  min-y max-y min-x max-x
  (color (make-rgb :r 0 :b 0 :g 0)))

(defun map-num (num src-min src-max dest-min dest-max)
  "(map-num 0.5 -1 1 -50 50) => 25.0"
  (/ (- (+ (* (- num src-min) (- dest-max dest-min)) (* dest-min src-max)) (* dest-min src-min)) (- src-max src-min)))

(defmethod render ((p plot))
  (let ((prev-point nil)
        (lam (plot-lam p)))
    (loop for x from -100 below 100
          do (let* ((x (map-num x -100 100 (plot-min-x p) (plot-max-x p)))
                    (new-y (funcall lam x))
                    (new-point (make-point-2d :x x :y new-y)))
               (if prev-point
                   (render (make-line-2d
                            :point1 prev-point
                            :point2 new-point)))
               (setf prev-point new-point)))))

(defstruct discrete-plot
  "a discrete plot, one that connects a finite amount of points"
  points (color (make-rgb :r 0 :b 0 :g 0)))

(defmethod render ((p discrete-plot))
  (let* ((points (discrete-plot-points p))
         (prev-point (elt points 0)))
    (loop for i from 1 below (length points)
          do (let ((new-point (elt points i)))
               (render (make-line-2d
                        :point1 prev-point
                        :point2 new-point))
               (setf prev-point new-point)))))

example usage:

(defun sdl2-test ()
  (defparameter *win* (make-instance 'window :draw-axis t :width 750 :height 750))
  (defparameter *axis* (make-axis
                        :min-x -1
                        :max-x 3
                        :max-y 3
                        :min-y -1
                        :pos (make-point-2d :x 100 :y 100)
                        :width 750
                        :height 750))
  (window-add-renderable *win* *axis*)
  (axis-add-renderable *axis* (make-plot :lam (lambda (x) (* x x)) :min-x -2 :max-x 1))
  ;; (axis-add-renderable *axis* (make-plot :lam (lambda (x) (sin x)) :min-x -2 :max-x 1))
  ;; (axis-add-renderable *axis* (make-plot :lam (lambda (x) (tan x)) :min-x -2 :max-x 1))
  ;; (axis-add-renderable *axis* (make-plot :lam (lambda (x) (cos x)) :min-x -2 :max-x 1))
  (axis-add-renderable
   *axis*
   (make-discrete-plot :points
                       (list (make-point-2d :x -1 :y 1)
                             (make-point-2d :x 0 :y 0)
                             (make-point-2d :x 2 :y 1))))
  (sb-thread:make-thread (lambda () (window-run *win*))))

we can create objects and add them dynamically to the window:

(window-add-renderable
 *win*
 (make-discrete-plot :points
                     (list (make-point-2d :x (/ (random 10) 10) :y (/ (random 10) 10))
                           (make-point-2d :x (/ (random 10) 10) :y (/ (random 10) 10)))))

rendering text wasnt as simple as i expected, i ended up using freetype because thats what most webpages seemed to promote. the library cl-freetype2 provides freetype bindings for sbcl, example usage is here https://github.com/rpav/cl-freetype2/blob/master/doc/example.md

(ql:quickload :cl-freetype2)

toy around with it to make sure it works:

;; make sure your encoding is set to UTF-8
;; load font, change according to your system, im on linux
(defparameter *face* (freetype2:new-face "/usr/share/fonts/TTF/CascadiaCode.ttf"))
;; Set the size to 24 points and 36 DPI
(freetype2:set-char-size *face* (* 24 64) 0 36 36)
;; trivial output:
(freetype2:print-with-face *face* "Hello")
(ft2:get-char-index *face* #\a)
(ft2:load-char *face* #\A)
(ft2:render-glyph *face*)
(ft2:do-string-render (*face* "o" bitmap x y)
  (print (ft2:bitmap-to-array bitmap)))

it was hard to figure things out as this was my first time rendering text at such a low level and couldnt find proper documentation for cl-freetype2, had to browse through the source code i tried to adapt the code from http://3bb.cc/tutorials/cl-opengl/textures.html:

(defstruct renderable-text ()
  text pos (color (make-rgb :r 255 :b 255 :g 255)))

(defmethod render ((txt renderable-text))
  ;; Render text using FreeType2
  (let ((face (freetype2:new-face "/usr/share/fonts/TTF/CascadiaCode.ttf"))
        (text (renderable-text-text txt))
        ;; (x (point-2d-x (renderable-text-pos txt)))
        ;; (y (point-2d-y (renderable-text-pos txt)))
        (texture (car (gl:gen-textures 1))))
    (freetype2:set-char-size face (* 48 64) 0 36 36)
    (gl:enable :texture-2d)
    (gl:bind-texture :texture-2d texture)
    (gl:tex-parameter :texture-2d :texture-min-filter :linear)
    (gl:color 1 1 1)
    (ft2:do-string-render (face "l" bitmap x y)
      (let ((gray-colored-pixels (list->vector (apply #'concatenate (append '(list) (array->list (ft2:bitmap-to-array bitmap)))))))
        (gl:tex-image-2d :texture-2d 0 :luminance 64 64 0 :luminance :unsigned-byte gray-colored-pixels)
        )
      (gl:with-primitives :quads
        (gl:tex-coord 0 0)
        (gl:vertex -1 1)
        (gl:tex-coord 1 0)
        (gl:vertex 0 1)
        (gl:tex-coord 1 1)
        (gl:vertex 0 0)
        (gl:tex-coord 0 1)
        (gl:vertex -1 0)))
    (gl:delete-textures (list texture))))

(defun freetype-test ()
  (defparameter *win* (make-instance 'window :draw-axis t :width 750 :height 750))
  (window-add-renderable *win* (make-renderable-text
                                :text "hello"
                                :pos (make-point-2d :x 10 :y 10)))
  (window-run *win*))

couldn't get this to work so i gave up and decided to use raylib since it provides more features than sdl2 (on its own, for example sdl2 doesnt have a way to draw a line with a specific thickness) and i wouldnt even have to deal with opengl

cl-raylib

on arch linux had to install it using pacman (raylib) cl-raylib is currently not on quicklist so you gotta fetch it manually:

git clone https://github.com/longlene/cl-raylib.git ~/.quicklisp/local-projects/cl-raylib
(ql:quickload :cl-raylib)
(ql:quickload :3d-vectors) ;; we need this library to work with cl-raylib..

its pretty simple to get started with:

(defun raylib-first-test ()
  (let ((screen-width 800)
        (screen-height 450))
    (raylib:with-window (screen-width screen-height "raylib [core] example - basic window")
      ;; (raylib:set-target-fps 60)
      (loop
        until (raylib:window-should-close) ; dectect window close button or ESC key
        do (raylib:with-drawing
             (raylib:clear-background raylib:+raywhite+)
             (raylib:draw-fps 20 20)
             (raylib:draw-text "Congrats! You created your first window!" 190 200 20 raylib:+lightgray+))))))

we need some way to say whether an object is renderable or not, to discriminate renderable objects and prevent an attempt to render other objects, we use cl's defgeneric and construct some basic renderable types:

(defstruct rgb
  "rgb color"
  (r 0) (g 0) (b 0))

(defstruct point-2d ()
  x y (color (make-rgb :r 0 :b 0 :g 0)))

(defstruct line-2d
  p1 p2 (color (make-rgb :r 0 :b 0 :g 0)) (width 1.0))
(defgeneric render (obj)
  (:documentation "a function to render an object"))

(defmethod render ((p point-2d))
  (raylib:draw-pixel (point-2d-x p) (point-2d-y p)
                     (raylib:make-rgba
                      (rgb-r (point-2d-color p))
                      (rgb-g (point-2d-color p))
                      (rgb-b (point-2d-color p)))))

(defmethod render ((line line-2d))
  (with-slots (p1 p2 color width) line
    (raylib:draw-line-ex
     (3d-vectors:vec (point-2d-x p1) (point-2d-y p1))
     (3d-vectors:vec (point-2d-x p2) (point-2d-y p2))
     (float width)
     (raylib:make-rgba
      (rgb-r color)
      (rgb-g color)
      (rgb-b color)))))

we present the basic code for opening a window and drawing to it:

(defclass window ()
  ((renderables :initform '() :accessor window-renderables)
   (width :initarg :width :initform 750 :accessor window-width)
   (height :initarg :height :initform 750 :accessor window-height)))

(defmethod window-run ((win window))
  (let ((my-should-close nil))
    (with-slots (width height) win
      (raylib:with-window (width height "test window")
        (raylib:set-target-fps 60)
        (loop
          until my-should-close ;;(or (raylib:window-should-close) my-should-close)
          do (raylib:with-drawing
               (raylib:clear-background raylib:+raywhite+)
               (livesupport:update-repl-link) ;; accept requests from slime/sly, for live programming
               (livesupport:continuable (window-render win)))
               ;; (raylib:draw-fps 20 20)
             (when (raylib:is-key-pressed :key-q)
               (setf my-should-close t))
          )))))

(defmethod window-render ((win window))
  "render the renderables"
  (loop for renderable in (window-renderables win)
        do (render renderable)))

(defmethod window-add-renderable ((win window) renderable)
  "add a renderable to renderables, a renderable is an object that has the generic function render"
  (push renderable (window-renderables win)))

example usage:

(defun raylib-second-test ()
  (defparameter *win* (make-instance 'window :width 750 :height 750))
  (window-add-renderable *win*
                         (make-line-2d :p1 (make-point-2d :x 0 :y 0)
                                       :p2 (make-point-2d :x 300 :y 300)))
  (window-run *win*))

we need to implement math plotting functionality, but..

unfortunately raylib doesnt allow matrix manipulation (atleast for 2d cameras) i had to figure out an easy way to allow objects to render themselves to a normalized screen space, for example we might want to draw a mathematical function from x=0 to x=10, and y might vary from 0-100 if the function was , but in raylib's window 0,0 is at the top left and y increases as we go down the window, we need some sort of normalization, it is fairly easy to implement a normalization function/matrix, but consider an axes object with many math functions, how would the functions know where the axis' 0,0 point is? they'd have to have a reference of the axis object, but it doesnt make much sense for them to have a pointer to their parent, in opengl this was possible because the parent was able to manipulate its viewport and its rendering matrix and then let its children draw themselves to a normalized portion of the window, as far as i can tell, raylib doesnt provide such functionality by default, so i had to come up with something, and thought about using a macro (eventually settled on a different approach, explained below), perhaps call it with-normalization, that takes a normalization function, code to execute, modifies every call to a point's x and y and normalizes it before it is passed to raylib rendering functions, lets see how well that goes so basically we want the accessors of the point-2d class to behave differently inside our macro, which begs the question, is that even possible? can we have it so that a specific function behaves differently inside a macro? my first guess was i could modify the body passed to a macro to wrap the calls to point-2d-x and point-2d-y with another function, this would introduce some problems, but then i found out anaphoric macro, so that's what im gonna use consider the following macro:

(defmacro with-normalization (&body body)
  `(flet ((point-2d-x (p) (/ (point-2d-x p) 10)))
     ,@body))

in the simple case it works fine but not in others:

CL-USER> (setf a (make-point-2d :x 10 :y 10))
#S(POINT-2D :NIL NIL :X 10 :Y 10 :COLOR #S(RGB :R 0 :G 0 :B 0))
CL-USER> (with-normalization a)
#S(POINT-2D :NIL NIL :X 10 :Y 10 :COLOR #S(RGB :R 0 :G 0 :B 0))
CL-USER> (with-normalization (point-2d-x a))
1 (1 bit, #x1, #o1, #b1)
CL-USER> (with-normalization (with-normalization (point-2d-x a)))
1/10 (0.1, 10%)

this works in a lexical scope, calling an outer function that makes a call to point-2d-x wouldnt work as we want because its not shadowed dynamically this can be done, but its already ugly as it is and uglier approaches isnt something im looking for

so i just decided to pass rendering parameters around, i.e. each function receives a matrix/function that normalizes things and uses that on every object before drawing it, so we modify the generic render function to take a transformation matrix and so we have to rewrite the rendering functions for all renderable types we have, which for now are just a point and a line

(defgeneric render (obj transformation-matrix)
  (:documentation "a function to render an object using the given transformation matrix"))

(defmethod point-2d-transform ((p point-2d) transformation-matrix)
  (let* ((x (point-2d-x p))
         (y (point-2d-y p))
         (point-as-matrix (list->array (map 'list #'list (list x y 1))))
         (transformed-point-as-matrix (matrix-mul transformation-matrix point-as-matrix))
         (transformed-point-as-list (array->list transformed-point-as-matrix)))
    (make-point-2d :x (car (car transformed-point-as-list))
                   :y (car (car (cdr transformed-point-as-list)))
                   :color (point-2d-color p))))

(defmethod render ((p point-2d) transformation-matrix)
  (let ((p (point-2d-transform p transformation-matrix)))
    (raylib:draw-pixel (round (point-2d-x p)) (round (point-2d-y p))
                       (raylib:make-rgba
                        (rgb-r (point-2d-color p))
                        (rgb-g (point-2d-color p))
                        (rgb-b (point-2d-color p))))))

(defmethod render ((line line-2d) transformation-matrix)
  (with-slots (p1 p2 color width) line
    (let ((p1 (point-2d-transform p1 transformation-matrix))
          (p2 (point-2d-transform p2 transformation-matrix)))
      (raylib:draw-line-ex
       (3d-vectors:vec (point-2d-x p1) (point-2d-y p1))
       (3d-vectors:vec (point-2d-x p2) (point-2d-y p2))
       (float width)
       (raylib:make-rgba
        (rgb-r color)
        (rgb-g color)
        (rgb-b color))))))

i wanted a simple way to draw continuous and discrete math functions, so i thought about implementing an axis object first, which would hold these plots and pass the proper transformation matrix to them, plus it would take care of actually rendering the axes on the screen here im linearly mapping grids using the transformation matrix for it

(defparameter *default-transformation-matrix* #2A((1 0 0) (0 1 0) (0 0 1)))

(defmethod window-render ((win window))
  "render the renderables"
  (loop for renderable in (window-renderables win)
        do (render renderable *default-transformation-matrix*)))

(defstruct axis
  "a renderable, can hold other renderables which would be drawn locally (transformed), pos is the (point-2d) position in the window"
  (renderables '()) pos width height (color (make-rgb :r 0 :b 0 :g 0))
  ;; bounds of the axes
  (min-y -1) (max-y 1) (min-x -1) (max-x 1))

(defmethod axis-add-renderable ((a axis) renderable)
  "add a renderable to renderables, a renderable is an object that has the generic function render"
  (push renderable (axis-renderables a)))

;; for some reason text is oddly positioned with an offset, might be related: https://github.com/raysan5/raylib/issues/908
(defun draw-text (text size x y)
  (raylib:draw-text
   text
   (round (- x (/ (raylib:measure-text text size) 2)))
   (round (+ y (/ (3d-vectors:vy (raylib:measure-text-ex (raylib:get-font-default) text (float size) 1.0)) 2)))
   size
   raylib:+black+))

(defmethod render ((a axis) transformation-matrix)
  (with-slots (min-y min-x max-y max-x width height pos) a
    (let* ((pos-x (point-2d-x pos))
           (pos-y (point-2d-y pos))
           (children-transformation-matrix
             (map-grid
              transformation-matrix
              (list min-x max-x)
              (list pos-x (+ pos-x width))
              (list min-y max-y)
              (list (+ pos-y height) pos-y)))
           (origin (point-2d-transform (make-point-2d :x 0 :y 0) children-transformation-matrix))
           (tick-distance 75))
      ;; render children (renderables)
      (loop for renderable in (axis-renderables a)
            do (render
                renderable
                children-transformation-matrix))
      ;; draw the ticks on the axes
      (loop for x from (point-2d-x origin) below (+ pos-x width) by tick-distance
            do (render
                (make-line-2d
                 :p1 (make-point-2d
                      :x x :y (- (point-2d-y origin) 10))
                 :p2 (make-point-2d
                      :x x :y (- (point-2d-y origin) -10))
                 :width 3)
                transformation-matrix)
               (when (> x (point-2d-x origin))
                 (draw-text
                  (format nil "~,3f" (map-num x (point-2d-x origin) (+ pos-x width) 0 max-x))
                  20
                  x
                  (- (point-2d-y origin) -10))))
      (loop for x from (point-2d-x origin) above pos-x by tick-distance
            do (render
                (make-line-2d
                 :p1 (make-point-2d
                      :x x :y (- (point-2d-y origin) 10))
                 :p2 (make-point-2d
                      :x x :y (- (point-2d-y origin) -10))
                 :width 3)
                transformation-matrix)
               (when (< x (point-2d-x origin))
                 (draw-text
                  (format nil "~,3f" (map-num x (point-2d-x origin) pos-x 0 min-x))
                  20
                  x
                  (- (point-2d-y origin) -10))))
      (loop for y from (point-2d-y origin) above pos-y by tick-distance
            do (render
                (make-line-2d
                 :p1 (make-point-2d
                      :x (- (point-2d-x origin) 10) :y y)
                 :p2 (make-point-2d
                      :x (- (point-2d-x origin) -10) :y y)
                 :width 3)
                transformation-matrix)
               (when (< y (point-2d-y origin))
                 (draw-text
                  (format nil "~,3f" (map-num y (point-2d-y origin) pos-y 0 max-y))
                  20
                  (point-2d-x origin)
                  y)))
      (loop for y from (point-2d-y origin) below (+ pos-y height) by tick-distance
            do (render
                (make-line-2d
                 :p1 (make-point-2d
                      :x (- (point-2d-x origin) 10) :y y)
                 :p2 (make-point-2d
                      :x (- (point-2d-x origin) -10) :y y)
                 :width 3)
                transformation-matrix)
               (when (> y (point-2d-y origin))
                 (draw-text
                  (format nil "~,3f" (map-num y (point-2d-y origin) (+ pos-y height) 0 min-y))
                  20
                  (point-2d-x origin)
                  y)))
      ;; draw the axes
      (render
       (make-line-2d
        :p1 (make-point-2d
             :x min-x :y 0)
        :p2 (make-point-2d
             :x max-x :y 0)
        :width 3)
       children-transformation-matrix)
      (render
       (make-line-2d
        :p1 (make-point-2d
             :x 0 :y max-y)
        :p2 (make-point-2d
             :x 0 :y min-y)
        :width 3)
       children-transformation-matrix))))

(defstruct plot
  "lam is the lambda function to call with x (should return y)"
  lam
  ;; bounds of the plot (drawing bound of the plots)
  min-y max-y min-x max-x
  (color (make-rgb :r 0 :b 0 :g 0)))

(defun map-num (num src-min src-max dest-min dest-max)
  "(map-num 0.5 -1 1 -50 50) => 25.0"
  (/ (- (+ (* (- num src-min) (- dest-max dest-min)) (* dest-min src-max)) (* dest-min src-min)) (- src-max src-min)))

(defmethod render ((p plot) transformation-matrix)
  (let ((prev-point nil)
        (lam (plot-lam p))
        (iterations/2 25)) ;; number of iterations divided by 2
    (loop for x from (- iterations/2) upto iterations/2
          do (let* ((x (map-num x (- iterations/2) iterations/2 (plot-min-x p) (plot-max-x p)))
                    (new-y (funcall lam x))
                    (new-point (make-point-2d :x x :y new-y)))
               (if prev-point
                   (render (make-line-2d
                            :p1 prev-point
                            :p2 new-point)
                           transformation-matrix))
               (setf prev-point new-point)))))

(defstruct discrete-plot
  "a discrete plot, one that connects a finite amount of points (not really the definition of 'discrete')"
  points (color (make-rgb :r 0 :b 0 :g 0)))

(defmethod render ((p discrete-plot) transformation-matrix)
  (let* ((points (discrete-plot-points p))
         (prev-point (elt points 0)))
    (loop for i from 1 below (length points)
          do (let ((new-point (elt points i)))
               (render (make-line-2d
                        :p1 prev-point
                        :p2 new-point)
                       transformation-matrix)
               (setf prev-point new-point)))))

example usage:

(defun raylib-third-test ()
  (defparameter *win* (make-instance 'window :width 750 :height 750))
  (defparameter *axis* (make-axis
                        :min-x -10
                        :max-x 10
                        :max-y 20
                        :min-y -5
                        :pos (make-point-2d :x 0 :y 0)
                        :width 750
                        :height 750))
  (window-add-renderable *win* *axis*)
  (axis-add-renderable *axis* (make-plot :lam (lambda (x) (* x x)) :min-x -10 :max-x 10))
  ;; (axis-add-renderable *axis* (make-plot :lam (lambda (x) (sin x)) :min-x -10 :max-x 10))
  ;; (axis-add-renderable *axis* (make-plot :lam (lambda (x) (tan x)) :min-x -10 :max-x 10))
  ;; (axis-add-renderable *axis* (make-plot :lam (lambda (x) (cos x)) :min-x -10 :max-x 10))
  (sb-thread:make-thread (lambda () (window-run *win*))))

hmmm, im very tempted to try and visualize the mandelbrot now, so lets do it, im thinking about just writing the mandelbrot as a function and map it to every pixel on the screen, maybe the function would return a value in 0-255 that describes how "bright" a point is? and we'd just turn it into an rgb? i think that'd do

(defun complex-square (a b)
  (values (- (expt a 2) (expt b 2)) (* 2 a b)))

(defun complex-abs(a b)
  (sqrt (+ (expt (abs a) 2) (expt (abs b) 2))))

(defun mandelbrot (c-a c-b)
  "a is the real number that lies on the real axis, b is a real number which is the component of the imaginary axis, the function returns a value 0-255 that represents the brightness of the point to be used when drawing it"
  (let ((iterations 20)
        (z-a 0)
        (z-b 0)
        (last-i 0))
    (loop for i from 0 below iterations
          do (multiple-value-bind (squared-z-a squared-z-b) (complex-square z-a z-b)
               (setf z-a (+ squared-z-a c-a))
               (setf z-b (+ squared-z-b c-b)))
             (setf last-i i)
             (when (> (complex-abs z-a z-b) 2)
               (return)))
    (if (> last-i 5)
        (map-num last-i 10 (1- iterations) 254 0)
        255)))

(defmethod render ((thing (eql :mandelbrot)) transformation-matrix)
  "render function specialized for the mandelbrot"
  (loop for x from -1.6 below 0.5 by 0.015 ;; some bounds of the mandelbrot i already know smaller than [-2,2]
        do (loop for y from -0.9 below 1 by 0.015
                 do (let ((brightness (mandelbrot x y)))
                      (when (< brightness 255)
                        (render
                         (make-point-2d :x x :y y :color (make-rgb :r (round brightness)
                                                                   :g (round brightness)
                                                                   :b (round brightness)))
                         transformation-matrix))))))

(defun mandelbrot-test ()
  (defparameter *win* (make-instance 'window :width 400 :height 400))
  (defparameter *axis* (make-axis
                        :min-x -2
                        :max-x 2
                        :max-y 2
                        :min-y -2
                        :pos (make-point-2d :x 0 :y 0)
                        :width 500
                        :height 500))
  (window-add-renderable *win* *axis*)
  (axis-add-renderable *axis* :mandelbrot)
  (sb-thread:make-thread (lambda () (window-run *win*))))

use (mandelbrot-test) to run this (obviously) she's pretty but slow as fuck to kill the window now we have to press "q" continuously, because the main loop doesnt reach the code that checks whether a key is pressed until the frame has finished drawing, and the mandelbrot takes some time to draw