(require "asdf")
(asdf:load-system :screwlisps-knowledge/ellipse-hulls)
(use-package :screwlisps-knowledge/ellipse-hulls)
(let ((maj-ax 3) (min-ax 2) (rot-rad 1) (tx 0) (ty -1))
(defparameter *hulls* (get-ellipse-hulls maj-ax min-ax
rot-rad
tx ty)))
(asdf:load-system :screwlisps-knowledge/simple-gnuplot)
(use-package :screwlisps-knowledge/simple-gnuplot)
(apply 'gnuplot "one ellipse" *hulls*)
Well, it’s here: codeberg.org/tfw/screwlisps-knowledge/src/branch/main/ellipse-hulls.lisp
Ellipses (such as ovals) are a useful test scenario in many cases, because they are shaped by two clear, orthogonal (right-angled) directions. Orbits happen in space in shakey non-circle ellipses. Except for special cases like circles, there is no closed formula or especially good way to calculate their circumference (length of one 360 degree cycle).
On the other hand, with just a little bit of dark magic and pure lisp, we can basically trace vectors just-inside any ellipse we want, described by its semi-major axis, semi-minor axis, rotation (in rad because we’re rad), x and y translations.
The dark magic is that in order to get a clearly defined top arc and bottom arc, I coarsely bin the points around the ellipse, and take the topmost and bottommost point in each bin as being on the top and bottom arcs of the ellipse. The ends of the top arc are pegged to the ends of the bottom arc.
To clean up at least the initial sampling of the ellipse, I introduce cl-series
, which is a traditional lisp macro package (“80% of Haskell done by one person in the 70s”) though it was maintained by more people since. Raymond Toy recently updated the online documentation of cl-series for us. cl-series introduces a clean, implicit, declarative style for working with finite result implications of multiple infinite series via lazy evaluation.
eev
style is to be transparent if not necessarily unsubtle. So
(eepitch-shell)
mkdir -p ~/gits
mkdir -p ~/common-lisp
cd ~/gits
git clone ssh://git@codeberg.org/tfw/screwlisps-knowledge.git
cd ~/common-lisp
ln -s ~/gits/screwlisps-knowledge
cd ~/gits
git clone https://gitlab.common-lisp.net/rtoy/cl-series.git
cd ~/common-lisp
ln -s ~/gits/cl-series
Nb. cl-series
has no dependencies, and its results if, you succeeded, are pure lisp.
cd ~/common-lisp/screwlisps-knowledge
touch ellipse-hulls.lisp
Proximally I wanted ellipses I could throw into our lisp simple-gnuplot described by separate top arc and a bottom arcs. Whether I should have wanted that, who knows, but here we are.
~/common-lisp/screwlisps-knowledge/ellipse-hulls.lisp
Definition(uiop:define-package :screwlisps-knowledge/ellipse-hulls
(:mix :series :cl)
(:export #:get-ellipse-hulls))
(in-package :screwlisps-knowledge/ellipse-hulls)
(eval-when (:compile-toplevel :load-toplevel :execute)
(series::install))
(defun get-ellipse-hulls
(major-axis minor-axis rotation dx dy
&aux
rotated coarse-x)
;;Get (x y) for each degree of a rotated, translated ellipse
(let* ((radianses (scan-range :upto (* 2 pi) :by 1/360))
(cos-angle (#Mcos radianses))
;;set major axis
(major-axis (series major-axis))
(x-normed (#M* major-axis cos-angle))
(sin-angle (#Msin radianses))
;;set minor axis
(minor-axis (series minor-axis))
(y-normed (#M* minor-axis sin-angle))
;;set rotation radians
(rot-radians (series rotation))
;;rotated x.
(x-rot-x (#M* x-normed (#Mcos rot-radians)))
(x-rot-y (#M- (#M* y-normed (#Msin rot-radians))))
;;summing and coarsening x.
(x (#M/ (#Mfround (#M+ x-rot-x x-rot-y)
(series 0.1))
(series 10.0)))
;;rotated y.
(y-rot-x (#M* x-normed (#Msin rot-radians)))
(y-rot-y (#M* y-normed (#Mcos rot-radians)))
;;summing y
(y (#M+ y-rot-x y-rot-y))
;;translating
(tx (#M+ x (series dx)))
(ty (#M+ y (series dy))))
(setq rotated
(collect
(#Mlist tx ty)))
;;sort
(setq rotated
(sort rotated '< :key 'car))
;; coarsely bin st. each bin has both top and bottom hull in it.
(loop
:with results := (list)
:for prev-x := nil :then x
:for pair :in rotated
:for (x y) := pair
:if (equal prev-x x)
:do
(push pair (car results))
:else
:do
(push`(,pair) results)
:finally
(return
(setq coarse-x results)))
;; Take the highest and lowest points in each bin as the perimeter.
(loop
:for bin :in coarse-x
:for (binmin bimmax)
:= (loop :with min := nil
:with max := nil
:for pair :in bin
:for (x y) := pair
:do
(cond ((null min)
(setq min pair))
((< y (cadr min))
(setq min pair)))
(cond ((null max)
(setq max pair))
((> y (cadr max))
(setq max pair)))
:finally (return (list min max)))
:collect binmin :into bin-mins
:collect bimmax :into bin-maxes
:finally
(setf (car bin-maxes) (car bin-mins)
(car (last bin-maxes)) (car (last bin-mins)))
(return
(list bin-mins bin-maxes)))))
(setf (documentation 'get-ellipse-hulls 'function)
"get-ellipse-hulls (major-radius minor-radius rotation-radians translation-x translation-y)
All the arguments should be floats or float-like.
Returns a list of two lists of points:
First the lower hull, then the upper hull to make up the rotated, translated ellipse.
The leftmost and right-most points of the top hull are changed to be equal to those of
the bottom hull.")
(eval-when (:compile-toplevel :load-toplevel :execute)
(series::install :remove t))
Probably the interesting part here is the way the series macro package is installed and removed to allow the compile time expansion of the series expressions inside get-ellipse-hulls
. Well, there are a few interesting parts here.
Let me know how this new lisp / article / whatever is working out for you on the Mastodon. Yes, I know I have been writing a lot to try and compress myself into the size of my several week old kitten.
Show at 0UTC on Wednesday as always.
screwlisp proposes kittens