tittle/resources/cljs/tittle.cljs

225 lines
7.3 KiB
Clojure

(ns tittle)
(def turtle (atom {:theta 0 :x 0 :y 0 :pen :up :nib 3 :ink "blue"}))
(defn log-turtle!
"Prints the state of the turtle to the browser console."
[]
(.log js/console
(apply str
(cons "Turtle: "
(map #(str % " " (@turtle %) "; ") (keys @turtle))))))
(log-turtle!)
(def playing-field (.getElementById js/document "playing-field"))
(defn- not-a-number!
[n]
(throw (js/Error. (str "not a number: " n))))
(defn number-or-error!
"If `n` is a number, return it, else throw an exception."
[n]
(cond (.isNaN js/Number n) (not-a-number! n)
(number? n) n
:else (not-a-number! n)))
(defn sanitise-angle
"Take this `angle`, and return a number between 0 and 360 that represents
it as an angular measurement."
[angle]
(let [a (abs angle)
p (pos? angle)
v (cond (.isNaN js/Number a) 0
(< a 0.5) 0
(<= a 360) a
: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!
"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."
[angle]
(if (number? angle)
(swap! turtle assoc :theta
(sanitise-angle
(+ (:theta @turtle) angle)))
(not-a-number! angle))
(.info js/console (str "(turn! " angle
") :: :theta now " (:theta @turtle)))
(log-turtle!))
(defn turn-to!
"Turn the turtle to face `angle`, expressed in degrees with respect to the
X axis. If `angle` is not a number, throw an exception."
[angle]
(if (number? angle)
(swap! turtle assoc :theta (sanitise-angle angle))
(not-a-number! angle))
(.info js/console (str "(turn-to! " angle
") :: :theta now " (:theta @turtle))))
(defn pen-down!
"Put the turtle's pen down."
[]
(.info js/console "pen-down!")
(swap! turtle assoc :pen :down))
(defn pen-up!
"Lift the turtle's pen up"
[]
(.info js/console "pen-up!")
(swap! turtle assoc :pen :up))
(defn pen-down?
"Return `true` if the turtle's pen is down, else `false`."
[]
(let [v (= (@turtle :pen) :down)]
(.info js/console "(pen-down?) =>" v)
v))
(defn pen-up?
"Return `true` if the turtle's pen is not down, else `false`."
[]
(not (pen-down?)))
(defn move-to!
"Move the turtle absolutely to the coordinates `x`, `y`. If the
pen is down, create a line element."
[x y]
(.info js/console (str "(move-to! " x " " y ")"))
(when (map number-or-error! [x y])
(let [x' (:x @turtle)
y' (:y @turtle)]
(when (pen-down?)
(let [elt (.createElementNS js/document "http://www.w3.org/2000/svg" "line")
id (gensym "line")]
(.setAttribute elt "id" id)
(.setAttribute elt "x1" x')
(.setAttribute elt "y1" y')
(.setAttribute elt "x2" x)
(.setAttribute elt "y2" y)
(.setAttribute elt "stroke" (or (:ink @turtle) "blue"))
(.setAttribute elt "stroke-width" (or (:nib @turtle) 3))
(.appendChild playing-field elt)))
(swap! turtle assoc :x x :y y))))
(def ^:const pi 3.141592653589793)
(defn degrees->radians
"Return the equivalent, in radians, of this `angle` espressed in degrees"
[angle]
(let [v (when (number-or-error! angle)
(* angle (/ pi 180)))]
(.log js/console (str "(degrees->radians " angle ") => " v))
v))
(defn radians->degrees
"Return the equivalent, in degrees, of this `angle` espressed in radians"
[angle]
(let [v (when (number-or-error! angle)
(/ (* angle 180) pi))]
(.log js/console (str "(radians->degrees " angle ") => " v))
v))
(defn sin
"Return the sine of this `angle`, considered to be expressed in degrees."
[angle]
(.sin js/Math (degrees->radians angle)))
(defn cos
"Return the cosine of this `angle`, considered to be expressed in degrees."
[angle]
(.cos js/Math (degrees->radians angle)))
(defn move!
"Move the turtle forward on its current heading by `distance` units."
[distance]
(.info js/console (str "(move! " distance ")"))
(move-to! (+ (:x @turtle) (* distance (sin (:theta @turtle))))
(+ (:y @turtle) (* distance (cos (:theta @turtle))))))
(defn set-ink!
"Set the ink with which the turtle draws to this `colour`, which
should be a string representation of a colour known to CSS."
[colour]
(.log js/console (str "(set-ink! " colour ")"))
(when (string? colour)
(swap! turtle assoc :ink colour)))
(defn set-nib!
"Set the nib (width) of the pen to this `n`, which should be a number."
[n]
(when (number-or-error! n)
(swap! turtle assoc :nib n)))
(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]
(log-turtle!)
(when (> depth 0)
(pen-down!)
(set-ink! (if (<= depth 2) "green" "brown"))
(move! length)
(turn! curvature)
(draw-tree! (* length trunk-fraction)
(* left-branch branch-fraction)
(* right-branch branch-fraction)
curvature
branch-fraction
trunk-fraction
(dec depth))
(turn! (- 0 (+ curvature left-branch)))
(draw-tree! (* length trunk-fraction)
left-branch
right-branch
curvature
branch-fraction
trunk-fraction
(dec depth))
(turn! (+ left-branch right-branch))
(draw-tree! (* length trunk-fraction)
left-branch
right-branch
curvature
branch-fraction
trunk-fraction
(dec depth))
(turn! (- 0 right-branch))
(pen-up!)
(move! (- 0 length))))
(defn draw-polygon!
"Draw a regular polygon, with this number of `sides`, each of
this `side-length`."
[sides side-length]
(when {<= 3 sides 360}
(let [angle (/ 360 sides)]
(pen-down!)
(loop [side 0]
(turn! angle)
(move! side-length)
(when (< side sides)
(recur (inc side))))
(pen-up!))))