Tuesday, August 14, 2018

The Naming of Places (Part 12): Map Interaction

As much as I'd like Dragons Abound to produce perfect maps every time, that's not a realistic goal.  Procedural generation can either be very complex and limited and get it right all the time, or be less complex and more creative and make some mistakes.  I prefer the latter approach.  This is particularly evident in naming, where I'd have to implement true “artificial intelligence" with a deep understanding of language, culture and history to have a program that created good names all the time.  For the most part, when Dragons Abound makes a poor choice, I'm happy to hit the “Generate" button again and just create a new map.  But there are lots of names on the Dragons Abound maps, so it's likely that one or two of them on every map are clunkers.  If I like a map it would be nice to just re-generate those names and keep the rest of the map.

One possibility is to import the map into an editor like Inkscape and manually fix the names.  I've always imagined this as the final stage for Dragons Abound maps that were going to be used for something (like a FRP campaign).  Bring the map into an editor and clean up any rough edges, tweak areas that don't look quite right, etc.  (Whether Inkscape can handle a Dragons Abound map is another question.)  But that's a bit cumbersome for day-to-day use.

Another possibility is to do something through the program parameters.  I have a vague intention to implement an option to read in feature names from a file (or user input), as a way to let a user provide the names for the map.  That's rather a lot of work for a marginal feature, and has some other drawbacks.  I could implement some sort of ad-hoc override, but again it seems like a lot of work that would probably end up being very finicky to use.

What I'd really like to be able to to do is just click on a name on a finished map and have the program generate a new name.  One drawback with this approach is that the new name might not fit onto the map in the same spot as the old name, e.g., if I replace the name “Fu" with “GenericNameVille" the new name might end up overlapping something else on the map.  So I'll need some way to deal with that problem, too.

Unlike Azgarr, whose map has always been interactiveDragons Abound has always been a batch process -- you click “Generate" and some time later a completed but static map pops up.  So adding interactivity requires some new framework.  Happily, Dragons Abound uses the d3js library, and d3js is really intended for interactive graphics, so it has built-in support for interacting with SVG.  Specifically, it has support for drag and drop, and pan and zoom.  For this effort, I'll be using drag and drop.

The d3js support for drag and drop is illustrated in this example, which I will try to embed here:


If this worked, you should be able to click on the circles and drag them around.

You can follow the link above to see the details of the code, but the basic notion is that I will attach an “event handler" to the SVG elements I want to be interactive (in this case, the text labels) and the event handler will then be called when certain events happen -- specifically when the user clicks on the element, drags the element around, and releases the mouse.  As it turns out, it's actually easier to implement “drag a label around the map" than “click for a new name" so will implement that first.  Drag & drop is pretty useful even without “click for a new name" because there's often one or two labels on the map that I'd like to tweak into a new position.

Every SVG element has a position, so to drag an element around the map, all I have to do is update the position of the element to be the position of the mouse.  The browser will take care of redrawing the element in its new position.  So as the mouse moves around and generates “drag" events, the event handler will change the position of the SVG element, the browser will redraw the element in the new position, and the element will follow the mouse around.  The event handler code to do this isn't much more than this:
function dragged(d) {
  d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
}
This takes the element that was clicked upon (“d3.select(this)") and sets its current position (cx, cy) to be the current position of the mouse (d3.event.x, d3.event.y).

Of course, it isn't quite that simple in practice.

One problem is that the position of a label is the center of the label, but the user can click anywhere on the label to select it.  So if I click (say) near the end of the label, the above code immediately updates the center of the label to be the position of the mouse -- so the label “jumps" to center itself under the mouse.  That's a little jarring.  The solution is to use the relative mouse movement -- if the use clicks and drags 15 pixels left, the label should move 15 pixels left of its current position, regardless of where on the label the user clicked.  As it turns out, the drag event also has the relative move (d3.event.dx, d3.event.dy) so this is easily implemented.

A more substantial problem is that the labels in Dragons Abound are not just simple SVG text elements.  Each label is actually three elements: the text element, a mask element, and a halo element.  This is evident when a label overlaps another map element:
You can see the masking and halo effects particularly at the right end of the name where it overlaps the city icon.  To make matters worse, because SVG doesn't support multi-line text, multi-line labels are actually collections of single line labels.  So drag and drop on labels has to move all of these different elements appropriately.  This is mostly just bookkeeping, but does complicate the code.

(Technical details:  The drag event handler only has access to the element clicked upon and the event itself.  So how can the handler access the elements that weren't clicked upon, such as the mask and the other elements in a multi-line label?  The answer is to create a specific drag event handler for each label as a Javascript closure that has access to all the elements of the label.)

Another problem is that some of the Dragons Abound labels are curved, which means they are set to follow an (invisible) SVG path element.  Changing the position of these elements only slides them along the path.  To move the label to a new position requires moving the underlying path.  That's harder than moving a simple element, so I'm going to leave that for another day!

I discovered a problem of another sort when my initial implementation didn't work at all.  None of the elements were responding to events.  Eventually I figured out that the map had a large (mostly transparent) image element covering the entire map (to add a paper texture to the map).  Clicking was selecting this element.  I had assumed that any element without an event handler would just ignore mouse clicks, but apparently that's not the case.  To keep that element from eating mouse events, you need to set the CSS pointer-events property to “none."

After working through those problems:
Responsiveness is not very good -- I assume because the complexity of the SVG makes the repainting of the label as it moves slow -- but it certainly works well enough!

Now let me get to the original point of all this -- to rename the label on a mouse click.  To distinguish renaming from dragging, I'll check to see whether the label has moved at all between the mouse down and the mouse up.  If it hasn't, then I can assume it was a mouse click indicating a renaming.

The first step is to create event handlers for “mouse down" and “mouse up" and get those working.  To start, I'll create a simple handler that turns the label outline red on mouse down and back to black on mouse up:
The next step is to keep track of the position of the label at the start and stop and see if I can distinguish a drag from a click.  Here I'm trying to turn the label red on a click:
This isn't completely obvious, but here I first drag and drop the label, and then I click on the label.  So it looks like that is working.

Now I have to see if I can replace the text when I detect a click.  Here's a test just replacing the label with a test string:
All that remains then is to call the proper name generator to create a new name:
Note that the new label is centered at the same spot as the original label.  Looks pretty good, but looking closely the “halo" (white blurred text behind the main text) looks like it isn't changing.  Poking through the code, I realize that I'm generating a new name for each of the three parts of the label.  Oops!  Better to generate one new name and use it three times:
So that works pretty well.  Except ... it doesn't really work for multi-line labels.  Suppose, for example, that I have a two line label “Serenity/Bay" and I generate a new name which is “Happy/Ocelot/Bay."  What do I do with the extra line?  And it's actually worse than that, because once I create a multi-line label, it's hard to know which SVG text elements belong to which line.

So back to the drawing board.  For the multi-line labels, the only approach that doesn't involve a lot of complicated bookkeeping is to delete the old label and replace it with a new label.  This is a little mind-bending --  the label creation function creates a label with an event handler that deletes the label and then calls the same label creation function to create a new label with an event handler that deletes the label...
But somehow it all works.  So now I have the ability to re-generate labels I don't like, and shift around labels that don't get placed perfectly.  I'm still no Azgarr, but a little bit of interactive functionality like this is very helpful for “tweaking" a map into shape.


1 comment:

  1. For performance issues with SVG, the most efficient method to speed things up is to not render the filters in edit mode. A Gaussian blur as used in the drop shadows eats processing power. Same goes for the many objects like mountains and trees, rendering only their outlines would probably help quite a bit.

    ReplyDelete