L-System Studio - Live Demo
An environment for creating and exploring L-Systems.
- 30+ presets
- Including the Koch Curve, Quadratic Gosper, and Square Sierpinski
- Unlimited iterations
- It uses lazy evaluation to generate, translate, and render L-Systems with
billionstrillionsinfinitely many symbols
- It uses lazy evaluation to generate, translate, and render L-Systems with
- 2D camera
- Supports an infinite canvas since you can position the camera anywhere you want
- Supports panning horizontally and vertically
- Supports zooming in and out
- Performant
- Render 1,000,000 instructions per frame at 60 frames per second
When hunorg/L-System-Studio was first submitted to Built with Elm I was excited to play with the application. However, my experience was tainted a bit because the application bogged down my browser when I tried rendering any of the interesting L-Systems for what I considered to be a very small number of iterations. Occassionally, the application would even fail completely. I also wasn't able to view the rendering properly since a 2D viewing pipeline wasn't available, i.e. world coordinates and canvavs coordinates coincided. I knew these shortcomings weren't inherent to Elm but rather to the way in which the application was built. These bad experiences prompted me to review the code and think about ways in which I could improve upon it.
I don't want performance at the expense of sanity. I want to be able to express what's in my mind in a modular way while still being able to achieve reasonable performance.
My first foray into the code led to me improving the L-System generation which I wrote about in Diary of an Elm Developer - Lazy L-System generation and Diary of an Elm Developer - Improving Rules.lookup. These initial successes drew me further into the project until I was fully consumed by it.
L-System generation was solved but I wasn't 100% certain that it would lead to improvements in translation and rendering. In order for the performance to be improved, both translation and rendering needed to be done lazily as well.
Translation involved keeping track of the turtle's state. It took me a while to figure it out but in the end I was able to keep translation lazy. Data.Translator.translate uses Lib.Sequence.filterMapWithState to map and filter lazily over a sequence while threading state.
Rendering involved dropping SVG and drawing on an HTML5 canvas instead. Due to the potential of a generated L-System to require a billion+ SVG nodes I knew I needed to switch to HTML5 canvas for output. I never used canvas before so this requirement sent me on an interesting and fruitful journey where I learned about HTML5 canvas, requestAnimationFrame, web components, the difference between JavaScript attributes and properties, the 2D viewing pipeline, and so much more. I explored a variety of questions and scenarios in separate branches before settling on my final solution.
| Branch | Experiment |
|---|---|
| svg-experiment | Is it really true that I can't use SVG? I ran into a bug when rendering a certain number of SVG/HTML nodes. |
| explore-using-a-web-component | Can I follow what joakin/elm-canvas did and use a web component? As I tried to increase the amount of instructions I rendered per frame I saw artifacts in the drawing which showed that instructions were being skipped on the first frame. Maybe it was a timing issue but I wasn't able to resolve it. |
| canvas-experiment | Can I stream drawing commands to the canvas? Yes! The port solution is correct and performant. I can crank up the amount of instructions I render per frame and I see no visual anomalies. |
I settled on streaming drawing instructions via ports to an HTML5 canvas.
The rest of the work wasn't specific to L-Systems so I won't go into that here.
These are some of the things I currently do to improve canvas performance:
- I batch canvas calls together
- If
frames per second (fps) = 30andinstructions per frame (ipf) = 10then300 instructions per second (ips)would be sent to the canvas to be drawn - Since I use
requestAnimationFrameto coordinate the drawing it distributes that many instructions over the one second based on the time elapsed between calls torequestAnimationFrame - So in reality, if
0.5 secondselapsed between calls torequestAnimationFramethen150 instructionswould be sent to the canvas over the port to be drawn as a single polyline
- If
- I clear the canvas using
ctx.clearRect(0, 0, width, height) - I avoid floating point coordinates
- World coordinates use floats but device (canvas) coordinates use integers, see
Data.Transformer
- World coordinates use floats but device (canvas) coordinates use integers, see
- I optimize my animations with
requestAnimationFrame
The application is nowhere near complete but it satisfies my goals of being performant and allowing me to explore interesting L-Systems without having the browser crash on me. That said here are a few things that could be improved:
- The generated L-System string may have multiple forward movements one after the other
- These can be combined into one forward movement of a larger length
- Currently 10 consecutive forward movements would lead to 10 line drawing instructions
- When the view transformation is performed, multiple world coordinates get mapped to the same canvas coordinates in succession
- The repeats can be removed
- Lines entirely outside the canvas can be skipped so as to avoid sending them over the port to the canvas only to be clipped by the canvas
- Drawing lines of even width, i.e.
lineWidthis an even number, so that it looks good on the canvas- Currently, I use this neat idea to get my
1pxwidth lines to be drawn using1pxwithout anti-aliasing effects - How to draw very thin lines?
- HTML5 Canvas translate(0.5,0.5) not fixing line blurryness
- How can I generalize it to work for any line width?
- Currently, I use this neat idea to get my
- Add color support
- Design a nicer UI
I'm equally excited about the future work that this project generated for me as well.
- Extract an L-System rules and generator module, see
Data.RulesandData.Generator - Extract a 2D camera module, see
Data.Transformer - Extract an animation timing module for regulating work to be done within a given number of frames per second, see
Lib.TimerandData.Renderer - Explore using Taylor series expansion to calculate sine and cosine to arbitrary precision
- Explore
Lib.Field- It seems to be a promising abstraction upon which to base a form library
This project is managed with Devbox and a few Bash scripts. Enter the development environment with devbox shell.
build-development # alias: b
build-productionserve-development # alias: s
serve-productiondeploy-productionformat # alias: fIndividually:
check-scripts
test-elm # alias: t
test-elm-main
review # alias: rAll at once:
check- hunorg/L-System-Studio is the original application upon which this one is based
- Paul Bourke's L-System User Notes contains useful information on L-Systems and a collection of examples that I used for my presets
- YouTube: Why Functional Programming Matters by John Hughes at Functional Conf 2016
- This video reminded me of the benefits of lazy evaluation and re-introduced me to the concept of whole value programming
- It played a major role in having me pursue improvements to the application through a lazy lens
- Computer Graphics 2nd Edition by Donald Hearn and Pauline M. Baker (1994)
- "Chapter 6: Two-Dimensional Viewing" helped me learn how to set up a 2D viewing pipeline which led to the infinite canvas, panning, and zooming features
- I learned how to go from world coordinates to viewing coordinates to normalized viewing coordinates to device (canvas) coordinates
- MDN: Canvas tutorial
- YouTube: Jake Archibald on the web browser event loop, setTimeout, micro tasks, requestAnimationFrame, ...
