DEV Community

f_subal
f_subal

Posted on

SVG and non-pixel units

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 )1, or T-shirt authoring tools which prints your photo on L-sized one.

Generally, making authoring tools with HTML elements is a hard thing ( draggable <div>s, lots of contenteditables, 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".

<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.

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...

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...

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

// 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
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.2

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.

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

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

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

    ...
}
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.

  1. I found that the Pages App in icloud.com is using SVG for word processing. 

  2. You can also accomplish this by Function.prototype.bind though. 

Top comments (0)