Documentation and tidy up.

This commit is contained in:
Simon Brooke 2025-08-30 16:13:44 +01:00
parent a4c64297f0
commit 1b5aeeb52e
3 changed files with 199 additions and 60 deletions

100
README.md
View file

@ -8,10 +8,108 @@ See [Papert, S: Mindstorms](https://www.worldofbooks.com/en-gb/products/mindstor
![Polygons generated with Tittle](polygons.svg) ![Polygons generated with Tittle](polygons.svg)
As a proof of concept, this kind of proves the concept; but it doesn't work, the trigonometry functions are pretty badly broken. As a proof of concept, this proves the concept. There's a lot more to do.
I have not (yet) managed to get a scittle REPL running. I have not (yet) managed to get a scittle REPL running.
Clojure (and thus Scittle) is a fairly pure functional language. It's not good at imperative things, although of course it can do them. Turtle Graphics is by its nature imperative: you're commanding the turtle. So this is not a very idiomatic use of Scittle, and it's not very efficient; it invites you to write recursive functions (and of course I wish to encourage you to write recursive functions), but recursive functions in Scittle are not fast.
## Usage
At this stage, edit `index.html`
### Functions
Generally speaking, functions whose names end in exclamation points are imperative functions: they change the state of things by side-effect, and the return value may not be particularly relevant. I would normally consider imperative functions bad practice in Clojure.
#### (cos *angle*)
Return the cosine of this `angle`, considered to be expressed in degrees.
#### (degrees->radians *angle*)
Return the equivalent, in radians, of this `angle` espressed in degrees.
#### (draw-polygon! *sides* *side-length*)
Draw a regular polygon, with this number of `sides`, each of this `side-length`.
#### (draw-tree *length* *left-branch* *right-branch* *curvature* *branch-fraction* *trunk-fraction* *depth*)
Draw a tree. This is a fairly crude tree-drawing algorithm; there's lots of ways it can be improved, consider it a plac to start. Parameters (all numeric) as follows:
* `length`: the length of the current segment of the tree;
* `left-branch`: the amount to turn left, with respect to the parent segment, to draw a left branch, in degrees;
* `right-branch`: the amount to turn right, with respect to the parent segment, to draw a right branch, in degrees;
* `curvature`: the amount to turn to draw the next segment of the current stem, in degrees;
* `branch-fraction`: the length of a branch segment, as a proportion of the length of its parent segment;
* `trunk-fraction`: the length of the next segment of a stem, as a proportion of the length of its parent segment;
* `depth`: the maximum depth of recursion to allow.
#### (log-turtle!)
Prints the state of the turtle to the browser console.
#### (move! *distance*)
Move the turtle forward on its current heading by `distance` units.
**NOTE THAT** this function invokes `move-to!`, q.v., which will create a
line element if the pen is down.
#### (move-to! *x* *y*)
Move the turtle absolutely to the coordinates `x`, `y`. If the pen is down, create a line element.
**NOTE THAT** this is the only function which creates new graphical elements.
#### (number-or-error! *n*)
If `n` is a number, return it, else throw an exception.
#### (pen-down!)
Put the turtle's pen down (i.e., cause line segments to be created).
#### (pen-down?)
Return `true` if the turtle's pen is down, else `false`.
#### (pen-up!)
Lift the turtle's pen up (line segments will not be created).
#### (pen-up?)
Return `true` if the turtle's pen is not down, else `false`.
#### (radians->degrees *angle*)
Return the equivalent, in degrees, of this `angle` espressed in radians.
#### (sanitise-angle *angle*)
Take this `angle`, and return a number between 0 and 360 that represents it as an angular measurement.
#### (set-ink! *colour*)
Set the ink with which the turtle draws to this `colour`, which should be a string representation of a colour known to CSS.
#### (sin *angle*)
Return the sine of this `angle`, considered to be expressed in degrees.
#### (turn! *angle*)
Turn the turtle clockwise by this `angle`, expressed in degrees with respect to the X axis. If `angle` is not a number, throw an exception.
#### (turn-to! *angle*)
Turn the turtle to face `angle`, expressed in degrees with respect to the X axis. If `angle` is not a number, throw an exception.
**TODO: Note that 180° is currently straight up; this is not intended, it is intended that 0° should be straight up, and this change will be made
before version 1.0.
## Licence ## Licence
Copyright © Simon Brooke 2025 Copyright © Simon Brooke 2025

View file

@ -18,16 +18,19 @@
<script type="application/x-scittle"> <script type="application/x-scittle">
(def turtle (atom {:theta 0 :x 0 :y 0 :pen :up :ink "blue"})) (def turtle (atom {:theta 0 :x 0 :y 0 :pen :up :ink "blue"}))
(defn log-turtle! [] (defn log-turtle!
[]
(.log js/console (.log js/console
(apply str (apply str
(map #(str % " " (@turtle %) "; ") (keys @turtle))))) (cons "Turtle: "
(map #(str % " " (@turtle %) "; ") (keys @turtle))))))
(log-turtle!) (log-turtle!)
(def playing-field (.getElementById js/document "playing-field")) (def playing-field (.getElementById js/document "playing-field"))
(defn- not-a-number! [n] (defn- not-a-number!
[n]
(throw (js/Error. (str "not a number: " n)))) (throw (js/Error. (str "not a number: " n))))
(defn number-or-error! (defn number-or-error!
@ -46,7 +49,6 @@
v (cond (.isNaN js/Number a) 0 v (cond (.isNaN js/Number a) 0
(< a 0.5) 0 (< a 0.5) 0
(<= a 360) a (<= a 360) a
;; TODO: `rem` is possibly wrong when we get into negative numbers
:else (loop [r a] :else (loop [r a]
(if (<= r 360) r (if (<= r 360) r
(recur (- r 360))))) (recur (- r 360)))))
@ -58,7 +60,6 @@
"Turn the turtle clockwise by this `angle`, expressed in degrees with "Turn the turtle clockwise by this `angle`, expressed in degrees with
respect to the X axis. If `angle` is not a number, throw an exception." respect to the X axis. If `angle` is not a number, throw an exception."
[angle] [angle]
(.info js/console (str "(turn! " angle ")"))
(if (number? angle) (if (number? angle)
(swap! turtle assoc :theta (swap! turtle assoc :theta
(sanitise-angle (sanitise-angle
@ -70,9 +71,12 @@
(defn turn-to! (defn turn-to!
"Turn the turtle to face `angle`, expressed in degrees with respect to the "Turn the turtle to face `angle`, expressed in degrees with respect to the
X axis. If `angle` is not a number, throw an exception." X axis. If `angle` is not a number, throw an exception.
**TODO: Note that 180&deg; is currently straight up; this is not intended, it
is intended that 0&deg; should be straight up, and this change will be made
before version 1.0."
[angle] [angle]
(.info js/console (str "(turn-to! " angle ")"))
(if (number? angle) (if (number? angle)
(swap! turtle assoc :theta (sanitise-angle angle)) (swap! turtle assoc :theta (sanitise-angle angle))
(not-a-number! angle)) (not-a-number! angle))
@ -80,13 +84,13 @@
") :: :theta now " (:theta @turtle)))) ") :: :theta now " (:theta @turtle))))
(defn pen-down! (defn pen-down!
"Put the turtle's pen down." "Put the turtle's pen down (i.e., cause line segments to be created)."
[] []
(.info js/console "pen-down!") (.info js/console "pen-down!")
(swap! turtle assoc :pen :down)) (swap! turtle assoc :pen :down))
(defn pen-up! (defn pen-up!
"Lift the turtle's pen up" "Lift the turtle's pen up (line segments will not be created)."
[] []
(.info js/console "pen-up!") (.info js/console "pen-up!")
(swap! turtle assoc :pen :up)) (swap! turtle assoc :pen :up))
@ -104,7 +108,11 @@
(not (pen-down?))) (not (pen-down?)))
(defn move-to! (defn move-to!
"Move the turtle absolutely to the coordinates `x`, `y`." "Move the turtle absolutely to the coordinates `x`, `y`. If the
pen is down, create a line element.
**NOTE THAT** this is the only function which creates new graphical
elements."
[x y] [x y]
(.info js/console (str "(move-to! " x " " y ")")) (.info js/console (str "(move-to! " x " " y ")"))
(when (map number-or-error! [x y]) (when (map number-or-error! [x y])
@ -112,14 +120,14 @@
y' (:y @turtle)] y' (:y @turtle)]
(when (pen-down?) (when (pen-down?)
(let [elt (.createElementNS js/document "http://www.w3.org/2000/svg" "line") (let [elt (.createElementNS js/document "http://www.w3.org/2000/svg" "line")
id (gensym "line")] id (gensym "line")]
(.setAttribute elt "id" id) (.setAttribute elt "id" id)
(.setAttribute elt "x1" x') (.setAttribute elt "x1" x')
(.setAttribute elt "y1" y') (.setAttribute elt "y1" y')
(.setAttribute elt "x2" x) (.setAttribute elt "x2" x)
(.setAttribute elt "y2" y) (.setAttribute elt "y2" y)
(.setAttribute elt "stroke" (or (:ink @turtle) "blue")) (.setAttribute elt "stroke" (or (:ink @turtle) "blue"))
(.appendChild playing-field elt))) (.appendChild playing-field elt)))
(swap! turtle assoc :x x :y y)))) (swap! turtle assoc :x x :y y))))
(def ^:const pi 3.141592653589793) (def ^:const pi 3.141592653589793)
@ -143,16 +151,12 @@
(defn sin (defn sin
"Return the sine of this `angle`, considered to be expressed in degrees." "Return the sine of this `angle`, considered to be expressed in degrees."
[angle] [angle]
(let [v (.sin js/Math (degrees->radians angle))] (.sin js/Math (degrees->radians angle)))
(.log js/console (str "(sin " angle ") => " v))
v))
(defn cos (defn cos
"Return the cosine of this `angle`, considered to be expressed in degrees." "Return the cosine of this `angle`, considered to be expressed in degrees."
[angle] [angle]
(let [v (.cos js/Math (degrees->radians angle))] (.cos js/Math (degrees->radians angle)))
(.log js/console (str "(cos " angle ") => " v))
v))
(defn move! (defn move!
"Move the turtle forward on its current heading by `distance` units." "Move the turtle forward on its current heading by `distance` units."
@ -170,6 +174,22 @@
(swap! turtle assoc :ink colour))) (swap! turtle assoc :ink colour)))
(defn draw-tree! (defn draw-tree!
"Draw a tree. This is a fairly crude tree-drawing algorithm;
there's lots of ways it can be improved, consider it a place
to start. Parameters (all numeric) as follows:
`length`: the length of the current segment of the tree;
`left-branch`: the amount to turn left, with respect to the
parent segment, to draw a left branch, in degrees;
`right-branch`: the amount to turn right, with respect to the
parent segment, to draw a right branch, in degrees;
`curvature`: the amount to turn to draw the next segment of
the current stem, in degrees;
`branch-fraction`: the length of a branch segment, as a
proportion of the length of its parent segment;
`trunk-fraction`: the length of the next segment of a stem,
as a proportion of the length of its parent segment;
`depth`: the maximum depth of recursion to allow."
[length left-branch right-branch curvature branch-fraction trunk-fraction depth] [length left-branch right-branch curvature branch-fraction trunk-fraction depth]
(log-turtle!) (log-turtle!)
(when (> depth 0) (when (> depth 0)
@ -200,11 +220,13 @@
branch-fraction branch-fraction
trunk-fraction trunk-fraction
(dec depth)) (dec depth))
(turn! (- 0 right-branch curvature)) (turn! (- 0 right-branch))
(pen-up!) (pen-up!)
(move! (- 0 length)))) (move! (- 0 length))))
(defn draw-polygon! (defn draw-polygon!
"Draw a regular polygon, with this number of `sides`, each of
this `side-length`."
[sides side-length] [sides side-length]
(when {<= 3 sides 360} (when {<= 3 sides 360}
(let [angle (/ 360 sides)] (let [angle (/ 360 sides)]
@ -219,11 +241,16 @@
(log-turtle!) (log-turtle!)
(pen-up!) (pen-up!)
(log-turtle!) (log-turtle!)
(move-to! 350 350) (move-to! 350 700)
(log-turtle!) (log-turtle!)
(turn-to! 180) (turn-to! 180)
;; (draw-tree! 100 70 60 5 0.25 0.7 3) (draw-tree! 100 20 16 8 0.2 0.8 8)
(doall (map #(draw-polygon! % 100) (range 3 20))) (set-ink! "blue")
(doall (map #(let [v (* 16 (mod % 16))
c (str "rgb(" v "," v "," (- 256 v)")")]
(set-ink! c)
(draw-polygon! % 100))
(range 3 16)))
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,17 +1,21 @@
(ns tittle) (ns tittle)
(def turtle (atom {:theta 0 :x 0 :y 0 :pen :up :ink "blue"})) (def turtle (atom {:theta 0 :x 0 :y 0 :pen :up :ink "blue"}))
(defn log-turtle! [] (defn log-turtle!
"Prints the state of the turtle to the browser console."
[]
(.log js/console (.log js/console
(apply str (apply str
(map #(str % " " (@turtle %) "; ") (keys @turtle))))) (cons "Turtle: "
(map #(str % " " (@turtle %) "; ") (keys @turtle))))))
(log-turtle!) (log-turtle!)
(def playing-field (.getElementById js/document "playing-field")) (def playing-field (.getElementById js/document "playing-field"))
(defn- not-a-number! [n] (defn- not-a-number!
[n]
(throw (js/Error. (str "not a number: " n)))) (throw (js/Error. (str "not a number: " n))))
(defn number-or-error! (defn number-or-error!
@ -22,23 +26,29 @@
:else (not-a-number! n))) :else (not-a-number! n)))
(defn sanitise-angle (defn sanitise-angle
"Take this `angle`, and return a number between 0 and 360 that represents
it as an angular measurement."
[angle] [angle]
(let [v (cond (.isNaN js/Number angle) 0 (let [a (abs angle)
(< (abs angle) 0.5) 0 p (pos? angle)
(< (abs angle) 360) angle v (cond (.isNaN js/Number a) 0
:else (rem 360 angle))] (< a 0.5) 0
(.log js/console (str "(sanitise-angle " angle ") -> " v)) (<= a 360) a
v)) :else (loop [r a]
(if (<= r 360) r
(recur (- r 360)))))
v' (if p v (- 360 v))]
(.log js/console (str "(sanitise-angle " angle ") -> " v'))
v'))
(defn turn! (defn turn!
"Turn the turtle clockwise by this `angle`, expressed in degrees with "Turn the turtle clockwise by this `angle`, expressed in degrees with
respect to the X axis. If `angle` is not a number, throw an exception." respect to the X axis. If `angle` is not a number, throw an exception."
[angle] [angle]
(.info js/console (str "(turn! " angle ")"))
(if (number? angle) (if (number? angle)
(swap! turtle assoc :theta (swap! turtle assoc :theta
(sanitise-angle (sanitise-angle
(rem 360 (+ (:theta @turtle) angle)))) (+ (:theta @turtle) angle)))
(not-a-number! angle)) (not-a-number! angle))
(.info js/console (str "(turn! " angle (.info js/console (str "(turn! " angle
") :: :theta now " (:theta @turtle))) ") :: :theta now " (:theta @turtle)))
@ -48,7 +58,6 @@
"Turn the turtle to face `angle`, expressed in degrees with respect to the "Turn the turtle to face `angle`, expressed in degrees with respect to the
X axis. If `angle` is not a number, throw an exception." X axis. If `angle` is not a number, throw an exception."
[angle] [angle]
(.info js/console (str "(turn-to! " angle ")"))
(if (number? angle) (if (number? angle)
(swap! turtle assoc :theta (sanitise-angle angle)) (swap! turtle assoc :theta (sanitise-angle angle))
(not-a-number! angle)) (not-a-number! angle))
@ -80,7 +89,8 @@
(not (pen-down?))) (not (pen-down?)))
(defn move-to! (defn move-to!
"Move the turtle absolutely to the coordinates `x`, `y`." "Move the turtle absolutely to the coordinates `x`, `y`. If the
pen is down, create a line element."
[x y] [x y]
(.info js/console (str "(move-to! " x " " y ")")) (.info js/console (str "(move-to! " x " " y ")"))
(when (map number-or-error! [x y]) (when (map number-or-error! [x y])
@ -119,16 +129,12 @@
(defn sin (defn sin
"Return the sine of this `angle`, considered to be expressed in degrees." "Return the sine of this `angle`, considered to be expressed in degrees."
[angle] [angle]
(let [v (.sin js/Math (degrees->radians angle))] (.sin js/Math (degrees->radians angle)))
(.log js/console (str "(sin " angle ") => " v))
v))
(defn cos (defn cos
"Return the cosine of this `angle`, considered to be expressed in degrees." "Return the cosine of this `angle`, considered to be expressed in degrees."
[angle] [angle]
(let [v (.cos js/Math (degrees->radians angle))] (.cos js/Math (degrees->radians angle)))
(.log js/console (str "(cos " angle ") => " v))
v))
(defn move! (defn move!
"Move the turtle forward on its current heading by `distance` units." "Move the turtle forward on its current heading by `distance` units."
@ -146,6 +152,22 @@
(swap! turtle assoc :ink colour))) (swap! turtle assoc :ink colour)))
(defn draw-tree! (defn draw-tree!
"Draw a tree. This is a fairly crude tree-drawing algorithm;
there's lots of ways it can be improved, consider it a place
to start. Parameters (all numeric) as follows:
`length`: the length of the current segment of the tree;
`left-branch`: the amount to turn left, with respect to the
parent segment, to draw a left branch, in degrees;
`right-branch`: the amount to turn right, with respect to the
parent segment, to draw a right branch, in degrees;
`curvature`: the amount to turn to draw the next segment of
the current stem, in degrees;
`branch-fraction`: the length of a branch segment, as a
proportion of the length of its parent segment;
`trunk-fraction`: the length of the next segment of a stem,
as a proportion of the length of its parent segment;
`depth`: the maximum depth of recursion to allow."
[length left-branch right-branch curvature branch-fraction trunk-fraction depth] [length left-branch right-branch curvature branch-fraction trunk-fraction depth]
(log-turtle!) (log-turtle!)
(when (> depth 0) (when (> depth 0)
@ -176,11 +198,13 @@
branch-fraction branch-fraction
trunk-fraction trunk-fraction
(dec depth)) (dec depth))
(turn! (- 0 right-branch curvature)) (turn! (- 0 right-branch))
(pen-up!) (pen-up!)
(move! (- 0 length)))) (move! (- 0 length))))
(defn draw-polygon! (defn draw-polygon!
"Draw a regular polygon, with this number of `sides`, each of
this `side-length`."
[sides side-length] [sides side-length]
(when {<= 3 sides 360} (when {<= 3 sides 360}
(let [angle (/ 360 sides)] (let [angle (/ 360 sides)]
@ -191,13 +215,3 @@
(when (< side sides) (when (< side sides)
(recur (inc side)))) (recur (inc side))))
(pen-up!)))) (pen-up!))))
(log-turtle!)
(pen-up!)
(log-turtle!)
(move-to! 500 500)
(log-turtle!)
(turn-to! 180)
;; (draw-tree! 100 70 60 5 0.25 0.7 3)
;; (map #(draw-polygon! % 100) (range 3 20))
(draw-polygon! 3 100)