Tixel Viewer

Tagged as lisp, clim, time series

Written on 2022-04-14 by Daniel 'jackdaniel' KochmaƄski

I'm currently having fun with modeling motion with non-linear transformations. While doing that I've written a simple viewer for points scattered in time and decided to share it as a separate piece.

To visualise the plane over time we need to introduce a concept of the scene. The scene is a set of frames that present a time from t0 to end-time stepped by dt. As for scaling to the "observer time", a variable dt/s specifies the number of frames per second. For simplicity we will assume that t0=0.

(in-package #:clim-user)

;;; Poor man's double buffering. The proper interface is a WIP.
(defmacro with-scene ((stream) &body body)
  (check-type stream symbol)
  `(let* ((pixmap (with-output-to-pixmap (,stream ,stream)
                    ,@body))
          (width (pixmap-width pixmap))
          (height (pixmap-height pixmap)))
     (medium-copy-area pixmap 0 0 width height ,stream 0 0)
     (deallocate-pixmap pixmap)
     (finish-output ,stream)))

(defun draw-frame (stream jiff dt points)
  (draw-rectangle* stream -50 -50 50 50 :ink +grey90+)
  (draw-rectangle* stream -50 -50 50 50 :filled nil)
  (loop with thickness = 10
        for (x y time) in points
        when (= time jiff) do
          (draw-point* stream x y :ink +dark-red+ :line-thickness thickness)))

(defparameter *dt/s* 1) ;; fps-esque

(defun project-scene (stream end-time dt points)
  (loop for jiff from 0 upto end-time by dt do
    (with-scene (stream)
      (let ((blurb (format nil "Jiff = ~6,2f / ~6,2f" jiff end-time)))
        (draw-text* stream blurb 10 250
                    :align-x :left :align-y :bottom :text-family :fix))
      (with-translation (stream 60 60)
        (draw-frame stream jiff dt points)))
    (sleep (/ 1 *dt/s*))))

Let's display the scene in a loop and allow to recompile functions and points at runtime - because that's what Lisp programmers do.

;;; The scene time and dt are constant in one scene, new values will be used
;;; when the scene is replayed. We may freely manipulate the speed though.
(defparameter *scene-time* 30)
(defparameter *dt* 10)

(defparameter *points*
  (loop for time from 0 below 40 by 10
        for +dist from 10 by 10
        for -dist = (- +dist)
        collect (list 0 0 time)
        collect (list +dist +dist time)
        collect (list +dist -dist time)
        collect (list -dist -dist time)
        collect (list -dist +dist time)))

(defun start-scene ()
  (with-output-to-drawing-stream
      (stream nil nil :scroll-bars nil :borders nil :width 240 :height 260)
    (sleep .1) ;; don't ask (.. some vms delay the window creation, i.e kwin)
    (handler-case
        (loop while (sheet-grafted-p stream)
              do (project-scene stream *scene-time* *dt* *points*))
      (error (c)
        (princ c *debug-io*)))
    (close stream)))

Can we really draw a point on a plane though? Are pixels little squares1? In the code above we take a crude approach of treating the time as if it were discrete. If we change the time resolution (or a point position), then the scene will start to blink or even worse, some points will be missed by the scanline:

  • dt = 10 : all points appear as expected
  • dt = 10/4: all points appear but only for a little while (blinking)
  • dt = 20 : um, we might have dropped something
  • dt = 0.1 : good luck finding your points (remember to set dt/s 100)

When we step by dt=10 we implicitly make the point thickness in time to be 10. It is time (ha!) to make the point thickness explicit and independent from the value of dt. Instead of checking (= jiff time) we'll see whether the tixel's is "inside" the point or not. This is the very same approach as suggested in the Rendering Conventions for Geometric Shapes (CLIM).

But what is the tixel? Let's come up with a definition:

A tixel (as in "time pixel") is a little cube.. No, I'm joking, see the first footnote. Let's try again.

A tixel (as in "time pixel") is a point sample. An animation is a three-dimensional array of point samples (tixels).

A toxel is a "time voxel" and is also a point sample. A theater is a four-dimensional array of point samples (toxels).

A scene is a (n+1)-dimensional array of point samples where one of dimensions represents the time. For n=2 it is an animation and for n=3 it is a theater.

The thickness on the T axis is the "time span" of the point. It represents how long it remains on the drawing plane. For example when the point thickness is 3 then it will remain on the screen for three time units. When the thickness is smaller than dt, then some points may "fall of the grid".

(defun inside-p (jiff dt time radius)
  ;; (= time jiff)                   ; old definition (wrong)
  ;; (<= (abs (- time jiff)) radius) ; intuitive(?), also wrong. 
  (let ((tixel-center (+ jiff (/ dt 2))))
    (and (<= (- time radius) tixel-center)
         (< tixel-center (+ time radius)))))

(defun draw-frame (stream jiff dt points)
  (draw-rectangle* stream -51 -51 51 51)
  (draw-rectangle* stream -50 -50 50 50 :ink +grey90+)
  (loop with thickness = 10
        with radius = (/ thickness 2)
        for (x y time) in points
        when (inside-p jiff dt time radius)
          do (draw-point* stream x y :ink +dark-red+ :line-thickness thickness)))

This definition is discrete and has the advantage that points that do not overlap won't appear on the drawing plane at the same jiff.

Now let's "rotate" the frame to see the time scanline in the vertical and the horizontal slices of points on the plane. To have more fun, remember to do it while the scene is running! Let's change the time to start at -50 and end at +50, so it is consistent with x/y resolutions.

(defun draw-horizontal-slice (stream jiff dt points)
  (draw-rectangle* stream -51 -51 51 51)
  (draw-rectangle* stream -50 -50 50 50 :ink +light-cyan+)
  (with-drawing-options (stream :clipping-region (make-rectangle* -50 -50 50 50))
    (draw-rectangle* stream -50 jiff  50 (+ jiff dt) :ink +green+)
    (loop with thickness = 10
          for (x y time) in points do
            (draw-point* stream x time :ink +dark-red+ :line-thickness thickness))))

(defun draw-vertical-slice (stream jiff dt points)
  (draw-rectangle* stream -51 -51 51 51)
  (draw-rectangle* stream -50 -50 50 50 :ink +light-yellow+)
  (with-drawing-options (stream :clipping-region (make-rectangle* -50 -50 50 50))
    (draw-rectangle* stream jiff -50 (+ jiff dt) 50 :ink +green+)
    (loop with thickness = 10
          for (x y time) in points do
            (draw-point* stream time y :ink +dark-red+ :line-thickness thickness))))

(defparameter *dt/s* 10)

(defun project-scene (stream end-time dt points)
  (loop for jiff from -50 upto end-time by dt do
    (handler-case
        (with-scene (stream)
          (let ((blurb (format nil "Jiff = ~6,2f / ~6,2f" jiff end-time)))
            (draw-text* stream blurb 10 250
                        :align-x :left :align-y :bottom :text-family :fix))
          (with-translation (stream 60 60)
            (draw-frame stream jiff dt points))
          (with-translation (stream 170 60)
            (draw-vertical-slice stream jiff dt points))
          (with-translation (stream 60 170)
            (draw-horizontal-slice stream jiff dt points)))
      (error (c)
        (format *debug-io* "~a. (yes, ignoring)." c)))
    (sleep (/ 1 *dt/s*))))

(defparameter *scene-time* 50)
(defparameter *dt* 10)

The green line represents the current time slice [jiff, jiff+dt]. When the center of the slice is "inside" of the point, then that point is visible.

Points are nice, but how about projecting a line? We can't go wrong with the Bresenham's line algorithm. For sake of simplicity let's assume that tixels are cubes of size [10dx,10dy,10dt] and draw a line on the plane XT by collecting appropriate points in the center of corresponding tixels.

(defun collect-line-points (x0 t0 x1 t1)
  (let* ((dx (abs (- x1 x0)))
         (dt (- (abs (- t1 t0))))
         (sx (if (< x0 x1) 1 -1))
         (st (if (< t0 t1) 1 -1))
         (err (+ dx dt))
         (er2 nil)
         (coord-seq nil))
    (loop
      (setf er2 (* 2 err))
      (push (list (+ x0 .5) .5 (+ t0 .5)) coord-seq)
      (when (>= er2 dt)
        (when (= x0 x1)
          (return))
        (incf err dt)
        (incf x0 sx))
      (when (<= er2 dx)
        (when (= t0 t1)
          (return))
        (incf err dx)
        (incf t0 st)))
    coord-seq))

(defparameter *points*
  (collect-line-points -25 -40 +25 40))

The effect is as if a point is moving. This was a short exhibition, but we could go even further:

  • add antialiasing in time (this would be visible as a "blur" effect)
  • add a perspective projection matrix as the fourth slice preview
  • show other interesting curves in time (i.e bezier curve for easying)
  • instead of rasterizing the line we could use q signed distance field
  • points could be small spheres instead of being small cubes

But all that is displaying a static scene in time. All points are predefined and the scene is simply "replayed". This is not the motion I want to see, so I'll stop at this little demo. That said it is a useful construct to think about static scenes.

Footnotes

1 Hint: it isn't.