SVG and non-pixel units
Tags: svg, react, typescript
Published: 2019-03-16 13:54:39

---
title: SVG and non-pixel units
published: true
description: Rendering millimeter inside your browser.
tags: ['svg', 'react', 'typescript']
---

Sometimes you want to render non-pixel things on browser. Like when you are making a word processor which prints text on A4 paper ( 210x297mm )[^icloud], or T-shirt authoring tools which prints your photo on L-sized one.

[^icloud]: I found that the Pages App in icloud.com is using SVG for word processing.

Generally, making authoring tools with HTML elements is a hard thing ( draggable `<div>`s, lots of `contenteditable`s, and everything is in pixels). But making everything with canvas is another hell. That's where SVG shines.

### SVG as an interface ( between screens and prints ) 

SVG has a great attribute named `viewBox`. It represents the size of container ( or say, canvas size ). It can be applied apart from `width` or `height`, which means that you can do something like "please show this 200x200 image in 50x50" ( the `200x200` part is `viewBox` ).

Yes, `viewBox` can be a "border of two unit systems", like "please show this 200x200 mm image in 50x50 px".

```tsx
<svg
    width="50" // px
    height="50" // px
    viewBox="0 0 200 200" // Millimeter
>
  <rect width="80" height="80" fill="blue" /> // MILLIMETER !!
</svg>
```

The `<rect>` inside, and everything inside this `<svg>` now represents millimeter-measured things.

### Interconverting pixel and millimeter

Consider dragging the `<rect>` inside the `<svg>`. Since mouse cursor moves in pixels, you have to convert mouse motion from pixels to millimeters.

Let `SVGCanvas` is a `React.Component` which renders `<svg>` with `viewBox`.

```tsx
class SVGCanvas extends React.Component<Props> {
    ...

    render () {
        const { children } = this.props
        return <svg width="50" height="50" viewBox="0 0 200 200">{children}</svg>
    }
}
```

Usually, the `width`, `height` and `viewBox` would be computed like...

```tsx
class SVGCanvas extends React.Component<Props> {
    get widthPx () { ... }
    get heightPx () { ... }
    get widthMm () { ... }
    get heightMm () { ... }

    get viewBox() {
        return `0 0 ${this.widthMm} ${this.heightMm}`
    }

    render () {
        const { children } = this.props
        return <svg width={this.widthPx} height={this.heightPx} viewBox={this.viewBox}>{children}</svg>
    }
}
```

Now you know that we can have a `currentDpm` (dots per millimeters) or `currentDpi` (dots per inch) from the computed values.

It's much simpler to get `dpm`, but `dpi` is more popular in authoring softwares (like Adobe Photoshop). So we'll get the latter like...

```tsx
class SVGCanvas extends React.Component<Props> {
    ...

    get currentDpi() {
        return this.widthPx / this.widthMm * 25.4
    }

    render () {
        const { children } = this.props
        return <svg width={this.widthPx} height={this.heightPx} viewBox={this.viewBox}>{children}</svg>
    }
}
```

Good! Now we have the way to convert pixels to millimeters.

Function px ➔ mm is known as the following ( since 1 inch = 25.4 mm ).

> mm = (px / dpi) * 25.4

So you can

```tsx
// using brand type
// https://basarat.gitbooks.io/typescript/docs/tips/nominalTyping.html
type Pixel = number & { _Pixel: never }
type Millimeter = number & { _Millimeter: never }

const makePx2mm = (dpi: number) => (px: Pixel) => (px / dpi * 25.4) as Millimeter
```

```tsx
class SVGCanvas extends React.Component<Props> {
    ...

    get px2mm() {
        return makePx2mm(this.currentDpi)
    }

    render () {
        const { children } = this.props
        return <svg width={this.widthPx} height={this.heightPx} viewBox={this.viewBox}>{children}</svg>
    }
}
```

I recommend to make px ➔ mm as a "curried" function. Making `px2mm` naively will result in a binary function, but you often want to only pass pixel value. So it is better to "bind" `dpi` in advance.[^curry]

[^curry]: You can also accomplish this by `Function.prototype.bind` though.

### Following to window resize

Now we have `px2mm` in our component. But in real world apps, `dpi` ( or `this.widthPx` ) might change dynamically. Especially when window resizes.

This is where curried function proves its merits.

```tsx
class SVGCanvas extends React.Component<Props, State> {
    ...

    componentDidMount() {
        document.addEventListener('resize', this.handleResize)
    }

    handleResize () {
        // calculate current width / height to show
        this.setState({ widthPx: ..., heightPx: ... })
    }

    ...
}
```

```tsx
class SVGCanvas extends React.Component<Props, State> {
    get currentDpi() {
        // using state.widthPx !!
        return this.state.widthPx / this.widthMm * 25.4
    }

    get px2mm() {
        return makePx2mm(this.currentDpi)
    }

    ...
}
```

Great! Now your `px2mm` will change dynamically when browser window resizes.

### Conclusion

- SVG has a `viewBox`, which make SVG as an interface between screens and prints.
- You can interconvert pixels to millimeter using simple math.
- With currying px ➔ mm converter, it easily follows window resizing.
- Putting them in a component makes your SVG an authoring tool.