Description
This issue is a stub to discuss the design of a potential Style language feature.
Currently we have a fixed set of shape definitions that will get rendered as SVG code (Circle
, Line
, Rectangle
, etc.). The proposal is to allow a generic definition of the form
Element e = elementname {
attribute1: value1
attribute2: value2
...
}
These tag definitions would be rendered by a direct translation into the corresponding SVG code
<elementname attribute1=value1 attribute2=value2>
In some sense this is just an extension of the existing SVG passthrough functionality (with a similar set of warts).
Motivation
One of the original design goals for Penrose is that "There should be no inherent limit to visual sophistication." At present, there is quite a lot that can be expressed in the output format (SVG) but is not exposed through Penrose. Adding an Element
construct to Style provides an "escape hatch" that exposes essentially all of SVG to Penrose, much in the same way that the passthrough feature already exposes significant additional functionality. It would for instance enable us to specify filters, animations, etc., which are currently not available through the fixed set of shape definitions.
In principle this design also enables Penrose to generate arbitrary XML code, which need not conform to the SVG schema. For instance, one could immediately use Penrose to construct 3D diagrams in COLLADA format.
Finally, the design may also lighten the developer burden, in the sense that we do not have to expose such features in a one-by-one fashion. Moreover, it lightens the burden on the Penrose team to fully design and spec out system capabilities (gradients, animation, interactivity, etc.), enabling our user community to drive the development of new features in a more organic way. For instance, rather than trying to imagine what kinds of animation features and use cases might be relevant to users, we provide a generic mechanism that lets them specify whatever they want—albeit in a way that is low-level and perhaps not completely ergonomic. Common usage patterns can then be developed into more thoughtful language/system features as they emerge.
Of course, this design is not without some potential pitfalls—discussed below.
Nested Elements
SVG (and XML more generally) relies on the ability to specify nested tags. We already have support for nesting through the Group
shape:
shape G = Group {
shapes: [ shape1, shape2, … ]
}
Likewise, it would make sense to allow the proposed Element
definitions to be nested. For instance, the definitions
Element s1 = stop {
stopColor: #f00
offset: "0"
}
Element offset2 = animate {
attributeName: "offset"
values: ".1;.9;.1"
dur: "2s"
repeatCount: "indefinite"
}
Element s2 = stop {
stopColor: #fff
children: [ offset2 ]
}
Element s3 = stop {
stopColor: #00c
offset: "1"
}
Element C = linearGradient {
children: [ stop 1, stop2, stop3 ]
gradientTransform: "rotate(45)"
id: "myGradient"
}
would get rendered as the SVG code
<linearGradient id="myGradient">
<stop offset="0" stop-color="#f00"/>
<stop offset=".5" stop-color="#fff">
<animate attributeName="offset" values=".1;.9;.1" dur="2s" repeatCount="indefinite"/>
</stop>
<stop offset="1" stop-color="#00c"/>
</linearGradient>
producing an animated gradient like this:
Interaction with Optimization
A big challenge when designing anything for Penrose is that we are not simply building a renderer—we are also building an optimizer/layout engine. Here, a very legitimate concern is that attributes of the elements may be ignored by the optimizer. There are at least a couple paths we could take here:
- Forbid certain element types. For example, since we already have a native
Circle
shape that plays well with optimization, we could forbid the definitionElement C = circle
(which would otherwise generate a raw<circle>
element in the rendered SVG). This way, our handling of shape optimization is unchanged. A downside here, perhaps, is that we may lose some functionality inCircle
(e.g., using an animated gradient) unless this shape definition is also augmented to allowed childElements
. - Abandon shape-based library functions. We could also jettison all library functions that currently take a shape as an argument, and instead express constraints/objectives purely in terms of scalar attributes. E.g.,
tangentTo( circle1, circle2 )
would need to be written asensure norm( circle1.center - circle2.center ) == circle1.r + circle2.r
. We already moved somewhat in this direction with @liangyiliang's revision of the Style library to allow most functions to be invoked directly (rather than through shapes). However, in addition to making some common functions more verbose, this makes it a bit harder to do generic programming (e.g.,tangentTo( x.icons, y.icons )
, where the icons forx
andy
may range over a wide variety of shapes). - Don't optimize
Element
s. This is perhaps a nice middle ground between the two above: we simply provide no Style functions that define constraints/objectives forElement
s. So for instance, someone can writeElement C = circle
, but, similar to proposal (1) can't pass this element in to a function likedisjoint
. Similar to proposal (2), they can still write their own objectives/constraints in terms of the scalar attributes of the elements (e.g., the center and radius). This adds a bit of cruft to the language, since there are now two ways to specify a circle: one that can be optimized, and one that can't. But if this functionality is for "experts only," perhaps it's an ok middle ground. - Give some
Element
s special treatment. For instance, if a Style program statesdisjoint( x.element, y.element )
we could do inference on theelementname
(e.g., is it a circle?), sending it down the appropriate path if so. If attributes are specified that we do not support (e.g., the rotation of a rectangle), we can issue a warning that these attributes will be ignored for the purposes of layout. This path is perhaps the most "complete," but also requires the most development effort.
As much as this issue is important to resolve, it is a bit tangential to the main use case for Element
: incorporating "pass through" SVG features that we don't yet support (such as gradients and animations). The goal is not really to replace our basic shape definitions, which already work fine. (Though perhaps if there's some elegant solution, it's worth rolling shape definitions into this design someday down the line, for simplicity's sake.)
Implementation
Implementation of a basic prototype shouldn't actually be so hard. A first step is to copy/paste/modify the existing Group
shape, which already handles nesting. The main change that would need to be made to the parser is recognizing lines of the form
Element e = elementname
where elementname
is an arbitrary string (perhaps within some pattern), rather than one of a list of predefined shapes. Fortunately, we already do this kind of parsing for SVG passthrough (i.e., a field can have an arbitrary, unquoted name, without needing to be listed a priori).
Interactions and Other Issues
There does not seem to be much interaction between this feature and other parts of the system, nor would it seem to cause any ambiguity for the parser. At its core, we're essentially just defining one more shape. (This shape just happens to be particularly flexible.)
Similar to SVG passthrough, SVG code generated in this way may not validate. Rather than place the validation burden on the Penrose compiler, Style programmers can simply rely on existing tools that do XML validation. A "convenience feature," perhaps, would be to integrate such validators with the IDE—but this is a somewhat orthogonal issue.