When I first started working with the laser and finally got them coming together at the same time, I decided to have the letters of the title be shootable. A traditional bounds check (where you draw a rectangle around each letter and check if the point fired at is within the rectangle) would have worked, however, with these thin and swooping letters the box would have covered so much extra space I couldn’t imagine it being very fun to play. You shoot the middle of the o in John and suddenly the o disappears. Or look at that y, the bounding box would be huge. Anything anywhere near it would have counted. That would get old quick.
So then I figured a click event on the path or letter would know whether I was on the actual stroke of the letter (stroke in the writing letters sense of the word, not in the vector sense). This was great. I could fire the laser, and, using a well-timed delay, a moment later the letter would disappear as the beam “hit” it. Perfect. Timing that I only tested on one browser and one screen, what could go wrong?
A week later, I tried it on my other monitor, one whose pixel density was very different, and found out the laser moved slower. Now when you fired the laser, the letter would disappear well before the laser made it to the letter. I tried a couple of other browsers between each monitor and the only consistency was inconsistency.
I next multiplied the speed of my lasers (which was not the speed of light) by the pixel ratio of the screen. This fixed one browser on one monitor but broke a different browser on a different monitor. With the time delay between when a laser is fired and when an object could be hit, particularly if the object is moving, what I really needed to do was to calculate a hit at the point the laser hit or touched the object I was shooting at.
Again an overlapping box method would have made shooting the letters rather conspicuously bad, so I searched and searched for a solution that would allow me to check if a point was literally on a letter stroke or not. Finally I found isPointInFill() and all my problems were solved and I lived happily ever after… okay, that’s not true, but isPointInFill() did solve a lot of problems while only creating a marginal number of new ones.
First of all, I had to change the letters of my title from lovely letters rendered in a font into SVG shapes. This wasn’t too bad as I still had the layout of the title in a graphics program (I happen to like Affinity Designer, myself) so I exported it as SVG turning the letters into shapes and voila I had my targets. However, if I covered the entire page with an SVG the largest part of which was transparent, everything looked good and I could shoot out the letters, but I couldn’t click on anything behind it. Eventually I figured out that I could use the pointer-events CSS property to set the SVG element itself to pointer-events: none and set all of its children, path elements, to pointer-events: all.
Another issue was that some lines of the letters are really, really thin. And while I didn’t want users to fire nowhere near the letter and have it disappear, I also didn’t want them to try to hit a letter and have difficulty. That’s when I turned to isPointInStroke which works the same as isPointInFill, but checks the stroke of the SVG element. What? “The letters don’t have strokes,” you say? Well, that would be true if they weren’t transparent. This allowed me to expand the target area, while still keeping the aesthetic of the original design.
A third issue that came up, though came up later when I was working on the game mode, is that isPointInFill and isPointInStroke won’t work on a SVG group element. They only work on a drawn element/shape element. This probably prevents a bunch of significant performance issues, but it’s still annoying. The enemies I had planned would not be simple shapes. So I had to create a new shape behind them that would have the same area, but not show outside of them.
This also became an issue when I was working on the comets (you can regain two health if you shoot a comet). The animation for them was a sprite sheet animation that stepped between the frames as a background image. Without an SVG in place, I either needed to create a whole new collision detection system just for it or put it in an SVG. Neither was desirable, particularly because in my initial tests I couldn’t get the background-image sprite sheet to show up in SVG. Plus, I still would have needed an SVG shape to fix the targeting issue. Finally, I figured out I could put an SVG in front of the animation and set its fill to transparent. That worked like gray matter in a zombie trap.
However I’ll discuss the biggest and hardest issue to fix in Part 4: Why that’s a Horse of a Different Coordinate System!.
