Dashed lines

I was implementing word hovering behavior with my text system and I was trying to copy how Artifact’s looks, and it made realize I didn’t have a proper dashed line drawing function. My previous function had this signature:

function layer:dashed_line(x1, y1, x2, y2, dash_size, gap_size, color, line_width, z)

Which I believe is correct. When defining a dashed line, you want to define the line’s two points, and you also want to define the dash and gap sizes. The problem was with how the function worked.

It naively started from the first point and added a dash, then it jumped the gap size, then it added another dash, and so on until it reached the second point. The issue is that dashed lines created like this will often have the end on the second point not be finished on a dash of the given dash size. This looks incorrect:

Notice how the end of the moving line never has a dash on it. And notice how the right ends of the underlines also never have a dash. Compare this to when it looks right:

I believe the solution Artifact uses is to center all dashes by the remaining amount on the right side so that it looks less jarring:

Artifact’s solution is probably the best one, but I decided to do something different and to instead have dashes drawn on both ends, and then fill the rest of the inner line with dashes and gaps that are appropriately spaced apart.

The disadvantage of this approach is that the gap size chosen by the user becomes more of a hint, as spacing dashes by equal gap sizes while having both ends start on dashes means that for line lengths which don’t neatly divide by dash + gap + dash pairs, it wouldn’t look right. Therefore, the procedure has to choose gap sizes that are approximations to what the user requested. For my purposes this is an acceptable tradeoff.

Now for the procedure. The high level explanation is as follows:

  1. Add edge dashes, start dash count at 0.
  2. Repeat until dashes can’t be added anymore:
    2.1. Increase dash count by 1.
    2.2. Decrease inner line length (line length without the edge dashes) by dash size.
    2.3. Add the current number of dashes in dash count while spacing them with equally sized gaps. This gap size is calculated dividing the inner line length by the dash count plus one, i.e. if there are two dashes, three equally sized gaps dividing those two dashes are required.
    2.4. If the inner line length is lower than a single dash size, dashes can’t be added anymore and the loop is over.
  3. Find the gap size that is closest to what the user requested.
  4. Draw the dash positions that were added for this closest gap size.

What this procedure does is create multiple tables of dash positions, each with a different number of dashes. At first, a table is created with the edge dashes (the ends of the line) and with a single dash in the middle. This dash in the middle will be spaced by equally sized gaps. So if a line has length 10, and dashes have length 1, the line will be 1 + gap + 1 + gap + 1, all of this has to add to 10, so gap will come out as 3.5.

If the user requested a gap size of 4, for instance, then the gap size calculated here of 3.5 is pretty close to it, and this combination of dashes may be the end result that is used if another that’s closer isn’t found. This process repeats in increments of one until dashes can’t be further added to the inner line.

To solidify the idea further, another example with the same line length of 10, but with 3 inner dashes instead: now the line will be 1 + gap + 1 + gap + 1 + gap + 1 + gap + 1, three inner dashes, and four gaps. Because all this has to add to 10, gap will be 1.25.

If the user requested a gap size of 4, then this gap size of 1.25 will be too low compared to the earlier 3.5 values, therefore that earlier number of inner dashes (one) is likely to be the final result.

This entire procedure creates multiple tables of dash positions that ultimately won’t be used. Therefore it’s a pretty expensive function, and it also gets more expensive the longer the line is and the smaller the dash + gap values. Overall it’s not a good solution performance wise, but there are probably ways to improve it (I will not explore those). Artifact’s solution of just centering all dashes is simpler and a lot more performant.

The code to make this happen:

I believe most of this should be self-explanatory given the high level explanation. However, a few clarifications.

gap_sizes is a table that contains all gap sizes tried until any more dashes can’t be added to the inner line. The index of each gap size corresponds to how many dashes were required to generate that gap size. Using the previous example of the line with length 10, if dashes stopped being added at three dashes, the gap_sizes table would look like this:

gap_sizes = {3.5, 2, 1.25}

At one and three dashes the gap size values were 3.5 and 1.25, and at 2 dashes the value comes out as 2.

The gap_size_index_to_inner_dash_positions table is the table that will hold all the dash positions. Each index of the table corresponds to the gap size index in the gap_sizes table. So gap_size_index_to_inner_dash_positions[1] will correspond to the inner dash position of the single dash added to that inner line (separated by 3.5 gaps on both sides).

After the while loop, there’s a condition that checks if the length of the gap_sizes table is zero. If it is, it means the line’s length is lower than a single dash size, so the line is just drawn normally from the first to the second edge.

And finally at the end the final dash_positions table is created by merging the table that contains both edge dashes (edge_dash_positions) and the table that contains all inner dashes (gap_size_index_to_inner_dash_positions[closest_gap_size_index]).

I believe these are the most pressing clarifications required. If you’re trying to follow this code and you have further questions, feel free to e-mail me.

It’s worth considering Artifact’s solution given how much simpler it seems to be. Let’s see its implementation, which is the naive implementation mentioned at the start of the post, with the addition that it centers all dashes at the end:

It’s a wildly simpler implementation that also looks just about as right. The only advantage my method has on it is that when drawing arbitrary shapes made up of dashed lines, it will often look more correct. For example, suppose two dashed rectangles:

The one on the left is drawn using four lines in the Artifact style, while the one on the right is drawn using four lines in my style. The one on the right looks like you’d expect a dashed rectangle to look, with the corners solid and the edges of the rectangle being dashed, while the one on the left looks incorrect. This follows from how both procedures differ.

Artifact’s solution is superior for situations like word hovering, while mine is superior for situations like drawing arbitrary dashed shapes. The game I’m working on needs both, so in the end I’ll use both solutions.


Source code: https://github.com/a327ex/Anchor-dashed-lines.
Relevant functions are in the layer.lua file.

Song of the day (Frog96 - Letter to the Black World)

Tags
code

Date
2025-02-27 08:45