Welcome to the first installment of Tensegritics Curiosities our newsletter on ClojureDart, Clojure, algorithms and cool stuff!
Two topics today:
By default flutter
is globally installed, this causes issues as soon as you have two projects and you upgrade Flutter for one project, only to discover later your other project is now broken.
The standard answer is to use fvm
-- the Flutter version manager -- but then you have to prefix all flutter invocations by fvm
, including invocations you have no control over like the ones by cljd
.
Luckily direnv
comes to the rescue! direnv
is an utility which hooks into the shell to set environment variables depending on the current directory.
Once you have installed fvm
and direnv
, you can define flutter version per project:
cd path/to/my/new/or/existing/project
fvm use 3.13.1 #or whatever version you choose
echo "PATH_add .fvm/flutter_sdk/bin/" >> .envrc
direnv allow
You can check that flutter --version
returns now the expected version.
To use a different version for this project, you now only need to do fvm use 3.10.0
for example -- no need to touch direnv
anew.
Oh! I was going to forget: after changing Flutter version, always always always clean your project:
clj -M:cljd clean
flutter clean
flutter pub cache repair
Now you are good to go!
Many years ago I revisited in Clojure some classic algorithms (like Tarjan's SCC) and Michael Fogus suggested me to cover Bresenham's line algorithm -- which I never did until today!
Early 90s, as a teenager I wondered how lines were efficiently drawn on computers. Pre-internet and in a rural town, I couldn't get an answer. Actually more than lines, circles puzzled me because the only way I knew to draw circles involved a for
loop, cosine and sine to create a round-enough polygon or dense-enough sequence of pixels.
One day, bored away in front of a tiled floor, I realized that I could jump from one tile to the other following the simple rule that if I'm too close to the center I go to the tile right in front of me, else I go front and left (assuming the center was on my left). This could allow to draw one eighth of a circle, hence a full circle through symmetry!
This solution looked neat because it wouldn't produce polygons too coarse or unconnected pixels. However it used a square root for the distance: sqrt(x*x+y*y)<r
. Luckily we could just compare the squares instead: x*x+y*y<r2
where r2
is the squared radius.
It was good but it still had two expensive multiplications (the Z80 I used didn't even have a MUL
instruction).
Let's say we jumped on the front tile (y+1
) then the new squared distance is x*x+(y+1)*(y+1)
which expands into x*x+y*y+2*y+1
, which is the previous distance plus 2*y+1
! one increment, one bit shift, one addition instead of two multiplications! And the other case (going to x-1 y+1
) can be simplified too: in this case the square distance change by 2*(y-x+1)
.
Here is the Clojure code:
(ns t10s.curiosities.issue1
(:require ["package:flutter/material.dart" :as m]
["dart:ui" :as ui]
[cljd.flutter :as f]))
(defn main []
(f/run
(m/MaterialApp
.title "Welcome to Flutter"
.theme (m/ThemeData .primarySwatch m.Colors/pink))
.home
(m/Scaffold
.appBar (m/AppBar
.title (m/Text "Drawing circles with only + and <<")))
.body
(m/CustomPaint .painter
(reify :extends m/CustomPainter
(paint [_ canvas size]
(.translate canvas (/ (.-width size) 2) (/ (.-height size) 2))
; Obviously it's for educational purpose, on modern hardware it's counter-productive to do
; it like that! .drawPoints alone must be more complex than our circle computation
(loop [x 110 y 0 e2 0]
(when (<= y x)
(.drawPoints canvas ui/PointMode.points
[(m/Offset x y) (m/Offset (- x) y) (m/Offset (- x) (- y)) (m/Offset x (- y))
(m/Offset y x) (m/Offset y (- x)) (m/Offset (- y) (- x)) (m/Offset (- y) x)]
(doto (m/Paint) (.-strokeWidth! 1)))
(if (pos? e2)
(recur (dec x) (inc y) (+ e2 (* 2 (- y x -1))))
(recur x (inc y) (+ e2 (* 2 y) 1))))))
(shouldRepaint [_ _] false)))
(m/Container)))
Which displays as
Now that I knew how to draw a circle efficiently, I turned my attention to lines, but this will be for the next Curiosities issue!