Browser…​unplugged
Showcases of modern Browser technologies

April 27, 2020

Moderate between CSS and SVG for the path microformat with library pathfit

In the last few years a lot of efforts have been made to dovetail CSS and SVG. There is even a resolution in the SVG 2 spec not to add any more presentation attributes, prefering instead pure CSS properties. Nonetheless, some parts resist their harmonization with a vigor. Path microsyntax is one of them.

(While this article is really about introducing a library, its first part is a general essay about parsers. Whoever is not interested, can find out further down how pathfit scales path descriptions to container elements.)

In CSS we are used to positions and measurements having a unit that is appended to the number. In contrast, SVG has the concept of a user space coordinate system that can be transformed and re-defined at any place within the XML tree. The coordinates themselves thus can remain unitless.

Both concepts have drawn grooves deep into their language grammar and lead to basic concepts that are no longer easy to align.

In addition, there is a difference in the manner how words are separated. The SVG approach is more permissive in nature, leaving commas to be generally optional. Even whitespace can be left off, as long as adjecent words can be uniquely discerned. CSS differentiates between the meaning of spaces (separating keywords) and commas (outlining lists).

🔗 Why have paths no units?

The micro syntac of the path definition attribute (<path d="..." />), having originated in SVG, by now founds its way into CSS: besides referencing <path> elements by their URL, for example in properties like shape-outside, there are some properties that accept the path() function as their value, using the path syntax for a string argument: clip-path, motion-path, shape-inside and shape-outside.

Now that browsers implement these properties, developers are complaining ever more often that dimensionless paths de facto only represent px units, which will not act responsively. “We want percentage values!” is the outcry I read again and again.

But I can see no viable way to realise that demand. Neither can the CSS parser handle the path grammar, nor is the SVG parser designed to include numbers with units. Downwards compatible changes have severe challenges to master.

🔗 Paths in the CSS parser

Let's have a look at a path definition attribute. All the following string variants represent the same AST, and all follow a typical pattern that some common software uses for serialization:

M0,20h20q5-10,20-10,5,10,2,20z            <- Illustrator
M 0,20 h 20 q 5,-10 20,-10 5,10 20,20 z   <- Inkscape
M 0 20 h 20 q 5 -10 20 -10 5 10 20 20 z   <- D3

In YAML, the tree could maybe look as follows:

- command: M
  sequence:
  - coordinate_pair: { x: 0, y: 20 }
- command: h
  sequence:
  - coordinate: 20
- command: q
  sequence:
  - control_point: { x: 5, y: -10 }
    coordinate_pair: { x: 20, y: -10 }
  - control_point: { x: 5, y: 10 }
    coordinate_pair: { x: 20, y: 20 }
- command: z

The same sequences, if interpreted as CSS, would lead to three different results. The first one would be already unusable after tokenization :

- token: name
  value: M0
- token: comma
- token: dimension
  value: 20
  type: integer
  ident: h20q5-10 # see https://www.w3.org/TR/css-values/#component-whitespace
- token: comma
- token: number
  value: -10
  type: integer
- token: comma
# etc

For the second variant, commas would be interpreted as a higher level structure, leaving the result as a nested list that rips the coordinates apart:

- [M, 0]
- [20, h, 20, q, 5]
- [-10, 20]
- [-10, 5]
- [10, 20]
- [20, z]

Only the third variant would lead to a result (a simple list of names and numbers) that left the parser with a chance to interpret the grammar correctly.

To summarize: The CSS parser cannot interpret path commands reliably. Processing of the path microsyntax must be done by the corresponding SVG parser component. That is the reason why the path argument is escaped in quotes, as a precondition to handing it on unaltered.

🔗 Units in the SVG parser

SVG in version 2 describes attribute values almost always by linking to CSS Values and Units . Also most length values  are specified this way. The one notable exception are path definitions, which have their own EBNF grammar . The obvious reason is, the mix of commands and numbers is just as unfit for CSS treatment as the permissive handling of whitespace and commas.

Would it be possible to enhance the parser described like this to also understand length units? There are some stumbling blocks.

The following sequence is a mix of numbers and letters:

M1e2, 3l4.5in, 6Q 7,8 9,10

The first M is a path command. The e is part of a number written in scientific notation, the l is a path command again, and the letters in represent a length unit. But what about the Q?

As a path command, it represents a quadratic Bezier curve, which is parametrized by the four following numbers (two absolute coordinates). But there also is a length unit Q, measuring a quarter-millimeter. Honestly, I didn't know about that before finding it in the spec , but it exists and a short test showed it is implemented by browsers.

A parser that aims at being downward compatible cannot abandon its basic concepts and must maintain its “greedy” mode of operation:

The processing of the EBNF must consume as much of a given EBNF production as possible, stopping at the point when a character is encountered which no longer satisfies the production.

Which means: if a number can be followed by a unit, then first everything following the number is tried to be interpreted as a unit. Only after that the consumation of the production “length” is complete, and the next unit can be understood as a command.

Thus the Q would be coerced to be a unit associated with the preceding number — leaving downward compatibility impossible.

An interesting variant was proposed  by Amelia Bellamy-Royds in 2016: Wrap all numbers that have units, or even computations, in round brackets. Like this (her example):

M(10px)(1em)
H(100%-10px)
A(10px)(5mm)0 0 0 (100%)(1em+5mm)
V(1em+3in-5mm)
A(10px)(5mm)0 0 0(100%-10px)(1em+3in)
H(10px)
A(10px)(5mm)0 0 0(0px)(1em+3in-5mm)
V(1em+5mm)
A(10px)(5mm)0 0 0 Z

So far, this lead to nothing, although the proposition was added to the milestones  for a future SVG version 2.1.

Computing with paths

That leads us to the replacement solution: if paths utilized in CSS should adapt to units, they have to be recomputed in advance to suit user space coordinates.

One use case is especially important here: relative units are able to responsively adapt. It is not that vital in this case that every individual number gets its own unit. There would be much to be gained if at least the path as a whole can be subjugated to transformations.

One could say: well, that is what the transform CSS property is for. And for clip-path that is most probably enough.

But looking at shape-inside and shape-outside things get more involved. The text flowing around the path gets also transformed. And for motion-path the element to be animated would also be subject to the transformation. It would be neccessary to define an extraneous inner element with an inverse transformation to counteract this.

And this would still not achieve responsiveness, because relative units do not work with all transformation functions. Especially the most important one, scale(), works only with dimensionless numbers. It cannont react for example to changes in the size of the viewport. How would you write down that the result of scaling should be a percentage of a container element?

🔗 Size adaptation as a standard task

This is the problem my library pathfit tackles. MY thanks got to Jhey Tompkins for starting me  on this project.

At its heart it rewrites path definitions by applying arbitrary affine transforms to them. Like this:

source path:     'M 0,0 100,100'
transformation:  translate(50, 0) scale(0.5, 0.8)
resulting path:  'M 50,0 100,80'

This, by the way, would have not required a new library, as it already exists . It is only that the functionality aimed at here, scaling a path responsively to its environment, needs some more steps of computation.

Therefore the library exposes two more methods of computation. If an original size is given for a path, i. e. a rectangle the path "fits in", the path can be scaled such that it will fit into another rectangle with different dimension just the same way. For example:

source path:     'M 0,0 100,0 100,100 0,100 z'
source size:     width: 100px; height: 100px;
target size:     width: 50px; height: 50px;
resulting path:  'M 0,0 50,0 50,50 0,50 z'

Everyone who has done this sort of fitting will note that it needs to be more involved than this. There is a problem when the aspect ratios of the source and target rectangles are not matching. Then, more information is needed to describe the mode and the position of the "fit".

There are two syntax variants for this:

  • SVG has a source viewBox with four parameters for position and size, and attribute preserveAspectRatio for describing how to fit it to its target.

  • CSS only knows about two values for an intrinsic with and height of the source, but then the properties object-fit (or background-size) and object-position (or background-position) constitute a richer syntax for describing the intended result.

pathfit implements both variants.

With preserveAspectRatio an application could look like this:

const pathfitter = new Pathfit({
    viewBox: '0 0 100 100',
    preserveAspectRatio: 'xMinYMid meet'
});
pathfitter.set_path('M 0,0 100,0 100,100 0,100 z');

const container = document.querySelector('.motionContainer');
const moving = container.querySelector('.moving');

const containerObserver = new ResizeObserver((entries) => {
    const {width, height} = entries.pop().contentRect;

    const scaled_path = pathfitter.scale_with_aspect_Ratio(width, height);

    moving.style.offsetPath = `path('${scaled_path}')`;
});

containerObserver.observe(container);

With object-fit I can present a complete and executable example:

See the Pen Responsive offset-path with pathfit.js by ccprog (@ccprog) on CodePen.

A small tip to wrap it up: Like all npm packages, pathfit can be linked directly from unpkg.com :

<script src="https://unpkg.com/pathfit/pathfit.js"></script>