Using SVG in Create-React-App (color/text manipulation, download as png)

Introduction

I was asked to build a small app that takes in user input and create a downloadable image using those inputs. The user inputs their name and three colors (using html color picker) and the result image renders according to those inputs.

SVG

After researching for different ways to manipulate images, I came across ‘svg’. Svg stands for ‘scalable vector images’, and put short, instead of saving image pixel by pixel as in other image files, svg saves ‘paths’ of the image and ‘draw’ this paths when called. More to the introduction to svg here.

Below is a example of a svg image that I used in this app.

<svg width="250" height="250" xmlns="http://www.w3.org/2000/svg">
    <title>Energy Ball</title>
    <g>
        <ellipse ry="220" rx="220" id="outer" cy="250" cx="250" fill="#b6f4f4">
        <ellipse ry="170" rx="170" id="middle" cy="250" cx="250" fill="#d2f3f2"> 
        <ellipse ry="120" rx="120" id="inner" cy="250" cx="250" fill="#ffffff">
    </g>
</svg>

The upsides

1. Easy to manipulate features using css and javascript

Svg is written in XML format, meaning that contents of svg can be reached and changed easily with css and js, just the same way used for html. For instance, changing the fill color of the outer ellipse of the above example can be done easily done by javascript as below.

const circle = document.querySelector('#outer');
let color = "#ffffff"

circle.setAttribute("fill", color);

2. Small in size

Unlike image files that take up lots of memory, size of svg is far smaller and there fore much lighter to use and manipulate.

The downsides

1. Not user friendly

As a regular user of a web page, when you provide a downloadable image as a svg file format, which probably opens with a web browser and does not function as the same way as other image file formats, it wouldn’t mean much to them. This means when the user downloads an image, the svg image would have to be converted into a png or jpg image and then provided to the user. After working to solve this for a few days, this isn’t always easy as it sounds.

2. Code can ge messy.

I’m not really sure if I mastered the concept of svg, I could write a cleaner code, for instance by importing the image as a component in react, but at this stage and at this level, for the svg to work perfectly, I added most of the svg directly into my html or react component, which made the code look a lot messier than it actually should be.

SVG and React

After testing out different features of svg in html and vanilla JS stage, I created a react app and implemented it on the app.

State and Props for Svg

Instead of directly changing the features of the svg like on vanillaJS, when using react, these meant-to-be-changed features could be set as JSX variables. These variables follow the state of the app, which is served to the component as a prop. When the user inputs different values using the input tags of html, the state of the app changes, which re-renders the image to follow that state.

The state section of App JS

class App extends Component {
  constructor() {
    super();
    this.state = {
      input: '',
      nameInput: '',
      colors: ['#b6f4f4', '#d2f3f2', '#ffffff'],
      route: 'start',
    }
  }
  ...

Changing the State According to user Input

Function

onNameChange = (event) => {
    this.setState({nameInput: event.target.value});
}

onColorChange = (event) => {
let key = event.target.name * 1 - 1;
let val = event.target.value;
this.setState((state) => {
    let colors = state.colors.map((x, i) => {
    if (key === i) {
        return val;
    } else {
        return x;
    }
    });
    return { colors, };
})

Providing Props to Components

render() {
    return (
        <div className="App">
        { this.state.route === 'start' 
            ? <FrontPage onRouteChange={this.onRouteChange}/>
            : this.state.route === 'name'
            ? <NameInput onNameChange={this.onNameChange} onRouteChange={this.onRouteChange} />
            : this.state.route === 'color'
            ? <ColorInput colors={this.state.colors} onColorChange={this.onColorChange} onRouteChange={this.onRouteChange}/>
            : <Result username={this.state.nameInput} colors={this.state.colors}/>
        }
        </div>
    );
}

Example of variables as attribute value in components

const {username, colors} = this.props
return(
    <div>
    ...
        <circle r="200" transform="matrix(1 0 0 -1 540 220)" fill={colors[0]} />
        <circle r="150" transform="matrix(1 0 0 -1 540 220)" fill={colors[1]} />
        <circle r="100" transform="matrix(1 0 0 -1 540 220)" fill={colors[2]} />
    </div>
)

Converting Svg to Png and Download

After trying out countless different packages and method, the ‘save-svg-as-png’ package worked for my app (npm link). Here’s how.

  1. import package using require
import React from 'react';
import './Result.css';
const saveSvgAsPng = require('save-svg-as-png')
  1. use Ref to reach svg section of the component
class Result extends React.Component {
    constructor(props) {
        super(props);
        this.svgRef = React.createRef();
    }
    ...
    <svg ref={this.svgRef} id='resultImage' width="300" height="300" viewBox='0 0 1080 1080' fill="none" xmlns="http://www.w3.org/2000/svg">
    ...

This allows the method of the component to use the svg codes as in querySelector in VanillaJS

  1. use the package in function to convert and proceed download
onDownloadClick = () => {
    const node = this.svgRef.current;
    saveSvgAsPng.default.saveSvgAsPng(node,'')
}

...
<button onClick={() => this.onDownloadClick()}> 
    Download
</button>
...

The entire flow

Mind that some parts of the code is left out for simplicity sake, thus the code might not work just as below

import React from 'react';
const saveSvgAsPng = require('save-svg-as-png')

class Result extends React.Component {
    constructor(props) {
        super(props);
        this.svgRef = React.createRef();
    }

    onDownloadClick = () => {
        const node = this.svgRef.current;
        saveSvgAsPng.default.saveSvgAsPng(node,'')
    }

    render() {
        const {username, colors} = this.props
        return (
            <div className='content-area'>
                <section className='svgContainer'>
                    <svg ref={this.svgRef} id='resultImage' width="300" height="300" viewBox='0 0 1080 1080' fill="none" xmlns="http://www.w3.org/2000/svg">
                    <g clipPath="url(#clip0)">
                        <rect width="1080" height="1080" fill="#5E2CA5"/>
                        <circle r="200" transform="matrix(1 0 0 -1 540 220)" fill={colors[0]} />
                        <circle r="150" transform="matrix(1 0 0 -1 540 220)" fill={colors[1]} />
                        <circle r="100" transform="matrix(1 0 0 -1 540 220)" fill={colors[2]} />
                    </g>
                    <defs>
                        <clipPath id="clip0">
                            <rect width="1080" height="1080" fill="white"/>
                        </clipPath>
                    </defs>
                </svg>
                </section>
                <br/>
                <br/>
                <button onClick={() => this.onDownloadClick()}>Download</button>
            </div>
        )
    }
}


export default Result

Hope this helps someone save bit of time figuring out svg and react.