Below are some thoughts on how to implement perspective distortions in Inkscape.
Goal 0: SVG compatibility. A perspective object must look the same in Batik.
Goal 1: A perspective object must be preserved as such and be always editable. That is, it must remember the original objects and the envelope parameters, and regenerate the view whenever any of the two changes. In other words, a perspective must be an object, not a one-time operation.
Goal 3: A perspective object must be a group that may include many objects. That is, I want to be able to select any number of objects and make them into a single perspective object with a common envelope. The objects thus combined should preserve their own fills, strokes, trasparency, etc.
The implementation consists of two new SPObjects, SPEnvelope and SPEnveloped.
- SPEnvelope is represented by a <g> element with these extension attributes:
sodipodi:role="inkscape:envelope" inkscape:envelope="<a representation of the envelope>"
- Suggestions as to the optimal representation of the envelope are solicited. At the very minimum, the envelope is four plain ("cusp") nodes that the enveloped objects are inscribed into. Ultimately, I want it to be able to store 1) four bezier nodes with handles, so that the edjes of the envelope may be made non-linear; and 2) a lattice (mesh) of more than four nodes, with all segments being beziers and with internal nodes having 4 handles each and non-corner edge nodes having 3 handles each. Even if we only implement simple four-node linear envelope, I want the envelope representation to be easily extensible to cover 1) and 2). (Note to Nathan: it may be worthwhile to add nodes that connect more than 2 segments into your new path representation. Even though SVG does not allow branching nodes, they may be necessary for editing meshes such as the distortion mesh described here, and probably in other situations too.)
- SPEnveloped is an object inside an SPEnvelope. I do not see how we could preserve other inkscape: abstractions alongside SPEnveloped, so I think only a <path> SVG object can be an SPEnveloped, and if you apply perspective to text, star, spiral, etc., they are converted to path first. Thus, we need the following attributes for this object:
sodipodi:role="inkscape:enveloped" d="<distorted path, regenerated automatically>" inkscape:original="<original path>"
- This is similar to the SPOffset object which also stores the original path and the offset radius and regenerates d=. The difference is that SPEnveloped must walk up to its ancestor(s) in order to find out the envelope and distort its path accordingly.
At the first stage, I think we can do without adding a new tool for perspective. Instead, the node tool will work just fine.
- You select some object(s) and give a command (from the menu or by a shortcut), "Perspective Envelope". At this point all non-paths are converted to paths, all selected objects are grouped, and the group becomes an SPEnvelope with a default rectangular envelope. The visible display does not change (except that the z-order of some objects may change due to grouping).
- Now you can scale, move, rotate etc. the envelope object with selector. This tool is not aware of SPEnvelope and treats it as any group, i.e. by adding a transform= attribute to the <g>.
- Now, with the envelope object selected, you switch to the node tool. That tool is aware of this object type and, recognizing it, constructs and displays a four-handle knotholder, similar to those used for tweaking spirals, stars, etc. You drag the corner handles, and the shapes are distorted accordingly. Later when we support non-linear envelopes, a nodepath of four nodes complete with their bezier controls may be created instead of a knotholder.
- When you have the perspective you need, you may de-envelope the object by the regular Convert to Paths command. It ungroups the <g>, applying its transform= to the individual objects just as the Ungroup command does now, and removes the inkscape:original and sodipodi:role attributes leaving you with a bunch of plain <path>s.
- There are a few issues when you select one or more objects _inside_ an envelope. We could attempt to prohibit this by disabling ctrl+click for SPEnvelope groups, but this will not prevent XML editor from selecting objects inside an envelope. So how should tools behave in that case?
- Selector and all other ways to move/transform objects (top panel, Align and Transform dialogs, etc) will treat the object as usual, figuring out its position and bounding box based on d=, and therefore will reflect the perspective-transformed path, not the original. However, unlike the normal <path>, transforms will be added to an SPEnveloped object as a transform= attribute, and the algorithm generating the distorted path must _first_ transform the inkscape:original path with this transform and _then_ apply the perspective to it. In other words, a user will see and transform the distorted path, but the resulting transform will be first applied to the original path, and then the transformed path will be perspective-distorted to yield the visible path. Whether this interface will be sufficiently well-behaving to be usable is an interesting question; I think there will be some "jumps" and "jerks" when you try to resize or move an enveloped object (maybe only if it's enveloped together with other objects and not alone), but how it will actually _feel_ is hard to say at this time.
- In node tool, it makes no sense to try to edit the d= of the enveloped path, because there's perhaps no simple way to translate these edits to the inkscape:original path. Therefore, I think the best thing we can do is to teach the node tool to handle SPEnveloped objects by generating a nodepath from their inkscape:original path instead of d=. This way, when you select an object inside an envelope and switch to the node tool, what you will see is a mismatch between the visible path and the nodepath nodes. You edit the nodepath nodes that show you the original undistorted path, and you see at once how the perspective-distorted path responds. I think that with proper explanations and hints, such an interface is logical and may be even convenient.
- The only problem is that currently, nodepath only displays its own nodes and bezier handles, while it does not display the line segments between the nodes (normally the object itself does that). Therefore when editing an object inside envelope, what you'll see instead of the original path is a collection of unconnected nodes "hanging in the air". To fix this, we must program a way to show "helper" non-AA bezier lines in addition to knots, ctrllines, and ctrlrects (there's even a comment somewhere in the code on this being a good idea). Later we can also use these ctrl-beziers, for example, to implement yet another selection cue mode in selector, whereby the outline (not bbox) of each selected object is "highlighted" by superimposing a ctrl-bezier on it.
- All other tools do not apply to the SPEnveloped objects because they can only be simple paths (no text, stars, etc.)