Some rough notes/explanations on freezing and how it relates to optimizing the backend API. Eventually these notes should go into the user manual.
1 The issue
Most often one wants to be able to describe a diagram consisting of a collection of shapes, some of them obtained by transforming (e.g. rotating, shearing, scaling, translating) other shapes, and then draw the whole thing using a consistent line width. It would be strange if, say, a large circle was drawn with a thicker line just because it happened to be generated by scaling a smaller circle. Diagrams are supposed to be scale-invariant but in actuality it would be surprising for them to be truly scale-invariant with regards to line width.
Another way of thinking about it is that we want to think of diagrams as 'abstract geometric entities' (e.g. perfect circles, etc.) with the details of how they are actually drawn being a separate issue.
...On the other hand, sometimes we really do want, say, scaling a diagram to scale the line width proportionally! It really depends on the situation.
Note that other things in addition to line width may have the same issue, e.g. patterns/textures like gradients, fill patterns, etc. (once we add support for those).
2 Solution #1: transformations always affect attributes
One possible "solution" is to say, in essence, "To heck with thinking of diagrams as abstract geometric entities; transformations will always transform attributes, period." This forces the user to place attribute annotations carefully in order to produce the desired behavior. For example, in order to produce a scene consisting of variously scaled shapes, all drawn with a consistent line width, the user must apply the line width attribute after doing all the requisite scaling.
Put more simply, the following snippets of code would produce different output:
foo # scale 2 # lw 1
foo # lw 1 # scale 2
This is certainly possible, but has some drawbacks. Most notable is that if a user obtains several diagrams, each of which has already had a line width applied to it, it may be practically impossible to assemble them into a single diagram with consistent-looking lines.
3 Solution #2: 'freeze'
Another solution (the one currently adopted by diagrams) is to have a primitive operation called
freeze such that transformations normally do not affect attributes such as line width, but after applying
freeze, they do. Intuitively,
freeze can be thought of as taking a "snapshot" of the physical realization of a diagram, and from then on transformations apply to the realization/drawing rather than to the abstract geometric ideal underlying it.
For example, under this scheme
foo # scale 2 # lw 1 foo # lw 1 # scale 2
have the same meaning, but
foo # lw 1 # freeze # scale 2
4 Semantics of 'freeze'
5 Implementation of 'freeze'
6 Rough notes dumped here, need to edit
The reason this is difficult is that some of the transformations
- should* apply to styles and some *shouldn't*. But the
transformations and styles occur in the tree all mixed up. (Also, transformations can affect some attributes!) The way we handled this in the past was to never give any transformations directly to a backend. We only give *fully transformed primitives*. e.g. the cairo backend's implementation of withStyle works by *first* performing the given rendering operation under *no* transformations (since it is given fully transformed primitives), then apply just the transformations above freeze and do the actual stroking.
If we want to be able to apply transformations incrementally while we're walking down the tree this gets tricky. I suppose we could do
- inverse* transformations before doing the stroking corresponding to
the transformations *underneath* the freeze...
Even just continuing to fully apply the transformations to primitives and allowing styles to be applied early would go a long way, I think. We could also do some work to coalesce styles. e.g. consider
foo1 = hcat (map circle [1,2,3,4,5]) # fc blue
foo2 = hcat (map (fc blue . circle) [1,2,3,4,5])
It would be nice if these actually generated the same output, i.e. the programmer didn't have to worry about making such changes to their code in order to "optimize" it.