Firefox’s SVG support is pretty good, but it has one irritating shortcoming: it’s impossible to select text. Being able to select and copy text from slides is an important feature for us to have, so it was necessary to come up with a workaround. I decided to copy all of the text from the SVG into invisible <div>s that were placed on top of the SVG. It would appear the same to the user, but whenever they attempted to select text they’d actually be interacting with HTML elements instead of SVG elements.
Unfortunately, elements inside of an SVG document don’t use the same coordinate system as the rest of the HTML document. In fact, they don’t even use a single consistent coordinate system. For example, you can add the property transform=”scale(0.5)” to reduce the size of an element and all its contents by one half. This has the effect that an element with width=”200″ inside of this element would be equivalent to an element with width=”100″ outside of it.
The other most common transformation is translate(x, y), which shifts an element and its contents by the specified amount. The two accounted for almost all of the transformations I encountered in files I was working with, so they’re the ones I focused on addressing. It’s also possible to rotate() or skew() the coordinate system, but handling those would make things many times more complicated for relatively little payoff.
When I started working on this, I quickly realized another issue: if you allow the SVG document to be resized, for example to fit to the size of the window, this causes the relationship between its internal coordinate system and its external dimensions to be inconsistent. If you make an <img> wider without making it taller, the image will be stretched to fit. If you make an <svg> wider without making it taller, it will just increase its internal width and shift its contents so they remain centred. I could have a <text> element at (0, 0) and position a <div> on top of relative to the SVG, but if I stretch the SVG the <text> element may move to (100, 0) while the <div> remains fixed.
I addressed this by making sure that the SVG elements maintained their aspect ratio when they were resized. As long as I did this and defined the coordinates in terms of percentages of the SVG’s overall dimensions, things would be fine. However, there’s no built-in way to force an SVG to maintain a fixed aspect ratio like there is for <img>s. My solution generates something like this:
<div style=”position: relative;”>
<img src=”data:…” style=”width: 100%; height: auto; display: block;” />
<svg style=”position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%;”>…</svg>
I use a canvas to generate a transparent image with the same dimensions as the SVG initially had. The image is scaled to fit the available width; its height will automatically adjust to maintain the same aspect ratio. This causes the dimensions of the parent <div> to be stretched to match. I have the <svg> absolutely positioned to fill this <div>, causing it to match the dimensions of the <img>, causing it to maintain a fixed aspect ratio. Any elements I want to position relative to the SVG (the text overlays) can also be positioned relative to the <div>.
This works, thankfully. Being forced to recalculate all of the coordinates whenever the window was resized would have been obnoxiously laggy in many cases.
Within the SVG document, each element’s coordinates and dimensions are defined relative to a “viewport” element. In our case this will be the closest ancestor that defines a transformation or, if no ancestor is transformed, the root SVG element.
There may not have been a function to get the coordinates and dimensions of an element relative the HTML document, or even relative to the root SVG element, but if there were at least some way to find the coordinates and dimensions relative to the viewport then it would just be a matter of stepping up through the viewport, the viewport’s viewport, the viewport’s viewport’s viewport and so on, using the internal and external dimensions and coordinates of each element to work out the linear relationship between their coordinate systems and iteratively applying these relationships until you’re left with the relationship between the element and the root SVG.
At first, I thought I had found such a function: element.getBBox(). Playing with it in the console, it gave me the values that looked like they could be right, and I wrote the rest of the logic assuming they were. When I finally ran the code, it sort-of worked: on some slides, the text would be positioned perfectly. However, on others it would be too low, too high, or stretched awkwardly.
It turns out that getBBox() wasn’t returning the actual dimensions and coordinates of the element itself, but of a bounding box containing all of the rendered elements inside of the element. If you have a giant <g> element but the only renderable element it contained was a tiny <rect>, you’d end up with dimensions that were too small. If you had a tiny <g> element containing a <rect> that was far larger than it was, you’d end up dimensions that were too large.
I couldn’t find an elegant solution to this. What I ended up doing was, instead of applying getBBox() to the element itself, I created a <rect> inside of the element, gave it the necessary attributes so that it would have the same coordinates and dimensions as the element (x=”0″ y=”0″ width=”100%” height=”100%”), and then applied getBBox() to this rect instead. Because the <rect> was a renderable element and has no children, its bounding box would always be exactly the same as its coordinate and dimensions, which would be exactly the same as those of the element I was actually interested in. The rect was still considered renderable even if it had fill=”none” and stroke=”none”, so I left these elements in the <svg> instead of removing them. (If I removed them I’d need to re-add them the next time I needed to find the coordinates of the element, which would be slower and would happen several times for most elements, since they’d be the ancestors of multiple text elements.)
After implement this, everything finally worked! The hacky workarounds left me feeling a little dirty, my relief and satisfaction were enough to squash that. If you’re interested, you can find all of this code and a few other SVG-related functions in /js/svg-container.js.