I’m developing a (rather cool, I think) website/app (it’s going to go live somewhere in the next few weeks, depending on how fast AAPL approves it) and as a part of that today I wanted to develop a circular countdown. A what? Well, essentially we needed a circular area to gradually ‘fill up’ clockwise until a certain time had been reached. In common speak, I was making a clock.
Back in the stone ages this would have involved 360 images each depicting the clock in a different ‘position’, but this is 2016 so I was sure I could do better.
I initially figured I’d use a canvas-element for this, but quickly found out I could do much better: SVG with a clipping filter. SVG allows you to define a clipping range with an arc. The syntax is pretty arcane (involving its own mini-language) but I managed to get it working quickly enough. It looks something like this:
<svg xmlns="http://www.w3.org/2000/svg" height="400" width="400">
<defs>
<clipPath id="wtf">
<path fill="#109618" stroke-width="1" stroke="#ffffff" d="M200 0 A 200 200 0 1 1 27 100 L 200 200 L 200 0"/>
</clipPath>
</defs>
<foreignObject height="400" width="400" clip-path="url(#wtf)">
<img width="400" height="400" src="/css/clock/light.svg">
</foreignObject>
</svg>
The exact syntax here isn’t important (Google it if you’re curious) but the M(ove), A(rc) and L(ine) specify commands, followed by a bunch of parameters. Yes it’s illegible, but it works. We then import an HTML image using <foreignObject>
and specify the path we defined as a clip-path
attribute (you’re going to understand why it’s called #wtf
in a minute ;)). This simply refers to the ID we put on the clipPath
SVG element, as the standard says “if you omit an actual URL, #id refers to something in the same document”.
(Actually, writing SVG in Angular involves a few more workarounds, but they’re not relevant for this problem. They were easily solved using mostly ng-attr-
prefixes.)
So yay, I have this working. It looks roughly like this:
As you can imagine, the lighter area gradually progresses over time (in this random test case, 24 hours).
This is an Angular app, so I created a component just to display the clock so I could reuse it like a good programmer boy. That’s when things started getting weird…
My test case was working, and the directive was working fine where I used it first (which happened to be the app’s home page). I then wanted to use it somewhere else (e.g. /foo/
) and the clipping filter (and any other filters for that matter) refused to apply. Okay, wtf?
I literally spent a few hours debugging this. I copied the SVG verbatim to my local testpage: it worked fine. Okay, so not an issue with the SVG being generated. Maybe some CSS causes it not to work? (I had a flashback to Explorer days of yore where some things would only work if an element had “display”, whatever that was. We just used to give it zoom: 1
to force IE to behave. If you have no idea what I’m talking about: consider yourself blessed.) Copied everything to an empty page on the same server, loaded the CSS and Javascript the actual site also loads: yup, still works. So no CSS issue.
Okay, next test. I actually whipped up a quick empty page in the actual application and tried to display the raw SVG (which I by now knew for a 100,000,000% certain was supposed to work). Now it fails again.
Did I make an error copy/pasting somewhere? Nope, the SVG is exactly the same, yet it fails on one URL but not on the other.
Then finally I saw it. There was one other thing that differed between my raw HTML test page and the page generated from the application. The application has <base href="/">
in the head. And indeed, the ‘failing’ page was also attempting to load the home page in the background.
For those not familiar with Angular, it needs a <base>
tag for routing to work. Normally not a big deal, but having this tag causes #id
to no longer refer to the current document, but to the document specified in the tag. So my clipping directive was trying to load something from /index.html
(where of course it couldn’t find it, since it was in /foo.html
).
I’m sure there’s a reason this works like it does, since I could reproduce it in Firefox, Safari and Chrome. (At least they were consistent :)). But seriously, why? Okay, I get that <base>
tells the browser to “consider every link in this page to be prepended with the href
value unless declared more explicitly”, and from that perspective it sort of makes sense, but it kinda conflicts with the definition of “if omitted refers to an element in the current document”. I mean, what’s it gonna be people? The current document or whatever’s specified in <base>
? Also, I tried working around this by using stuff like “url(./#wtf)” but that didn’t work.
Anywho, in the end I ended up just prefixing stuff with essentially window.location.href
, and it worked everywhere. But I’m frankly surprised such a major pitfall isn’t documented better anywhere. Hope this saves someone a few gray hairs someday 🙂