Recreating an old Visualizer I swear I saw

I have vague memories of a visualizer that I will likely never encounter without looking for it again. It was a waveform display but it had these rectangles that would ride them like boats, not really like surfing. Now, you might be wondering why I'm doing this when my library creates, at best, video.

Well, I'm not even going to recreate that much of it, doing the discrete waveform handling is always a massive pain, so instead we'll cheese it with the combined magic of img-genner, and the magic of limits.

(require :img-genner)
#|------------------------------------------------------------------------------
 | The idea here is that the rectangles ripple along a sine wave, rotating with
 | it as it moves.
 |
 | To determine the angle the derivative of the wave must be known at the
 | center of the rectangles, and that must be turned into an angle
 |----------------------------------------------------------------------------|#

(defvar *rectangles* (loop for i from 0 below 24
                           collect(make-instance 'img-genner:rectangle
                                                 :width 10 :height 10
                                                 :origin
                                                 (img-genner:point (* 30.0 i) 240.0))))

With that in mind we can implement our derivation code, and the slope to angle conversion function. Normally, I wouldn't recommend using defconstant unless you're really sure that the constant won't change.

(defconstant +epsilon+ 0.0001)
(defun derive-at-point(function x)
  (/ (-
      (funcall function (+ x +epsilon+))
      (funcall function x))
     +epsilon+))

(defun slope-to-angle(rise-over-run)
  (atan rise-over-run))

Then we have to write the wave and coordinate conversion functions. We need the coordinate conversions because trigonometric functions aren't very great over the 640.

(defun wave-1(x)
  (+ (sin (* 2.0 x))
     (sin (/ x 3))
     (sin x)))

(defun xcoord(x time)
  (+ (/ x 10.0) time))

Then we need to be able to update each rectangle for a given time and with a given wave function.

#|
 | Move the rectangles to match the wave's height at that point
 |#
(defun match-wave-height(func time rectangle)
  (let* ((origin (slot-value rectangle 'img-genner:origin))
         (x (xcoord (aref origin 0) time)))
    (setf (aref origin 1) (+ 240 (* (funcall func x) 10)))
    )
  )
#|
 | This does the same for the angles
 |#
(defun match-wave-angle(func time rectangle)
  (let* ((origin (slot-value rectangle 'img-genner:origin))
         (x (xcoord (aref origin 0) time))
         (dy (derive-at-point func x)))
    (setf (slot-value rectangle 'img-genner:rotation) (slope-to-angle dy))
    )
  )
; And this runs both
(defun update-rectangles(func time)
  (loop for i in *rectangles*
        do(match-wave-height func time i)
        do(match-wave-angle func time i))
  )

Then we need to handle the streams and the drawing of it all.

(defparameter *image* (img-genner:make-image 640 480))
(defun draw-rectangles()
  (loop for i in *rectangles*
        do(img-genner:fill-shape i *image* (img-genner:static-color-stroker (img-genner:rgb 255 0 0)))))

(defun draw-wave(func time)
  (loop for ox from 0 below 640
        for x = (+ time (/ ox 10.0))
        for y = (- 480 (+ 240 (* 10 (funcall func x))))
                ; This is a cheap way to do a thick line poorly
                do(loop for off from 0 below 2
                        do(setf (aref *image* (floor (+ y off)) ox 2) 255))
        ))

(loop for time from 0.0 by (/ 1.0 25.0)
      for frame from 0
      repeat 1000
      do(update-rectangles #'wave-1 time)
      do(reset-image *image*)
      do(draw-rectangles)
      do(draw-wave #'wave-1 time)
      do(img-genner:save-image *image* (uiop:process-info-input *ffmpeg-writer*))
      do(print frame)
      )

(uiop:close-streams *ffmpeg-writer*)
(uiop:wait-process *ffmpeg-writer*)

And with that we get the above. There's a lot of room for improvement

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.