#SVG (Part 2)

It has been a while since my last post about SVG, the Table of Contents (ToC) design of Fumadocs also inspired many other cool designs.

I would like to dive into the core idea behind some recent improvements to Fumadocs' TOC.

Preview

#Basic Idea

You can find the source code at GitHub.

Here I assume you have read my last post about SVG, the newer Toc is basically the same idea, but added curvedness using the C command.

TOC

For example, this is the updated code of the svg between each div component.

<svg
  xmlns="http://www.w3.org/2000/svg"
  // control the SVG viewport:
  viewBox={`${Math.min(offset, upperOffset)} 0 ${Math.abs(upperOffset - offset)} 12`}
  style={{
    // " + 1" is needed because of stroke-width
    width: Math.abs(upperOffset - offset) + 1,
    height: 12,
  }}
>
  <path
    // the actual path
    d={`M ${upperOffset} 0 C ${upperOffset} 8 ${offset} 4 ${offset} 12`}
    stroke="black"
    strokeWidth="1"
    fill="none"
    className="stroke-fd-foreground/10"
  />
</svg>

We can vistualize the points as:

Points

Same for the client-side thumb SVG generator function:

if (i === 0) {
  d += ` M${offsetX} ${offsetY} L${offsetX} ${offsetY + height}`;
} else {
  d += ` C ${upperOffsetX} ${offsetY - 4} ${offsetX} ${upperOffsetY + 4} ${offsetX} ${offsetY} L${offsetX} ${offsetY + height}`;
}

Which is used for generating the above SVG line for ToC.

#Animating Thumb

Thumb

Previously, we used mask-image + transform for animating the thumb.

By applying a mask on the animated div, we can highlight a certain part of the ToC outline.

<div
  style={{
    maskImage: "<the encoded svg>",
  }}
>
  <div
    id="moving-bar"
    style={{
      backgroundColor: "red",
      // control the position
      transform: "<animated>",
      // control the vertical size
      height: "<animated>",
    }}
  />
</div>

This approach requires adding transition to height as well, which triggers layout re-arrangement (something that can be costy for rendering engines).

We can switch to clip-path, so by only animating the clip-path we can avoid the layout re-arrangement cost.

<div
  style={{
    clipPath: "<animated-box>",
  }}
>
  {/* it doesn't need to encode the svg */}
  <svg stroke="red">...</svg>
</div>

It is still the same idea of masking a highlighted div, but instead we construct a SVG, then mask it using the moving box.

The moving box is defined in clip-path as a rectangle, which can be animated with transition.

#Thumb Box

There is a tiny circle at the end of thumb, let's denote that by the "thumb box".

Thumb Box

The thumb box is relatively easier to create, just position it to the either top or end of the thumb.

<div
  style={{
    position: "absolute",
    width: 4,
    height: 4,
    backgroundColor: "red",
    transition: "translate 150ms",
    // 1.25px because the size of circle is 4x4px
    translate: `calc(${offset}px - 1.25px) ${top}px`,
  }}
/>

But another problem is how do we animate it, obviously when the position changes it will fly all over the place, rather than sticking to the TOC outline.

Currently, I kept it as-is since it will be resource-consuming to ensure the box sticking to TOC outline.

But it is definitely possible to workaround this, using browser-side JavaScript, it is easy to locate the y-position of the box. We want to obtain the corresponding x-position of box from SVG path.

I have explored offset-path, it is similar to translate but always ensure the element lies on a given path, instead of (x, y), it takes offset-distance: the path distance between starting point and the element's position.

But it only accepts path distance, there is no way to find the actual path distance from a given y-posiiton.

It turns out we must solve for the x at a given y, and this task is not trivial - it is equivalent to solving a high-order polynomial function. Luckily, there is no duplicated solutions. If you don't care about performance, you can use getPointAtLength() and keep iterating until you found a length that's very close to our given y.

I'll skip the actual coding part as it is relatively straightforward.

#Thanks for Reading

This time I have put more effort on the actual implementation part, hope you will find it useful.

Last Updated:

Leave comment